代码背景
javascript
var a = 1;
function fn(a) {
var a = 2;
function a() {}
var b = a;
console.log(a);
}
fn(3);
这段代码的输出结果是 2
,但背后隐藏着 JavaScript 的变量提升(Hoisting)、作用域、函数声明与变量声明的优先级等核心机制。本文将通过逐层分析,揭示代码执行的底层逻辑。
一、变量提升的本质
JavaScript 在代码执行前会进行预编译,将 变量声明 和 函数声明 提升到作用域顶部。需要注意的是:
- 变量声明 (如
var a
)会被提升,但 赋值操作 (如a = 1
)留在原地。 - 函数声明 (如
function a() {}
)会被整体提升,优先级高于变量声明。
原始代码在预编译阶段会被重构为以下形式:
javascript
// 全局作用域
var a; // 变量声明提升
a = 1; // 赋值留在原地
function fn(a) {
// 函数内部作用域
var a; // 参数 a 的声明(隐式)
var b; // 变量声明提升
function a() {} // 函数声明提升(覆盖参数 a)
a = 2; // 赋值操作留在原地
b = a; // 赋值操作留在原地
console.log(a);
}
fn(3);
二、执行过程的逐帧解析
1. 全局作用域初始化
- 变量
a
被声明并初始化为undefined
。 - 函数
fn
被完整声明。
执行 a = 1
后,全局变量 a
的值为 1
。
2. 调用 fn(3)
时的函数作用域
当执行 fn(3)
时,函数内部的作用域开始初始化:
- 参数
a
被隐式声明并赋值为3
(即a = 3
)。 - 变量
a
再次被声明(但已存在同名参数,此声明被忽略)。 - 变量
b
被声明并初始化为undefined
。 - 函数声明
a() {}
被提升,覆盖当前作用域的a
,此时a
变为函数对象。
3. 函数内部的赋值阶段
a = 2
:覆盖函数声明,将a
重新赋值为2
。b = a
:将a
的值2
赋给b
。console.log(a)
:输出当前作用域的a
,即2
。
三、关键机制解析
1. 函数参数与局部变量的优先级
函数参数会作为隐式变量声明,优先级等同于 var a
。如果函数内部再次声明同名变量,实际会指向同一个标识符。
2. 函数声明 vs 变量声明
- 函数声明 优先级高于 变量声明 ,因此
function a() {}
会覆盖参数a
。 - 但后续的
a = 2
是赋值操作,会覆盖函数声明的值。
3. 变量提升的陷阱
以下代码看似矛盾:
javascript
var a = 2;
function a() {}
实际上会被解析为:
javascript
function a() {} // 函数声明提升
var a; // 变量声明被忽略(已有同名函数)
a = 2; // 覆盖函数声明
四、总结:代码执行顺序的黄金法则
-
预编译阶段:
- 提升函数声明。
- 提升变量声明(忽略重复声明)。
- 函数参数隐式声明。
-
执行阶段:
- 按代码顺序执行赋值操作。
- 作用域链从内向外查找变量。
五、如何避免类似问题?
- 避免同名标识符:函数参数、局部变量、函数声明不要重名。
- 使用
let
和const
:ES6 的块级作用域可减少变量提升带来的副作用。 - 理解执行顺序:始终牢记函数声明 > 参数 > 变量声明。
六、思考题
如果将代码改为:
javascript
let a = 1;
function fn(a) {
let a = 2; // 这里会报错吗?
console.log(a);
}
fn(3);
答案是什么?欢迎在评论区讨论!