【JS】怎么理解for循环中用let声明的迭代变量每次是新的变量?

背景

最近在总结基础知识,然后看了阮一峰老师的es6教程,其中谈及let以及块级作用域的时候,举了一个经典的例子,代码如下:

var a = [];

for (var i = 0; i < 10; i++) {

// 作用域a

a[i] = function () {

// 作用域b

console.log(i);

};

}

a[6](); // 10

因为es5不存在块级作用域,所以迭代变量i泄露了,然后对于a数组内每一个函数内的i都是向上查询作用域a的,所以结果是10。这个没问题。

下面的例子是用let来声明迭代变量的

var a = [];

for (let i = 0; i < 10; i++) {

a[i] = function () {

console.log(i);

};

}

a[6](); // 6

老师是这样解释的

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。

疑问
我想不通每一次循环的i其实都是一个新的变量这个过程是怎么样的,如果我理解为每次迭代都是新的一个块级作用域,那么迭代变量的迭代(i++)是如何传递给下一个块级作用域呢?

自知之明
虽然我知道结果,也知道这样的问题是转牛角尖,就是好奇问问。希望各路英雄指点迷津。


2016.11.20 21:00
刚刚想到了一个办法,尝试看看es6经过babel如何转化成es5的。所以大家可以看看转码过后是这样的:

"use strict";

var a = [];

var _loop = function _loop(i) {

a[i] = function () {

console.log(i);

};

};

for (var i = 0; i < 10; i++) {

_loop(i);

}

a[6](); // 6

就像用var声明迭代变量的时候,用iife来充当块级作用域一样。但是转到let上,我似乎理解不到迭代变量是如何传递的

// 在执行for循环的时候,我能这么理解吗?

{ let i = 0;

{

a[i] = function () {

console.log(i);

};

i++;

}

}


2016.11.21 5:57
结合网友 @边城 & @eyesofkids 的回答,我的理解是:

var a = [];

{ let k = 0;

for (;k < 10;) {

let i = k; // 这一步是内部进行转换的,可以看看下面我对 @边城 的神奇代码的理解

a[i] = function () {

console.log(i);

};

console.log("in block", i);

console.log("in for expression", k);

k++;

}

}

a[6](); // 6

这样的结构,可以用 @边城 的神奇代码来检测:

