混子前端汇总面试常问知识点

1,087 阅读24分钟
混子前端分享一些面试常问干货,同时也帮助大家复习和巩固一波基础

ps:文章全部由个人总结,知识点分析不全面请自行查漏补缺

页面布局

你必须知道的经典问题:

假设高度已知,写出三栏布局,其中左栏/右栏宽度各为300px;中间自适应。

(ps:此题作为一个小白也要至少答出3种方法,以下分享连混子前端都知道的5种方法)

  • 浮动float  [优点:兼容性好;缺点:脱离文档流]

HTML部分:
<section class="layout float">
    <article class='left-right-center'>
        <div class="left"></div>
        <div class="right"></div>
        <div class="center"></div>
    </article>
</section>

CSS部分:
.layout article div{
    height: 100px;
}
.layout.float .left{
    float: left;
    width: 300px;
    background: red;
}
.layout.float .right{
    float: right;
    width: 300px;
    background: blue;
}
.layout.float .center{
    background: yellow;
}

  • 定位absolute  [优点:快捷配合JS使用;缺点:子元素脱离文档流,可使用性差]

HTML部分:
<section class="layout absolute">
    <article class='left-right-center'>
        <div class="left"></div>
        <div class="center"></div>
        <div class="right"></div>
    </article>
</section>

CSS部分:.layout article div{
    height: 100px;
}
.layout.absolute .left,.layout.absolute .right,.layout.absolute .center{
    position: absolute;
}
.layout.absolute .left{
    left: 0px;
    width: 300px;
    background: red;
}
.layout.absolute .right{
    right: 0px;
    width: 300px;
    background: blue;
}
.layout.absolute .center{
    left: 300px;
    right: 300px;
    background: yellow;
}

  • 弹性盒flex  [完美解决上面2个布局的缺点]

HTML部分:
<section class="layout flexbox">
    <article class='left-right-center'>
        <div class="left"></div>
        <div class="center"></div>
        <div class="right"></div>
    </article>
</section>

CSS部分:
.layout article div{
    height: 100px;
}
.layout.flexbox .left-right-center{
    display: flex;
}
.layout.flexbox .left{
    width: 300px;
    background: red;
}
.layout.flexbox .center{
    flex: 1;
    background: yellow;
}
.layout.flexbox .right{
    width: 300px;
    background: blue;
}

  • 表格table  [优点:兼容性好;缺点:其中某个单元格高度超出,其它两栏也超出]

HTML部分:
<section class="layout table">
    <article class='left-right-center'>
        <div class="left"></div>
        <div class="center"></div>
        <div class="right"></div>
    </article>
</section>

CSS部分:
.layout article div{
    height: 100px;
}
.layout.table .left-right-center{
    width: 100%;
    display: table;
    height: 100px;
}
.layout.table .left-right-center>div{
    display: table-cell;
}
.layout.table .left{
    width: 300px;
    background: red;
}
.layout.table .center{
    background: yellow;
}
.layout.table .right{
    width: 300px;
    background: blue;
}

  • 网格grid  [不用栅格来模拟网格布局 / 代码量简化很多]

HTML部分:
<section class="layout grid">
    <article class='left-right-center'>
        <div class="left"></div>
        <div class="center"></div>
        <div class="right"></div>
    </article>
</section>

CSS部分:
.layout article div{
    height: 100px;
}
.layout.grid .left-right-center{
    display: grid;
    width: 100%;
    grid-template-rows: 100px;
    grid-template-columns: 300px auto 300px;
}
.layout.grid .left{
    background: red;
}
.layout.grid .center{
    background: yellow;
}
.layout.grid .right{
    background: blue;
}

补充:未知高度时,弹性盒flex / 表格table很适用,不修改代码

思考:上下高度固定,中间自适应(ps:混子前端都会的,你也必须会!)

CSS盒模型

你必须知道的盒子模型:

标准盒模型 + IE盒模型

标准模型:不包含padding和border值,content即盒子width / height

IE盒模型:盒子width / height包含padding和border值


CSS如何设置这两种模型:

box-sizing: content-box;    // 标准盒模型(默认)
box-sizing: border-box;    // IE盒模型

JS如何设置盒模型对应宽和高

