JavaScript 声明提升 (Hoisting) 学习笔记
声明提升是 JavaScript 中一个非常独特且容易让人困惑的机制。它指的是:在代码执行之前,JavaScript 引擎会将变量和函数的声明部分“提升”到当前作用域的顶部,但赋值部分不会提升。
这意味着,无论你在代码的哪个位置声明变量或函数,它们都像是在作用域顶部被声明了一样。
一、核心机制:提升什么?不提升什么?
理解提升的关键在于区分声明 (Declaration)和赋值 (Initialization/Assignment)。
| 声明方式 | 声明提升? | 赋值提升? | 提升后的行为 |
|---|---|---|---|
var | ✅ 是 | ❌ 否 | 初始化为undefined |
let/const | ✅ 是 (语法层面) | ❌ 否 | 进入暂时性死区 (TDZ),访问报错 |
function(声明) | ✅ 是 | ✅ 是 (整个函数体) | 函数完全可用 |
function(表达式) | ✅ 是 (变量部分) | ❌ 否 | 变量为undefined,调用报错 |
二、var的声明提升
var是最典型的提升案例。代码执行前,var声明会被提升到作用域顶部,并初始化为undefined。
1. 基本示例
console.log(age);// 输出:undefined (不报错)varage=25;console.log(age);// 输出:25引擎实际执行顺序(逻辑上):
varage;// 声明提升,初始化为 undefinedconsole.log(age);// undefinedage=25;// 赋值console.log(age);// 252. 函数内的提升
functiontest(){console.log(name);// undefinedvarname="Alice";console.log(name);// "Alice"}test();等价于:
functiontest(){varname;// 提升console.log(name);// undefinedname="Alice";console.log(name);// "Alice"}3. 重复声明
var允许在同一作用域内重复声明同一个变量,不会报错,后面的声明会被忽略。
vara=1;vara=2;console.log(a);// 2三、let和const的声明提升与 TDZ
let和const也会被提升,但它们的行为与var截然不同。
1. 暂时性死区 (Temporal Dead Zone, TDZ)
在代码执行流到达声明语句之前,变量处于“未初始化”状态。此时访问该变量会抛出ReferenceError。这个区域称为TDZ。
console.log(age);// ❌ ReferenceError: Cannot access 'age' before initializationletage=25;为什么?
虽然声明被提升了,但初始化(赋值)没有。在let age = 25;这一行执行之前,age存在于作用域中,但不可访问。
2.const必须初始化
const声明必须在声明时立即赋值,否则报错。
constPI;// ❌ SyntaxError: Missing initializer in const declarationconstPI=3.14;// ✅3. 块级作用域的影响
let和const是块级作用域(Block Scope),提升仅限于当前块{}内。
if(true){console.log(x);// ❌ ReferenceErrorletx=10;}四、函数声明 vs 函数表达式
这是提升中最容易混淆的地方。
1. 函数声明 (Function Declaration)
完全提升。函数名和函数体都会被提升到作用域顶部。可以在声明之前调用。
sayHello();// ✅ 输出:Hello!functionsayHello(){console.log("Hello!");}等价于:
functionsayHello(){console.log("Hello!");}sayHello();2. 函数表达式 (Function Expression)
只提升变量声明,不提升赋值。变量初始化为undefined。如果在赋值前调用,会报错TypeError。
sayHi();// ❌ TypeError: sayHi is not a functionvarsayHi=function(){console.log("Hi!");};等价于:
varsayHi;// 声明提升,值为 undefinedsayHi();// ❌ TypeError: sayHi is not a functionsayHi=function(){...};// 赋值3. 箭头函数
箭头函数本质上是函数表达式,行为与var+ 函数表达式一致。
constadd=(a,b)=>a+b;// 如果用 varvaradd=(a,b)=>a+b;// 在赋值前调用 add 会报错五、混合提升场景
当var、let、const、函数声明混用时,提升优先级和规则如下:
- 函数声明优先级最高(完全提升)。
var声明次之(提升并初始化为undefined)。let/const最后(提升但进入 TDZ)。
示例 1:同名变量冲突
console.log(a);// undefined (var 提升)vara=1;leta=2;// ❌ SyntaxError: Identifier 'a' has already been declared注意:在同一作用域内,不能同时用var和let/const声明同名变量。
示例 2:函数与变量同名
console.log(test);// 输出函数定义 (函数声明优先)vartest=100;console.log(test);// 输出 100 (var 赋值覆盖了函数)functiontest(){return"function";}执行流程:
- 函数
test被完全提升。 var test声明被提升(但被函数声明覆盖,不重复声明)。- 执行
console.log(test)-> 输出函数。 - 执行
var test = 100-> 变量test被赋值为 100。 - 执行
console.log(test)-> 输出 100。
六、常见陷阱与最佳实践
1. 陷阱:在声明前使用var
// 容易写出难以调试的 bugif(condition){vardata=fetchData();}console.log(data);// 即使 condition 为 false,data 也是 undefined,而不是报错解决:使用let或const,利用 TDZ 在错误使用时立即报错。
2. 陷阱:循环中的var
varfuncs=[];for(vari=0;i<3;i++){funcs.push(function(){console.log(i);});}funcs[0]();// 输出 3 (所有函数共享同一个 i)funcs[1]();// 输出 3funcs[2]();// 输出 3解决:使用let,let是块级作用域,每次循环都会创建新的i。
letfuncs=[];for(leti=0;i<3;i++){funcs.push(function(){console.log(i);});}funcs[0]();// 输出 0funcs[1]();// 输出 1funcs[2]();// 输出 23. 最佳实践
- 始终使用
let和const:避免var带来的提升陷阱和变量污染。 - 声明在前,使用在后:养成在作用域顶部声明变量的习惯(虽然现代 JS 不强制,但可读性好)。
- 不要依赖提升:即使函数声明可以提前调用,也建议将函数定义放在调用之前,提高代码可读性。
- 利用 TDZ:如果代码在声明前访问了变量,
let/const会立即报错,帮助你快速发现逻辑错误。
七、总结
| 特性 | var | let/const | 函数声明 |
|---|---|---|---|
| 声明提升 | ✅ 是 | ✅ 是 (语法上) | ✅ 是 |
| 初始化提升 | ✅ 是 (undefined) | ❌ 否 (TDZ) | ✅ 是 (整个函数) |
| 作用域 | 函数作用域 | 块级作用域 | 函数作用域 |
| 重复声明 | ✅ 允许 | ❌ 不允许 | ❌ 不允许 |
| 访问未初始化变量 | 返回undefined | 抛出ReferenceError | N/A |
一句话总结:
JavaScript 引擎在执行代码前会先扫描并提升声明。
var提升并初始化为undefined,let/const提升但进入“暂时性死区”,函数声明则完全提升。为了代码的健壮性和可读性,请优先使用let和const,并遵循“先声明后使用”的原则。