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只为读懂与迁移旧脚本;