1、dom.style.width/height;    // 获取内联样式宽/高[局限性]
2、dom.currentStyle.width/height;    // 获取是渲染后的宽/高 [只有ie支持]3、window.getComputedStyle(dom).width/height;    // 支持chrome、firefox
4、dom.getBoundingClientRect().width/height[left/right/top/bottom];    // 很少人知道,获取渲染后width/height,计算一个元素绝对位置,根据视窗(viewport),常用获取left/right/top/bottom

边距重叠

常见问题:子元素高度100px,margin-top: 10px,此时父元素高度 ?

解:父元素是100px;如果父元素设置overflow: hidden,就是110px;

[就是给父元素创建BFC(块级格式化)] 

BFC:边距重叠解决方案

基本概念:块级格式化上下文

原理(BFC渲染规则):

  1. BFC垂直方向边距不发生重叠
  2. BFC区域不会与浮动元素重叠(清除浮动)
  3. BFC在页面是独立容器,外面元素不会影响里面元素,里面元素不会影响外面元素
  4. 计算BFC高度时候,浮动元素也会参与计算

怎样创建BFC,除了给父元素添加overflow: hidden创建BFC,还有那些?

  1. float值除none以外
  2. position值不是static或relative就创建了BFC
  3. display: inline-block,table-cell和table相关的几个属性都可以创建
  4. overflow不是visiable也可以创建

DOM事件

你必须知道几个关于DOM的知识点

DOM事件级别(标准级别)

一代:DOM 0级

element.onClick = function(){
    // TODO
}

为什么从DOM 0级直接到DOM 2级了?DOM 1级标准没有设定与事件相关的东西

二代:DOM 2级

element.addEventListener('click', function(){
    // TODO
}, false);

三代:DOM 3级 

新增了鼠标,键盘等事件

element.addEventListener('keyup', function(){
    // TODO
}, false);

事件模型

DOM事件流

浏览器在为当前页面与用户交互过程中,如:点击鼠标是怎样传到页面上(如何响应的)?

混子前端认为分为三个阶段:

  1. 捕获[事件通过捕获到达目标元素]
  2. 目标阶段[点击按钮]
  3. 冒泡到window对象[目标阶段上传到window对象]

DOM事件捕获的具体流程

捕获:window > document > HTML > body  > HTML结构 > 一层层传递到目标元素

冒泡:捕获反过来就是冒泡,从目标元素传递到window

Event对象

  1. event.perventDefault()阻止默认事件,如:<a>标签绑定click事件,点击会阻止跳转默认行为
  2. event.stopPropagation()阻止冒泡行为
  3. event.stopImmediatePropagation(),如:button绑定两个click事件(a,b),现在想a执行后不执行b[在a响应函数添加event.stopImmediatePropagation(),会阻止b的执行],用来阻断
  4. event.currentTarget当前绑定事件元素
  5. event.target当前被点击元素
注意:4和5在没冒泡是一样的值,但在用了事件委托的情况就不一样了。

如:ul > li 点击触发,给ul绑定onclick方法,此时:
target:<li>

currentTarget:<ul>

自定义事件

var eve = new Event('custome');    // 声明自定义事件
ev.addEventListener('custome', function(){
    console.log('custome');
});
ev.dispatchEvent(eve);    // dispatchEvent api触发

HTTP协议

协议主要特点

  1. 简单快速
  2. 灵活
  3. 无连接
  4. 无状态
ps:你以为这样就完了?以下是你必须知道的扩展说明:
  1. uri是固定的,图片等(简单快速)
  2. http头部分有类型,通过http协议完成不同数据类型传输(灵活)
  3. 链接一次会断掉,不会保持链接(无连接)
  4. 客户端和服务端请求图片,http协议帮助建立链接和传输,完成后断开,下次客户端在过来,服务端是没办法区分上次和这次是不是同一个人,即服务端没记录(无连接)

补充:网站现在记住的是身份,不是http协议做到的,而是通过服务端加session做到的

HTTP报文

客户端输入url,服务端响应;

报文分为2部分,即:请求报文 响应报文,如图所示:

HTTP方法

GET:获取资源

POST:传输资源

PUT:更新资源
DELETE:删除资源

HEAD:获得报文首部

