你不知道的Javascript(上卷) | 第四章难点与细节解读(提升)

379 阅读7分钟

写在前面

作为《你不知道的Javascript》忠实读者,多次拜读该著作,本专栏用来分享我对该书的解读,适合希望深入了解这本书的读者阅读 电子书下载网址:zh.101-c.online

本文建议在阅读过《你不知道的Javascript》第四章之后再看,这样可以更好了理解我为什么这样写

第四章——提升

一、在编译阶段进行声明提升的目的是什么?

先看原文表述

image.png

原文明确说明了声明提升的过程是在编译阶段的某一阶段进行的,但是现在我们要讨论的是为什么要在编译阶段进行声明提升,或者是为什么声明提升要在编译阶段进行,这有什么实际的意义

1. 为什么要在编译阶段做声明提升?

​(1)确保作用域的正确性

  • JavaScript 采用词法作用域(Lexical Scoping)​,即变量的作用域在代码编写时就已确定​(而非运行时)。
  • 如果不在编译阶段预先绑定变量,引擎在运行时可能无法正确解析变量引用,导致作用域混乱。
​(2)优化执行效率
  • 在编译阶段完成所有变量和函数的注册后,执行阶段只需快速查找作用域,无需动态解析声明。
  • 如果没有提升,引擎每次遇到变量都需要检查是否已声明,性能会大幅下降。
​(3)兼容早期 JavaScript 的设计
  • JavaScript 早期设计目标是简单易用,允许开发者以更自由的顺序编写代码(如函数调用在前、定义在后)。

  • 声明提升让以下代码能正常运行:

    greet(); // "Hello!"(函数声明被提升)
    function greet() {
      console.log("Hello!");
    }
    
​(4)避免“暂时性死区”(TDZ)问题
  • let/const 虽然也有“提升”,但没有初始化​(TDZ 机制),这是为了更严格的变量管理。
  • var 的完全提升(声明+初始化)是早期语言的妥协设计,但提升了代码容错性。

2. 声明提升的实际意义

​(1)代码可预测性
  • 开发者可以在函数内的任何位置使用变量​(即使赋值在后),而不会因顺序问题报错。

    function foo() {
      console.log(x); // undefined(而不是报错)
      var x = 10;
    }
    
​(2)函数优先(Function Hoisting)​
  • 函数声明的优先级高于变量声明,因此函数可以在定义前调用:

    foo(); // "I am a function"(函数声明覆盖了同名变量)
    var foo = "I am a variable";
    function foo() {
      console.log("I am a function");
    }
    
​(3)模块化与作用域管理
  • 通过提升机制,JavaScript 能更早建立模块的依赖关系,避免运行时作用域冲突。

二、var声明提升的作用域限制

在声明提升中主要涉及了两个问题

  • 1.var的声明提升
  • 2.函数的声明提升

这里我们来讨论var的声明提升

先看原文表述

image.png

原文也详细讲述了var的声明提升的知识,但是却遗漏了一个重要的知识,那就是var的作用域限制,我们知道对于var来讲,只有两个作用域限制——全局作用域和函数作用域,并没有块级作用域这就意味着对于if或者for等块级作用域var的声明不会被限制在{}中,而是上一级函数作用域或者全局作用域

var 声明会被提升到当前作用域的顶部:

console.log(a); // 输出 undefined 而不是报错
var a = 10;

实际执行顺序相当于:

var a; // 声明提升
console.log(a); // undefined
a = 10; // 赋值留在原地

没有块级作用域

var 不识别 {} 块级作用域:

for (var i = 0; i < 3; i++) {
  // i 在整个函数中都可用
}
console.log(i); // 输出 3,而不是报错

但是下面的却会报错

console.log(a); // ❌ 报错:ReferenceError: a is not defined
function test() {
  var a = 1;
}

三、函数声明提升提升的什么

