在《你不知道的Javascript》中,有一道题
for(var i=0; i < 6; i++) {
setTimeout(function(){
console.log(i);
},0);
}
答案是6个6。
这道题粗略的解释是i
属于全局作用域,在异步定时器里,也引用的是相同作用域中的i
,当定时器启动时,i
已经变成了6,所以打印出来的结果并不是我们预期的0、1、2、3、4、5。
那么有没有更加专业(zhuangbility)一点的说法呢?有的,我们可以从执行上下文和闭包的角度来解题。
今天我们来深入说一说其中的原理
闭包是什么
首先我们需要知道闭包究竟是什么东西。
MDN中对于闭包的解释是这样的:
这么专业的解释肯定看不懂啦,我们来看看它给的例子:
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
以上代码非常简单,它的大概意思就是displayName
函数里并没有name
这个变量,它引用了外层作用域中的name
,那么displayName
就是一个闭包
所以我们用大白话定义一下:闭包 = 函数 + 访问外层作用域的变量。
执行上下文与闭包的关系
要明白执行上下文与闭包的关系,首先我们需要写一下上面例子中的执行上下文过程:
1、创建全局上下文,压栈
ECStack=[globalContext]
2、全局上下文初始化完成
globalContext={
VO:{ //全局变量对象中有个init函数变量声明
init:referance to function init
},
Scope:[globalContext.VO],
this:globalContext.VO
}
同时init函数内部有个[[scope]]属性保存了globalContext.VO
3、调用函数,创建函数上下文后压栈
ECStack=[globalContext,initContext]
4、进入上下文,此时函数并没执行,初始化函数上下文,将[[scope]]内部属性复制给函数上下文中的Scope
属性,并将当前上下文中的AO(活动变量对象)放到最前面
initContext={
AO:{
arguments:{
length:0
},
name:undefined,
displayName:referance to function displayName
},
Scope:[AO,globalContext.VO],
this:undefined
}
5、函数代码执行,变量对象完成赋值
initContext={
AO:{
arguments:{
length:0
},
name:Mozilla,
displayName:referance to function displayName
},
Scope:[AO,globalContext.VO],
this:undefined
}
6、遇到displayName
代码,此时内部生成[[scope]]属性保存外层作用域层级链
displayName.[[scope]]=[initContext.AO,globalContext.VO]
7、开始调用displayName
函数,创建函数上下文,压栈
ECStack=[globalContext,initContext,displayNameContext]
8、进入函数上下文,初始化displayNameContext
,把活动对象压入作用域链中
displayNameContext={
AO:{
arguments:{
length:0
},
}
Scope:[AO,initContext.AO,globalContext.VO]
}
9、函数执行,完成变量对象赋值,然后找到外层的name,打印
10、开始弹栈
// 先弹displayName的上下文
ECStack=[globalContext,initContext]
// 再弹init的上下文
ECStack=[globalContext]
以上就是执行上下文的过程,而displayName
之所以能够访问到外层作用域中的name
,就是因为displayName
中的Scope
属性,里面保存了initContext和全局上下文中的变量对象。
displayName.Scope=[AO,initContext.AO,globalContext.VO]
有了这个属性,即使initContext
被弹出执行栈了,displayName
同样可以获取到它的变量对象。
这就是闭包的底层逻辑。同时也是闭包和执行上下文的关系
回到开始
现在我们来重新审视一下最开始的代码
for(var i=0; i < 6; i++) {
setTimeout(function(){
console.log(i);
},0);
}
当执行function函数时,此时的执行上下文有什么呢?全局上下文变量对象中有一个i
globalContext.VO={
i:6
}
我们可以修改一下代码使它变成我们想要的0、1、2、3、4、5
1、第一种改法
for(var i=0; i < 6; i++) {
setTimeout(function(i){
console.log(i);
},0,i);
}
使用这种方法,会让setTimeout函数传i
给function
,此时匿名函数第一次执行时它的上下文中是这样的
functionContext={
AO:{
arguments:{
0:0, // 实参中的 i
length:1
},
i:0,
}
...
}
函数调用时,i
已经被定时器当成参数传递给匿名函数了。第一次匿名函数执行时i
为0。
2、第二种改法
for (var i = 0; i < 6; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 0);
})(i);
}
类似第一种改法,只是此时i
保存在外层立即执行函数的变量对象里面
外层匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
内层匿名函数的Scope属性为
[AO,外层匿名函数Context.AO,globalContext.VO]
内层匿名函数会顺着作用域链查找i
,找到外层匿名函数Context.AO
,取到i
的值
3、第三种改法
for(let i=0; i < 6; i++) {
setTimeout(function(){
console.log(i);
},0);
}
使用let
声明不会将i
挂在全局变量对象下(跟var声明不是同一个)。此时开辟了不同的变量对象。
定时器参数函数Context={
AO:{
length:0
},
Scope:[AO,无名氏作用域变量对象,globalContext.VO]
}
于是定时器参数函数function
顺着Scope
属性找到对应的i
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!