一篇搞明白JS变量提升

[Musings]
一篇文章搞清楚 JS 变量提升

最基础的变量提升

1
2
3
4
5
var a = 100;
console.log(a); // 100
console.log(b); // undefined
var b = 100;
console.log(b); // 100

真正的运行的时候编译器其实偷偷做了这一步:

1
2
3
4
5
6
var a = 100;
var b; //我提升了!升华了!✅ 声明提升到作用域顶部
console.log(a); // 100
console.log(b); // undefined
b = 100; //✅ 赋值留在原地
console.log(b); // 100

行,这是第一步,变量提升,接着看看函数提升

1
2
3
4
5
6
var a = 100;
test(); //test
console.log(a); // 100
function test() {
console.log("test");
}

看完变量和方法的变量提升,我们来看看 let 和 const

1
2
3
4
5
let a = 100;
console.log(a); // 100
console.log(b); // ReferenceError
let b = 100;
console.log(b); // 上一步就挂了,走不到这一步的

接着我们继续看看方法体作用于中的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function test() {
for (var i = 0; i < 3; i++) {
console.log(i); // 0,1,2
}
console.log(i); //3
})();
console.log(i); //ReferenceError

(function test() {
for (let i = 0; i < 3; i++) {
console.log(i); // 0,1,2
}
console.log(i); //ReferenceError
})();

基本到这差不多就能理解变量提升了,但是有一种比较特殊的异步+变量提升的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function test() {
// var i 被提升到这里
for (var i = 0; i < 3; i++) {
// 3个setTimeout回调都闭包引用同一个 i
setTimeout(() => {
console.log(i); //3,3,3
}, 100);
}
// 循环结束时 i = 3
// 100ms后所有回调执行,都读取同一个 i(值为3)
})();

(function test() {
for (let i = 0; i < 3; i++) {
// 每次迭代创建新的词法环境,有独立的 i
setTimeout(() => {
console.log(i); //0,1,2
}, 100);
}
})();

解释下这里发生这个情况的原因:

简洁的表达就是
“var 让所有回调共享同一个变量引用,最终都看到循环结束值 3。let 每次迭代创建新的词法环境,每个回调捕获独立的变量值。”

我脑子里的啰嗦想法就是 ⬇️

var 的作用域是函数作用域而 let 的作用与是块级作用域,所以这里所有的异步任务会被丢到异步队列,然后在执行 log 的时候,i 已经变成 3 了,所以输出了三遍
而 let 是块级作用域,所以在执行的时候每一个 log 函数都带一个变量 i,每个变量 i 在单独的内存空间里,所以不会互相污染,这就是 var 导致的变量提升所带来影响,从本身的额块级作用域被提升到了函数作用域,所以被 log 函数共享了内存空间

以上我的讲解都是自己的理解,实际上对于 let i 的变量在内存中都是独立的这点,得根据 javascript 引擎的写法来看,可能引擎会重用寄存器中的地址所以从
描述规范上来说,更准确的讲法是,let i 在词法环境里是互相隔离的

继续变量提升的case:

1
2
3
4
5
6
7
8
9
console.log(typeof func); // "function" ✅

var func = "I'm a variable";

function func() {
return "I'm a function";
}

console.log(typeof func); // "string" ✅

显而易见,函数提升的优先级高于变量

最后感受下块级作用域的隔离

1
2
3
4
5
6
7
8
9
10
11
12
{
function innerFunc() {
var secret = "我是秘密"; // 🔒 函数作用域
return secret;
}

console.log(innerFunc()); // "我是秘密" ✅
// console.log(secret); // ❌ ReferenceError ✅
}

console.log(innerFunc()); // ❌ 可能报错(取决于环境,严格模式下会报错)

最后附上一个对比图吧

特性 var let/const 函数声明
提升 ⚠️暂时性死区 ⚠️暂时性死区
作用域 函数级 块级 块级
重复说明
异步闭包 共享应用 独立 独立