前端面试小册(含90页PDF前端高频面试题及其推荐解答等)
互联网发展迅猛之余也伴随着互联网寒冬,行业不景气这样的词,等毕业季去各个求职网站投简历,去各个人才市场找机会,才发现四处碰壁,作为应届求职者更需要打好基础,明确发展规划,跟上行业步伐。下面是本人2019年秋招前端面试经历,结合个人博客和牛油们面经中的高频问题以及行业前辈们复习资料的综合整理,包含基础篇、Vue框架篇、HTTP&浏览器、构建工具篇、安全篇、算法篇,欢迎交流斧正。希望大家在毕业季都能一帆风顺,从容斩获OFFER
怀着学习的态度完成了一份90页PDF , 近140+commits,近100+前端面试题及推荐解答,资源合集的面试小册。因为篇幅有限,下面是小册前三篇的各五道前端高频面试题及推荐解答,这个项目已经在github上开源,欢迎大家取用:https://github.com/okaychen/FE-Interview-Brochure
浏览器解析渲染页面过程
大致过程:
HTML解析构建DOM-CSS解析构建CSSOM树-根据DOM树和CSSOM树构建render树-根据render树进行布局渲染render layer-根据计算的布局信息进行绘制
不同浏览器的内核不同,所以渲染过程其中有部分细节有不一样,以webkit主流程为例:
一篇很棒的文章(需科学上网):How Browser Work
dog cheng有话说:浏览器解析渲染页面过程是一个复杂的过程,其中有不少的细节和规则,如果把上面分享的文章翻译成译文,至少有3~5页PDF左右,所以这里只能总结大致过程(作为面试回答【很可能让回答的尽可能详细】了解来说已经足够,更深入的了解可以好好读下上面那篇文章)
较详细过程:
HTML解析构建DOM树:其中HTML Parser就起到了将HTML标记解析成DOM Tree的作用,HTML Parser将文本的HTML文档,提炼出关键信息,嵌套层级的树形结构,便于计算拓展;这其中也有很多的规则和操作,比如容错机制,识别特殊标签<br></br>等
CSS解析构建CSSOM树:CSS Parser将很多个CSS文件中的样式合并解析出具有树形结构Style Rules,也叫做CSSOM。
※其中还有一个细节是浏览器解析文档:当遇到<script>标签的时候会停止解析文档,立即解析脚本,将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM Tree和CSSOM上
根据DOM树和CSSOM树构建Render树:Render Tree的构建其实就是DOM Tree和CSSOM Attach的过程,在webkit中,解析样式和创建呈现器的过程称为"附加",每个DOM节点都有一个"attach"方法,Render Tree其实就相当于一个计算好样式,与HTML对应的Tree
根据Render树进行布局渲染render layer:创建渲染树后,Layout根据根据渲染树中渲染对象的信息,计算好每一个渲染对象的位置和尺寸,将其放在浏览器窗口的正确位置,某些时候会在文档布局完成之后进行DOM修改,重新布局的过程就称为回流
※其中计算(样式计算)一个复杂的过程,因为DOM中的一个元素可以对应样式表中的多个元素,Firefox采用了规则树和样式上下文树来简化样式计算,规则树包含了所有已知规则的匹配路径,样式上下文包含端值,webkit也有样式对象,但它们不保存在类似上下文树这样的结构中,只是由DOM节点指向此类对象的相关样式
根据计算的布局信息进行绘制:绘制阶段则会遍历呈现树,并调用呈现器的paint方法,将呈现器的内容显示在屏幕上,绘制的顺序其实就是元素进入堆栈样式上下文的顺序,例如,块呈现器的堆栈顺序如下:1.背景颜色,2.背景图片,3.边框,4.子代,5.轮廓
移动端点透现象有遇到过嘛
首先需要了解的是,移动端在touch上一共有4个事件,
执行顺序为touchstart -> touchmove -> touchend -> touchcancel
当用户点击屏幕时,会触发touch和click事件,touch事件会优先处理,touch事件经过捕获,目标,冒泡一系列流程处理完成之后,才会触发click,所有我们经常会谈到移动端点击事件300ms延迟的问题
移动端点击事件300ms问题,常见的解决方案:
- 阻止用户双击缩放,并限制视口大小
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
- 设置csstouch-action用于指定某个给定的区域是否允许用户操作,以及如何相应用户操作
* { touch-action: none; }
- fastclick.js来解决,其原理是在检测到touchend事件的时候,会通过自定义事件立即触发模拟一个click事件,并在300ms之后把真正的click事件阻止掉
点透现象:
发生条件:①按钮A和按钮B不是后代继承关系,②A发生touch,A touch后立即消失,B绑定click,③A z-index大于B,即 A 显示在 B 浮层之上
发生原因:当点击屏幕时,系统生成touch和click两个事件,touch先执行,touch执行完之后A消失,然后要执行click的时候,就会发现用户点击的是B,所以就执行了B的click
解决方法:①阻止默认事件,在touch的某个时间段执行event.preventDefault,去取消系统生成的click事件,一半在 touchend 中执行。②要消失的元素延迟300ms后在消失
margin塌陷和合并问题
首先,margin塌陷是相对于父子级关系的两个元素,而margin合并是相对两个兄弟级关系的两个元素
两个兄弟级关系的元素,垂直方向上的margin,其外边距会发生重叠现象,两者两个的外边距取的是两个所设置margin的最大值,就是所说的margin合并问题
两个父子级关系的元素,垂直方向上的margin会粘合在一起,外层和模型的margin-top取两个元素中margin-top的最大值,发生margin塌陷的内层元素相对于整个文档移动
解决方案:两者都可以通过触发BFC来解决
CSS定位的方式有哪些分别相对于谁
static(默认值) absolute(绝对定位,相对于最近已定位的父元素,如果没有则相对于<html>) relative(相对定位,相对于自身) fixed(固定定位,相对于窗口) sticky(2017年浏览器开始支持,粘性定位)
absolute会使元素位置与文档流无关,不占据空间,absolute 定位的元素和其他元素重叠
relative相对定位时,无论元素是否移动,仍然占据原来的空间
sticky是2017年浏览器才开始支持,会产生动态效果,类似relative和fixed的结合,一个实例是"动态固定",生效前提是必须搭配top,left,bottom,right一起使用,不能省略,否则等同于relative定位,不产生"动态固定"的效果
移动端布局的解决方案,平时怎么做的处理
- 使用Flexbox
- 百分比布局结合媒体查询
- 使用rem
rem转换像素大小(根元素的大小乘以rem值),取决与页面根元素的字体大小,即HTML元素的字体大小
em转换像素大小(em值乘以使用em单位的元素的字体大小),比如一个div的字体大小为16px,那么10em就是180px(或者接近它)
rem平时怎么做的转换:为了方便计算,时常将html的字体大小设置为62.5%,那么12px就会是1.2rem
列表无限滚动曾经有遇到过嘛
简单列表滚动加载是监听滚动条在满足条件的时候触发回调,然后通过把新的元素加入到页面页尾的方法完成,但是如果用户加载过多列表数据(比如我这一个列表页有一万条数据需要展示),那么用户不断加载,页面不断增加新的元素,很容易就导致页面元素过多而造成卡顿,所以就提出的列表的无限滚动加载,主要是在删除原有元素并且维持高度的基础上,生成并加载新的数据
如果滚动过快怎么办,高频率触发事件解决方案-防抖和节流
节流:在一段时间内不管触发了多少次都只认为触发了一次,等计时结束进行响应(假设设置的时间为2000ms,再触发了事件的2000ms之内,你在多少触发该事件,都不会有任何作用,它只为第一个事件等待2000ms。时间一到,它就会执行了。 )
//时间戳方式 function throttle(fn,delay){ let pre = Date.now(); return function(){ let context = this; let args = arguments; let now = Date.now(); if(now - pre > delay){ fn.apply(context,args); pre = Date.now(); } } } //定时器方式 function throttle(fn,delay){ let timer = null; return function(){ let context = this; let args = arguments; if(!timer){ timer = setTimeout(function(){ fn.apply(context,args); timer = null; },delay) } } }
防抖:在某段时间内,不管你触发了多少次事件,都只认最后一次(假设设置时间为2000ms,如果触发事件的2000ms之内,你再次触发该事件,就会给新的事件添加新的计时器,之前的事件统统作废。只执行最后一次触发的事件。)
//定时器方式 function debounce(fn,delay){ let timer = null; return function(){ let context = this; let args = arguments; clearTimeout(timer); timer = setTimeout(function(){ fn.apply(context,args); },delay) } }
解释一下变量提升
JavaScript引擎会先进行预解析,获取所有变量的声明复制undefined,然后逐行执行,这导致所有被声明的变量,都会被提升到代码的头部(被提升的只有变量,值不会被提升),也就是变量提升(hoisting)
console.log(a) // undefined var a = 1 function b(){ console.log(a) } b() // 1
预解析阶段会先获的变量a赋值undefined,并将var a = undefined放在最顶端,此时a = 1还在原来的位置,实际就是:
var a = undefined console.log(a) // undefined a = 1 function b(){ console.log(a) } b() // 1
然后会是执行阶段,逐行执行造成了打印出a是undefined
Promise的了解,手撕Promise(Promise.all或者Promise.race)
Promise是一种异步编程的解决方法,相比容易陷入回调地狱的回调函数,采用链式调用的方式更合理也更方便,Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),接受一个作为函数作为参数,该函数有两个参数,分别是resolve和reject两个函数
// Promise的模拟实现 class _Promise { constructor(fn) { let _this = this; this._queue = []; this._success = null; this._error = null; this.status = ''; fn((...arg) => { // resolve if (_this.status != 'error') { _this.status = 'success'; _this._success = arg; _this._queue.forEach(json => { json.fn1(...arg) }) } }, (...arg) => { // reject if (_this.status != 'success') { _this.status = 'error'; _this._error = arg; _this._queue.forEach(json => { json.fn2(...arg) }) } }) } then(fn1, fn2) { let _this = this; return new _Promise((resolve, reject) => { if (_this.status == 'success') { resolve(fn1(..._this._success)) } else if (_this.status == 'error') { fn2(..._this._error) } else { _this._queue.push({fn1,fn2}); } }) } }
Promise.all和Promise.race在实际应用中的比较,比如从接口中获取数据,等待所有数据到达后执行某些操作可以用前者,如果从几个接口中获取相同的数据哪个接口先到就用哪个可以使用后者
//Promise.all的模拟实现(race的实现类似) Promise.prototype._all = interable => { let results = []; let promiseCount = 0; return new Promise(function (resolve, reject) { for (let val of iterable) { Promise.resolve(val).then(res => { promiseCount++; results[i] = res; if (promiseCount === interable.length) { return resolve(results); } }, err => { return reject(err); }) } }) }
对eventloop事件循环机制的了解
首先,JavaScript一大特点就是单线程,这样的设计让它在同一时间只做一件事;作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM,避免了复杂性,比如假设JavaScript有两个线程,那么在同一时间进行添加删除节点操作,为浏览器分辨以哪个线程为准带来困难,所以单线程是它作为浏览器脚本语言的优势,也是它的核心特征。
注:虽然为了利用多核CPU的计算能力,HTML5提出了web worker标准,允许JavaScript创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,所以也并没有改变JavaScript单线程的本质
那么,单线程就意味着,所有任务需要排队,前一个任务结束才会执行后一个任务,所以为了提高CPU的利用效率,就把所有任务分成了同步任务(synchronous)和异步任务(asynchronous),同步任务在主线程顺序执行,异步任务则不进入主线程而是进入到任务队列(task queue)中。在主线程上会形成一个执行栈,等执行栈中所有任务执行完毕之后,会去任务队列中查看有哪些事件,此时异步任务结束等待状态,进入执行栈中,开始执行。
主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
HTTP1.1相比1.0的区别有哪些
目前通用标准是HTTP1.1,在1.0的基础上升级加了部分功能,主要从连接方式,缓存,带宽优化(断点传输),host头域,错误提示等方面做了改进和优化
- 连接方式:HTTP1.0使用短连接(非持久连接);HTTP1.1默认采用带流水线的长连接(持久连接),PS:在持久连接的基础上可选的是管道化的连接
- 缓存:HTTP1.1新增例如ETag,If-None-Match,Cache-Control等更多的缓存控制策略
- 带宽优化:HTTP1.0在断连后不得不下载完整的包,存在一些带宽浪费的现象;HTTP1.1则支持断点续传的功能,在请求消息中加入range 头域,允许请求资源的某个部分,在响应消息中Content-Range头域中声明了返回这部分对象的偏移值和长度
- host头域:在HTTP1.0中每台服务器都绑定一个唯一的ip地址,所有传递消息中的URL并没有传递主机名;HTTP1.1请求和响应消息都应支持host头域,且请求消息中没有host头域名会抛出一个错误(400 Bad Request)
- 错误提示:HTTP1.1新增24个状态响应码,比如409(请求的资源与资源当前状态冲突),410(服务器上某个资源被永久性删除);相比HTTP1.0只定义了16个状态响应码
HTTP2相比HTTP1的优势和特点
HTTP2相比HTTP1.x有4大特点,二进制分帧,头部压缩,服务端推送和多路复用
①二进制分帧:HTTP2使用二进制格式传输数据,而HTTP1.x使用文本格式,二进制协议解析更高效
②头部压缩:HTTP1.x每次请求和发送都携带不常改变的,冗杂的头部数据,给网络带来额外负担;而HTTP2在客户端和服务器使用"部首表"来追踪和存储之前发送的键值对,对于相同的数据,不再每次通过每次请求和响应发送
可以简单理解为只发送差异数据,而不是发送全部头部,从而减少头部信息量
③服务端推送:服务端可以在发送页面HTML时主动推送其他资源,而不用等到浏览器解析到相应位置时,发起请求再响应
④多路复用:在HTTP1.x中如果想并发多个请求,需要多个TCP连接,并且浏览器为了控制资源,一般对单个域名有6-8个TCP连接数量的限制;而在HTTP2中
- 同个域名所有通信都在单个连接下进行
- 单个连接可以承载任意数量的双向数据流
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送
HTTP的缓存过程(强缓存和协商缓存)
通常过程如下:
- 判断是否过期(服务器会通知浏览器一个缓存时间,相关头部信息在Cache-Control和Expires中),如果时间未过期,则直接从缓存中取,即强缓存;
- Cache-Control
- 其中max-age = <seconds>设置缓存存储的最大周期,超过这个时间缓存将会被认为过期,与Expires相反,时间是相对于请求的时间
- public 表示响应可以被任何对象缓存,即使是通常不可缓存的内容
- private 表示缓存只能被单个用户缓存,不能作为共享缓存(即***服务器不可缓存它)
- no-cache 告诉浏览器、缓存服务器,不管本地副本是否过期,使用副本前一定要到源服务器进行副本有效校验
- no-store 缓存不应该存储有关客户端请求或服务器响应的任何内容
- Expires
- expires 字段规定了缓存的资源的过期时间,该字段时间格式使用GMT时间标准时间格式, js通过new Date().toUTCString()得到,由于时间期限是由服务器生成,存在着服务端和客户端的时间误差,相比cache-control优先级较低
- Cache-Control
- 那么如果判断缓存时间已经过期,将会采用协商缓存策略
- Last-Modified响应头和If-Modified-Since请求头
- Last-Modified 表示资源最后的修改时间,在浏览器第一次发送HTTP请求时,服务器会返回该响应头
- 那么浏览器在下次发起HTTP请求时,会带上If-Modified-Since请求头,其值就是第一次发送HTTP请求时,服务器设置在Last-Modified响应头中的值
- 两种情况:如果资源最后修改时间大于If-Modified-Since,说明资源有被改动过,则响应完整的资源内容,返回状态码200;如果小于或者等于,说明资源未被修改,则响应状态码304,告知浏览器可以继续使用所保存的缓存
- Etag响应头和If-None-Match请求头
- Etag值为当前资源在服务器的唯一标识
- 类比上面Last-Modified响应头和If-Modified-Since请求头,请求头中If-None-Match将会和资源的唯一标标识进行对比,如果不同,则说明资源被修改过,响应200;如果相同,则说明资源未改动,响应304
- Last-Modified响应头和If-Modified-Since请求头
前端性能优化的方案有哪些
前端性能优化七大常用的优化手段:减少请求数量,减小资源大小,优化网络连接,优化资源加载,减少回流重绘,使用更好性能的API和构建优化
- 减少请求数量
- 文件合并,并按需分配(公共库合并,其他页面组件按需分配)
- 图片处理:使用雪碧图,将图片转码base64内嵌到HTML中,使用字体图片代替图片
- 减少重定向:尽量避免使用重定向,当页面发生了重定向,就会延迟整个HTML文档的传输
- 使用缓存:即利用浏览器的强缓存和协商缓存
- 不使用CSS @import:CSS的@import会造成额外的请求
- 避免使用空的src和href:a标签设置空的href,会重定向到当前的页面地址,form设置空的method,会提交表单到当前的页面地址
- 减小资源大小
- 压缩:静态资源删除无效冗余代码并压缩
- webp:更小体积
- 开启gzip:HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术
- 优化网络连接
- 使用CDN
- 使用DNS预解析:DNS Prefetch,即DNS预解析就是根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,来提高网站的访问速度
// 方法是在 head 标签里面写上几个 link 标签 <link rel="dns-prefecth" href="https://www.google.com"> <link rel="dns-prefecth" href="https://www.google-analytics.com">
- 并行连接:由于在HTTP1.1协议下,chrome每个域名最大并发数是6个。使用多个域名,可以增加并发数
- 管道化连接:在HTTP2协议中,可以开启管道化连接,即单条连接的多路复用
- 优化资源加载
- 减少重绘回流
- 使用性能更好的API
- 构建优化:如webpack优化
参考:前端性能优化的七大手段
HTTPS相比HTTP的区别,讲一下HTTPS的实现过程
https相比之下是安全版的http,其实就是HTTP over TSL,因为http都是使用明文传输的,对于敏感信息的传输就很不安全,https正是为了解决http存在的安全隐患而出现的
https的实现也是一个较复杂的过程,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,在保证了通信效率的同时防止窃听,同时结合CA证书以及数字签名来最大程度保证安全性:
- 对称加密:通信双方都使用用一个密钥进行加密解密,比如特工接头的暗号,就属于对称加密;这种方式虽然简单性能好,但是无法解决首次把密钥发给对方的问题,容易被黑客拦截密钥
- 非对称加密:由私钥+公钥组成的密钥对
- 即用私钥加密的数据,只要公钥才能解密;用公钥加密的数据,只有私钥才能解密
- 通信双方都有自己的一套密钥对,通信前将自己的公钥发给对方
- 然后拿着这个公钥来加密数据响应给对方,等对方收到之后,再用自己的私钥进行解密
非对称加密虽然安全性更高,但是带来的问题就是速度较慢,影响性能
结合两种方式,将对称加密的密钥使用非对称加密的公钥进行加密,然后再发送出去,然后接收方使用自己的私钥进行解密得到对称加密的密钥
但是又带来一个问题,即中间人问题:
如果此时在客户端和服务端之间存在一个中间人,这个中间人只需要把原本双方通信互发的公钥,换成自己的公钥,这样中间人就可以轻松解密通信双方所发送的数据
这时候就需要一个安全的第三方的颁布证书,来证明身份,防止中间攻击:所以就有了CA证书,仔细观察浏览器地址栏,会有一个"小锁"的标志,点开里面有证明身份的CA证书信息
但是仍然存在的一个问题是,如果中间人篡改了CA证书,那么这个证书不就无效了?所以就添加了新的保护方案:数字签名
数字签名就是用证书自带的HASH算法对证书内容进行一个HASH得到一个摘要,再用CA的私钥进行加密,最终组成数字签名
当别人把他的证书发过来时,用同样的HASH算法得到信息摘要,然后再用CA的公钥对数字签名进行解密,得到CA创建的消息摘要,两者一对比就知道中间有没有被篡改了
通过这样的方式,https最大化保证了通信的安全性
写在后面
篇幅有限,上面留下了小册中前三篇的各五道高频问题,更多问题以及资源合集,在Github可以看到
那么,最后为了突出主题呢,还是要写一些对于这份小册的愿景吧:如果你是应届生(当然,大牛除外),正面临找前端开发的工作,或者即将成为毕业生的预备生,我相信这份前端面试小册多多少少会帮到你,在这"不景气"的"寒冬"之季,我们仍要提高自身综合素质,坚持技术生活,通过不断给自己设立小目标并实现来督促自身学习提高,2020只争朝夕不负韶华
#前端工程师120道面试常考题##实习##面经##面试流程##前端##校招#