POST和GET的区别

  1. GET在浏览器回退时是无害的,而POST会再次提交请求
  2. GET产生的URL地址可以被收藏,而POST不可以
  3. GET请求会被浏览器主动缓存,而POST不会,除非手动设置
  4. GET请求只能进行URL编码,而POST支持多种编码方式
  5. GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留 
  6. GET请求在URL中传递的参数是有长度限制的(2kb),而POST没有限制
  7. 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
  8. GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
  9. GET参数通过URL传递,而POST放在Request body中

HTTP状态码

大概要知道的:
  1. 1xx:指示信息
  2. 2xx:成功
  3. 3xx:重定向
  4. 4xx:客户端错误
  5. 5xx:服务器错误

如果你对自己有追求,那以下你也要知道:

200:客户端请求成功

206:客户发送了一个带有Range头的GET请求,服务器完成;如:客户端发起0-10000字节请求,服务器返回206,服务端会根据Range头部截一部分,响应回客户端(客户端要求的Range范围)

301:所请求页面已经转移至新的url(永久重定向)

302:所请求页面已经临时转移至新的url(临时重定向)

304:客户端口有缓冲的文档并发出了一个条件性的请求,服务器告诉客户端,原来缓冲的文档还可以继续使用 [ 服务器告诉客户端有缓存,可以取缓存用,不用在服务器取 ]

400:客户端有语法错误,不能被服务器所理解

401:请求未经授权

403:对被请求页面访问禁止

404:资源不存在

500:服务器错误

503:服务器临时过载或宕机

HTTP持久连接

HTTP协议采用“请求 -- 应答”模式,当使用普通模式,即非keep-alive(持久连接)时,每个请求/应答客户端和服务器都要新建一个连接,完成之后立即断开连接(HTTP无连接协议)

当使用keep-alive模式(持久连接)时,keep-alive功能使客户端到服务端的连接持久有效,当出现对服务器的后续请求时,keep-alive功能避免了建立或者重新建立连接

注意:持久连接1.1版本才支持,1.0版本不支持

管线化

当使用持久连接的情况下,某个连接上消息的传递类似于:

请求1 > 响应1 > 请求2 > 响应2 > 请求3 > 响应3

某个连接上的消息变成了类似以下这样(管线化):

[请求1 > 请求2 > 请求3 ] > [响应1 > 响应2 > 响应3]

注意:

  1. 只有GET和HEAD请求可以管线化,POST有所限制
  2. 服务端支持才可以,chrome和firefox默认并未开启管线化

原型链

创建对象的几种方法

  1. 字面量

    var o1 = {name: 'o1'};
    var o11 = new Object({name: 'o11'});

  2. 显示构造函数

    var M = function(){this.name='o2'};
    var o2 = new M();

  3. 借用Object.create()

    var P = {name: 'o3'};
    var o3 = Object.create(P)

Object.create()创建对象是用原型链连接的;o3的__proto__指向P对象,下文会说o3.__proto__指向构造函数的原型对象,也就是说原型就是P

o3.__proto__ === P;    // true

ps:Object.create()方法是把参数中对象作为一个新对象原型对象赋给o3的,o3本身不具备name属性,是通过原型链连接它的原型对象;

原型 / 构造函数 / 实例 / 原型链


ps:通过prototype 和 __proto__完成原型链向上找的过程,Object.prototype是整个原型链的顶端,到这里未知

问题来了:原型对象和原型链到底起什么作用?

解答:构造函数中增加很多属性和方法,实例就可以共用,当有很多个实例想去公用方法时,不可能每个都拷贝一份,现在考虑应该有个东西存,这个东西就是原型对象

注意:

  1. 构造函数 > 函数才会有prototype,对象是没有的
  2. 只有实例对象有__proto__ [ 为什么函数也有,函数也是对象 ]
  3. 构造函数prototype属性使实例和原型对象产生关联,修改了prototype就是修改实例上一级原型对象

instanceof原理


ps:主要在这条原型链上的构造函数都是实例对象构造函数

instanceof原理是 实例对象__proto__属性 和 构造函数 prototype判断是不是同一个引用,返回true / false

JS code:
function M (){ this.name = name };
var o = new M()
o instanceof M;    // true
o instanceof Object;    // true

注意为什么 o instanceof Object 也返回true ?

JS code:
o.__proto__ === M.prototype;    // true
M.prototype.__proto__ === Object.prototype;    // true

为了区分此现象,建议大家日常使用constructor

