细谈变量声明

546 阅读4分钟

先通过一个经典例子来引入,如下:

经典例子:

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

​ 上面的代码中,变量 i 是 var 声明的,在全局范围内都有效,所以全局只有一个变量 i。每一次循环,变量 i 的值都会发生改变,而循环内,被赋值给数组 a 的函数内部的 console.log(i) 中的 i 是指向全局的 i。

​ 也就是说,所有数组 a 的成员中的 i 指向的都是同一个 i,导致函数运执行时,它发先自己没有这个变量 i ,于是向它的作用域链中查找变量i,而这个时候,for 循环已经结束 i 的值已经变为10,所以最终打印出来的都是10.

解决方案之一:

  • for 循环的计数器就很适合使用 let 命令,计数器 i 用 let 声明后只在 for 循环体内有效,在循环外引用就会报错。
var a = [];
// () 内有一个隐藏作用域
for(let i = 0; i < 10; i++) {
    // 相当于这里隐藏了一行:let i = 隐藏作用域的 i。
    a[i] = function () {
        console.log(i);
    };
}
a[6](); // 6

​ 上面的代码中,变量 i 是 let 声明的,当前的 i 只在本轮循环有效。所以每一次循环的 i 其实都是一个新的变量,于是 a[6]() 会输出6,JS 引擎内部会记住上一轮循环的值,初始化本轮的变量 i 时,就在上一轮循环的基础上进行计算。(也就是在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次)

  • 补充:for 循环还有一个特别之处,就是设置循环遍历的那部分是一个父作用域,而循环体内是一个单独的子作用域。
  • 当 let / const 与 for 一起使用时,就会有一个每次迭代绑定(per-iteration bindings)的概念。

通过下面的解释,对变量声明将会有一个更好的理解

细谈变量声明

js变量声明 可以分为 **创建——create、初始化——initialize、赋值——assign 这几个过程。

var 声明

function fn() {
    var x = 1;
    var y = 2;
}
fn();

执行 fn 时,会有以下过程(不完全):

  1. 进入 fn 中,为 fn 创建一个环境。
  2. 找到 fn 中所有用 var 声明的变量,在这个环境中 [创建] 这些变量(即 x 和 y)。
  3. 将这些变量 [初始化] 为 underfined。
  4. 开始执行代码。
  5. x = 1 将 x 变量 [赋值] 为 1。
  6. y = 2 将 y 变量 [赋值] 为 2。
  • 也就是说明 var 声明会在代码执行前就将 [ 创建变量,并将其初始化为 undefined ]。
  • 这可以解释在变量 [赋值] 之前 console.log(x) 会得到 undefined。

function 声明

fn2()

function fn2() {
    console.log(2);
}

过程:

  1. 找到所有用 function 声明的变量,在执行环境中 [创建] 这些变量。
  2. 将这些变量 [初始化][赋值] 为 function() { console.log(2) }。
  3. 开始执行 fn2()。
  • 也就是说 function 声明会在代码执行前就 [创建、初始化并赋值]

let 声明

{
    let x = 1;
    x = 2;
}

{} 里面的过程:

  1. 找到所有用 let 声明的变量,在环境中 [创建] 这些变量。
  2. 开始执行代码(注意变量还没有从初始化)
  3. 执行 x = 1,将 x [初始化] 为 1 (这并不是一次赋值,若代码为 let x ,就将 x 初始化为 undefined)
  4. 执行 x = 2,对 x 进行 [赋值]
  • 这也就解释了在 let x 之前使用 x 会报错的原因:
    • console.log(x) 中的 x 指的是下面的 x,而不是全局的 x。
    • 执行 log 时 x 还没 [初始化],所以不能使用(也就是所谓的暂时死区,就是不能在初始化之前,使用变量 )。
  • let 的 [创建] 过程被提升了,但是初始化没有提升。

补充:let x = x 报错之后,再次 let x 依然会报错?

这个问题说明:如果 let x 的初始化过程失败了,那么

  1. x 变量就永远处于 [创建] 状态。
  2. 无法再对 x 进行初始化(初始化只有一次机会,失败后就没有了)。
  3. 由于 x 无法被初始化,所以 x 永远在暂时死区。

const 声明

const 和 let 只有一个区别,那就是 const 只有 [创建]和[初始化],没有 [赋值] 过程。

总结:

​ 在执行上下文创建阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为 undefined (在 var 的情况下)或保持未初始化 uninitialized(在 letconst 的情况下)。

​ 这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined),但是如果在声明前访问 letconst 定义的变量就会提示引用错误的原因(也就是我们所常说的暂时性死区(TDZ,temporal dead zone))。

注:JavaScript 执行上下文详解 ---> JavaScript 执行上下文(ES3版 与 ES5版)


参考资料:

我用了两个月的时间才理解 let

阮一峰——ES6标准入门