初心-杨瑞超个人博客诚邀您加入qq群(IT-程序猿-技术交流群):757345416
大家注意了,教大家一道口诀:
同步优先、异步靠边、回调垫底(读起来不顺)
用公式表达就是:
同步 => 异步 => 回调
有一道经典的面试题:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('i: ',i);
}, 1000);
}
console.log(i);
//输出
5
i: 5
i: 5
i: 5
i: 5
i: 5
分析:
1、for循环和循环体外部的console是同步的,所以先执行for循环,再执行外部的console.log。(同步优先)
2、for循环里面有一个setTimeout回调,他是垫底的存在,只能最后执行。(回调垫底)
那么,为什么我们最先输出的是5呢?
非常好理解,for循环先执行,但是不会给setTimeout传参(回调垫底),等for循环执行完,就会给setTimeout传参,而外部的console打印出5是因为for循环执行完成了。
我们给第一个例子加一行代码。
for (var i = 0; i < 5; ++i) {
setTimeout(function() {
console.log('2: ',i);
}, 1000);
console.log('1: ', i); //新加一行代码
}
console.log(i);
//输出
1: 0
1: 1
1: 2
1: 3
1: 4
5
2: 5
2: 5
2: 5
2: 5
2: 5
这个例子可以很清楚的看到先执行for循环,for循环里面的console是同步的,所以先输出,for循环结束后,执行外部的console输出5,最后再执行setTimeout回调 55555。
那么怎么解决此问题呢?
我们用let解决
for (let i = 0; i < 5; ++i) {
setTimeout(function() {
console.log('2: ',i);
}, 1000);
}
console.log(i);
//输出
i is not defined
2: 0
2: 1
2: 2
2: 3
2: 4
为什么i会报错呢?解析:
let是ES6语法,ES5中的变量作用域是函数,而let语法的作用域是当前块,在这里就是for循环体。
在这里,let本质上就是形成了一个闭包。也就是下面这种写法一样的意思。下面代码同理:
var loop = function (_i) {
setTimeout(function() {
console.log('2:', _i);
}, 1000);
};
for (var _i = 0; _i < 5; _i++) {
loop(_i);
}
console.log(i);
解析:我们来分析一下,用了let作为变量i的定义之后,for循环每执行一次,都会先给setTimeout传参,准确的说是给loop传参,loop形成了一个闭包,这样就执行了5个loop,每个loop传的参数分别是0,1,2,3,4,然后loop里面的setTimeout会进入消息队列排队等候。当外部的console执行完毕,因为for循环里的i变成了一个新的变量 _i ,所以在外部的console.log(i)是不存在的。
现在可以解释闭包的概念了:当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。
我知道你又要我解释这句话了,loop(_i)是外部函数,setTimeout是内部函数,当setTimeout被loop的变量访问的时候,就形成了一个闭包。
闭包实例:
function t() {
var a = 10;
var b = function() {
console.log(a);
}
b();
}
t(); //输出 10
接着我就举一个包含同步、异步、回调的例子
let a = new Promise(
function(resolve, reject) {
console.log(1)
setTimeout(() => console.log(2), 0)
console.log(3)
console.log(4)
resolve(true)
}
)
a.then(v => {
console.log(8)
})
let b = new Promise(
function() {
console.log(5)
setTimeout(() => console.log(6), 0)
}
)
console.log(7)
1、看同步代码:a变量是一个Promise,我们知道Promise是异步的,是指他的then()和catch()方法,Promise本身还是同步的,所以这里先执行a变量内部的Promise同步代码。(同步优先)。
console.log(1)
setTimeout(() => console.log(2), 0) //回调
console.log(3)
console.log(4)
2、Promise内部有4个console,第二个是一个setTimeout回调(回调垫底)。所以这里先输出1,3,4回调的方法丢到消息队列中排队等着。
3、接着执行resolve(true),进入then(),then是异步,下面还有同步没执行完呢,所以then也滚去消息队列排队等候。(真可怜)(异步靠边)
4、b变量也是一个Promise,和a一样,执行内部的同步代码,输出5,setTimeout滚去消息队列排队等候。
5、最下面同步输出7。
6、同步的代码执行完了,JavaScript就跑去消息队列呼叫异步的代码:异步,出来执行了。这里只有一个异步then,所以输出8。
7、异步也over,轮到回调的孩子们:回调,出来执行了。这里有2个回调在排队,他们的时间都设置为0,所以不受时间影响,只跟排队先后顺序有关。则先输出a里面的回调2,最后输出b里面的回调6。
8、最终输出结果就是:1、3、4、5、7、8、2、6。
我们还可以稍微做一点修改,把a里面Promise的 setTimeout(() => console.log(2), 0)改成 setTimeout(() => console.log(2), 2),对,时间改成了2ms,为什么不改成1试试呢?1ms的话,浏览器都还没有反应过来呢。你改成大于或等于2的数字就能看到2个setTimeout的输出顺序发生了变化。所以回调函数正常情况下是在消息队列顺序执行的,但是使用setTimeout的时候,还需要注意时间的大小也会改变它的顺序。
文章到此结束,希望对你的学习有帮助!
注:本文章是对原文https://segmentfault.com/a/1190000008922457二次编辑,如有侵权,请联系作者删除。