JS code:
o.__proto__.constructor === M;    // true
o.__proto__.constructor === Object;    // false

所以说使用constructor判断比instanceof更严谨

new运算符(原理)

  1. 一个对象被创建(理解成字面量),它继承构造函数prototype [ 构造函数被执行,执行时候,相同的传参被传入 ]
  2. 同时上下文this会被指定为这个新实例 
  3. 构造函数如果返回了一个新对象,那这个对象会取代new出来结果 [ 如果构造函数没有返回对象,那new出来的结果为步骤1创建的对象 ]

JS code:

var newF = function(func){
    var o = Object.create(func.prototype);    // 1.创建对象关联构造函数原型对象
    var k = func.call(o);    // 2.执行构造函数
    if(typeof k === 'object'){    // 3.判断构造函数运行结果,是否为对象类型
        return k;
    }else{
        return o;
    }
}

通信类

同源策略及限制

源:协议 / 域名 / 端口 80(默认),这三个有一项不同就跨域了

限制:不是同一个源文档没有权利修改另一个源文档

  1. cookie、localStorage 和 indexDB无法读取
  2. DOM无法获得
  3. Ajax请求无法获得(只适合同源策略)

前后端如何通信

  1. Ajax [ 同源策略 ]
  2. WebSocket [ 不限制同源策略 ]
  3. CORS [ 支持同源和跨域 ]
  4. ..

如何创建Ajax

var xhr = XMLHttpRequest ? new XMLHttpRequest() : new Window.ActiveXObject("Microsoft.XMLHTTP");    // 兼容写法
xhr.setRequestHeader();    // header头部信息
xhr.open("GET","/try/ajax/ajax_info.txt", true);    
xhr.send();    // 发送
// xhr.onload判断xhr.status = 200 || 206 || 304 

跨域通信几种方式

Ajax是不能发送跨域通信的,浏览器在识别用Ajax发送一个跨域请求时,它会在http头部加origin来允许跨域通信,如果不加就被拦截,这也是前端混子所理解CORS原理

JSONP

原理:在出现postMessage和CORS前,用script标签的异步加载实现;

JS code:
// 发送
<script src="http://www.abc.com/?data=name&callback=jsonp"></script>

// 返回
<script>
    jsonp({
        data:{}
    })   </script>

// 发送
document.getElementByTagName('head')[0].appendChild('script');

       实现:

      1.告诉服务端callback名称,将来是作为函数名返回的

      2.本地必须有jsonp全局函数,后面才能把数据执行出来

Hash

应用场景:页面A通过iframe潜入跨域页面B并发送消息

var B = document.getElementByTagName('iframe');
B.src = B.src + '#' + 'data';

window.onhashchange = function(){    // 写在B页面中, 监听hash有没有改变
    var data = window.location.hash;
}

postMessage

应用场景同上

// A页面发送到B
B.window.postMessage('data', 'http://B.com');

// B页面接收A
window.addEventListener('message',function(event){
    console.log(event.origin);    // 拿到A的源
    console.log(event.source);    // Awindow的对象
    console.log(event.data);    // 发送的消息拿到了
}, false);

Websocket

var ws = new Websocket('wss: echo.websocket.org');    // 指向服务器地址, ws不加密, wss加密
ws.onopen = function(){ ws.send(); };    // 发送出去
ws.onmessage = function(ev){ 
    ev.data;
    ws.close;    // close断开
}

CORS

新型的Ajax通信api:fetch 实现CORS通信

fetch('/some/url',{
    method: 'get'
}).then(function(response){
    
}).catch(function(error){

});

安全类

CSRF

CSRF:称为跨站请求伪造


ps:引诱点击后访问网站A后,浏览器自动上传cookie,A网站对身份重新确认(合法),就执行了动作,完成CSRF攻击

实现CSRF不可缺少的条件:

  1. 访问A网站是登陆的
  2. 接口存在漏洞

CSRF防御措施:

  1. token验证:刚才浏览器只自动上传cookie,没有自动上传token,token是注册成功后服务器会往本地存储token,在访问接口时携带token,如果是上图场景,只会自动上传cookie,不会上传token
  2. Refer验证(refer是页面来源):服务器判断页面来源是不是站点下页面
  3. 隐藏令牌(类似token):隐藏才http header头中,不会放在连接上,本质与token无区别,只是使用方式

XSS

