写在前面
作为《你不知道的Javascript》忠实读者,多次拜读该著作,本专栏用来分享我对该书的解读,适合希望深入了解这本书的读者阅读 电子书下载网址:zh.101-c.online
本文建议在阅读过《你不知道的Javascript》第四章之后再看,这样可以更好了理解我为什么这样写
第四章——提升
一、在编译阶段进行声明提升的目的是什么?
先看原文表述
原文明确说明了声明提升的过程是在编译阶段的某一阶段进行的,但是现在我们要讨论的是为什么要在编译阶段进行声明提升,或者是为什么声明提升要在编译阶段进行,这有什么实际的意义?
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的声明提升
先看原文表述
原文也详细讲述了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不会声明提升?
实际上,let 和 const 确实存在声明提升(Hoisting),但它们的行为与 var 不同,主要体现在 作用域绑定和初始化时机 上。以下是详细解释:
1. let 和 const 的“提升”与 var 的区别
| 特性 | var | let / const |
|---|---|---|
| 声明提升 | ✅ 提升并初始化为 undefined | ✅ 提升但未初始化(TDZ 阶段) |
| 作用域 | 函数作用域或全局作用域 | 块级作用域({} 内有效) |
| 重复声明 | ✅ 允许重复声明 | ❌ 报错(SyntaxError) |
| 初始化时机 | 声明时初始化为 undefined | 执行到声明语句时才初始化 |
2. 关键概念:暂时性死区(TDZ, Temporal Dead Zone)
let 和 const 的声明会被提升到块级作用域的顶部,但在代码执行到声明语句之前,变量处于 “暂时性死区”(TDZ),此时访问变量会报错。
实例分析
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 2;
执行过程:
-
编译阶段:
- 引擎发现
let a,将其绑定到当前块级作用域(提升)。 - 但 不会初始化为
undefined(与var不同)。
- 引擎发现
-
执行阶段:
- 执行
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的作用域精髓。