# 变量声明提升

文中内容摘自“参考”部分

# 什么是提升呢?

在编译阶段,即在代码执行前的几微秒内,将对其进行扫描以查找函数和变量声明。所有这些函数和变量声明都被添加到称为 Lexical Environment(词法环境) 的 JavaScript 数据结构内部的内存中。这样,即使在源代码中实际声明它们之前也可以使用它们。

# 什么是词法环境?

词法环境是保存标识符变量映射的数据结构。 (这里的标识符是指变量/函数的名称,而变量是对实际对象[包括函数对象]或原始值的引用)。

词法环境在概念上是这样的:

LexicalEnvironment = {
  Identifier:  <value>,
  Identifier:  <function object>
}

简而言之,词法环境是程序执行过程中变量和函数存在的地方。

# 声明提升

# 1️⃣ function 函数声明提升

helloWorld();  // 打印 “ Hello World!” 到控制台
function helloWorld(){
  console.log('Hello World!');
}

⭐️ 重点: 函数声明是在编译阶段添加到内存的,因此可以在实际函数声明之前在代码中对其进行访问。

因此,以上代码的词法环境如下所示:

lexicalEnvironment = {
  helloWorld: < func >
}

因此,当 JavaScript 引擎遇到对 helloWorld() 的调用时,它将查看词法环境,找到该函数并能够执行它。

# 2️⃣ function 函数表达式提升

⭐️ 重点: JavaScript 仅提升函数声明,不提升函数表达式。

例如:该代码将无法正常工作。

helloWorld();  // TypeError: helloWorld is not a function
var helloWorld = function(){
  console.log('Hello World!');
}

由于 JavaScript 仅提升声明,而不初始化(赋值),因此 helloWorld 将被视为变量,而不是函数。由于 helloWorldvar 变量,因此引擎在提升过程中将分配未定义的值。

因此,此代码将起作用。

var helloWorld = function(){
  console.log('Hello World!');  // prints 'Hello World!'
}
helloWorld();

# 3️⃣ var 变量提升

来看一些示例,以了解 var 变量的提升。

console.log(a); // outputs 'undefined'
var a = 3;

我们预期为3,但未定义。为什么?

⭐️重点: 请记住,JavaScript 仅是提升声明,而不是初始化。也就是说,在编译期间,JavaScript 仅将函数和变量声明存储在内存中,而不存储它们的分配(值)。

但为什么是 undefined 呢?

⭐️重点: 当 JavaScript 引擎在编译阶段找到 var 变量声明时,它将将该变量添加到词法环境中,并使用 undefined 对其进行初始化,然后在执行过程中,当到达代码中实际分配的行时,它将进行分配该值到变量。

因此,以上代码的初始词法环境如下所示:

lexicalEnvironment = {
  a: undefined
}

这就是为什么我们得到未定义而不是3的原因。当引擎到达执行实际分配的行(在执行过程中)时,它将在其词法环境中更新变量的值。因此,赋值后的词法环境将如下所示:

lexicalEnvironment = {
  a: 3
}

# 4️⃣ ES6 中 letconst 的提升

先来看一些示例:

console.log(a);
let a = 3;

输出:

ReferenceError: a is not defined

报错了,那么是因为 letconst 变量没有被提升吗?

答案要复杂得多。所有声明(functionvarletconstclass)都会在 JavaScript 中提升,而 var 声明是使用 undefined 初始化的,但是 letconst 声明仍未初始化 (uninitialized)。

⭐️重点: 只有在运行时由 JavaScript 引擎评估其词法绑定(赋值)时,它们才会被初始化。这意味着您无法在引擎在源代码中声明的位置评估其值之前访问该变量。这就是我们所说的“临时死区”(Temporal Dead Zone,简写 TDZ),即从变量创建到其无法访问的初始化之间的时间跨度。

⭐️重点: 如果 JavaScript 引擎在声明它们的行上仍然找不到 letconst 变量的值,它将为它们分配 undefined 的值或返回错误(如果是 const)。

来看更多示例:

let a;
console.log(a); // outputs undefined
a = 5;

在编译阶段,JavaScript 引擎在这里遇到变量 a 并将其存储在词法环境中,但是由于它是一个 let 变量,因此引擎不会使用任何值对其进行初始化。因此,在编译阶段,词法环境将如下所示:

lexicalEnvironment = {
  a: <uninitialized>
}

现在,如果我们尝试在声明变量之前访问变量,那么 JavaScript 引擎将尝试从词法环境中获取变量的值,因为该变量未初始化,因此将引发引用错误。

在执行过程中,当引擎到达声明该变量的行时,它将尝试评估其绑定(值),因为该变量没有与之关联的值,因此它将为它分配 undefined

因此,在执行第一行后,词法环境将如下所示:

lexicalEnvironment = {
  a: undefined
}

然后将 undefined 记录到控制台,然后将 5 分配给控制台,并将词法环境更新为包含从 undefined5a 值。

注意—只要在声明变量之前不执行该代码,我们甚至可以在声明它们之前在代码(例如,函数主体)中引用let和const变量。

例如,此代码是完全有效的。

function foo () {
  console.log(a);
}
let a = 20;
foo();  // This is perfectly valid

但这会产生引用错误。

