【JavaScript】区分var、let、const,以及浅析变量提升、暂时性死区

288 阅读4分钟

一、var 与 let 的区别

1. let 声明的范围是块作用域,而var声明的范围是函数作用域。

什么是“块作用域”?

块级作用域由最近的一对包含花括号 {} 界定。换句话说, if 块、 while 块、 function块,甚至连单独的块 {} 也是 let 声明变量的作用域。

if (true) {
  let a;
}
console.log(a); // ReferenceError: a没有定义

while (true) {
  let b;
}
console.log(b); // ReferenceError: b没有定义

function foo() {
  let c;
}
console.log(c); // ReferenceError: c没有定义

{
  let d;
}
console.log(d); // ReferenceError: d没有定义

2. let不允许同一个块作用域中出现冗余声明,这样会导致报错。而var,可以反复多次声明同一个变量。

var name;
var name; 

let age;
let age; // SyntaxError;标识符age已经声明过了

3. let声明的变量不会在作用域中被提升,var会。

var name = "Jake";
// 等价于:
name = 'Jake';
var name;
console.log(name); // undefined
var name = 'Matt'; 

console.log(age); // ReferenceError:age没有定义
let age = 26;

4. 使用let在全局作用域中声明的变量不会成为 window 对象的属性,var声明的变量则会。

var name = 'Matt';
console.log(window.name); // 'Matt'

let age = 26;
console.log(window.age); // undefined

5. 使用var会造成 for 循环定义的迭代变量渗透到循环体外部,而使用let则解决了该问题。

for (var i = 0; i < 5; ++i) { /** 循环逻辑 */ }
console.log(i); // 5

for (let i = 0; i < 5; ++i) { /** 循环逻辑 */ }
console.log(i); // ReferenceError: i没有定义

二、let 与 const 的区别

const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const声明的变量会导致运行时错误。

const age = 26;
age = 36; // TypeError: 给常量赋值

const声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制。

const person = {};
person.name = 'Matt'; // ok

三、对一些概念的浅析

1. 什么是“提升”?

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”(hoisting)。

2. “变量提升”的本质?

1)从“编译器”的角度

首先,对编译器有一个初步的概念:

引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。 因此,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

引用《你不知道的 JavaScript (上)》中第 4 章-提升 中的例子,如下:

var a = 2;

当我们看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明:var a; 和 a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段

a = 2; 
var a; 
console.log( a ); // 2
// 等价于:
var a; 
a = 2; 
console.log( a ); // 2
console.log( a ); // undefined
var a = 2;
// 等价于:
var a; 
console.log( a ); // undefined
a = 2;

注: 只有声明本身会被“提升”,而赋值或其他运行逻辑会留在原地。

2)从“上下文”的角度

首先,对上下文有一个初步的概念:

执行上下文(简称“上下文”),变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。

执行上下文分两个阶段创建:

  • 创建阶段:在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
  • 执行阶段:创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。

沿用上面的例子进行说明,如下:

console.log( a ); // ①
var a = 2;
console.log( a ); // ②

在执行上下文创建阶段,①和②处的变量 a 被初始化设置为undefined。在执行上下文执行阶段,当执行到①处时,a 没有被赋值,所以此处的 a 的值仍为undefined;当执行到②处时,a 在上一行代码中进行了赋值操作,所以此处的 a 值为 2。

综上,根本不存在任何的“提升”,变量一直“停留”在原地。

  • 提升:只是变量的创建过程(在执行上下文创建阶段完成)和真实赋值过程(在执行上下文执行阶段完成)的不同步带来的一种错觉。
  • 本质:执行上下文在不同阶段完成不同的工作。

3. 什么是“暂时性死区”?

在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError 。

4. “暂时性死区”的本质?

在执行上下文创建阶段,var声明的变量被设置为undefinedletconst声明的变量保持未初始化。所以,可以在声明之前访问var定义的变量,但如果在声明之前访问letconst定义的变量就会提示引用错误。

四、参考资料