var (ES5) 和 let (ES6) 区别

变量提升(声)

在 JS 代码的编译阶段:把当前作用域中所有带 var / function 关键字的进行提前的声明和定义 【变量提升机制】

  • var的只是提前声明(declare)var a;,如果只声明没有赋值,默认值是undefined

    1
    2
    3
    4
    5
    6
    7
    console.log(a);
    var a = 13;

    // 等价于
    var a; // 只声明没有赋值,默认为undefined
    console.log(a);
    a = 13;
    • 输出:undefined
  • function不仅声明,而且还定义了(defined),准确来说就是让变量和某个值进行关联。

    1
    2
    3
    4
    greet(); // 输出: "Hello"
    function greet() {
    console.log("Hello");
    }

let 和 var 的区别

letconst 不存在变量提升机制

创建变量的六种方式中:

  • var / function 有变量提升
  • let / const / class / import 没有这个机制

var 允许重复声明,而 let 不允许重复声明

所谓重复:不管之前通过什么方法,只要当前栈内存中存在了这个变量,我们再次声明这个变量就是重复声明。

相同的作用域(或执行上下文中

  • 使用 var/function 关键词声明变量,允许重复声明。声明第一次之后,之后再遇到就不会再重复声明了。
    • 要注意 letvar 在同一作用域中的冲突
  • 使用 let/const 关键词声明变量,不允许重复声明。浏览器会校验当前作用域中是否已经存在这个变量了,如果已经存在了,则再次基于let等重新声明就会报错
1
2
3
4
var a = 12
console.log(a)
var a = 13 // 没有问题,var 允许重复声明
console.log(a)
1
2
3
4
console.log(a)  // => 这行代码就已经不会执行了
var a = 12
let a = 13 // => SyntaxError: Identifier 'a' has already been declared
console.log(a)
1
2
3
4
console.log(a)
let a = 13
var a = 12 // => SyntaxError: Identifier 'a' has already been declared
console.log(a)

暂时性死区问题

let 能解决 typeof 检测时出现的暂时性死区问题

letvar 更严谨

1
2
console.log(a)
// => ReferenceError: a is not defined

typeof a 本应报错,未报错

1
2
console.log(typeof a)
// => 'undefined' 这是浏览器的bug,本应报错,因为没有a(暂时性死区)

使用 let

返回:不能在a被定义之前使用它,解决暂时性死区问题。

1
2
3
console.log(typeof a)
// => ReferenceError: Cannot access 'a' before initialization
let a

暂时性死区 (TDZ) 是指在 letconst 变量被正式声明之前,这些变量在其作用域内是不可访问的。

即使这些变量在代码中已声明,直到声明位置代码被执行前,它们都处于一个死区状态。在此状态下,如果尝试访问这些变量,将会导致 ReferenceError

给 window 设置对应的属性

  • 隐式声明:相当于给全局对象window设置了一个属性 a

    1
    2
    3
    a = 10 // 相当于给全局对象window设置了一个属性a
    console.log(a)
    console.log(window.a) // => window.a = 10
  • var 创建:在全局作用域下声明了一个变量 b(全局变量)。

    • 在这种情况下,也相当于给全局对象 window 增加了一个对应的属性 b。【只有全局作用域具备这个特点】
    1
    2
    3
    var b = 14
    console.log(b)
    console.log(window.b) // => window.b = 14
  • let 创建:仅仅在全局作用域下声明了一个变量 c(全局变量)。

    • 在这种情况下,并未给全局对象 window 增加对应的属性 c
    1
    2
    3
    let c = 15
    console.log(c) // => 15
    console.log(window.c) // => undefined

经典问题

变量提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = 99;            // 全局变量a
f(); // f是函数,虽然定义在调用的后面,但是函数声明会提升到作用域的顶部。
console.log(a); // a => 99, 此时是全局变量的a
function f() {
console.log(a); // 当前的a变量是下面变量a声明提升后,默认值undefined
var a = 10;
console.log(a); // a => 10
}

/**
输出结果:
undefined
10
99
*/

若是对上述代码进行一些变体

1
2
3
4
5
6
7
8
9
10
var a = 99;            // 全局变量a
f();
console.log(a); // a => 99
function f() {
console.log(a); // 这里会引发 ReferenceError。因为在 `let a` 的初始化之前,`a` 处于 TDZ。
let a = 10;
console.log(a);
}

// 程序编译错误,不会有输出

在函数 f 内部,let a = 10 声明会创建一个局部变量 a,它遮蔽了全局变量 a

在函数体内,let a = 10 的声明会被提升到函数作用域的顶部,但尚未初始化。在初始化之前对 a 的访问会引发 ReferenceError,因为 let 变量在 TDZ 内是不可访问的。全局的 a 不会被引用,因为局部 a 优先级更高。

ES6可以用let定义块级作用域变量

块级作用域

在ES6之前,我们都是用 var 来声明变量,而且 JS 只有函数作用域全局作用域没有块级作用域,所以{}限定不了var声明变量的访问范围。

  • var 的作用域是函数作用域全局作用域

    • 块级作用域的 {}var 没有影响。在其中声明的 var 变量会被提升到整个函数或全局范围内。
    • 在函数内部声明的 var 变量会被提升到函数的顶部,并且在整个函数体内都可以访问。
    • 在全局范围内声明的 var 变量会成为全局对象的属性,并且在任何地方都可以访问。
    1
    2
    3
    4
    { 
    var i = 9;
    }
    console.log(i); // 9
  • let:可以声明块级作用域的变量。【ES6新增

    1
    2
    3
    4
    { 
    let i = 9;
    }
    console.log(i); // => ReferenceError: i is not defined

for 循环问题

JS中的 for 循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。

  • 使用 var

    1
    2
    3
    4
    5
    6
    7
    for (var i = 0; i <10; i++) {  
    setTimeout(function() { // 同步注册回调函数到 异步的 宏任务队列。
    console.log(i); // 执行此代码时,同步代码for循环已经执行完成
    }, 0);
    }
    // 输出结果:
    10 10 10 10 10 10 10 10 10 10
  • 使用 let

    • i 虽然在全局作用域声明,但是在 for 循环体局部作用域中使用的时候,变量会被固定,不受外界干扰。
    1
    2
    3
    4
    5
    6
    7
    for (let i = 0; i < 10; i++) { 
    setTimeout(function() {
    console.log(i); // i 是循环体内局部作用域,不受外界影响。
    }, 0);
    }
    // 输出结果:
    0 1 2 3 4 5 6 7 8 9