function foo() {
 console.log(a); // ReferenceError: a is not defined
}
foo(); // This is not valid
let a = 20;

# 5️⃣ ES6 中 class 声明的提升

就像 letconst 声明一样,JavaScript 中的 class 也会被提升,就像 letconst 声明一样,它们将保持未初始化状态直到评估。因此,它们也受到“临时死区”的影响。例如:

let peter = new Person('Peter', 25); // ReferenceError: Person is  
                                     // not defined
console.log(peter);
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

因此,要访问这些类,您必须先声明它们。例如:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }

因此,再次在编译阶段,上述代码的词法环境将如下所示:

lexicalEnvironment = {
  Person: <uninitialized>
}

引擎评估完 class 语句后,它将使用该值初始化该 class。

lexicalEnvironment = {
  Person: <Person object>
}

# 6️⃣ ES6 中 class 表达式的提升

就像函数表达式一样,类表达式也不会提升。例如,此代码无效。

let peter = new Person('Peter', 25); // ReferenceError: Person is  
                                     // not defined
console.log(peter);
let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

正确的方法是这样的:

let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }

# 7️⃣ ES6 中 块级作用域的提升

在 ES6 中提出了块级作用域的概念,块级作用域中声明的变量也会存在变量提升,但是部分提升的方式和其他作用域稍微不同。

console.log('a1', a)
{
  // function a
  console.log('a2', a)
  a = 100 //  var a = 100
  // 100
  console.log('a3', a)
  function a() {}
  // 100
  console.log('a4', a)
}
// 100
console.log('a5', a)

允许在块级作用域内声明函数。函数声明类似于 var,即会提升到全局作用域或函数作用域的头部。同时,函数声明还会提升到所在的块级作用域的头部。

块级作用域内声明的函数会有两个操作:

  1. 提升到全局作用域;
  2. 提升到所在块级作用域内部。

这两个过程以及提升的时机用下面的代码来描述(来自 what-are-the-precise-semantics-of-block-level-functions-in-es6 (opens new window)

// 在函数内的块级作用域内声明了一个函数 compat
function enclosing() {{function compat() {}}}

提升的过程表示如下:

function enclosing() {
  var compat₀ = undefined; // function-scoped{
    let compat₁ = function compat() {}; // block-scoped
    …
    compat₀ = compat₁;}}

提升的过程存在三个步骤:

  1. 在块级作用域外层,产生一个用 var 声明的变量,并赋值为 undefined,类似于块级作用域内部 var 声明的变量;
  2. 在块级作用域词法分析阶段,在顶部用 let 声明一个同名变量,并赋值为这个函数。注意内层不仅提升了而且赋值了。
  3. 在原来函数声明的那一行,把内层用 let 声明的变量的值赋值给块级作用域外层用 var 声明的同名变量。外层的变量就是在这个时候被赋值的。

从第1步可以知道 a1undefined,从第2步可以知道 a2function a,从第3步可以知道 a5100

# 总结

因此,现在我们知道,在提升过程中,JavaScript 引擎不会实际移动代码。正确理解提升机制将有助于您避免将来由于提升而引起的任何错误和混乱。为避免未定义的变量或引用错误等提升的副作用,请始终尝试在变量的各自作用域顶部声明变量,并始终在声明变量时尝试初始化变量。

  • 函数声明:会提升,在当前作用域的任何区域都可访问

  • 函数表达式:不提升

  • var 变量:会提升,会被初始化为 undefined

  • letconst 变量:会提升,会被初始化为 uninitialized。提升到变量赋值之间的区域存在暂时性死区

  • class 声明:与 letconst 行为一致

  • class 表达式:与函数表达式行为一致

  • 块级作用域:对于 函数声明和 var 变量来说会提升,不仅是当前作用域,还有父级作用域

    • 在父级作用域中访问时,如果在代码块之前,皆为 undefined,代码块之后取决于代码块中对变量的操作
    • 在当前作用域中访问时,与全局和函数作用域一致

对于同名的函数声明与 var 变量,函数声明要优于 var 变量提升,var 变量被函数声明覆盖。例如:

console.log(a) // function a() {}

var a = '123'
console.log(a) //  函数 a 被赋值覆盖了,'123'

function a() {
    
}
console.log(a) // '123'

# QA

Q: es6 里面说 letconst 不提升

A: 下面代码中,在函数作用域内部,对于 let 声明的变量如果不存在提升,那么 x 会输出全局的 "global",然而却报错了,说:初始化前不能访问 x,说明是存在提升的。

x = "global";
// function scope:
(function() {
  console.log(x); // not "global"

//   var x = 'function';
  let x = 'function';
}());

对于提升的总结可以更加细致一点:

  • letconst 的「创建」过程(词法环境)被提升了,但是初始化没有提升。
  • var 的「创建」和「初始化」都被提升了。
  • function 的「创建」「初始化」和「赋值」都被提升了。

块级作用域对于 letconst 也是一样的行为:提升了创建过程,没有提升初始化。例如:

x = "global";
// block scope (not for `var`s):
{
  console.log(x); // not "global"

//     let/const/… x;
//   let x = 'block'
  const x = 'block'
}

因此对于 JavaScript 中的变量声明提升指的是 “创建过程的提升”。

# 参考