你的第一感觉以下代码会输出什么呢?
for(let i = 0 , _s = setTimeout(() => console.log('for1', i));
setTimeout(() => console.log('for2', i)), i < 5;
setTimeout(() => console.log('for3', i))
) {
setTimeout(() => console.log('i', i))
i++
}
实际运行结果如下:
for1 0
for2 1
i 1
for3 2
for2 2
i 2
for3 3
for2 3
i 3
for3 4
for2 4
i 4
for3 5
for2 5
i 5
for3 5
for2 5
如果你预计for1、for2、for3都会输出5的话,恭喜你,Babel也是这么想的:
// 上述JavaScript代码的babel编译结果
"use strict";
var _loop = function _loop(_i, _s) {
setTimeout(function () {
return console.log('i', _i);
});
_i++;
i = _i;
};
for (var i = 0, _s = setTimeout(function () {
return console.log('for1', i);
}); setTimeout(function () {
return console.log('for2', i);
}), i < 5; setTimeout(function () {
return console.log('for3', i);
})) {
_loop(i, _s);
}
Babel版运行结果:
for1 5
for2 5
i 1
for3 5
for2 5
i 2
for3 5
for2 5
i 3
for3 5
for2 5
i 4
for3 5
for2 5
i 5
for3 5
for2 5
与直接运行结果并不一致。 特别是直接运行结果最后连续输出5个5令人费解。
先说闭包
function f() {
let i = 0
setTimeout(() => console.log(i))
i++
}
闭包在JavaScript中可以解释为函数
以及函数引用的上级作用域中的变量
,上述例子就形成了闭包。
这里的函数是指箭头函数() => console.log(i)
,引用的变量则是i
在本篇中通过使用闭包可以在for
循环全部结束后仍然保留对当前作用域下i
的引用,从而让我们窥视for
循环中作用域的分布。
从头开始
起源是我看见知乎上的一篇文章我用了两个月的时间才理解 let 简单回顾一下let的基础知识:
for(var i = 0; i < 5; i++) {
// 依次输出5 5 5 5 5
setTimeout(() => console.log(i))
}
for(let i = 0; i < 5; i++) {
// 依次输出0 1 2 3 4
setTimeout(() => console.log(i))
}
这里无论是var还是let我们都只声明了一个变量,let本身并不能解释为何两者输出不一致。 真正的原因是for循环中的黑魔法。
for循环中可以近似为:
for(let i = 0; i < 5; i++) {
// 每次循环重新声明
let _i = i
// 依次输出0 1 2 3 4
setTimeout(() => console.log(_i))
}
但是我们都知道在循环体中对i
重新赋值的话是可以影响到下一轮以及第二、三个表达式中的i
的,所以在每轮循环后还需要将_i
的值赋值给真实的i
for(let i = 0; i < 5; i++) {
// 每次循环重新声明
let _i = i
// 依次输出0 1 2 3 4
setTimeout(() => console.log(_i))
_i++
// 循环体结束后将块级作用域的变量重新赋值到真实的变量
i = _i
}
于是我猜测这里假设的真实变量就是我们在for
语句中第一个表达式声明的let i = 0
,而第二、三表达式中的i < 5; i++
则跟第一个表达式中的变量是同一个声明。
《ECMAScript 6 入门》
中写到:
现在我们回到一开始的代码,Babel的编译完全符合我们至今的猜想:
- 使用每次循环体中重新声明变量
- 循环体结束后对真实的变量重新赋值
for
括号三个表达式中的变量为同一个声明
// 上述JavaScript代码的babel编译结果
"use strict";
// 使用函数作用域重新声明了_i
var _loop = function _loop(_i, _s) {
setTimeout(function () {
return console.log('i', _i);
});
_i++;
// 循环体结束后对真实的i重新赋值
i = _i;
};
// 三个表达式中的i为同一个声明
for (var i = 0, _s = setTimeout(function () {
return console.log('for1', i);
}); setTimeout(function () {
return console.log('for2', i);
}), i < 5; setTimeout(function () {
return console.log('for3', i);
})) {
_loop(i, _s);
}
分析真实运行结果
for(let i = 0 , _s = setTimeout(() => console.log('for1', i));
setTimeout(() => console.log('for2', i)), i < 5;
setTimeout(() => console.log('for3', i))
) {
setTimeout(() => console.log('i', i))
i++
}
运行结果
for1 0
for2 1
i 1
for3 2
for2 2
i 2
for3 3
for2 3
i 3
for3 4
for2 4
i 4
for3 5
for2 5
i 5
for3 5
for2 5
- 使用每次循环体中重新声明变量
这点在此得到了确认。
- 循环体结束后对真实的变量重新赋值
for
括号三个表达式中的变量为同一个声明
而这两点在此得到了否定。 我们逐一分析。
第一个表达式中:
最初声明的i
从未改变,可以看出每次循环使用的全都是重新声明过的变量,并且没有将修改过的值重新赋值回第一个表达式中的i。
第二、三个表达式中:
和循环体中的变量一样,也是使用了重新声明的变量,可以认为每次重新声明后第二、三表达式和循环体使用同一个新变量。
这里问题的关键是重新声明变量的时机,如果按照我们印象中的理解:
第二表达式 -> 循环体 -> 第三表达式
这个顺序,然后在最开头重新声明变量的话,输出应为:
for1 0
for2 1
i 1
for3 1
for2 2
i 2
for3 2
for2 3
i 3
for3 3
for2 4
i 4
for3 4
for2 5
i 5
for3 5
for2 5
与实际结果不符。 想得到正确的结果,可以有多种理解,一种简单的理解是(不一定与V8实际行为一致): 第三表达式 -> 第二表达式 -> 循环体 只需要第一次循环时不执行第三表达式即可。
使用代码描述如下:
let i = 0
setTimeout(() => console.log('for1', i))
let _next = i, first = true
while(true) {
let _i = _next
if(first) {
first = false
} else {
// 第三表达式
setTimeout(() => console.log('for3', _i))
}
// 第二表达式
setTimeout(() => console.log('for2', _i))
if(!(_i < 5)) break
// 循环体
{
setTimeout(() => console.log('i', _i))
_i++
}
_next = _i
}
当i
为引用类型时也能保持一致:
for(let x = { i: 0, t: '' }, _s = setTimeout(() => console.log('for1', x));
setTimeout(() => console.log('for2', x)), x.i < 5;
setTimeout(() => console.log('for3', x)), x.i++) {
setTimeout(() => {
console.log('x', x)
})
x = { i: x.i, t: x.t + 'a' }
}
let x = { i: 0, t: '' }
setTimeout(() => console.log('for1', x))
let _next = x, first = true
while(true) {
let _x = _next
if(first) {
first = false
} else {
setTimeout(() => console.log('for3', _x))
_x.i++
}
setTimeout(() => console.log('for2', _x))
if(!(_x.i < 5)) break
{
setTimeout(() => console.log('x', _x))
_x = { i: _x.i, t: _x.t + 'a' }
}
_next = _x
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!