前言
预编译是JavaScript中一个重要的概念,经常出现在面试中。JavaScript中的"var"变量提升和预编译是紧密相关的概念,它们在理解JavaScript中变量声明和作用域的工作方式时非常重要。今天我们来深入聊聊预编译,让大家理解预编译和变量提升的底层逻辑。本文会使用简单的例子以及通俗易懂的语言,小白没压力。
变量提升(Hoisting)
我们先来看一串代码:
js
var a = 123
console.log(a);
很明显,答案输出123.让我们对这串代码改动一下。
js
console.log(x); // 输出undefined
var x = 5;
按正常逻辑来说,这串代码应该会报错,但是我们可以发现实际上输出的是undefined,这是因为使用var声明变量会出现变量提升的效果,就相当于:
js
var a
console.log(a);
a = 5
这就是变量提升
在JavaScript中,变量声明(使用var关键字)会被"提升"到其作用域的顶部,这意味着在变量声明之前使用变量是合法的,尽管在代码中实际声明变量的位置之前。这是因为JavaScript引擎在执行代码之前会将变量声明提升到作用域的顶部。又比如:
js
function example() {
console.log(x); // 输出 undefined
var x
X = 5
console.log(x); // 输出 5
}
example();
相当于:
js
function example() {
var x
console.log(x); // 输出 undefined
x = 5;
console.log(x); // 输出 5
}
example();
那为什么会变量提升呢?JavaScript的编译过程通常分为两个主要步骤:预编译(Compilation)和执行(Execution)。预编译是指JavaScript引擎在实际执行代码之前,对代码进行一些处理,包括变量提升和函数声明。在预编译阶段,JavaScript引擎会扫描代码,找到所有变量声明和函数声明,并将它们提升到适当的作用域。
预编译
-
预编译发生在函数执行之前
-
预编译发生在全局区
预编译发生在函数执行之前
- 创建AO对象 (Action Object)
- 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined
- 将形参和实参值统一
- 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
就拿上述为例:
js
function example(x) {
console.log(x); //输出 2
var x
x = 5
console.log(x); //输出 5
}
example(2);
js
//1.首先我们先创建一个AO对象
AO:{
//2.开始找变量声明和形参
x : undefined
//3.将形参和实参值统一
x :undefined ==> x : 2
//4.由于函数体内没有函数声明,跳过该步骤
}
我们已经完成一次预编译了,现在函数开始执行,由上到下,当函数执行到第一个 console.log(x) 时,它将从AO对象中寻找x的值 ,输出为2.函数继续向下执行,变量声明,跳过,当发现赋值语句 x = 5 时,将AO对象中x的值改变为5 . 函数继续向下执行到 第二个console.log(x) 时,输出为 5.
相信看到了这里,小伙伴们对预编译已经有一定了解了,接下来我们一起来看看一道有点难度的题:
js
function foo(a, b) {
console.log(a); //输出 1
c = 0
var c;
a = 3 // a : undefined ==> 3 函数执行时
b = 2 // b : undefined ==> 2
console.log(b); // 输出 2
function b() { }
function d() { }
console.log(b); // 输出 2
}
foo(1)
接下来我们看看是怎么预编译的: `
js
AO:{
a : undefined ==> a : 1 //统一值
b : undefined ==> b : function b(){}
c : undefined ==> d : function d(){}
//函数声明,属性名为函数名,值赋予函数体
}
当编译完成后再去执行函数,这样我们就很容易得出答案啦。
预编译发生在全局区
- 创建GO对象 (Global Object)
- 找变量声明,将变量声明作为GO的属性名,值为undefined
- 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体
我们直接上例子看看是怎么个事
js
var a = 100
function fn() {
console.log(a);
}
js
GO:{//找变量声明
a : undefined
//找函数声明
fn : function fn()
}
fn()
但我们可以发现,函数预编译和全局预编译在一段代码中通常一起发生,比如上图,当预编译全局完成时 ,开始执行,当执行完 a = 100后,准备执行函数fn(),这时预编译发生在函数执行时。
再来一个例子,他们结合在一起时:
js
a = 100
function fn() {
console.log(a); // 输出 100
b = 200
var b // 输出 200
console.log(b)
}
fn()
var global
预编译完成之后 ,我们发现这样GO对象和AO对象都会存在:
js
GO: {
a : undefined
fn : function fn()
}
AO: {
b : undefined
}
AO里面没有a,为什么当console.log(a)时会输出呢?而我们发现,GO函数里有a,是不是用了GO里面的a呢?这里我们就要引出调用栈的概念了
调用栈
上面所说的AO、GO对象其实是属于执行上下文的一部分,便于我们理解
调用栈用于跟踪函数的调用顺序和执行上下文的管理。每当函数被调用,一个新的执行上下文会被推入调用栈,表示该函数的执行。 全局预编译和函数预编译的结果在执行上下文中存储,然后被推入调用栈。调用栈的顶部始终包含当前正在执行的函数的上下文。概念可能有点难懂,接下来我们上图理解:

全局执行上下文分为变量环境和词法环境,而我们用var声明的变量存在变量环境中。我们都知道栈是先进后出,为什么这里全局执行上下文在栈底部呢,也就是比fn执行上下文先进来,因为在调用栈中,全局预编译通常会在函数预编译之前完成,因此全局预编译的结果位于调用栈的底部,而函数的预编译结果则根据函数调用的顺序依次位于调用栈中。
当函数执行时,需要输出a,但是在fn执行上下文里面并没有找到a这个东西,如果变量在当前函数的执行上下文找不到,JavaScript引擎会在全局执行上下文中寻找变量,如下图。

这也就解释了为什么最后可以输出a。
总结
当我们了解了预编译及调用栈时,我们再遇到这种问题就不用害怕了。而这类问题在面试题中也很容易遇到,小伙伴们一定要了解底层逻辑,在以后做题或面试时根本不慌。
今天的内容就到这啦,如果你觉得小编写的还不错的话,或者对你有所启发,请给小编一个辛苦的赞吧