这里我们不讨论函数声明的优先性,也不讨论函数声明的提升和函数表达式的提升的区别,我们只讨论函数声明的提升提升的什么东西,提升的是foo()还是提升的foo()后面{}中的内容?

事实上,是把整个函数声明提升了,下面举个例子 函数声明会被完整地提升到当前作用域的顶部,包括函数名和函数体:

// 调用在声明之前
sayHello(); // 输出 "Hello!"

function sayHello() {
  console.log("Hello!");
}

实际执行顺序相当于:

// 函数声明被提升到顶部
function sayHello() {
  console.log("Hello!");
}

// 然后执行调用
sayHello();

四、为什么let和const不会声明提升?

实际上,letconst确实存在声明提升​(Hoisting),但它们的行为与 var 不同,主要体现在 ​作用域绑定和初始化时机 上。以下是详细解释:


1. letconst 的“提升”与 var 的区别

特性varlet / const
声明提升✅ 提升并初始化为 undefined✅ 提升但未初始化(TDZ 阶段)
作用域函数作用域或全局作用域块级作用域({} 内有效)
重复声明✅ 允许重复声明❌ 报错(SyntaxError)
初始化时机声明时初始化为 undefined执行到声明语句时才初始化

2. 关键概念:暂时性死区(TDZ, Temporal Dead Zone)​

letconst 的声明会被提升到块级作用域的顶部,但在代码执行到声明语句之前,变量处于 ​​“暂时性死区”​​(TDZ),此时访问变量会报错。

实例分析
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 2;

执行过程

  1. 编译阶段

    • 引擎发现 let a,将其绑定到当前块级作用域(提升)。
    • 不会初始化为 undefined(与 var 不同)。
  2. 执行阶段

    • 执行 console.log(a) 时,a 已声明但未初始化,触发 TDZ 错误。
    • 执行到 let a = 2 时,a 才被初始化并赋值。

3. 为什么设计 TDZ?

​(1)避免逻辑错误
  • var 的变量提升可能导致开发者误用未赋值的变量(如 undefined),而 TDZ 强制要求 ​先声明后使用

    // var 的问题
    console.log(x); // undefined(容易引发 bug)
    var x = 10;
    
    // let 的解决方案
    console.log(y); // 直接报错(更早暴露问题)
    let y = 10;
    
​(2)支持块级作用域
  • let/const 的 TDZ 机制与块级作用域({})紧密相关,确保变量仅在当前块内有效。

    {
      console.log(z); // ReferenceError
      let z = 5;
    }
    
​(3)兼容性和语言进化
  • JavaScript 早期设计(ES5)因 var 的提升问题饱受诟病,ES6 通过 let/const + TDZ 提供更严格的变量管理。

4. 从引擎视角看 let/const 的提升

(1)编译阶段
  • 扫描代码,识别所有 let/const 声明,并将其绑定到作用域。
  • 不分配内存​(与 var 不同),变量处于未初始化状态(TDZ)。
(2)执行阶段
  • 执行到声明语句(如 let a = 2)时,才分配内存并初始化。
  • 在此之前访问变量会触发 TDZ 错误。

总结

通过对《你不知道的JavaScript》第四章的深入解读,我们系统性地剖析了JavaScript中声明提升(Hoisting)的核心机制。从编译阶段的变量绑定到执行阶段的作用域查找,从var的函数作用域限制到let/const的暂时性死区(TDZ)设计,这些概念共同构成了JavaScript变量系统的底层逻辑。理解这些机制不仅能帮助我们规避常见的变量污染问题,更能培养对语言设计哲学的深刻认知。

正如Kyle Simpson在书中强调的,声明提升不是语言的缺陷,而是JavaScript实现词法作用域的必要机制。无论是早期的var还是现代的let/const,其设计都体现了语言在灵活性与严谨性之间的平衡。建议读者将本文作为实践指南,结合书中示例深入体会不同声明方式的特性,从而写出更健壮、可维护的代码。当你能准确预判每一行代码的变量行为时,就真正掌握了JavaScript的作用域精髓。