XSS称为跨域脚本攻击

攻击原理:不需要做登陆验证,核心原理是向页面注入脚本 [ 评论区是最好最入css攻击的,在提交区里面写script标签,img标签等 ],xss是利用合法渠道注入JS

防御措施:宗旨是不让页面执行插入的JS脚本

XSS与CSRF对比区别

  1. XSS是向页面注入JS运行,JS函数体做事情
  2. CSRF是利用本身漏洞帮我执行接口
  3. 方式不一样,CSRF依赖用户登陆网站

渲染机制类

DOCTYPE的作用

你要知道的:DTD(document type definition)告诉浏览器我是什么类型,浏览器根据类型来判断用什么引擎优化它;如:XML / HTML

这里是作用:DOCTYPE是声明文档类型和DTD规范的,直接告诉浏览器当前文档包含哪些DTD(文档类型)

<!DOCTYPE HTML>    // html5

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//en" "http://www.w3. org/TR/html4/strict.dtd">    // HTML4严格:不包含展示性和弃用元素(front)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//en" "http://www.w3. org/TR/html4/loose.dtd">    // HTML4传统:包含展示性和弃用元素(front)

浏览器渲染过程


重排Reflow

浏览器根据各种样式计算放在它该出现的问题,如:JS修改div中内容(即触发Reflow)

常见触发Reflow

  1. 增加/删除/修改 导致Reflow或Repaint
  2. 移动DOM未知(CSS3动画)
  3. 修改CSS样式(宽高/display)
  4. Resize窗口有可能(移动端没有这个问题)或滚动时
  5. 修改字体

重绘Repaint

屏幕上页面的呈现内容

常见触发Repaint

  1. DOM移动
  2. CSS改动
  3. ... ( 太多了,只要判断现在呈现内容有没有改变 )

JS运行机制

JS是单线程的(同一事件只能做一件事);这里混子前端拿道面烂了的面试题来举例:

JS代码:

for (var i = 1;i <= 5;i ++) {
    setTimeout(function timer() {
        console.log(i)
    },i * 1000)
}

对JS作用域、闭包以及事件循环等概念不了解的伙伴会想当然的回答:

第一次循环,隔一秒输出1;

第二次循环,隔一秒输出2;

第三次循环,隔一秒输出3;

第四次循环,隔一秒输出4;

第五次循环,隔一秒输出5;

或者还有伙伴预期结果是分别输出数字1~5,每秒依次,每次一个。

但实际结果大家粘贴到控制台就都知道,以一秒的频率输出五个6。

下面来过一下上面提到的知识点:

作用域:这里我引用《你不知道的javascript》中的一个比喻,可以把作用域链想象成一座高楼,第一层代表当前执行作用域,楼的顶层代表全局作用域。我们在查找变量时会先在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上找,以此类推。到达顶层后(全局作用域),可能找到了你所需的变量,也可能没找到,但无论如何查找过程都将停止。

闭包:我的理解是在传递函数类型的变量时,该函数会保留定义它的所在函数的作用域。读起来可能比较绕,或者可以简单的这么理解,A函数中定义了B函数并且它返回了B函数,那么不管B函数在哪里被调用或如何被调用,它都会保留A函数的作用域。

事件循环:这个概念深入起来很复杂,下面新开一个段落详谈:

说起事件循环,不得不提起任务队列。事件循环只有一个,但任务队列可能有多个,任务队列可分为宏任务(macro-task)微任务(micro-task)
XHR回调、事件回调(鼠标键盘事件)、setImmediate、setTimeout、setInterval、indexedDB数据库操作等I/O以及UI rendering都属于宏任务(也有文章说UI render不属于宏任务,目前还没有定论)

process.nextTick、Promise.then、Object.observer(已经被废弃)、MutationObserver(html5新特性)属于微任务。注意进入到任务队列的是具体的执行任务的函数。

比如上述例子setTimeout()中的timer函数。另外不同类型的任务会分别进入到他们所属类型的任务队列,比如所有setTimeout()的回调都会进入到setTimeout任务队列,所有then()回调都会进入到then队列。当前的整体代码我们可以认为是宏任务。事件循环从当前整体代码开始第一次事件循环,然后再执行队列中所有的微任务,当微任务执行完毕之后,事件循环再找到其中一个宏任务队列并执行其中的所有任务,然后再找到一个微任务队列并执行里面的所有任务,就这样一直循环下去。这就是我所理解的事件循环。

