干货分享(二)——深入理解JS预编译
引言
大家好呀!今天我们要探讨的是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对象中。
- step2: fn函数声明,fn具有一个内部属性
[[scope]]
,并且会创建一个属于自己的AO(Activation Object)对象,创建方法与GO类似。
- step3:将fn[[scope]]的0号位指向自己的AO,1号位指向GO
- step4:foo函数声明,foo也具有一个内部属性
[[scope]]
,并且会创建一个属于自己的AO(Activation Object)对象。
- step5:将fn[[scope]]的0号位指向自己的AO,1号位指向fn的GO
经过编译阶段,函数的 [[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),按照作用域链从GO中查找a,
所以按照上面的执行步骤,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
- console.log(a); 先在自己的作用域链顶端0也就是AO中寻找a,找到输出
所以按照上面的执行步骤,输出的是:
小测试
现在,让我们来做一个小测试吧!在下面的代码中,尝试解释模拟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的预编译过程了。如果有哪里不对就要再仔细回顾一下上面的内容了。
结语
通过本文的介绍,相信大家对JavaScript预编译有了更深入的了解。预编译不仅可以帮助我们更好地理解JavaScript代码的执行机制,还可以帮助我们编写出更加高效和可维护的代码。希望本文能够对大家有所帮助,也欢迎大家在评论区留言讨论更多关于JavaScript预编译的话题。今天的内容就到这里了。如果你觉得这篇文章有帮助或有所启发,别忘了给我一个鼓励的赞哦!
转载自:https://juejin.cn/post/7362084722819989544