起因:👇 一道面试题
最近,一位朋友参加面试时,遇到了这样一道笔试题,引起了我的兴趣:
var foo = 1;
function fn() {
foo = 3;
return;
function foo() {
// ...
}
}
fn();
console.log(foo);
这个例子中包含了变量提升,还涉及到了函数声明与变量声明的提升差异。接下来,通过这个例子,我们探讨下JavaScript中的变量提升机制。
变量提升机制解析 🎉
JavaScript代码执行的两阶段
在了解变量提升之前,我们首先需要简单了解JavaScript代码执行的两个阶段:编译阶段和执行阶段。
- 在
编译阶段
,JavaScript引擎会对代码进行遍历,识别出所有的变量和函数声明,并将它们提升至它们所在作用域的顶部。 - 紧接着,在
执行阶段
,代码会按照编写的逻辑顺序从上至下执行。
// 编译阶段
var a = 2; // 声明变量a并分配内存空间
function foo(b) { // 声明函数foo并分配内存空间
return b * 2;
}
// 执行阶段
console.log(foo(a)); // 输出: 4
![](https://img-blog.csdnimg.cn/img_convert/8ac2fd1cec661ac6cbfc9168bcee7cbf.webp?x-oss-process=image/format,png)
变量提升的细节
变量提升发生在JavaScript的编译阶段,具体细节我们接着往下看…
变量提升 ✨✨✨
变量提升是指var
声明的变量会被提升到其作用域的最顶端。然而,值得注意的是,虽然声明被提升,但赋值操作不会提升。
这意味着,即便是变量在代码中后面被声明,其在编译阶段已经被确认,但直到执行到赋值操作时,这个变量才会被赋予实际的值。
重要提示:变量提升仅针对声明操作,而非赋值操作。
console.log(a); // undefined
var a = 10;
console.log(a); // 10
尽管变量a
是在console.log(a)
之后被声明的,但由于变量提升的效果,它已经在当前作用域的顶部“存在”了。因此,第一次调用console.log(a)
时,输出的是undefined
(因为此时尚未赋值),而非抛出引用错误。
函数声明提升 ✨✨✨
与变量提升类似,函数声明(使用function
关键字的那种)也会被提升至它们所在作用域的顶端,不过不同的是,函数的提升包括函数名和函数体。
这意味着,在函数声明之前就可以调用该函数,因为在代码执行之前,JavaScript引擎已经知晓了函数的存在。
console.log(foo()); // "foo"
function foo() {
return "foo";
}
console.log(foo()); // "foo"
函数foo
被提升到全局作用域的顶部,因此在函数声明之前调用foo
也能够正常获取到函数定义,而不会抛出引用错误。
变量提升与函数声明提升的区别
尽管变量提升和函数声明提升听起来类似,但它们之间存在着本质的区别:
- 变量提升仅提升变量的声明,而不提升赋值操作。
- 函数声明提升则将函数的整个声明(包括函数体)都提升到作用域顶部。
下表简要比较了两者的区别:
特征 | 变量提升 (var ) | 函数声明提升 (function ) |
---|---|---|
提升内容 | 仅变量名 | 函数名及函数体 |
初始化值 | undefined | 函数定义 |
赋值提升 | 否 | 是 |
作用域 | 作用域内部 | 作用域内部 |
常见问题 | 可能导致逻辑混乱 | 较少导致混乱 |
面试题解析:😤 变量与函数声明的提升冲突
现在,让我们回到文章开头提到的面试题:
var foo = 1;
function fn() {
foo = 3;
return;
function foo() {
// todo
}
}
fn();
console.log(foo); // 输出:1
在面试题中,fn
函数内部有一个函数声明 function foo() {}
,这个声明会被提升到 fn
函数作用域的顶部。
同时,foo = 3
这行代码是一个变量赋值操作,它会找到 fn
函数作用域内的 foo
变量,并尝试给它赋值。但是由于函数声明 function foo() {}
已经提升了,它成为了 fn
函数作用域内的 foo
变量,所以 foo = 3
实际上是在尝试给这个函数赋值,而不是修改全局变量 foo
。
然而,由于 return
语句紧随 foo = 3
,这意味着 foo = 3
这行代码实际上从未被执行。return
语句会导致 fn
函数立即结束,任何在 return
之后的代码都不会执行。因此,foo = 3
这行代码被忽略了,函数内部的 foo
函数也没有被赋值。
foo = 3
这行代码并不会影响全局变量 foo
的值,因为它在 return
语句之后。因此,fn
函数执行后,全局变量 foo
的值仍然是 1。
ES6中的变量提升
let 和 const 关键字
ES6引入的let
和const
关键字为变量提升带来了新的规则。它们虽然也会被提升,但不会被初始化为undefined
,而是处于“暂时性死区”(TDZ)直到实际的声明语句执行。这意味着,在声明之前尝试访问这些变量会抛出引用错误,从而避免了var
带来的问题。
console.log(b); // 引用错误:b is not defined
let b = 10;
console.log(b); // 输出:10
大家看一下,这里使用了 let,结果是不是和 var 不一样了!
这里是一个表格,详细对比了 var
、let
和 const
在不同特性方面的区别:
特性 | var | let | const |
---|---|---|---|
作用域 | 函数作用域(function scope) | 块级作用域(block scope) | 块级作用域(block scope) |
提升行为 | 变量声明被提升,初始化不提升 | 提升,但存在暂时性死区 | 提升,但存在暂时性死区 |
重声明 | 同一作用域内可重声明 | 同一作用域内不能重声明 | 同一作用域内不能重声明 |
可变性 | 可重新赋值 | 可重新赋值 | 声明后不能重新赋值 |
使用建议 | 避免使用 | 需要变量可变时使用 | 值不变时使用,推荐用于常量 |
说明:
var
的函数作用域意味着它在整个函数中都可见,甚至在声明前。let
和const
的块级作用域使它们只在定义它们的代码块(如:循环、条件语句等)内可见。
箭头函数 和 class
在 ES6 中,除了传统的函数声明,还引入了箭头函数和 class
语法。这些新的函数和类声明也遵循提升规则,但是与 ES5 中的函数声明有一些不同。
箭头函数是表达式,它们不会被提升到作用域的顶部。如果你尝试在声明之前调用箭头函数,你会得到一个引用错误。
console.log(arrowFn()); // ReferenceError: arrowFn is not defined
const arrowFn = () => console.log('Hello from arrow function');
arrowFn
作为一个常量声明(使用 const
),它不会被提升。因此,尝试在声明之前调用它会导致引用错误。
Class 声明也不会被提升。class
是一种新的语法,用于创建构造函数和原型继承的语法糖。与箭头函数一样,如果你尝试在声明之前访问 class
,你会得到一个引用错误。
let peter = new Person(); // ReferenceError: Cannot access 'Person' before initialization
class Person {
constructor(name) {
this.name = name;
}
}
总结,在 ES6 中,函数声明提升的规则有所改变:
- 传统函数声明:仍然会被提升到作用域的顶部,包括函数名和函数体。
- 箭头函数:不会被提升,因为它们是表达式形式的函数。
- Class 声明:不会被提升,因为
class
是一种新的语法,用于创建构造函数和原型继承的语法糖。
规避变量提升陷阱的策略 🔖🔖🔖
那么,了解了变量提升的机制和潜在问题后,我们可以采取以下措施规避陷阱:
1. 优先使用let
和const
它们解决了 var
的变量提升问题,可以避免var
带来的变量提升问题。
// 使用 var
for (var i = 0; i < 3; i++) {
var bar = i;
setTimeout(function() {
console.log(bar); // 2, 2, 2
}, 1000);
}
// 使用 let 和 const
for (let i = 0; i < 3; i++) {
let bar = i;
setTimeout(function() {
console.log(bar); // 0, 1, 2
}, 1000);
}
2. 将函数声明放在逻辑的顶部
如果你在函数内部使用函数声明,确保将这些声明放在函数体的顶部,这样就不会因为变量提升而导致意外的行为。
function example() {
function foo() {
// 函数声明被提升到 example 函数的顶部
}
// 其他逻辑
}
foo
函数声明被放在 example
函数体的顶部,这样可以避免因为函数声明提升而导致的混淆。
3. 使用立即执行函数表达式(IIFE)
立即执行函数表达式可以创建独立作用域,隔离变量。例如:
(function() {
var foo = 'Hello World';
// foo 在这个函数作用域内是局部的,不会影响到外部作用域
})();
// foo 在这里是不可访问的
IIFE 创建了一个新的作用域,foo
在这个作用域内是局部的,不会影响到外部作用域。
4. 使用函数表达式而不是函数声明
函数表达式不会被提升,因此你可以控制函数的创建和执行时机。例如:
const myFunction = function() {
// 函数表达式不会被提升
};
myFunction
是一个函数表达式,它不会被提升到顶部,因此你可以控制它的创建和执行时机。
总结 🎉🎉🎉
在开发过程中,我们通常按照从上到下的顺序编写代码逻辑,而不去刻意考虑变量提升和函数声明提升。为了避免提升带来的潜在问题,我们可以考虑以下最佳措施:
- 优先使用
let
和const
来声明变量。这样可以避免变量提升导致的意外行为,因为let
和const
声明的变量在赋值之前是不可访问的。 - 在需要的时候才声明函数和类。避免在作用域顶部之外的地方引用尚未声明的函数或类。
这样,我们可以在编写代码时最大程度地保持逻辑的清晰和正确性,减少由变量提升和函数声明提升引起的错误。