几分钟带你了解预编译,拿下大厂面试题 前言 正文 小结

前言

我们执行代码的过程,在 JavaScript 引擎的眼里可以分为两个重要的步骤,分别是预编译和执行。

预编译阶段会处理一些语法解析、变量声明提升等工作,为后续的代码执行做好准备;在预编辑完成后代码才开始执行

我会用底层逻辑详细讲解代码执行的过程需要经历的过程。并且解答:

  • 为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域。
  • 变量声明的声明提升和函数声明的整体提升是如何实现的。

正文

我们在开始了解什么是预编译和预编译要经历的过程之前,我们需要先了解函数的自带属性、作用域和作用域连。

函数的自带属性

在JavaScript中,函数有一些自带的属性,一下是一些常见的属性:

  1. length:表示函数的参数个数。
  2. prototype:指向函数的原型对象,原型对象用于定义构造函数的公共属性和方法。
  3. name:函数的名称。
  4. arguments:函数调用时传递的参数数组。

除了这些常见的属性外,函数还存在隐式属性,其中就包括[[scope]]属性。[[scope]]是 JavaScript 中函数的一个隐式属性,其中scope翻译为域或范围。[[scope]]属性仅供 JavaScript 引擎使用,我们无法直接访问。

在函数定义时,系统会通过scope的内部原理定期去调用它,但不会让用户去用。当函数执行时,系统会创建一个执行期上下文的内部对象,此时[[scope]]的值会发生变化。在函数内部访问变量时,实际访问的就是变量的scope(作用域),scope里有作用域链,系统会从作用域链底端依次向下去找变量。

预编译流程

我们用一个例子深入了解一下。

1
2
3
4
5
6
7
8
9
10
javascript复制代码function a() {
function b() {
var b = 55
console.log(a);
}
var a = 200
b()
}
var glob = 50
a()

这段代码会输出什么呢?

让我们通过这段代码一起跟着JavaScript 引擎进入底层世界。

该代码大致流程:首先在全局预编译代码,然后全局执行,然后调用函数a;停止全局执行,开始预编译函数体a,预编译结束后执行函数a,最后调用函数b;停止执行函数a,预编译函数b,预编译完成后执行b;执行完b函数后返回执行a函数,a函数执行完返回全局,然后结束。

  1. 首先JavaScript 引擎对代码进行预编译(发生在全局中):
1. 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
2. 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
3. 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。在这几个预编译的步骤中,只会在寻找变量声明和函数声明,其他语句一律跳过。按顺序依次执行完这些步骤后,我们可以得到一个Global Object。

屏幕截图 2024-04-27 133421.png

  1. 对代码的预编译结束后进行全局执行。
1
2
3
javascript复制代码function a() {}
var glob = 50
a()

在执行到a()时开始调用函数,这时JavaScript 引擎会停止执行代码而去调用a函数并且对a函数进行预编译再执行。
3. 在函数体a中进行预编译(发生在函数体中):

1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
3. 形参和实参相互统一。
4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。在这几个预编译的步骤中,需要按顺序依次执行。

进行a步骤:创建一个函数上下文对象(Activation Object)

屏幕截图 2024-04-27 135402.png

进行b步骤:在函数体里找形参和变量声明

1
2
3
4
5
6
7
8
9
10
javascript复制代码Activation Object={
a:undefined, (形参)
a:undefined (实参)
}//是错误的

因为对象里不能存在相同的键,所以如果会进行重叠覆盖

Activation Object={
a:undefined
}

进行c步骤:形参和实参相互统一。

1
2
3
javascript复制代码Activation Object={
a:undefined
}

进行d步骤:在函数体内找函数声明

1
2
3
4
javascript复制代码Activation Object={
a:undefined,
b:function
}
  1. 执行函数a。
1
2
3
4
5
6
7
8
javascript复制代码function a() {
function b() {
var b = 55
console.log(a);
}
var a = 200
b()
}

当执行到var a = 200

1
2
3
4
css复制代码Activation Object={
a:200,
b:function
}

屏幕截图 2024-04-27 163958.png

当执行到b()时调用b函数,停止执行函数a,对函数体b进行预编译。

  1. 在函数体b中进行预编译(发生在函数体中):
1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
3. 形参和实参相互统一。
4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。你会发现函数预编译的方法是一样的。

我们会得到函数b的函数上下文对象为

1
2
3
javascript复制代码Activation Object={
b:undefined
}
  1. 执行函数b
1
2
3
javascript复制代码Activation Object={
b:55
}

屏幕截图 2024-04-27 170005.png

代码执行完成。

小结

预编译的具体步骤。

  1. 在全局进行预编译(发生在全局中):
    1. 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
    2. 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
    3. 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。
  2. 在函数体中进行预编译(发生在函数体中):
    1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
    2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
    3. 形参和实参相互统一。
    4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。

解答

变量声明的声明提升和函数声明的整体提升是如何实现的?

根据预编译的流程,JavaScript 引擎找到变量声明和函数声明后会在运行前赋值,分别赋值为undefined和function(函数体),然后再运行。这样就实现了变量声明的声明提升和函数声明的整体提升。

作用域和作用域链

作用域是执行期上下文对象的集合,这种集合呈链式连接,我们把这种链状关系称之为作用域链。

我们通过这个代码进行解释。

1
2
3
4
5
6
7
8
9
10
javascript复制代码function a() {
function b() {
var b = 55
console.log(a);
}
var a = 200
b()
}
var glob = 50
a()

在这个代码中有3个作用域,分别是全局作用域和a.[[scope]]和b.[[scope]]。它们之间的关系是这样的。

屏幕截图 2024-04-27 170806.png

这个关系是怎么形成的呢?

  1. 在全局预编译完成后,函数a被整体提升生成作用域,并且作用域的0号位指向Global Object。

屏幕截图 2024-04-27 172549.png

  1. 在函数a预编译时:函数a的作用域的0号位指向自己的上下文对象,1号位指向Global Object;函数b在函数a的预编译过程中被整体提升,生成作用域,并且作用域的0号位指向a的作用域。

屏幕截图 2024-04-27 173104.png

  1. 在函数b预编译时:函数b的作用域的0号位指向自己的上下文对象;1号位指向函数a的作用域。

屏幕截图 2024-04-27 173444.png

通过这些步骤就可以理解作用域链是什么,怎么形成的。

解答

为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域?

因为在作用域中只能从低位向高位查找,不能从高位找回低位。

屏幕截图 2024-04-27 173104.png

我们通过在这张图进行理解。a函数执行阶段通过作用域的0号位查找需要的有效标识符,如果没有找到便通过作用域的1号位继续查找需要的有效标识符。

小结

我们通过预编译的底层逻辑解答了

  • 为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域。
  • 变量声明的声明提升和函数声明的整体提升是如何实现的。

并且了解了预编译的具体步骤:

  • 在全局进行预编译(发生在全局中):
    1. 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
    2. 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
    3. 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。
  • 在函数体中进行预编译(发生在函数体中):
    1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
    2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
    3. 形参和实参相互统一。
    4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。

最后我们用运行代码结尾吧

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码function test(a, b) {
console.log(a);
c = 0
var c;
a = 3
b = 2
console.log(b);
function b() { }
console.log(b);
}
test(1)

自己动手试试会输出什么。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%