我们知道最新的 js 具有块级作用域,那么块级作用域对闭包有何影响?来看下面这个例子:

案例题目


      function test(isTrue) {
        let a = 1;
        setTimeout(() => {
          a = 999;
        });
        if (isTrue) {
          let a = 2;
          return function () {
            return a;
          };
        } else {
          let a = 3;
          return function () {
            return a;
          };
        }
      }

      const getValue = test(false);
      setTimeout(() => {
        console.log("getValue", getValue());
      });

请问 getValue 输出结果是什么,答案是:3.
那我如果把所有 let 改成 var 呢?答案是:999

let时候为什么是3

原因在于:

  1. let + 新浏览器环境,可以共同让“块级作用域”概念生效,块级作用域生效后。当你下层某函数从“词法”层面引用上层变量时,他必然依次往上寻找---先找到挨着最近的那个“块作用域”---即 let a = 3
  2. 当找到词法作用域---即这个 let a = 3 所属块时,则要看调用栈中这个块当时的状态值。即你 getValue 函数被创建那一刻的时候,当时的块作用域内的 a 是几。我们这个例子的执行栈比较简单,很显然是 3.

var的时候为什么是999

原因在于:

1.即使浏览器支持块级作用域,也要配合 let/const 来使用,如果你用 var 声明,则该变量依然是挂到最近的那个外层的函数作用域内,即会挂在 test 函数内。因此函数执行时从上往下执行最终只是在 test 函数这个执行栈内出现了一个 a 变量,且test 函数执行后其状态值是 3.然后等到 setTimeout 执行完,其值变为 999.

  1. 当你外部通过getValue 函数来引用该变量 a 的时候,js 会基于词法作用域先去看 let a=3 那个位置的块作用域的执行栈内变量,然而发现这里没有 a,于是继续基于“词法作用域”向上找到 test 函数,于是从 test 函数的执行栈内变量中找到了 a,而 a 此时等于 999

for循环的例子

关于 for 循环配合 let,在ES6 教程中可能大家也看到过这个类似的例子

      var lists = document.querySelectorAll(".mylist li");
      for (let i = 0, len = lists.length; i < len; i++) {
        lists[i].addEventListener("click", function () {
          alert(i);
        });
      }
  1. (let i = 0, len = lists.length; i < len; i++) 这块区域的 i < len; i++ 是会形成一个块作用域,另外下面花括号主体逻辑又会形成一个子的块作用域。 于是这里有3 个词法作用域:最外层函数-->i++作用域--->内层花括号作用域。
  2. 对于每个词法作用域,执行栈执行时也会在每个词法作用域执行那一刻,及时将状态保留下来。对于此 case,就是 for 循环每次循环,假设循环 10 次,那么每次都会给当时那一次形成一个执行栈。而由于每次执行栈执行的时候必然要经历“i++”和“花括号”两个词法作用域,所以执行栈他也会用相同的两层作用域概念来保存好当时那一刻的状态。
  3. 当你点击页面上的按钮(例如点击i 等于 2 的那个按钮),触发click 回调函数时,该会掉函数从词法上引用上层 i++ 那个块级作用域,于是js 要去i++那个块作用域内去找你的 i。
  4. js 寻找 i 的值的时候,需要基于你 click 回调函数当时的执行栈去找,即去找创建你此时点击的那个 click 回调函数的执行栈的上层,于是找到当时那一刻 i 等于 2时候的 i++块作用域执行栈里的状态,当时里面确实有个 i 且其值是 2.

假如换成var

1.在最新浏览器中词法作用域依然在。且运行时的调用栈依然会基于词法形成的块作用域进行状态保留
2.然而 var 声明的 i 会突破块作用域,成为外层函数作用域内的一个公共 i。于是 js 引擎在去调用栈里寻找状态时,发现 i++的块作用域调用栈状态内不存在 i 变量,于是基于往上层词法作用域规定的函数作用域内去找

  1. for 循环无论执行多少次,他只是形成了多次 i++块作用域的执行栈,而外层那个函数执行栈就“只执行了一次”。于是外层函数内的 i 变量其实也只存在于这“外层函数这个唯一的执行栈”内,而这个 i会随着内层 for 循环 持续递增,于是当你 click 的时候,他打印的是 “10”

总结

  1. 块级作用域的出现,让 js 执行栈内会保留“块级”的执行栈。
  2. 块级作用域的出现,导致闭包向上寻找状态引用时,碰到块作用域会先看“块级”的执行栈是否有要寻找的状态变量。
  3. 基于上述 2 点,for 循环使用 let 的时候,恰好可以让每次 for 循环都在调用栈内持续保存当次循环时候的“块级”调用栈状态,从而让你的 for 循环内嵌套的函数可以自动引用了当时变量 i 的“块级”执行时状态。

sheldon
947 声望1.6k 粉丝

echo sheldoncui