前言
我们执行代码的过程,在 JavaScript 引擎的眼里可以分为两个重要的步骤,分别是预编译和执行。
预编译阶段会处理一些语法解析、变量声明提升等工作,为后续的代码执行做好准备;在预编辑完成后代码才开始执行。
我会用底层逻辑详细讲解代码执行的过程需要经历的过程。并且解答:
- 为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域。
- 变量声明的声明提升和函数声明的整体提升是如何实现的。
正文
我们在开始了解什么是预编译和预编译要经历的过程之前,我们需要先了解函数的自带属性、作用域和作用域连。
函数的自带属性
在JavaScript中,函数有一些自带的属性,一下是一些常见的属性:
length
:表示函数的参数个数。prototype
:指向函数的原型对象,原型对象用于定义构造函数的公共属性和方法。name
:函数的名称。arguments
:函数调用时传递的参数数组。
除了这些常见的属性外,函数还存在隐式属性,其中就包括[[scope]]
属性。[[scope]]
是 JavaScript 中函数的一个隐式属性,其中scope
翻译为域或范围。[[scope]]
属性仅供 JavaScript 引擎使用,我们无法直接访问。
在函数定义时,系统会通过scope
的内部原理定期去调用它,但不会让用户去用。当函数执行时,系统会创建一个执行期上下文的内部对象,此时[[scope]]
的值会发生变化。在函数内部访问变量时,实际访问的就是变量的scope
(作用域),scope
里有作用域链,系统会从作用域链底端依次向下去找变量。
预编译流程
我们用一个例子深入了解一下。
1 | javascript复制代码function a() { |
这段代码会输出什么呢?
让我们通过这段代码一起跟着JavaScript 引擎进入底层世界。
该代码大致流程:首先在全局预编译代码,然后全局执行,然后调用函数a;停止全局执行,开始预编译函数体a,预编译结束后执行函数a,最后调用函数b;停止执行函数a,预编译函数b,预编译完成后执行b;执行完b函数后返回执行a函数,a函数执行完返回全局,然后结束。
- 首先JavaScript 引擎对代码进行预编译(发生在全局中):
1. 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
2. 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
3. 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。在这几个预编译的步骤中,只会在寻找变量声明和函数声明,其他语句一律跳过。按顺序依次执行完这些步骤后,我们可以得到一个Global Object。
- 对代码的预编译结束后进行全局执行。
1 | javascript复制代码function a() {} |
在执行到a()
时开始调用函数,这时JavaScript 引擎会停止执行代码而去调用a函数并且对a函数进行预编译再执行。
3. 在函数体a中进行预编译(发生在函数体中):
1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
3. 形参和实参相互统一。
4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。在这几个预编译的步骤中,需要按顺序依次执行。
进行a步骤:创建一个函数上下文对象(Activation Object)
进行b步骤:在函数体里找形参和变量声明
1 | javascript复制代码Activation Object={ |
进行c步骤:形参和实参相互统一。
1 | javascript复制代码Activation Object={ |
进行d步骤:在函数体内找函数声明
1 | javascript复制代码Activation Object={ |
- 执行函数a。
1 | javascript复制代码function a() { |
当执行到var a = 200
时
1 | css复制代码Activation Object={ |
当执行到b()
时调用b函数,停止执行函数a,对函数体b进行预编译。
- 在函数体b中进行预编译(发生在函数体中):
1. 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
3. 形参和实参相互统一。
4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。你会发现函数预编译的方法是一样的。
我们会得到函数b的函数上下文对象为
1 | javascript复制代码Activation Object={ |
- 执行函数b
1 | javascript复制代码Activation Object={ |
代码执行完成。
小结
预编译的具体步骤。
- 在全局进行预编译(发生在全局中):
- 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
- 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
- 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。
- 在函数体中进行预编译(发生在函数体中):
- 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
- 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
- 形参和实参相互统一。
- 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。
解答
变量声明的声明提升和函数声明的整体提升是如何实现的?
根据预编译的流程,JavaScript 引擎找到变量声明和函数声明后会在运行前赋值,分别赋值为undefined和function(函数体),然后再运行。这样就实现了变量声明的声明提升和函数声明的整体提升。
作用域和作用域链
作用域是执行期上下文对象的集合,这种集合呈链式连接,我们把这种链状关系称之为作用域链。
我们通过这个代码进行解释。
1 | javascript复制代码function a() { |
在这个代码中有3个作用域,分别是全局作用域和a.[[scope]]和b.[[scope]]。它们之间的关系是这样的。
这个关系是怎么形成的呢?
- 在全局预编译完成后,函数a被整体提升生成作用域,并且作用域的0号位指向Global Object。
- 在函数a预编译时:函数a的作用域的0号位指向自己的上下文对象,1号位指向Global Object;函数b在函数a的预编译过程中被整体提升,生成作用域,并且作用域的0号位指向a的作用域。
- 在函数b预编译时:函数b的作用域的0号位指向自己的上下文对象;1号位指向函数a的作用域。
通过这些步骤就可以理解作用域链是什么,怎么形成的。
解答
为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域?
因为在作用域中只能从低位向高位查找,不能从高位找回低位。
我们通过在这张图进行理解。a函数执行阶段通过作用域的0号位查找需要的有效标识符,如果没有找到便通过作用域的1号位继续查找需要的有效标识符。
小结
我们通过预编译的底层逻辑解答了
- 为什么外层作用域无法访问内层作用域,内层作用域为什么可以访问外层作用域。
- 变量声明的声明提升和函数声明的整体提升是如何实现的。
并且了解了预编译的具体步骤:
- 在全局进行预编译(发生在全局中):
- 创建全局上下文对象(Global Object)用于存储全局的有效标识符。
- 在全局找变量声明,将变量名作为Global Object的属性名,属性值为undefined。
- 在全局找函数声明,将函数名作为Global Object的属性名,属性值为该函数体。
- 在函数体中进行预编译(发生在函数体中):
- 创建一个函数上下文对象(Activation Object)用于存储函数中的有效标识符。
- 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。
- 形参和实参相互统一。
- 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。
最后我们用运行代码结尾吧
1 | javascript复制代码function test(a, b) { |
自己动手试试会输出什么。
本文转载自: 掘金