揭秘JavaScript预编译:解析执行前的关键步骤

154 阅读6分钟

前言

在JavaScript中,预编译是一个重要且常常被忽视的概念。在代码执行前,JavaScript引擎会进行一系列的预处理操作,这些操作包括变量和函数声明的提升、作用域的创建以及this关键字的绑定等。本文将深入探讨JavaScript预编译的过程,揭示其背后的秘密,并帮助读者更好地理解JavaScript代码在执行前的准备工作。通过对预编译过程的深入了解,读者将能够更加熟练地运用JavaScript语言,写出更加高效、可靠的代码。

函数是否存在声明提升?

我们来看这段代码:

    console.log(a);//undefined
    var a = 123//声明提升
    foo()//可以访问 函数声明整体提升
    function foo(){
        var a = 132
        console.log(a);
    }

在我们之前的文章中 ("跃迁ES6:探索JavaScript语言的最新进展(一)") 提到var 声明的变量是存在声明提升的,在访问a时会显示undefined,那我们在定义一个foo()方法前访问foo()是否能访问到呢?答案是:Yes!为什么我们并没有定义函数却能直接访问呢?这就是这篇文章的主要内容:“js的预编译”

JavaScript预编译

1. 预编译发生在函数执行之前(四部曲)

  • 创建AO对象(Action Object)记录有效标识符

  • 找形参和变量声明,将变量声明和形参作为AO属性名,值为undefined

  • 将形参和实参值统一

  • 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

    掘友们看见这四部曲时,可能会想:这这这,这是啥呀?能不能说简单点。那么我们将结合代码给大家讲解

function fn(a){
    console.log(a);//function a(){}
    var a = 123
    console.log(a);//123
    function a(){}//函数声明
    console.log(a);//123
    var b = function(){}//函数表达式
    console.log(b);//function b(){}
    function d(){}
    var d = a
    console.log(d);//123
}
//AO:{
// a:undefined 1 function a(){},
// b:undefined,
// d:undefined  function d() {}, 
//}
fn(1)

我们来看看到底是怎样运行的:

①创建AO对象(Action Object)记录有效标识符

此时,我们会在fn全局上下文创建一个AO对象用来记录有效标识符

② 找形参和变量声明,将变量声明和形参作为AO属性名,值为undefined

先看形参a,在AO对象创建了一个键值对,key为a,value 为undefined,没有其他形参了

此时再找变量声明:

var a = 123 这里又定义了a,将AO中的key: a覆盖,值依旧为undefined

var b = function(){} 又在AO中添加了一个键值对 b:function(){}

var d = a 添加键值对 d:undefined

此时形参和变量声明就找完了

③将形参和实参值统一

我们向foo()中传入了一个实参a=1,将AO中的a对应的value改为1

④在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

function a(){}//函数声明 这里将a又覆盖为function a(){}

function d(){} 这里创建键值对 d: function d(){}

到这里我们的预编译工作就全部结束了,现在开始运行

console.log(a);//function a(){}

a = 123 赋值将a改为123

console.log(a);//123

var b = function(){}//函数表达式 b赋值为 b = function(){}

console.log(b);//function b(){}

d =a 赋值

console.log(d);//123

此时,程序运行完毕!

2.预编译发生在全局

  • 创建GO对象(Global Object)
  • 找变量声明,将变量声明和形参作为GO属G性名,值为undefined
  • 在全局内找函数声明,将函数名作为GO对象的属性名,值赋予函数体

预编译发生在全局时,有三个步骤,与预编译发生在函数执行之前大致相同,只是全局编译没有形参而已

同样的,我们看代码理解:

global = 100
function fn(){
    console.log(global);//undefined
    global = 200
    console.log(global);//200
    var global = 300
}
fn()
var global

① 创建GO对象(Global Object)

在全局上下文创建一个GO对象

② 找变量声明,将变量声明作为GO属性名,值为undefined

这里我们开始找声明变量,不过多阐述 找到 global:undefined

③在全局内找函数声明,将函数名作为GO对象的属性名,值赋予函数体

找到 fn:function fn(){}

到这时我们的全局预编译就完成了,我们可以看到在运行时调用fn()方法,在调用之前,会展开对于fn()方法的预编译,开始执行四部曲。

最后我们再来看一段代码:

// Go :{
//     global:undefined 100
//     fn : function fn(){}
// }
var global = 100
function fn(){
    console.log(global);
}
// Ao:{
//
// }
fn()//100
//有一个调用栈
//全局执行上下文GO先入栈
// 全局执行上下文中有
//变量环境 :var声明的变量 global = 100 fn = func(){}
//词法环境:let,const声明的变量
//fn执行上下文再入栈
//此时fn中未声明变量和方法
//调用console.log(global);
//先找fn的词法环境,再找变量环境,没找到,但是有一个指针指向全局执行上下文
//于是又在全局执行上下文的词法环境中找,没找到在变量环境找

当我们在调用发fn()方法时,方法中没有global属性,但是仍然可以输出global = 100,这是为什么呢?这是因为有一个调用栈,全局执行上下文GO先入栈,全局执行上下文中有 变量环境 :var声明的变量 global = 100 ,fn = func(){},词法环境:let,const声明的变量,fn执行上下文再入栈,此时fn中未声明变量和方法,调用console.log(global),先找fn的词法环境,再找变量环境,没找到,但是有一个指针指向全局执行上下文,于是又在全局执行上下文的词法环境中找,没找到在变量环境找,找到global且值为100,输出。

结尾

总而言之,预编译是JavaScript中一个重要的概念,它通过提前处理变量和函数的声明,为代码执行阶段做准备。通过预编译,我们可以在代码执行之前创建变量和函数,并将它们存储在内存中供后续使用。了解和掌握预编译的原理和过程对于理解JavaScript的执行机制至关重要。通过充分利用预编译的特性,我们可以更好地组织和管理我们的代码,提高代码的可用性和可读性。

因此,在编写JavaScript代码时,我们应该充分利用预编译的特性,合理地声明变量和函数,以便代码能够更加清晰、易读和易于维护。