likes
comments
collection
share

干货分享(二)——深入理解JS预编译

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

引言

大家好呀!今天我们要探讨的是JavaScript中的一个神秘技能——预编译。听起来有点高大上的名词,但其实它就是让我们的代码更高效、更快速执行的秘密武器!快跟着小编来揭开这层神秘的面纱吧!

作用域链

在学习预编译之前,我们首先要了解JavaScript中的作用域链。

作用域链是一个由作用域对象构成的链式结构,用于决定在代码中查找变量时的顺序和范围。当在一个作用域中查找变量时,如果当前作用域中找不到该变量,JavaScript 引擎会沿着作用域链向上搜索,直到找到该变量或者到达全局作用域。

而在 JavaScript 中,每个函数都有一个内部属性 [[scope]],它等价于函数作用域链的引用,定义了该函数创建时的词法环境。[[scope]] 是一个隐藏属性,不能直接访问,它描述了在函数定义的位置的词法作用域链。

下面我们以一个简单的例子来描述一下[[scope]]的变换过程。

var a = 2;
function fn(a) {
    var b = 2;
    function foo() {
        var b = 2;
    }
}
  • step1:创建GO(Global Object)对象,并在全局中寻找有效标识符,加载GO对象中。

干货分享(二)——深入理解JS预编译

  • step2: fn函数声明,fn具有一个内部属性 [[scope]],并且会创建一个属于自己的AO(Activation Object)对象,创建方法与GO类似。

干货分享(二)——深入理解JS预编译

  • step3:将fn[[scope]]的0号位指向自己的AO,1号位指向GO

干货分享(二)——深入理解JS预编译

  • step4:foo函数声明,foo也具有一个内部属性 [[scope]],并且会创建一个属于自己的AO(Activation Object)对象。

干货分享(二)——深入理解JS预编译

  • step5:将fn[[scope]]的0号位指向自己的AO,1号位指向fn的GO

干货分享(二)——深入理解JS预编译

经过编译阶段,函数的 [[scope]] 属性会被设置,形成了在运行时所使用的实际作用域链。这种机制实现了 JavaScript 中的词法作用域规则,确保变量的正确解析和访问。

什么是预编译

一份程序在被执行时,JS引擎会分为几步进行:

  • step1:全局编译
  • step2:全局运行,如果在执行过程中遇到了函数调用
  • step3:函数编译
  • step4:函数运行,如果在执行过程中遇到了函数调用,再进行step3
  • step5:函数运行结束后,再进行step2
  • 直到程序执行完成

也就是说编译总在运行的前一秒,在运行之前才开始编译。

全局编译

在全局作用域中,JavaScript引擎会对全局变量和全局函数进行预编译处理。这意味着无论变量和函数在代码中的位置如何,都可以在全局作用域中被正确访问。

全局编译主要分为以下几步:

  • step1. 创建 GO 对象
  • step2. 找变量声明,将变量名作为GO的属性名,值为undifined
  • step3. 找函数声明,将函数名作为GO的属性名,值为该函数体

接下来让我们来看一个示例,你可以在看解释之前想一想会输出什么:

var a;
function a() { };
console.log(a);
a = 2;
  • step1:创建 GO 对象

    GO{}

  • step2: 在全局找变量声明,将变量名作为GO的属性名,值为undifined

    GO{ a:undifined }

  • step3: 在全局找函数声明,将函数名作为GO的属性名,值为该函数体

    GO{a:[Function: a]}

    对象中属性名不能相同,所以被a被修改

  • step4:全局运行

    • 运行到console.log(a),按照作用域链从GO中查找a,输出 [Function: a]
    • 运行到a = 2,GO变为GO{a:2}

所以按照上面的执行步骤,console.log(a);输出的是[Function: a]

函数编译

在函数作用域中,JavaScript引擎会对局部变量和函数进行预编译处理。这样,在函数内部就可以直接访问这些局部变量和函数,而无需担心作用域链的问题。

函数编译主要分为以下几步(只有遇到函数调用才会执行):

  • step1. 创建一个 AO 对象
  • step2. 找形参和变量声明,将形参和变量名作为AO的属性名,值为undifined
  • step3. 形参和实参统一
  • step4. 在函数体内找函数声明,将函数名作为GO的属性名,值为该函数体

接下来让我们来看一个示例,你可以在看解释之前想一想会输出什么:

var a = 2;
function fn(a) {
    console.log(a);
    var a = 2;
    function a() {}
    console.log(a);
}

fn(5);
  • step1:创建 GO 对象

    GO{}

  • step2: 在全局找变量声明,将变量名作为GO的属性名,值为undifined

    GO{ a:undifined }

  • step3: 在全局找函数声明,将函数名作为GO的属性名,值为该函数体

    GO{a:undifined, fn:[Function: fn]}

  • step4:全局运行

    • 运行到var a = 2;GO变为GO{a:2, fn:[Function: fn]}
    • 函数声明 跳过
    • 函数执行,进行函数编译
  • step5:创建 AO 对象

    • AO{}
  • step6:找形参和变量声明,将形参和变量名作为AO的属性名,值为undifined

    • 先找形参
    • AO{a:undifined, fn:[Function: fn]}
    • 再找变量声明
    • AO{a:undifined, fn:[Function: fn]}
  • step7:形参和实参统一

    • AO{a:5, fn:[Function: fn]}
  • step8: 在函数体内找函数声明,将函数名作为GO的属性名,值为该函数体

    • GO{a:[Function: a], fn:[Function: fn]}
  • step9: 函数执行

    • console.log(a); 先在自己的作用域链顶端0也就是AO中寻找a,找到输出[Function: a]
    • var a = 2; GO{a:2, fn:[Function: fn]}
    • console.log(a);先在自己的作用域链顶端0也就是AO中寻找a,找到输出2

所以按照上面的执行步骤,输出的是:

干货分享(二)——深入理解JS预编译

小测试

现在,让我们来做一个小测试吧!在下面的代码中,尝试解释模拟JS引擎的预编译过程,并且写成每个console.log()的输出。

function fn(a) {
    console.log(a);
    var a = 123;
    console.log(a);
    function a() { }
    console.log(a);
    var b = function () { } // 函数表达式
    console.log(b);
    function c() { }
    var c = a;
    console.log(c);
}
fn(1);

注意var b = function () { }其实是一个函数表达式,不是函数声明,不具有声明提升

最终输出结果为:

干货分享(二)——深入理解JS预编译

你模拟的对不对呢?对的话,那么恭喜你已经完美掌握JS引擎的JS的预编译过程了。如果有哪里不对就要再仔细回顾一下上面的内容了。

结语

通过本文的介绍,相信大家对JavaScript预编译有了更深入的了解。预编译不仅可以帮助我们更好地理解JavaScript代码的执行机制,还可以帮助我们编写出更加高效和可维护的代码。希望本文能够对大家有所帮助,也欢迎大家在评论区留言讨论更多关于JavaScript预编译的话题。今天的内容就到这里了。如果你觉得这篇文章有帮助或有所启发,别忘了给我一个鼓励的赞哦!

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