继续看个栗子:

JS代码:

console.log('global')

setTimeout(function () {
   console.log('timeout1')
   new Promise(function (resolve) {
     console.log('timeout1_promise')
       resolve()
   }).then(function () {
     console.log('timeout1_then')
  })
},2000)

for (var i = 1;i <= 5;i ++) {
  setTimeout(function() {
    console.log(i)
  },i*1000)
  console.log(i)
}

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
 }).then(function () {
  console.log('then1')
})

setTimeout(function () {
  console.log('timeout2')
  new Promise(function (resolve) {
    console.log('timeout2_promise')
    resolve()
  }).then(function () {
    console.log('timeout2_then')
  })
}, 1000)

new Promise(function (resolve) {
  console.log('promise2')
  resolve()
}).then(function () {
  console.log('then2')
})

来一步一步分析以上代码:

  1. 首先执行整体代码,“global”会被第一个打印出来。这是第一个输出
  2. 执行到第一个setTimeout时,发现它是宏任务,此时会新建一个setTimeout类型的宏任务队列并派发当前这个setTimeout的回调函数到刚建好的这个宏任务队列中去,并且轮到它执行时要延迟2秒后再执行。 
  3. 代码继续执行走到for循环,发现是循环5次setTimeout(),那就把这5个setTimeout中的回调函数依次派发到上面新建的setTimeout类型的宏任务队列中去,注意,这5个setTimeout的延迟分别是1到5秒。此时这个setTimeout类型的宏任务队列中应该有6个任务了。再执行for循环里的console.log(i),很简单,直接输出1,2,3,4,5,这是第二个输出
  4. 再执行到new Promise,Promise构造函数中的第一个参数在new的时候会直接执行,因此不会进入任何队列,所以第三个输出是"promise1",上面有说到Promise.then是微任务,那么这里会生成一个Promise.then类型的微任务队列,这里的then回调会被push进这个队列中。
  5. 再继续走,执行到第二个setTimeout,发现是宏任务,派发它的回调到上面setTimeout类型的宏任务队列中去。
  6. 再走到最后一个new Promise,很明显,这里会有第四个输出:"promise2",然后它的then中的回调也会被派发到上面的Promise.then类型的微任务队列中去。
  7. 第一轮事件循环的宏任务执行完成(整体代码可以看做宏任务)。此时微任务队列中只有一个Promise.then类型微任务队列,它里面有两个任务。宏任务队列中也只有一个setTimeout类型的宏任务队列。 
  8. 下面执行第一轮事件循环的微任务,很明显,会分别打印出"then1",和"then2"。分别是第五和第六个输出。此时第一轮事件循环完成。
  9. 开始第二轮事件循环:执行setTimeout类型队列(宏任务队列)中的所有任务。发现都有延时,但延时最短的是for循环中第一次循环push进来的那个setTimeout和上面第5个步骤中的第二个setTimeout,它们都只延时1s。它们会被同时执行,但前者先被push进来,所以先执行它!它的作用就是打印变量i,在当前作用域找变量i,木有!去它上层作用域(这里是全局作用域)找,找到了,但此时的i早已是6了。(为啥不是5,那你得去补补for循环的执行流程了)所以这里第七个输出是延时1s后打印出6。
  10. 紧接着执行第二个setTimeout,它会先后打印出"timeout2"和"timeout2_promise",这分别是第八和第九个输出。但这里发现了then,又把它push到上面已经被执行完的then队列中去。
  11. 这里要注意,因为出现了微任务then队列,所以这里会执行该队列中的所有任务(此时只有一个任务),即打印出"timeout2_then"。这是第十个输出
  12. 继续回过头来执行宏任务队列,此时是执行延时为2s的第一个setTimeout和for循环中第二次循环的那个setTimeout,跟上面一样,前者是第一个被push进来的,所以它先执行。这里会延时1秒(原因下面会解释)分别输出“timeout1”和“timeout1_promise”,但发现了里面也有一个then,于是push到then微任务队列并立即执行,输出了"timeout1_then"。紧接着执行for中第二次循环的setTimeout,输出6。注意这三个几乎是同时被打印出来的。他们分别是第十一到十三个输出
  13. 再就很简单了,把省下的for循环中后面三次循环被push进来的setTimeout依次执行,于是每隔1s输出一个6,连续输出3次。
  14. 第二轮事件循环结束,全部代码执行完毕。
