大厂面试题系列(四)-预编译🔥

370 阅读7分钟

大厂面试题系列(四)-预编译

前言

本期我们要讲的同样是面试干货-预编译的原理,有关于预编译的知识点在小黄书包括很多经典书籍上并没有给出详细的解释和介绍,所以在本文会给大家详细的讲解和介绍。

一.发生在函数体的预编译

1.关于预编译

我们再上一期的变量提升中了解到,变量提升是发生在编译阶段,而赋值操作是发生在执行阶段,而预编译,则是发生在执行的前一刻

2. 函数执行前一刻的预编译

这里我们引用一下上一期当中的代码示例:

1.1
function fn (a) {

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

    var a = 123

    console.log(a);     // 123

    function a() {}

    console.log(a);     // 123

    var b = function() {}

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

    function d() {}
}

fn(1)

还是这段熟悉的代码,唯一的不同之处就在于这次我们在调用函数fn的时候,传入了一个参数1。在上一期当中我们是直接使用变量提升的规则来理清楚了这段函数的运行结果,那么今天我们就用预编译的规则来理一理:

大家可以先跟我走一遍,存在不明白的地方很正常,先跟着我一起走完一遍,到最后小编会给出总结,到时候大家就能明白了。

来吧,首先如果有小伙伴对执行上下文还不太了解的话可以去我们系列第二篇(作用域进阶)了解一下执行上下文之后再来学习本期内容,会更好理解一些。

首先,我们都知道函数执行会产生一个函数的执行上下文,我们在这里简称为AO(Activation Object),那么在AO当中储存的正是函数当中各种变量的值,在例1.1当中,函数fn执行会产生一个AO对象。

在创建AO对象之后,我们先来找有无变量声明和形参,很显然在例1.1当中,第一个变量声明是var a,那么在AO中就有一个属性名为a的一个变量,那么它的值为多少呢?此时正处于预编译阶段,并不是执行阶段,所以不会执行赋值操作所以 此时a的值为 undefined

然后我们接着找变量声明,第二个变量声明为var b,注意,此处是一个函数表达式,并不是函数声明 , 那么同样,b 的值也为 undefined,好了,找到这里我们发现已经没有变量声明了。

那我们来找形参,有一个形参为a,传入的值为 1,我们将形参与实参的值统一,那么此时a 的值就为1.

走完上面那一步之后我们接着往下看,这个时候我们就可以在函数体里找函数声明,从上往下,第一个就是函数a的声明,那么好了,此时AO对象中已经存在一个变量a了,还会继续添加一个新的a为函数吗?很显然在JavaScript中是不允许声明两个相同的变量的,所以此时,a的值1将会被覆盖,被[Function: a]覆盖掉了,然后我们继续往下走第二个是函数d的声明,同样此时d的值为[Function: d]。

好,走到这里我们算是把函数执行前一刻的预编译理清楚了,然后就是函数内部从上往下执行代码,我们先来看看此时AO对象里面存放了哪些值?

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

当然在函数预编译的时候并不会产生上面这么一段代码,而是会产生一个结构,想了解的同学,在第二篇作用域进阶当中有一些图片大家可以看看。

那么好,预编译执行完了,我们开始执行代码,从上往下,第一个就是console.log(a),那么此时的a的值为[Function: a],所以这行代码运行的结果就为[Function: a]。 继续往下看,有一个a = 123的赋值操作,将a的值又重新赋值为了123,所以下面两个console.log(a)的值都为 123. ,接着往下走,有一个赋值操作将[Function: b]赋值给了变量b,所以最后一个console.log(b)的值就为[Function: b]。 到这里,整个过程就算结束了,得出的最后答案和我们通过变量提升直接得出的答案是一样的,还没有理清楚的小伙伴可以多看两边上面的解析,后面我们会一起总结。

二.发生在全局下的预编译

1.预编译不仅仅发生在函数体内

预编译同样会发生在全局下,那么在全局下的预编译又是怎样进行的呢?如果你已经理解了函数执行前一刻的预编译,那么接下来你就能很清晰的理解全局下的预编译,也是那么几个步骤,我们先一起走一遍,最后再做个总结。

2. 全局下的预编译

同样的,我们先来看一段简单的代码:

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

好,这段代码是不是异常简单,没错,确实简单,但是有个小细节需要大家注意,一会儿大家就知道了,那么你先想想,这段代码的运行结果会是多少呢?相信你的答案应该是100,没错,这段代码的运行结果的确为100,如果面试官问你,为什么是100呢? 那么你可能就会说因为函数内部可以访问到外部的变量所以为100,那么面试官又问了:为什么函数内部可以访问到外部的变量呢? 这个时候你就可以拿出我们第二篇中所讲到的作用域链的知识来解答了,好,那么真的是这么个流程嘛?我们一起来看看。

首先,我们执行的前一刻,全局会产生一个GO对象,就是全局的执行上下文,和函数一样,我们先找变量声明,有一个var global 的声明,所以此时global的值为undefined。

找完了变量声明我们再找函数声明,有个函数fn的声明,那么此时GO中又多了一个变量fn,值为[Function: fn],到这里全局的预编译就执行完了,然后就是函数fn也会进行预编译,按照上一节所讲的步骤可知函数fn的AO对象里面没有变量(本身自带的一些this,arguments除外),所以现在的AO和GO我们来看一下:

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

AO:{

}

然后执行阶段,因为赋值操作是在函数fn的调用之前,所以会先进行赋值操作,再进行函数的调用,所以最终代码运行结果为:100

三.预编译总结

好了讲到这里想必大家多多少少对预编译的过程有个大概的了解了,那么我再来给大家总结一下: 首先,预编译发生在函数执行前的前一刻 ( 四部曲 ) ( 函数体 )

  1. Js 引擎会创建 AO 对象(Activation Object)

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

  3. 将实参和形参值统一

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

发生在全局下的预编译:

  1. 创建 GO 对象

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

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

如果看到第三节还是一知半解的同学,我建议可以把小结多看几遍,然后根据总结的部分再回到前面去看看能不能自己写出来AO 和GO。

本期文章就写到这里啦,我们下期见~