for (let i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {

let i; //这里没有报错,就意味着这里跟作用域a不同,换做k可能更好理解

console.log("in for block", i);

}

// 运行结果如下

in for block undefined

in for expression 0

in for block undefined

in for expression 1

in for block undefined

in for expression 2

for (let i = 0; i < 3; console.log("in for expression", i), i++) {

let k;

console.log("in for block", k);

}

// 运行结果如下

in for block undefined

in for expression 0

in for block undefined

in for expression 1

in for block undefined

in for expression 2

回答

这是在for语句中的var与let的差异:

for (let x...)的循环在每次迭代时都为x创建新的绑定

以下用代码直接看会比较容易的理解。这个改进主要是为了要解决在for语句中的闭包结构的问题。

原来的使用var的代码,与去糖(desugar)后来看它在执行时是这样的模拟代码:

//原来代码

for (var i = 0; i < 10; i++) { setTimeout(()=>console.log("i:",i), 1000); }

// 不需要加区块符,因为区块也不会影响

var i;

i = 0;

if (i < 10)

setTimeout(()=>console.log("i:",i), 1000);

i++;

if (i < 10)

setTimeout(()=>console.log("i:",i), 1000);

i++;

//...

而使用了let后,会有块级作用域的影响,原来的代码与执行时的去糖模拟代码如下:

// 原来代码

for (let i = 0; i < 10; i++) { setTimeout(()=>console.log("i:",i), 1000); }

// 用区块符区分每次循环的语句

// 每次for语句开始,i指定为一个全域刻度__status,这只是方便说明而已

// __status会记录for语句i最后的值

{ let i;

i = 0;

__status = {i};

}

{ let {i} = __status;

if (i < 10)

setTimeout(()=>console.log("i:",i), 1000);

__status = {i};

}

{ let {i} = __status;

i++;

if (i < 10)

setTimeout(()=>console.log("i:",i), 1000);

__status = {i};

}

//...


为何可以这样模拟?因为在ES标准中,有一段是关于CreatePerIterationEnvironment,也就是for语句每次循环所要建立环境的步骤,里面有提及有关词法环境的相关步骤(LexicalEnvironment),这与使用let时会有关。所以,如果你使用了let而不是var,let的变量除了作用域是在for区块中,而且会为每次循环执行建立新的词法环境(LexicalEnvironment),拷贝所有的变量名称与值到下个循环执行。以最简单的方式改写原先的问题中的代码,相当于下面这样写:

var a = [];

{ let k;

for (k = 0; k < 10; k++) {

let i = k; //注意这里,每次循环都会创建一个新的i变量

a[i] = function () {

console.log(i);

};

}

}

a[6](); // 6

最近在复习es6,发现阮一峰老师在教程上更新了,也说明了这个问题。如下。

你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

其中引擎内部会记住上一轮循环的值,可以看看 @陆沛 同学的答案,同时感谢各位的解答。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

按你的理解,还有点不对,虽然跨块,但你变的仍然是 i 本身,我认为应该是这样(仍然按你的思路)

{

let i = 0;

{

let _i = i;

a[_i] = function() {

console.log(_i);

};

}

i++;

}

再来给你看个神奇的示例,可以证明你的理解基本正确(虽然给的代码错了,但是我理解了你的意思)

for (var i = 0; i < 3; console.log("in for expression", i), i++) {

let i;

console.log("in for block", i);

}

【JS】怎么理解for循环中用let声明的迭代变量每次是新的变量?

首先。。你最后写的那个5个{}大括号的东西我看不懂。。

然后let在for里声明,在for之外无效,所以?所以其实就是个闭包函数
简单来说就像下面这样(我把遍历过程省去了)

//当遍历到i=6时

function fnFor() {

let i = 6;

return function() {

console.log(i);

}

}

//a[i]就相当于这里的:

return function() {

console.log(i);

}

//上面这个匿名函数又等同于fnFor()

//所以a[6]就相当于:

return function() {

console.log(6);

}

//最后a[6]()就相当于fnFor()();

【JS】怎么理解for循环中用let声明的迭代变量每次是新的变量?
【JS】怎么理解for循环中用let声明的迭代变量每次是新的变量?

说明:

1. var 的情况, 回调函数执行的时候是从外层的闭包中获取i值

2. let 的情况,回调函数从block环境中获取i值, 实测可以看出,每次block环境的 i都发生了变化。

判定每次循环进入块,的确都创建了一个新的block环境

3. 楼主 ES5 模拟ES6的方法,让人蛮有收获:如果想避免callback受到这样的闭包影响,

可以创建一个函数,将外侧闭包的变量作为参数调用。

这样在该函数执行的时候,就会将外侧闭包中变量的"及时"值传递到函数内侧,回调函数将从新创建的函数闭包中获取变量值。

核心就是通过执行新创建的函数, 作为参数传入"及时"值.

首先,ES6是一个标准,标准要求实现这样的效果。
然后,具体的实现,是引擎决定的。
倒推一下实现的机制,当let出现在for循环中时,引擎将for循环分成了两个作用域:

  • 圆括号中的作用域
  • 方括号中的作用域

例如对于下面的代码极其作用域

for (let i = 0; i < 5; i++ /*这里是圆括号中的作用域*/) {

/* 这里是方括号中的作用域 */

setTimeout(() => {

console.log(i);

}, 0)

}

这一点,可以在楼主的写的神奇代码中得到验证。
在对这段代码进行处理时,为了实现let的效果,其实是做了下面的处理

for (let j = 0; j < 5; j++) {

// 声明变量并且重新绑定,每次循环都绑定一次

let i = j;

setTimeout(() => {

console.log(i);

}, 0)

}

由于每次循环都绑定一次,所以i在每次循环时值都会更新。最后在打印时,按照作用域链往上寻找,找到括号作用域中的i,这个i的值由于每次都重新绑定过,所以会得到保留,打印出0,1,2,3,4

这就是闭包啊 所以有了let关键字无需应用闭包

以上是 【JS】怎么理解for循环中用let声明的迭代变量每次是新的变量? 的全部内容, 来源链接: utcz.com/a/82461.html

回到顶部