这里解释下为什么上面第12步不是延迟2秒再输出“timeout1”和“timeout1_promise”,这时需要理解setTimeout()延时参数的意思,这个延迟时间始终是相对主程序执行完毕的那个时间算的 ,并且多个setTimeout执行的先后顺序也是由这个延迟时间决定的。  

再回过头来看上面那个问题,理解了事件循环的机制,问题就很简单了。for循环时setTimeout()不是立即执行的,它们的回调被push到了宏任务队列当中,而在执行任务队列里的回调函数时,变量i早已变成了6。那如何得到想要的结果呢?很简单,原理就是需要给循环中的setTimeout()创建一个闭包作用域,让它执行的时候找到的变量i是正确的。

混子前端给出如下5种解决方案:

  1. 引入IIFE

    JS代码
    
    for(var i = 0;i < 5; i++){
        (function(i){
            setTimeout(function timer(){
                console.log(i);
            }, i * 1000)
        })(i);
    }
    

  2. 利用ES 6引入的let关键字

    JS代码
    
    for(let i = 0;i<5;i++) {
        setTimeout(function timer(){
            console.log(i);
        }, i * 1000);
    }
    

  3. 利用ES 5引入的bind函数

    JS代码
    
    for (var i=1; i<=5; i++) {
        setTimeout( function timer(i) {
            console.log(i);
        }.bind(null,i), i*1000 );
    }
    

  4. 利用setTimeout第三个参数

    JS代码
    
    for (var i=1; i<=5; i++) {
        setTimeout(function timer(i) {
            console.log(i);    
        }, i*1000,i );
    }
    

  5. 把setTimeout用一个方法单独出来形成闭包

    JS代码
    
    var loop = function (i) {
        setTimeout(function timer() {
            console.log(i);  
        }, i*1000);
    };
    
    for (var i = 1;i <= 5; i++) {
        loop(i);
    }

页面性能

  • 资源压缩合并,减少HTTP请求(把文件变小)
  • 非核心代码异步加载
    异步加载方式:
    1、动态脚本加载:document.creatElement创建标签/节点标签加到head/body
    2、defer:script上添加
    3、async:script上添加

    对比defer/async区别:
    defer是在HTML解析完才会执行,如果是多个按加载顺序执行
    async是在加载完之后立即执行,多个async的话,执行顺序和加载顺序无关
  • 利用浏览器缓存
    强缓存 / 协商缓存
  • 利用CDN[内容分发]让网络快速到服务端把文件下载下来
  • 预解析DNS
    高级浏览器在页面里a标签默认打开了DNS预解析,不加这句话,a标签也做了DNS预解析,但页面如果是HTTPS协议,浏览器是关闭预解析的,通过meta标签强制打开预解析:

    <meta http-equiv="x-dns-prefetch-control" content="on">
    
    <link rel="dns-prefetch" href="//host_name_to_prefetch.com">

错误监控

首先我们要知道错误分类

  1. 即时运行错误:代码错误
  2. 资源加载错误:img / js / css 加载失败

捕获方式:

(一)如果是即时性错误

  1. try...catch
  2. window.onerror 
  3. addEventListener

(二)如果是资源加载性错误

  1. Object.onerror  对 img / js 添加 onerror事件
  2. performance.getEntries() 获取所有已加载的资源,返回一个数组
  3. Error事件捕获window上[冒泡不会阻止捕获]

    JS代码
    
    window.addEventListener('error', function(){
        // ****
    }, true);

ps:如果JS错误是跨域的,可以在script标签添加crossorigin属性,同时在响应JS资源时,设置JS资源头Access-Control-Allow-Origin就可以拿到错误信息了

这里上报错误的原理:利用ajax通讯上报 / 也可以利用image对象上报

JS代码

(new Image()).src = "http://baidu.com/tesjk?r=tksjk";

利用这种方式比ajax简单,也能实现资源上报

算法类

由于本人也是混子前端,不一一列举,但作为一名合格的前端,算法是必不可少的。

ps:请随意吐槽本混子,会虚心接受


好了,混子前端就总结到这里,还请留意混子前端不定时的更新....