2026/1/2 10:57:42
网站建设
项目流程
珠海新盈科技有限公 网站建设,成都网站建设四川冠辰网站建设,百度网盘搜索引擎,南通自助模板建站各位开发者#xff0c;下午好#xff01;今天#xff0c;我们将深入探讨 JavaScript 中两个核心且经常被误解的概念#xff1a;词法作用域#xff08;Lexical Scoping#xff09;与变量提升#xff08;Hoisting#xff09;。这两个机制是理解 JavaScript 代码执行流程、…各位开发者下午好今天我们将深入探讨 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.2var变量提升 (varVariable 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.3let和const变量提升Temporal Dead Zone – TDZlet和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.4class声明提升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的值是undefinedundefined不是一个函数所以尝试调用它会产生TypeError。funcLetExpr()在let funcLetExpr ...语句执行之前调用会抛出ReferenceError因为它在 TDZ 中。4.6 总结 Hoisting 行为声明类型提升行为初始化值创建阶段访问行为声明前function声明完全提升声明和定义完整函数对象可访问可调用var变量声明提升定义不提升undefined可访问值为undefinedlet变量声明提升定义不提升进入 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为undefinedlet/const进入 TDZfunction完全初始化如何共同塑造了 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和constlet和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 应用程序。