跳到主要内容

var 提升、声明细节与 IIFE

var 提升的本质是什么?​

  • 机制: 创建阶段变量环境把 var 绑定初始化为 undefined;
  • 结果: var 声明可在语句之前使用;
  • 对比: let/const 创建阶段不初始化, 语句之前访问进入暂时性死区并抛 ReferenceError, 见 变量声明;

var 的重复声明与赋值提升有哪些细节?​

  • 重复声明: 允许, 同名 var 直接忽略, 既不报错也不清空旧值;
  • 提升时机: 函数 (或脚本) 开始时统一处理所有 var 声明, 与书写位置、分支是否执行都无关;
  • 只提升声明: var phrase = "Hello" 拆成"顶部声明"与"原处赋值"两步, 赋值不提升;
function sayHi() {
alert(phrase); // undefined; 声明已提升, 值还没有
var phrase = "Hello";
}
  • 等价改写: 上面等价于函数顶部 var phrase; 加上原处 phrase = "Hello";;
  • 死分支也算: if (false) { var phrase; } 永不执行, 但 phrase 仍已存在且为 undefined;
  • 边界: 嵌套函数内的 var 属于那个函数, 不会被外层函数提升;
  • 关联: 提升发生在创建阶段, 依赖执行上下文的 VariableEnvironment;

为什么要区分 LexicalEnvironment 与 VariableEnvironment?​

  • 起因: ES6 引入 let/const 的块级作用域;
  • 块执行流程: 保存当前词法环境为 oldEnv → 新建块级 Declarative 环境 (无 this 绑定, outer 为 oldEnv) → 将块内声明登记到对应环境 → 执行块 → 执行完恢复 oldEnv;
  • 结果: 块内 let/const/函数声明只存在于临时词法环境, 块外不可见;
  • 例外: var 声明会写回外层变量环境, 因此在块外仍可访问;
const a = 1;
{
const a = 2;
var test0 = function () {};
console.log(a); // 2
}
console.log(a); // 1; 块内 a 已随临时环境移除
console.log(test0()); // undefined; var 提升了 test0

var 为什么穿透块级作用域?​

  • 规则: var 只有函数作用域与全局作用域两种, 代码块不创建新绑定;
  • 表现: if/for 内声明的 var 在块外仍可见, 循环变量会活到函数结束;
if (true) var test = true;
test; // true

for (var i = 0; i < 10; i++) var one = 1;
i; // 10
one; // 1
  • 函数边界: 块包在函数里时, 变量停在函数作用域, 函数外访问抛 ReferenceError;
  • 原因: 早期代码块没有词法环境, var 是那段历史的遗留;
  • 对照: let/const 在块内建临时词法环境, 块结束即失效, 机制见上文"为什么要区分 LexicalEnvironment 与 VariableEnvironment?";

IIFE 解决了 var 的什么问题?​

  • 目的: 用函数作用域模拟块级作用域, 造一个立即使用又对外不可见的私有空间;
  • 形态: 函数表达式用括号包住后立即调用;
(function () {
var message = "Hello";
alert(message);
})();
  • 必须包括号的原因: 主代码流中以 function 开头会被当作函数声明, 而声明必须有名字且不能立即调用;
  • 本质: 括号让引擎按表达式上下文解析, 于是得到函数表达式而非声明;
  • 其他引导写法: !function(){...}()、+function(){...}()、(function(){...}()) 都是用运算符把语句变成表达式;
  • 现状: let/const 已提供块级作用域, 新代码不再需要 IIFE;

迁移旧脚本时 var 为什么要改成 let?​

  • 两个根因: 无块级作用域; 函数开始时统一处理声明;
  • 影响: 变量可见范围比预期大, 声明前读到的是 undefined 而非报错, 排查成本高;
  • 关联: 循环变量被回调共享的经典差异见 变量声明;
  • 结论: 新代码用 let/const; 理解 var 只为读懂与迁移旧脚本;