添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement . We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account for ( var i = 0 ; i < 10 ; i ++ ) { a [ i ] = function ( ) { console . log ( i ) ; a [ 6 ] ( ) ; // 10

let无陷阱

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
a[6](); // 6

当var声明变量时,不存在块级作用域,a数组内每个函数中的i都是向上查询作用域a,所以结果都是10

var a = [];
for (var i = 0; i < 10; i++) {
  // 作用域a
  a[i] = function () {
   // 作用域b
    console.log(i);
a[6](); // 10

当let声明变量时,由于存在块级作用域,每次循环i都是一个新的变量,都会创建一个新的词法作用域,所以最后是6,我们可以通过babel把let代码编译成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]();
  • var声明的变量会被提升到函数作用域;并且会被覆盖;在执行数组中函数时,函数本级执行上下文没有i这个变量,所以会向上级执行上下文中寻找;上级执行上下文中i已经被for循环修改为10的,所以执行函数中的function不会输出10;
  • let声明的变量在块级作用域生效,每个块级作用域的变量相互隔离,且声明的变量不会被提升;所以每次执行数组中的函数时,向上级执行上下文寻找i这个变量时寻找到的是定义时的值;
  • console.log(i) //以上for循环里的 i 我们理想是只在for循环里使用生效,**但实际上 i 是声明在外部作用域之中的。** // 这就意味着此处的i ,console.log(i);已经是10 了,且作用域内之后任何时候访问都是10,所以执行以下表达式,结果是10 a[6](); // 10

    let能解决这个问题是因为,es6中使用let,和const实现了块级作用域。

    var a = [];
    for (let i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
    console.log(i)
    // 此时的 i 是声明在for循环内的,且{ }中是独立的块级作用域, 它将 i 重新声明在了for循环的**每一次**迭代中。
    // 此处console.log(i) => 未定义。
    //所以此处执行a[6]函数,打印的i是当时声明在当时层的块级作用域中的 let i =6。
    a[6](); // 6
    

    使用let声明的变量在块级作用域内能强制执行更新变量,下面的两个例子对比:

    var a = [];
    for (var i = 0; i < 10; i++) {
          a[i] = function () {console.log(i);};
    a[0]();                // 10
    a[1]();                // 10
    a[6]();                // 10
    /********************/
    var a = [];
    for (let i = 0; i < 10; i++) {
          a[i] = function () {console.log(i);};
    a[0]();                // 0
    a[1]();                // 1
    a[6]();                // 6
    ```js

    for循环头部的let i不仅为循环本身声明了一个i,而是为循环的每一次迭代都更新声明了一个新的i。这意味着loop迭代内部创建的闭包封闭是每次迭代中的变量,也正如我们的期望一样。

    var在外层作用域中只有一个i,这个i被封闭进去,而不是每次迭代都会有一个新的i

    可以看到当我们在 for 循环中用 var 声明变量的时候,内部函数得到的值是循环结束后的值,而不是本次循环的值。
    这是因为用 var 声明的变量没有块级作用域,所以此时 i 属于 for 循环所在作用域中的变量,所以当我们执行函数的时候,每个函数都指向了同一个变量,而此时的变量 i 因为已经结束了循环,变成了 10,所以执行函数得到的结果是 10

    let 做了什么

    for 循环头部使用 let 声明变量 i 不止为 for 循环本身声明了一个 i,而是为循环的每一次迭代都重新声明了一个新的 i。所以循环内部的函数闭包封闭的是每次迭代中的变量,就像我们期望的那样。

    循环陷阱指的是用var声明循环变量:
    for(var i = 0; i < 10; i++) { setTimeout(()=> { console.log(i); }, 0); }

    这里会输出什么呢?
    10次定时器输出的10;显然不是我们想要的结果,我们想这个定时器从0到9记录输出;

    为什么会这样呢?
    这里用var定义循环变量i,i会被提升为全局变量,而setTimeout回调函数形成了个闭包环境引用了i,setTimeout回调执行需要等主线程的循环任务结束才会开始执行;
    最后一个循环得到了9,执行i++与判断条件相比不符合,结束循环,此时i=10了,意味着全局变量i等于10,所以setTimeout输出了10个10;

    为什么用let能解决呢?
    for(let i = 0; i < 10; i++) { setTimeout(()=> { console.log(i); }, 0); }

  • 首先let定义循环变量,每次循环都会产生一个块级作用域
  • Javascript引擎会记住上一次for循环的值来初始化本次循环变量,
  • 每次创建的闭包定时器都会引用对应循环的变量i,所以最后输出的结果是从0到9的输出值;
  • 总结一下:
    利用let声明的循环变量在每次循环都会创建一个独立的块级作用域,在循环内部创建的闭包环境每次应用的都是对应循环变量的值,不会被变量提升指全局,每次闭包环境中引用的就是对应块级作用域中的变量,所以说let能够解决循环陷阱

    在执行上下文中,扫描变量阶段
    整个函数执行上下文中var 申明的变量,都会被放到了变量环境里,
    而函数执行上下文中最外层的块内的let 申明的变量,被放到了词法环境栈里,词法环境只会存放当前块内的let申明的变量以及上层块内申明的变量。随着代码的执行,进入内部块后,会将内部块内let申明的变量推入词法环境栈顶,以此类推。

    执行代码的代码的时候,当遇到变量,会先在当前词法环境内自顶向下寻找变量,如果没找到,再去变量环境寻找,当跳出当前块后,词法环境栈顶的所有变量弹出。这也就导致了let和const只会在所属词法环境对应的块级作用域内才有效。

    这也就是let和const在整个代码编译和执行过程中的支持块级作用域的原理。

    在ES5标准中,for循环都是通过var来声明变量,而在js中,var是没有独立的作用域的,变量的访问会从当前作用域开始,顺着作用域链向上查找,因此,使用var声明在循环中的作用域实际上是共用的,var所定义的变量i也是共用的,因此当在循环中使用i绑定事件,其在执行时,for循环早就执行完了,变量i也多次被赋值。
    使用let可以解决循环陷阱,是因为let将变量绑定到当前所在的作用域中,形成一个块级作用域,每一次进入for循环都会有一个新的块级作用域,在事件响应函数执行时要访问变量i,会从它所在的块级作用域查找,此时的i相当于是被重新定义的。
    循环陷阱问题通常可以使用闭包的方式去模拟块级作用域解决,let使得这一问题的解决方案变得更简单。

    如果使用var声明变量进行for循环会有问题,是因为js没有块级作用域,只有函数作用域。
    所以变量i是公用的,导致变量被多次赋值,输出的结果并不是我们所期望的。
    let能解决这个问题是因为,es6中使用let和const实现了块级作用域。
    每次循环都会创建一个独立的块级作用域,在循环内部创建的闭包环境每次应用的都是对应循环变量的值,不会被变量提升指全局,每次闭包环境中引用的就是对应块级作用域中的变量,所以说let能够解决循环陷阱。

    大家都在说let是块作用域,每次循环都重新定义,我有一个疑问,for循环头中的let只在第一次循环开始前定义,后续每次都不会重新定义,这个“每次循环都重新定义”是如何得出的呢?

    update:
    如果循环头中的let每次是重新定义,那么将let替换为const应该是不会报错的,但似乎这个代码会抛异常

    var a = []
    for (const i=0; i<10; i++)
    a[i] = function(){ console.log(i) }