面试必看! 带你深入了解JS预编译(执行上下文)

202 阅读4分钟

前言

在许多大厂的面试题中,JavaScript预编译是经常会被问到的问题,本文作者将带领大家深入了解预编译。

预编译是什么

在 JavaScript 中,没有传统的编译过程,因为 JavaScript 是一种解释性语言,它通常是在运行时由浏览器或其他 JavaScript 运行环境逐行解释和执行的。但在执行代码之前,它会进行某些步骤,以确保代码能够按照 JavaScript 的规则正确执行。这类步骤便被称为预编译。

预编译预编译发生在代码片段执行前,预编译分为两种,一种是函数预编译,另一种是全局预编译,全局预编译发生在页面加载完成时执行,函数预编译发生在函数执行的前一刻。预编译会创建当前环境的执行上下文。

1. 函数预编译

JavaScript 中函数预编译指的是在执行函数内部代码之前,JavaScript 引擎执行的一系列步骤,以准备函数的上下文和作用域。这个过程确保了函数内部的变量和函数能够按照 JavaScript 的规则正确执行。以下是函数预编译的主要步骤:

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

  1. 创建AO对象 (Action Object)
  2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefind
  3. 将实参和形参值统一
  4. 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

接下来让我们随着案例一同了解函数在编译前发生的那些事

案例分析

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

1. 创建AO对象 (Action Object)

AO:{

}

2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefind

AO:{
    a:undefind ->undefind
    b:undefind
    d:undefind
}

我们可以看见由于func声明了一个变量a和形参a,但因为对象中没有重复的key,所以变量a的值被形参a的值覆盖了

3. 将实参和形参值统一

AO:{
    a:undefind ->undefind->1
    b:undefind
    d:undefind
}

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

AO:{
    a:undefind ->undefind-> 1 ->function a(){}
    b:undefind
    d:undefind->function d(){}
}

到此,函数体内的预编译步骤已经完成,让我们来执行这个函数

AO:{
    a :undefind ->undefind-> 1 ->function a(){} ,
    b : undefined ,
    d : undefined-> function d(){},

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

2. 全局预编译

在 JavaScript 中,全局预编译是指在执行任何函数或代码块之前,对全局作用域的代码进行预处理。以下是与全局预编译相关的主要概念和步骤

预编译发生在全局 (三部曲)

  1. 创建GO对象 (Global Object)
  2. 找变量声明,将变量声明作为GO的属性名,值为undefind
  3. 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体

案例分析

var global = 100
function fn(){
    console.log(global);
}
fn()

1. 创建GO对象 (Global Object)

GO:{

}

2. 找变量声明,将变量声明作为GO的属性名,值为undefind

GO:{
    global :undefined
}

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

GO:{
    global :undefined
    fn:function fn(){}
}

现在,我们开始执行代码

// GO:{
//     global :undefined ->100
//     fn:function fn(){}
// }

var global = 100
//global :undefined->100
function fn(){
    console.log(global);
}
//函数执行前我们开始执行函数的预编译
// AO:{
    
// }
//由于函数内部没有声明变量和形参,也没有函数声明,所以AO对象美元值
fn()

由于函数体内的console.log(global)在函数上下文(AO) 中找不到值,所以跳转到全局上下文(GO) 中寻找。所以console.log(global)的输出值为100。

image.png

调用栈与执行栈

通过上面两个例子你已经清楚了预编译的执行步骤了吧,现在来让我们学习下为什么会先从函数上下文(AO) 寻找,再跳到全局上下文(GO) 中寻找。

当js引擎第一次遇到我们写的脚本时,会创建一个全局执行的上下文,压入调用栈;以后每遇到一个函数调用就创建一个新的函数执行上下文并压入调用栈顶部。然后引擎会依次执行栈中的栈顶上下文,每执行完一个就弹出一个……一旦所有的代码执行完毕,js引擎就会从调用栈中移除全局执行上下文。

image.png 由于栈的操作是从栈顶到栈尾的,所以所以当我们在函数中console.log时,会先在函数执行上下文中寻找,如果找不到才能跳到全局执行上下文中寻找