一国两制:JS 变量提升与块级作用域的和平共处白皮书

41 阅读3分钟

一、ES5 只有“函数国土”——3 个反直觉现场

先把时间拨回 2015 年之前,ES5 里根本没有块级作用域,var 声明 + 变量提升 把代码变成“玄学现场”。

1. for 循环泄露:闭包全是 3

var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = function () { console.log(i); };
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3

底层原理

  • V8 编译阶段把 var i 提到全局 variableEnvironment;
  • 三次循环只是对同一块内存写值;
  • 函数 [[Scope]] 指向同一环境 → 执行时统一拿到最终值。

2. if 块“漂”到外面

function foo(flag) {
  if (flag) { var tmp = 'hello'; }
  console.log(tmp); // undefined 而不是 ReferenceError
}
foo(false);

底层原理
var tmp 在函数作用域内直接提升,块 {} 只是“视觉围栏”,不产生新环境。

3. 参数被变量声明覆盖

function bar(x) {
  console.log(x); // function x {}
  var x = 5;
  console.log(x); // 5
}
bar(10);

底层原理
形参 → 变量提升 → 函数声明 三股力量在 variableEnvironment 里“抢名字”,函数权重最高,于是第一次 log 看到函数体。


二、JS 执行机制速览

  1. V8 引擎两阶段

    • 编译:扫描声明,创建执行上下文(变量环境 + 词法环境)
    • 执行:逐行取值,遇到函数就压调用栈,完毕弹出回收内存
  2. 调用栈(以函数为单位)

    | show()       | ← 栈顶
    | global main  | ← 栈底
    
  3. 执行上下文内部

    ExecutionContext
    ├─ variableEnvironment   ← var / function 声明
    ├─ lexicalEnvironment    ← let / const / class
    └─ outerEnvironmentReference
    

三、变量提升——“快上车”的设计缺陷

  • 为什么当初要提升?

    1. 10 天工期,KPI 项目,浏览器大战,越简单越好
    2. 没有块级作用域,直接把变量统统抬到函数顶部,编译器实现最省事
  • 带来的坑

    1. 不知不觉覆盖全局
    2. 本应销毁的变量幸存下来
    3. 代码阅读跳来跳去,调试全靠 console

四、ES6「一国两制」——新区旧区分开治理

TC39 不能 break 互联网,于是设计“双轨制”:

居民养老地点制度是否提升
var / functionvariableEnvironment(旧区)函数作用域✅ 完全提升
let / const / classlexicalEnvironment(新区)块级作用域❌ 仅注册,不提升

引擎内部执行上下文示意图:

ExecutionContext
├─ variableEnvironment   ← var 挂这里
├─ lexicalEnvironment    ← let/const 挂这里(块级栈)
└─ outerEnv

五、新区也有红线:暂时性死区(TDZ)

来看一段“必挂”代码:

function show() {
  let a = 1;
  {
    console.log(a); // ReferenceError
    let a;
    a = 2;
  }
}
show();

拆解

  1. 进入内部块,V8 发现 let a → 新建 lexicalEnvironment 栈帧,注册 a 为“未初始化”;
  2. let a 之前的任何访问都会触发 TDZ;
  3. 即使外层 a = 1 已经存在,也被“同名遮蔽”但尚未初始化,于是报错。

一句话记忆

“块里重新 let,先写后用是王道;提前读取必爆炸,TDZ 教你做人。”


六、V8 底层栈图:块级作用域如何“用完即焚”

{
  let x = 10;
  console.log(x);
}
console.log(x); // ReferenceError

运行时 lexicalEnvironment 维护一个小型栈:

[global][funcEnv][blockEnv: x=10]   // 块内
[global][funcEnv]                      // 块结束,弹出 → x 销毁

块执行完,栈帧弹出,变量内存被回收,真正实现“所见即所得”的局部寿命。


七、写给日常开发的 3 条国策

  1. 默认 const → 实在需要再改 letvar 写进史书;
  2. {} 当国境线,绝不把 let/const 变量 leak 出去;
  3. 调试打开 DevTools Scope 面板,一眼分辨 variableEnvironment vs lexicalEnvironment。

八、30 秒口诀背完走人

var 养老置顶头,
let/const 块内留;
同名再 let 先别用,
TDZ 等你炸街口。


点赞、收藏、转发,评论区聊聊你踩过最惨的作用域坑!