JavaScript 词法环境与提升机制详解 — 从 TDZ 到块级作用域的底层原理 | 新宇宙博客
LexicalEnvironment
EnvironmentRecord
OuterReference
null
LexicalEnvironment
V8 实现细节 :V8 中词法环境被实现为 Context 对象,通过链表形式连接父级上下文。在优化编译(TurboFan)阶段,如果闭包未逃逸,V8 会将变量直接分配在栈上而非堆中。
代码示例 1:词法环境的创建 function outer ( ) {
let x = 10 ;
function inner ( ) {
let y = 20 ;
console .log (x + y);
}
inner ();
}
outer ();
全局执行上下文创建 → 全局词法环境
outer() 调用 → 创建 outer 的词法环境,OuterReference 指向全局
inner() 调用 → 创建 inner 的词法环境,OuterReference 指向 outer
2. 环境记录的三种类型
2.1 声明式环境记录 (Declarative Environment Record) function demo ( ) {
var a = 1 ;
let b = 2 ;
const c = 3 ;
function d ( ) {}
}
2.2 对象式环境记录 (Object Environment Record)
var globalVar = 'hello' ;
console .log (globalThis.globalVar );
let globalLet = 'world' ;
console .log (globalThis.globalLet );
2.3 全局环境记录 (Global Environment Record)
var x = 1 ;
let y = 2 ;
console .log (Object .getOwnPropertyDescriptor (globalThis, 'x' ));
console .log (Object .getOwnPropertyDescriptor (globalThis, 'y' ));
3. 提升的本质:编译阶段的声明注册 关键认知 :JavaScript 代码执行分为两个阶段:
编译阶段(Creation Phase) :解析代码,注册声明
执行阶段(Execution Phase) :逐行执行代码
console .log (a);
console .log (b);
var a = 1 ;
let b = 2 ;
node --print-bytecode --print-bytecode-filter=test test.js
function test ( ) {
console .log (x);
var x = 5 ;
}
4. var、let、const 的提升差异
function hoistingDemo ( ) {
console .log (v);
console .log (fn);
var v = 1 ;
let l = 2 ;
const c = 3 ;
function fn ( ) { return 'hello' ; }
console .log (v);
console .log (l);
console .log (c);
}
5. 函数声明 vs 函数表达式提升
sayHello ();
sayBye ();
function sayHello ( ) {
console .log ("Hello!" );
}
var sayBye = function ( ) {
console .log ("Bye!" );
};
条件声明的浏览器差异
if (true ) {
function foo ( ) { return 'block' ; }
}
console .log (foo ());
'use strict' ;
if (true ) {
function bar ( ) { return 'block' ; }
}
V8 (Chrome/Node.js) :非严格模式下,条件内的函数声明会提升到函数/全局作用域
SpiderMonkey (Firefox) :遵循相同的 Annex B 语义,但历史版本曾有差异
严格模式 :所有引擎统一为块级作用域
6. 暂时性死区 (TDZ) 深度解析 TDZ 是从块作用域顶部到变量声明语句之间的区域:
{
console .log (typeof x);
let x = 42 ;
console .log (typeof x);
}
console .log (typeof undeclared);
TDZ 的性能影响
function tdzPerf ( ) {
let sum = 0 ;
for (let i = 0 ; i < 1000000 ; i++) {
sum += i;
}
return sum;
}
V8 优化 :TurboFan 编译器在确认变量已初始化后,会消除后续的 TDZ 检查。这意味着在热循环中,let 和 var 性能差异可以忽略不计。
7. 块级作用域与循环变量
for (var i = 0 ; i < 3 ; i++) {
setTimeout (() => console .log (i), 100 );
}
for (let j = 0 ; j < 3 ; j++) {
setTimeout (() => console .log (j), 100 );
}
规范解读(§14.7.4.2) :for (let ...) 循环的每次迭代都会创建一个新的词法环境,将上一次迭代的绑定值复制到新环境中。这不仅仅是语法糖,而是规范层面的语义。
8. 实战案例
实战案例 1:模块初始化顺序陷阱
import { getValue } from './moduleB.js' ;
console .log (getValue ());
export const config = { debug : true };
import { config } from './moduleA.js' ;
let internalState = null ;
export function getValue ( ) {
return internalState || initState ();
}
function initState ( ) {
internalState = config ? 'debug' : 'production' ;
return internalState;
}
实战案例 2:Class 字段的 TDZ class Example {
a = this .b ;
b = 42 ;
constructor ( ) {
console .log (this .a );
console .log (this .b );
}
}
new Example ();
实战案例 3:switch 语句的作用域陷阱 function processCommand (cmd ) {
switch (cmd) {
case 'start' :
let result = startProcess ();
break ;
case 'stop' :
let result = stopProcess ();
break ;
}
}
function processCommandFixed (cmd ) {
switch (cmd) {
case 'start' : {
let result = startProcess ();
break ;
}
case 'stop' : {
let result = stopProcess ();
break ;
}
}
}
9. 深度追问
Q1:为什么 typeof 不能安全检测 TDZ 中的变量? 因为 ECMAScript 规范明确规定:对未初始化的绑定执行 GetValue 操作时,必须抛出 ReferenceError。typeof 的特殊处理仅适用于"不可解析的引用"(即从未声明的标识符),而非 TDZ 中的绑定。
Q2:var 在 ES6+ 中还有使用场景吗? 在模块顶层和函数内部,var 已经没有技术优势。唯一的边缘场景是需要利用 var 的函数作用域特性(如 eval 动态注入),但这本身也是反模式。
Q3:词法环境何时被 GC 回收? 当没有任何闭包引用某个词法环境时,它就可以被回收。V8 的隐藏类(Hidden Class)系统和逃逸分析会优化这一过程——如果编译器能证明闭包不会逃逸,变量甚至不会分配到堆上。
10. 总结表格 特性 varletconstfunction提升 ✅ 提升并初始化为 undefined ✅ 提升但不初始化 ✅ 提升但不初始化 ✅ 提升并初始化为函数体 TDZ ❌ ✅ ✅ ❌ 块级作用域 ❌ 函数/全局作用域 ✅ ✅ ✅(严格模式) 重复声明 ✅ 允许 ❌ SyntaxError ❌ SyntaxError ✅ 允许(覆盖) 全局属性 ✅ globalThis.x ❌ ❌ ✅ 循环迭代绑定 ❌ 共享 ✅ 每次新建 ✅ 每次新建 N/A