发布于 2024 年 2 月 26 日,星期一
JavaScript中的变量和函数提升是理解语言运行机制的关键。变量提升是指在代码执行前,变量声明会被提升到其作用域的顶部,但初始化保留在原位。函数提升则是指函数声明在代码执行前被提升到其作用域的顶部,使得函数可以在声明之前被调用。这两种提升机制的本质在于JavaScript的编译器在执行代码前会先扫描并处理所有的声明,确保变量和函数在作用域内是可访问的。理解这一点有助于避免常见的变量未定义错误,并能更清晰地组织代码结构。
系列首发于公众号『非同质前端札记』https://mp.weixin.qq.com/s?__biz=MzkyOTI2MzE0MQ==&mid=2247485576&idx=1&sn=5ddfe93f427f05f5d126dead859d0dc8&chksm=c20d73c2f57afad4bbea380dfa1bcc15367a4cc06bf5dd0603100e8bd7bb317009fa65442cdb&token=1071012447&lang=zh_CN#rd ,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。
提升
这篇文章,除非你已经理解了作用域、词法作用域、动态作用域、编译器、引擎
之间的联系,否则建议你先从之前的文章读起。JavaScript
代码在执行时是一行一行执行的,其实并不完全正确,有一种情况会导致这个假设是错误的。a = 2;
var a;
console.log(a); // ?这里会输出什么呢?
undefined
,因为 var a
声明是在 a = 2;
赋值之后的,他们会自然而然地认为变量被重新赋值了,因为会被赋予默认值 undefined
。但正确的输出结果为 2
;console.log(a); // ?这里会输出什么呢?
var a = 2;
鉴于上一个代码片段所表现出的某种非自上而下的行为特点,你可能会认为这段代码会输出 2
。或者还有人可能认为,由于变量 a
在使用前没有事先被声明过,会抛出 ReferenceError
异常。然而,两种猜测都不会,正确的输出结果为 undefined
。
那到底还是先有鸡还是先有蛋?到底是声明(蛋)在前,还是赋值(鸡)在前?,让我们带着这个问题接着向下看。
JavaScript
代码之前会首先对其进行编译。而编译阶段中的一部分工作就是先找到所有的声明,并用合适的作用域将他们关联起来。因此,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2;
时,你可能会认为这是一个声明。但 JavaScript
会将他们看成两个声明。var a
和 a = 2;
。第一个定义声明是在编译阶段进行的
,第二个赋值声明会被留在原地等待执行阶段
。var a; // 被提升后的声明
a = 2;
// var a; // 注意, var a 会被提升到顶部, 也就是上面提到的声明
console.log(a); // 2
// var a;
console.log(a); // undefined
var a = 2;
因此,这个过程就好像变量和函数声明从他们的代码中出现的位置被"移动"到了最上面,这个过程就叫做提升。
换句话说,先有蛋(声明)后有鸡(赋值)
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码的执行顺序,会造成非常严重的破坏。
考虑以下代码:
foo();
function foo() {
console.log(a);
var a = 2;
}
根据上面两个示例代码,先不要看答案。你可以试着将上面这段代码的解析后的结果写出来,巩固实践一下。
function foo() {
// var a; 提升后的声明
console.log(a); // undefined
var a = 2;
}
foo(); // foo 函数的声明也被隐含地提升了,因此第一行在调用 foo 可正常执行。
另外,需要注意的是,每个作用域都会进行提升操作
。这里的 foo(...)
函数自身也会在内容对 var a
进行提升(并不是提升到这个程序的最上方)。
再考虑以下代码:
foo(); // 会输出 success 吗?
var foo = function bar(){
console.log('success');
}
其实并不会,知道为什么吗?可以先自己想一下,再看下面的答案:
var foo;
foo(); // TypeError: foo is not a function
foo = function bar() {
console.log("success");
};
/**
你可能会疑惑为什么不是 ReferenceError?
因为后面的 var foo = ... 对 foo 进行提升,默认值为 undefined。因为并不会抛出 ReferenceError。
为什么会抛出 TypeError?
在前面几篇文章中我们说过,对变量进行一些不合规的操作时则会抛出 undefined, 因此,这里对 undefined 进行函数调用,则抛出 TypeError。
*/
因此,从上面的代码中得知,函数声明会被提升,但函数表达式并不会被提升。
再考虑以下代码:
foo();
bar();
var foo = function bar() {
console.log("success");
};
自己可以先试着写出这段代码的解析后的结果,在查看答案:
var foo;
foo(); // TypeError: foo is not a function
bar(); // ReferenceError: bar is not defined
foo = function {
var bar = ...self...
};
函数声明和变量声明都会被提升,但出现有多个 "重复" 声明的代码中是函数首先会被提升,然后才是变量。
foo(); // ?会输出什么呢?
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
}
自己可以先试着写出这段代码的解析后的结果,再查看答案:
function foo() {
console.log(1);
}
foo(); // 1
// var foo; 尽管 var foo; 声明出现在 function foo(...) 之前,但他还是重复声明,因此会被忽略。因为函数声明会被提升到普通变量之前。
// 此处函数表达式并不会被提升
foo = function () {
console.log(2);
}
再考虑以下代码:
foo(); // ?这里会输出什么呢?
function foo() {
console.log(1);
}
var foo = function () {
console.log(2);
}
function foo() {
console.log(3);
}
和之前一样,可先试试自己写出解析后的结果,再查看答案:
foo(); // 3
// 尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的函数声明的。
function foo() {
console.log(1);
}
var foo = function () {
console.log(2);
}
// 会使用这个函数的结果
function foo() {
console.log(3);
}
从上面代码可以看出,在同一个作用域内重复定义是很糟糕的,经常会导致各种奇怪的问题。
小测试:
foo(); // 这里会调用那个函数?
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}
自己先写出解析后的结果后,再来看看自己的答案是否正确:
foo(); // TypeError: foo is not a function
/**
为什么会抛出 TypeError 而不是 ReferenceError?
其实 foo(); 这段调用函数的代码会被解析成以下代码:
var foo;
foo();
看到这里,你应该明白,为什么会抛出 TypeError 异常了吧。如果还是没理解,建议你从头重新读起。
*/
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}
先有鸡(声明),后有蛋(赋值)。
var a = 2;
这段代码看起来是一个声明,但 JavaScript 引擎并不这么认为,它会将这段代码当做 var a
和 a = 2;
两个单独的声明来处理,第一个是在编译阶段执行的任务,第二个是在执行阶段执行的任务。 重复定义的函数声明后面的会覆盖前面的。
函数声明会被提升,但函数表达式并不会被提升。
Q:(question)
R:(result)
A:(attention matters)
D:(detail info)
S:(summary)
Ana:(analysis)
T:(tips)