likes
comments
collection
share

你了解预编译吗?几分钟带你了解它

作者站长头像
站长
· 阅读数 27

前言

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

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

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

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

正文

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

函数的自带属性

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

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

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

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

预编译流程

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

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。

你了解预编译吗?几分钟带你了解它
  1. 对代码的预编译结束后进行全局执行。

    function a() {}
    var glob = 50
    a()
    

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

  2. 在函数体a中进行预编译(发生在函数体中):

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

    在这几个预编译的步骤中,需要按顺序依次执行。

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

你了解预编译吗?几分钟带你了解它

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

Activation Object={
	a:undefined, (形参)
    a:undefined (实参)
}//是错误的

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

Activation Object={
	a:undefined 
}

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

Activation Object={
	a:undefined
}

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

Activation Object={
	a:undefined, 
    b:function
}
  1. 执行函数a。

    function a() {
        function b() {
            var b = 55
            console.log(a);
        }
        var a = 200
        b()
    }
    

    当执行到var a = 200

    Activation Object={
    	a:200, 
        b:function
    }
    
你了解预编译吗?几分钟带你了解它

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

  1. 在函数体b中进行预编译(发生在函数体中):

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

    你会发现函数预编译的方法是一样的。

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

    Activation Object={
    	b:undefined
    }
    
  2. 执行函数b

    Activation Object={
    	b:55
    }
    
你了解预编译吗?几分钟带你了解它

代码执行完成。

小结

预编译的具体步骤。

  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(函数体),然后再运行。这样就实现了变量声明的声明提升和函数声明的整体提升。

作用域和作用域链

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

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

function a() {
    function b() {
        var b = 55
        console.log(a);
    }
    var a = 200
    b()
}
var glob = 50
a()

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

你了解预编译吗?几分钟带你了解它

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

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

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

你了解预编译吗?几分钟带你了解它
  1. 在函数b预编译时:函数b的作用域的0号位指向自己的上下文对象;1号位指向函数a的作用域。

你了解预编译吗?几分钟带你了解它

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

解答

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

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

你了解预编译吗?几分钟带你了解它

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

小结

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

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

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

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

    2. 在函数体里找形参和变量声明,将形参和变量名作为Activation Object的属性名,属性值值为undefined。

    3. 形参和实参相互统一。

    4. 在函数体内找函数声明,将函数名作为Activation Object的属性名,属性值为该函数体。

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

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) 

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

转载自:https://juejin.cn/post/7362046189011435535
评论
请登录