各位开发者,下午好!
今天,我们将深入探讨 JavaScript 中两个核心且经常被误解的概念:词法作用域(Lexical Scoping)与变量提升(Hoisting)。这两个机制是理解 JavaScript 代码执行流程、尤其是其背后的执行上下文初始化阶段的关键。它们不仅决定了变量和函数的可见性,更深刻地影响着我们编写和调试代码的方式。作为一名编程专家,我的目标是带大家透过现象看本质,从 JavaScript 引擎的视角,解构这些概念,让它们变得清晰透明。
我们将从 JavaScript 的执行模型入手,逐步深入到执行上下文的创建阶段,详细剖析在这个阶段,函数与变量是如何被创建、初始化,以及它们如何共同构建起我们所熟知的“作用域链”。准备好了吗?让我们开始这场探索之旅。
一、JavaScript 执行模型:一切的起点
JavaScript 是一种单线程、非阻塞、异步的语言。它的代码执行是基于“执行上下文”(Execution Context)栈的。每当 JavaScript 引擎需要执行一段代码时,它都会创建一个新的执行上下文,并将其推入执行上下文栈。当这段代码执行完毕,对应的执行上下文就会从栈中弹出。
执行上下文可以分为几种类型:
- 全局执行上下文 (Global Execution Context):当 JavaScript 脚本开始执行时,会创建唯一的全局执行上下文。它在整个应用程序生命周期内都存在,并负责管理全局变量和函数。在浏览器环境中,
window对象就是全局对象;在 Node.js 中,则是global对象。this在全局上下文中指向全局对象。 - 函数执行上下文 (Function Execution Context):每当调用一个函数时,都会创建一个新的函数执行上下文。每个函数调用都会有自己独立的上下文,即使是同一个函数的多次调用,也会创建多个独立的上下文。
- Eval 执行上下文 (Eval Execution Context):使用
eval()函数执行的代码也会有自己的执行上下文,但由于eval的安全性和性能问题,在现代 JavaScript 开发中极少推荐使用。
今天,我们的重点将放在全局和函数执行上下文上。理解它们的生命周期,特别是它们的“创建阶段”,是掌握词法作用域和变量提升的关键。
二、执行上下文的生命周期:创建与执行
每个执行上下文的生命周期都包含两个主要阶段:
-
创建阶段 (Creation Phase):在这个阶段,执行上下文被创建,但代码尚未开始执行。JavaScript 引擎会在此阶段完成一系列重要的准备工作,包括:
- 创建词法环境(LexicalEnvironment)组件。
- 创建变量环境(VariableEnvironment)组件。
- 确定
this的指向(ThisBinding)。 - 这些组件的初始化是词法作用域和变量提升机制的核心。
-
执行阶段 (Execution Phase):在创建阶段完成后,JavaScript 引擎开始逐行执行代码。在这个阶段,变量被赋值,函数被调用,以及所有实际的程序逻辑都会被执行。
我们的深入探讨将聚焦于创建阶段,因为它揭示了函数和变量如何在代码执行之前就“准备就绪”。
2.1 词法环境 (LexicalEnvironment) 与 变量环境 (VariableEnvironment)
在 ES6 之前,JavaScript 规范中主要描述的是“变量环境”(VariableEnvironment)。而 ES6 引入了 let 和 const 关键字后,为了区分它们与 var 的不同行为,规范引入了“词法环境”(LexicalEnvironment)的概念。在现代 JavaScript 引擎的实现中,通常会将这两者融合或让 LexicalEnvironment 扮演更核心的角色。
从概念上讲:
-
词法环境 (LexicalEnvironment):这是一个抽象的概念,用于存储当前执行上下文中的所有声明(包括变量、函数、类等)的标识符和它们的值的映射。它由两部分组成:
- 环境记录器 (Environment Record):实际存储变量和函数声明的地方。
- 外部词法环境引用 (Outer Lexical Environment Reference):指向外部(父级)词法环境的引用,这是构建作用域链的关键。
-
变量环境 (VariableEnvironment):在 ES6 之后,它实际上是词法环境的一个内部组件,但它专门用于处理
var声明的变量和函数声明。在全局上下文中,它还包含了window或global对象的属性。我们可以理解为,var和function声明会被特殊处理并放置在 VariableEnvironment 中(或 LexicalEnvironment 的一个特定部分),而let和const声明则放置在 LexicalEnvironment 的另一个部分。
为了简化理解,我们可以将 LexicalEnvironment 视为一个“容器”,它在创建阶段被初始化,并保存了该上下文中的所有本地声明。
2.1.1 环境记录器 (Environment Record)
环境记录器是词法环境的核心部分,它负责存储当前作用域内的所有绑定(binding)。根据声明类型的不同,环境记录器又分为两种:
-
声明式环境记录器 (Declarative Environment Record):
- 用于存储函数声明 (
function)、let声明、const声明和class声明。 - 它直接存储这些标识符到它们的实际值或一个“未初始化”状态的映射。
- 对于
let和const,在它们的代码行被执行之前,它们的状态是“未初始化”(uninitialized),处于暂时性死区 (Temporal Dead Zone, TDZ)。
- 用于存储函数声明 (
-
对象环境记录器 (Object Environment Record):
- 主要用于全局执行上下文,它将全局对象(如浏览器中的
window)的属性暴露为环境记录器中的绑定。 - 同时,它也存储
var声明的变量,这些变量也会成为全局对象的属性。 - 在
with语句中也会创建对象环境记录器,但在严格模式下不推荐使用with。
- 主要用于全局执行上下文,它将全局对象(如浏览器中的
2.1.2 外部词法环境引用 (Outer Lexical Environment Reference)
这是词法环境的另一个至关重要的部分。它指向创建当前词法环境的那个词法环境。简单来说,它指向上层(父级)的作用域。正是这个引用,将所有独立的词法环境串联起来,形成了作用域链。当 JavaScript 引擎需要查找一个变量时,它会首先在当前词法环境的环境记录器中查找,如果找不到,就会沿着 Outer Lexical Environment Reference 向上查找,直到找到全局词法环境。如果仍然找不到,就会抛出 ReferenceError。
三、词法作用域 (Lexical Scoping):代码编写时决定
词法作用域,又称静态作用域,意味着变量和函数的可见性(即作用域)是在代码编写阶段(词法分析阶段)就被确定下来的,而不是在代码执行阶段。换句话说,函数在哪里被定义,它的作用域就决定了。它不关心函数在哪里被调用,只关心函数在哪里被声明。
这个概念通过 Outer Lexical Environment Reference 在执行上下文的创建阶段得以体现。当一个函数被创建时,它会“记住”自己被创建时的那个词法环境。这个“记住”就是通过其内部属性 [[Environment]] 来实现的,这个属性存储了其创建时的 Outer Lexical Environment Reference。
让我们看一个简单的例子:
var globalVar = "我是全局变量";
function outerFunction() {
var outerVar = "我是外部函数变量";
function innerFunction() {
var innerVar = "我是内部函数变量";
console.log(innerVar); // 访问 innerFunction 自身的变量
console.log(outerVar); // 访问 outerFunction 的变量
console.log(globalVar); // 访问全局变量
}
innerFunction(); // 调用内部函数
// console.log(innerVar); // 错误:innerVar 在这里不可访问
}
outerFunction(); // 调用外部函数
// console.log(outerVar); // 错误:outerVar 在这里不可访问
执行上下文创建阶段的视角:
-
全局执行上下文创建:
LexicalEnvironment的Environment Record包含globalVar(值为undefined,因为是var) 和outerFunction(值为完整的函数对象)。Outer Lexical Environment Reference为null(全局上下文的外部引用)。
-
调用
outerFunction()时,创建outerFunction执行上下文:LexicalEnvironment的Environment Record包含outerVar(值为undefined) 和innerFunction(值为完整的函数对象)。Outer Lexical Environment Reference指向全局执行上下文的词法环境。这是因为outerFunction是在全局作用域中定义的。
-
调用
innerFunction()时,创建innerFunction执行上下文:LexicalEnvironment的Environment Record包含innerVar(值为undefined)。Outer Lexical Environment Reference指向outerFunction执行上下文的词法环境。这是因为innerFunction是在outerFunction内部定义的。
变量查找过程:
当 innerFunction 内部执行 console.log(outerVar) 时:
- 首先在
innerFunction自身的LexicalEnvironment的Environment Record中查找outerVar。找不到。 - 通过
innerFunction的Outer Lexical Environment Reference上溯到outerFunction的LexicalEnvironment。 - 在
outerFunction的Environment Record中找到outerVar。使用它的值。
这就是词法作用域的工作原理。函数的作用域链在它被定义的那一刻就已经固定了。
四、变量提升 (Hoisting):声明在先,赋值在后
变量提升是 JavaScript 中一个经常引起混淆的特性,它并不是指代码真的被“移动”到文件顶部。更准确地说,变量提升是 JavaScript 引擎在执行上下文的创建阶段,将变量和函数的声明“注册”到其对应的词法环境(或变量环境)中的行为。这意味着,无论变量或函数在代码中实际声明的位置在哪里,它们的声明都会在代码执行之前就被处理。
然而,不同类型的声明(function, var, let, const, class)在提升时的初始化行为是不同的。
4.1 函数声明提升 (Function Declarations Hoisting)
函数声明是提升行为中最“完全”的一种。在执行上下文的创建阶段,函数声明不仅被注册到环境记录器中,而且会被完整地初始化,即函数对象会被创建并赋值给对应的标识符。这意味着你可以在函数声明之前调用它。
console.log(greet("Alice")); // 输出: Hello, Alice!
function greet(name) {
return "Hello, " + name + "!";
}
// 另一个例子
sayHello(); // 输出: Hello!
function sayHello() {
console.log("Hello!");
}
sayHello = function() { // 函数表达式,不会被提升
console.log("Goodbye!");
};
sayHello(); // 输出: Goodbye!
执行上下文创建阶段的视角:
- 全局执行上下文创建。
- JavaScript 引擎扫描代码,发现
greet函数声明。在LexicalEnvironment的Environment Record中创建greet绑定,并将其值初始化为完整的greet函数对象。 - 同样,扫描到第一个
sayHello函数声明,创建sayHello绑定,并将其值初始化为完整的sayHello函数对象。
因此,在代码执行阶段开始时,greet 和 sayHello 函数就已经完全可用了。
4.2 var 变量提升 (var Variable Hoisting)
var 声明的变量也会被提升,但其初始化行为与函数声明不同。在执行上下文的创建阶段,var 变量会被注册到环境记录器中,并被初始化为 undefined。实际的赋值操作则发生在代码的执行阶段。
console.log(myVar); // 输出: undefined
var myVar = 10;
console.log(myVar); // 输出: 10
// 另一个例子
var a = 1;
function test() {
console.log(a); // 输出: undefined (不是 1)
var a = 2;
console.log(a); // 输出: 2
}
test();
console.log(a); // 输出: 1
执行上下文创建阶段的视角:
例子一:
- 全局执行上下文创建。
- JavaScript 引擎扫描代码,发现
var myVar = 10;。在LexicalEnvironment的Environment Record中创建myVar绑定,并将其值初始化为undefined。 - 代码进入执行阶段:
console.log(myVar)执行,此时myVar的值是undefined。myVar = 10;执行,myVar的值被更新为10。console.log(myVar)执行,此时myVar的值是10。
例子二:test 函数内部
- 调用
test()时,创建test函数执行上下文。 - JavaScript 引擎扫描
test函数内部代码,发现var a = 2;。在test函数的LexicalEnvironment的Environment Record中创建局部a绑定,并将其值初始化为undefined。 - 代码进入执行阶段:
console.log(a)执行,此时test作用域内的a的值是undefined。注意:这里不会去查找全局的a,因为test内部已经声明了a,发生了“变量遮蔽” (variable shadowing)。a = 2;执行,局部a的值被更新为2。console.log(a)执行,此时局部a的值是2。
这种行为就是 var 变量提升的典型表现:声明被提升,但初始化值是 undefined。
4.3 let 和 const 变量提升(Temporal Dead Zone – TDZ)
let 和 const 声明的变量也存在提升行为,但它们的提升方式更为严格。在执行上下文的创建阶段,let 和 const 变量的声明会被注册到环境记录器中。但是,它们不会被初始化为 undefined。相反,它们会处于一种“未初始化”状态,直到它们在代码中实际的声明语句被执行。在这段从作用域开始到声明语句执行之间的区域,我们称之为暂时性死区 (Temporal Dead Zone, TDZ)。
在 TDZ 中访问 let 或 const 变量会导致 ReferenceError。
console.log(myLetVar); // ReferenceError: Cannot access 'myLetVar' before initialization
let myLetVar = 20;
console.log(myLetVar); // 输出: 20
// 另一个例子
console.log(myConstVar); // ReferenceError: Cannot access 'myConstVar' before initialization
const myConstVar = 30;
console.log(myConstVar); // 输出: 30
// TDZ 的范围
function checkTDZ() {
// 这里是 myBlockVar 的 TDZ
console.log(myBlockVar); // ReferenceError
let myBlockVar = "hello";
console.log(myBlockVar); // "hello"
}
checkTDZ();
执行上下文创建阶段的视角:
- 全局执行上下文创建。
- JavaScript 引擎扫描代码,发现
let myLetVar = 20;。在LexicalEnvironment的Environment Record中创建myLetVar绑定,并将其标记为未初始化。 - 代码进入执行阶段:
console.log(myLetVar)尝试访问myLetVar。由于myLetVar处于未初始化状态(TDZ),引擎抛出ReferenceError。- 如果
ReferenceError没有发生,let myLetVar = 20;会将myLetVar从 TDZ 中移出,并赋值为20。 console.log(myLetVar)此时可以正常访问myLetVar。
const 的行为与 let 相同,唯一的区别是 const 声明的变量一旦赋值就不能再修改。
4.4 class 声明提升
class 声明(class declaration)的行为与 let 和 const 类似,也存在暂时性死区。在类声明的代码行被执行之前,尝试访问该类会导致 ReferenceError。
// console.log(MyClass); // ReferenceError: Cannot access 'MyClass' before initialization
class MyClass {
constructor(name) {
this.name = name;
}
}
const instance = new MyClass("Test");
console.log(instance.name); // 输出: Test
执行上下文创建阶段的视角:
- 全局执行上下文创建。
- JavaScript 引擎扫描代码,发现
class MyClass { ... }。在LexicalEnvironment的Environment Record中创建MyClass绑定,并将其标记为未初始化(处于 TDZ)。 - 代码进入执行阶段:
- 尝试访问
MyClass会抛出ReferenceError。 class MyClass { ... }语句执行后,MyClass从 TDZ 中移出,并被赋值为实际的类定义。
- 尝试访问
4.5 函数表达式 (Function Expressions)
需要特别注意的是,函数表达式(Function Expression)不会被提升。它们被视为普通的变量赋值,其提升行为取决于用于声明该变量的关键字 (var, let, const)。
// funcDecl(); // 输出: "Hello from declaration" (函数声明被提升)
// funcExpr(); // TypeError: funcExpr is not a function (如果是 var) 或 ReferenceError (如果是 let/const)
function funcDecl() {
console.log("Hello from declaration");
}
var funcExpr = function() {
console.log("Hello from expression");
};
funcDecl(); // 输出: "Hello from declaration"
funcExpr(); // 输出: "Hello from expression"
// 使用 let 的函数表达式
// funcLetExpr(); // ReferenceError: Cannot access 'funcLetExpr' before initialization
let funcLetExpr = function() {
console.log("Hello from let expression");
};
funcLetExpr(); // 输出: "Hello from let expression"
执行上下文创建阶段的视角:
- 全局执行上下文创建。
- 扫描到
funcDecl函数声明:在LexicalEnvironment的Environment Record中创建funcDecl绑定,并初始化为完整的函数对象。 - 扫描到
var funcExpr = function() { ... };:在LexicalEnvironment的Environment Record中创建funcExpr绑定,并初始化为undefined。 - 扫描到
let funcLetExpr = function() { ... };:在LexicalEnvironment的Environment Record中创建funcLetExpr绑定,并标记为未初始化(TDZ)。
代码执行阶段:
funcDecl()可以立即执行,因为其在创建阶段已完全初始化。funcExpr()在var funcExpr = ...语句执行之前调用会失败,因为此时funcExpr的值是undefined,undefined不是一个函数,所以尝试调用它会产生TypeError。funcLetExpr()在let funcLetExpr = ...语句执行之前调用会抛出ReferenceError,因为它在 TDZ 中。
4.6 总结 Hoisting 行为
| 声明类型 | 提升行为 | 初始化值(创建阶段) | 访问行为(声明前) |
|---|---|---|---|
function 声明 |
完全提升(声明和定义) | 完整函数对象 | 可访问,可调用 |
var 变量 |
声明提升,定义不提升 | undefined |
可访问,值为 undefined |
let 变量 |
声明提升,定义不提升(进入 TDZ) | 未初始化 | 抛出 ReferenceError (在 TDZ 中) |
const 变量 |
声明提升,定义不提升(进入 TDZ) | 未初始化 | 抛出 ReferenceError (在 TDZ 中) |
class 声明 |
声明提升,定义不提升(进入 TDZ) | 未初始化 | 抛出 ReferenceError (在 TDZ 中) |
函数表达式 (var) |
var 变量提升规则 |
undefined |
可访问,值为 undefined,尝试调用会抛出 TypeError |
函数表达式 (let) |
let 变量提升规则(进入 TDZ) |
未初始化 | 抛出 ReferenceError (在 TDZ 中) |
五、词法作用域、变量提升与执行上下文创建阶段的协同作用
现在,让我们把词法作用域和变量提升这两个概念,与执行上下文的创建阶段紧密结合起来,看看它们是如何协同工作的。
当 JavaScript 引擎进入一个新的执行上下文的创建阶段时,它会执行以下关键步骤:
-
确定
Outer Lexical Environment Reference:- 这是词法作用域的核心。引擎根据当前代码的物理位置(即它在源代码中被定义的位置),确定当前词法环境应该指向哪个外部词法环境。这构建了作用域链的基础。
-
创建
Environment Record并处理函数声明:- 引擎扫描当前作用域中的所有
function声明。 - 对于每个函数声明,引擎会在
Environment Record中创建一个绑定,并将该标识符的值设置为一个完整的函数对象。这意味着函数声明在代码执行前就已经完全可用。
- 引擎扫描当前作用域中的所有
-
处理
var变量声明:- 引擎扫描当前作用域中的所有
var变量声明。 - 对于每个
var声明,引擎会在Environment Record中创建一个绑定,并将其值初始化为undefined。 - 如果存在同名的
var声明或function声明,function声明会优先,但var会被忽略(不会重新赋值undefined)。
- 引擎扫描当前作用域中的所有
-
处理
let,const,class声明:- 引擎扫描当前作用域中的所有
let,const,class声明。 - 对于每个这样的声明,引擎会在
Environment Record中创建一个绑定,但将其状态标记为未初始化。它们将处于暂时性死区 (TDZ),直到代码执行到实际的声明语句。
- 引擎扫描当前作用域中的所有
所有这些步骤都发生在代码的实际执行之前。 只有当这些准备工作完成后,执行上下文才会进入执行阶段,此时代码才开始逐行运行。
让我们通过一个综合的例子来理解:
var x = 10;
function foo() {
console.log(x); // 输出: undefined
var x = 20;
console.log(x); // 输出: 20
function bar() {
console.log(x); // 输出: 20
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 30;
console.log(y); // 输出: 30
}
bar();
}
foo();
console.log(x); // 输出: 10
逐步分析执行过程:
-
全局执行上下文创建阶段:
Outer Lexical Environment Reference:null。Environment Record:x: 绑定创建,初始化为undefined(来自var x = 10;)。foo: 绑定创建,初始化为完整的foo函数对象 (来自function foo() { ... })。
-
全局执行上下文执行阶段:
var x = 10;:x的值从undefined更新为10。foo();: 调用foo函数,创建foo函数执行上下文。
-
foo函数执行上下文创建阶段:Outer Lexical Environment Reference: 指向全局执行上下文的词法环境 (因为foo在全局作用域定义)。Environment Record:x: 绑定创建,初始化为undefined(来自var x = 20;,遮蔽了全局的x)。bar: 绑定创建,初始化为完整的bar函数对象 (来自function bar() { ... })。
-
foo函数执行上下文执行阶段:console.log(x);: 查找x。在foo的Environment Record中找到x,其值为undefined。输出undefined。var x = 20;:foo作用域内的x的值从undefined更新为20。console.log(x);: 查找x。在foo的Environment Record中找到x,其值为20。输出20。bar();: 调用bar函数,创建bar函数执行上下文。
-
bar函数执行上下文创建阶段:Outer Lexical Environment Reference: 指向foo函数执行上下文的词法环境 (因为bar在foo内部定义)。Environment Record:y: 绑定创建,标记为未初始化 (来自let y = 30;,处于 TDZ)。
-
bar函数执行上下文执行阶段:console.log(x);: 查找x。- 在
bar的Environment Record中查找x,未找到。 - 沿着
Outer Lexical Environment Reference上溯到foo的LexicalEnvironment。 - 在
foo的Environment Record中找到x,其值为20。输出20。
- 在
console.log(y);: 查找y。在bar的Environment Record中找到y,但其状态为未初始化。抛出ReferenceError。- (假设没有
ReferenceError,或者我们把console.log(y)放在let y = 30;之后) let y = 30;:y从 TDZ 中移出,并赋值为30。console.log(y);: 查找y。在bar的Environment Record中找到y,其值为30。输出30。
-
bar函数执行完毕,其执行上下文从栈中弹出。 -
foo函数执行完毕,其执行上下文从栈中弹出。 -
回到全局执行上下文执行阶段:
console.log(x);: 查找x。在全局的Environment Record中找到x,其值为10。输出10。
这个详细的流程展示了词法作用域(通过 Outer Lexical Environment Reference 决定变量查找路径)和变量提升(在创建阶段初始化 var 为 undefined,let/const 进入 TDZ,function 完全初始化)如何共同塑造了 JavaScript 代码的行为。
六、闭包:词法作用域的终极体现
理解了词法作用域和执行上下文的创建阶段,我们就能真正理解闭包(Closure)这个强大的概念。
闭包的定义:当一个函数能够记住并访问它被创建时的那个词法环境,即使它在那个词法环境之外被调用,那么这个函数和它所“记住”的词法环境就构成了一个闭包。
这正是 Outer Lexical Environment Reference 的威力所在。当内部函数被定义时,它捕获了其外部函数的词法环境。即使外部函数执行完毕,其对应的执行上下文从栈中弹出,但由于内部函数(闭包)仍然持有对该外部词法环境的引用,这个外部词法环境并不会被垃圾回收,而是会一直存在,直到闭包不再被引用。
function makeCounter() {
let count = 0; // count 变量属于 makeCounter 的词法环境
return function() { // 这是一个匿名函数(闭包)
count++;
console.log(count);
};
}
const counter1 = makeCounter(); // counter1 捕获了 makeCounter 第一次调用时的词法环境
const counter2 = makeCounter(); // counter2 捕获了 makeCounter 第二次调用时的词法环境(独立的)
counter1(); // 输出: 1
counter1(); // 输出: 2
counter2(); // 输出: 1 (独立的 count 变量)
counter2(); // 输出: 2
闭包的运作机制:
-
调用
makeCounter()(第一次):- 创建一个
makeCounter函数执行上下文。 - 其
LexicalEnvironment包含count变量(初始化为0)。 - 返回一个匿名函数。这个匿名函数被创建时,它的
[[Environment]]内部属性会存储对当前makeCounter执行上下文的LexicalEnvironment的引用。 makeCounter执行完毕,其执行上下文从栈中弹出。但由于匿名函数持有对其LexicalEnvironment的引用,这个LexicalEnvironment(包括count变量) 不会被垃圾回收。
- 创建一个
-
counter1()被调用:- 创建一个匿名函数执行上下文。
- 其
Outer Lexical Environment Reference指向第一次调用makeCounter时创建的那个词法环境。 - 在那个词法环境中找到
count,对其进行++操作。
-
调用
makeCounter()(第二次):- 再次创建一个全新的
makeCounter函数执行上下文。 - 其
LexicalEnvironment包含一个全新的count变量(初始化为0)。 - 返回另一个匿名函数。这个新的匿名函数捕获的是第二次调用
makeCounter时创建的词法环境。
- 再次创建一个全新的
这就是为什么 counter1 和 counter2 拥有独立的计数器。每个闭包都“记住”了它自己被创建时的独立词法环境。
闭包的常见应用场景:
- 数据私有化:创建模块化的代码,隐藏内部实现细节。
- 工厂函数:生成具有特定配置或状态的函数。
- 事件处理程序:在事件触发时能够访问定义时的环境变量。
- 函数柯里化/偏函数应用:创建更灵活的函数。
七、实践启示与最佳实践
理解词法作用域和变量提升,特别是它们在执行上下文创建阶段的机制,对于编写健壮、可维护的 JavaScript 代码至关重要。
-
优先使用
let和const:let和const引入了块级作用域和暂时性死区,这使得变量的行为更加可预测,减少了因var提升导致的一些意外情况(如循环变量问题)。- 它们鼓励开发者先声明后使用,避免了
undefined的困扰。 const更是强制了变量的不可变性(对于基本类型是值不可变,对于引用类型是引用不可变),有助于代码的稳定性和理解。
-
避免隐式全局变量:
- 在非严格模式下,不使用
var,let,const声明的变量会被自动视为全局变量。这很容易造成变量污染和命名冲突。始终显式声明你的变量。
- 在非严格模式下,不使用
-
将函数声明放在文件或作用域的顶部:
- 尽管函数声明会被完全提升,但为了代码的可读性和一致性,最好还是将它们放在逻辑单元的顶部。这使得读者一眼就能看到该作用域内可用的函数。
-
警惕
var的变量提升陷阱:- 尤其是在循环中,使用
var声明的循环变量会“泄露”到循环外部,或者被所有迭代共享同一个变量实例,这通常不是我们期望的行为。 -
for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 总是输出 3, 3, 3 }, 100 * i); } // 使用 let 解决 for (let j = 0; j < 3; j++) { setTimeout(function() { console.log(j); // 输出 0, 1, 2 }, 100 * j); } - 在
var的例子中,i是函数作用域的,setTimeout中的匿名函数形成了闭包,捕获了对同一个i的引用。当setTimeout执行时,循环早已结束,i的最终值是3。 - 使用
let时,每次循环迭代都会为j创建一个新的绑定,因此每个闭包都捕获了它自己迭代中的j值。
- 尤其是在循环中,使用
-
理解闭包的生命周期:
- 闭包会保留对其外部词法环境的引用,这可能导致内存泄漏,如果闭包不再需要但仍然被引用着。及时解除不再需要的闭包引用。
八、掌握 JavaScript 运行时
通过深入剖析 JavaScript 的执行上下文、词法环境、作用域链以及变量提升的各种行为,我们得以揭开 JavaScript 运行时机制的神秘面纱。这些看似复杂的概念,实则环环相扣,共同构建了 JavaScript 代码执行的底层逻辑。掌握它们,意味着我们不再仅仅是代码的编写者,更是其行为的预测者和掌控者。这将使我们能够编写出更健壮、更高效、更易于维护的 JavaScript 应用程序。