JavaScript 提升(Hoisting)与声明优先级:一篇文章说透
一、什么是提升(Hoisting)
JS 引擎在执行代码前会先扫描一遍,把声明提到作用域顶部。这就是"提升"。
关键:提升的是声明,不是赋值。
js
console.log(a); // undefined(不是报错!)
var a = 1;
等价于:
js
var a; // 声明被提升
console.log(a); // undefined
a = 1; // 赋值留在原地
二、函数声明 vs 变量声明:优先级铁律
核心规则(记住这一条就够了)
函数声明优先于变量声明。
同名时,函数声明覆盖变量声明。但后续的赋值可以覆盖函数。
验证代码
js
console.log(foo); // [Function: foo]
var foo = 'bar';
function foo() {
console.log('function');
}
console.log(foo); // 'bar'
为什么? 引擎处理顺序:
javascript
1. 函数声明 foo 提升 → foo = function
2. 变量声明 foo 提升,但 foo 已存在 → 忽略 var 声明
3. 执行 console.log(foo) → function
4. 执行 foo = 'bar' → 赋值覆盖函数
5. 执行 console.log(foo) → 'bar'
三、三种声明方式的提升差异
| 声明方式 | 提升 | 初始化值 | 可重复声明 |
|---|---|---|---|
function |
声明+函数体一起提升 | 函数体 | ✅ |
var |
只提升声明 | undefined |
✅ |
let/const |
提升但进入 TDZ | 无(访问报错) | ❌ |
class |
提升但进入 TDZ | 无(访问报错) | ❌ |
四、var vs let/const 的 TDZ(暂时性死区)
js
console.log(x); // ReferenceError
let x = 1;
let/const 也会提升,但从作用域顶部到声明行之间是 TDZ,访问就报错。
js
let a = 1;
function fn() {
console.log(a); // ReferenceError!不是 1
let a = 2;
}
fn();
原因 :函数内部 let a 也被提升了,导致外层 a 被遮蔽,但又没到声明行,触发 TDZ。
五、函数表达式 vs 函数声明
js
// 函数声明 --- 整体提升
foo(); // 能执行 ✅
function foo() {}
// 函数表达式 --- 只有变量提升
bar(); // TypeError: bar is not a function ❌
var bar = function() {};
提升后等价于:
js
function foo() {} // 整体提升
var bar; // 只有声明提升 = undefined
foo(); // OK
bar(); // bar 是 undefined,不是函数
bar = function() {};
六、块级作用域中的奇怪行为
js
console.log(foo); // undefined(不是函数!)
if (true) {
function foo() { return 1; }
}
ES6 之前函数声明不能出现在块中(语法错误)。 ES6 之后:块中的函数声明被当作 类似 var 处理------声明提升到外层,但初始化为 undefined,进入块时才赋值。
严格模式下行为不同,不做展开,记住:别在块里写函数声明。
七、面试级综合题
js
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
console.log(a); // ?
答案:1
解析:
- 函数
b内部,function a() {}被提升到b的作用域顶部 a = 10赋值的是函数 b 内部的局部 a,不是全局的那个- 外层
console.log(a)拿的还是全局的1
js
var foo = 1;
(function() {
console.log(foo); // ?
foo = 2;
console.log(foo); // ?
var foo = 3;
console.log(foo); // ?
console.log(window.foo); // ?
})();
答案:undefined → 2 → 3 → 1
解析 :IIFE 内部 var foo 被提升,遮蔽全局 foo。全过程只操作局部变量,window.foo 始终是 1。
八、终极记忆口诀
- 函数优先:同名时函数声明 > var 声明
- 赋值为王:任何赋值操作都会覆盖之前的值(包括函数)
- var 是 undefined :提升后默认值是
undefined - let 是死区:提升后在 TDZ,访问就炸
- 函数声明带身体:整个函数体一起提升
- 表达式不带身体:只有变量声明提升,函数体留原地
九、不要依赖提升
写代码时遵守两条规则,一辈子不踩坑:
js
// ✅ 变量先声明再用
const foo = 1;
console.log(foo);
// ✅ 函数写在前面再调用
function doSomething() { /* ... */ }
doSomething();
理解提升是为了看懂别人的坑 ,不是为了给自己挖坑。