likes
comments
collection
share

面试题:预编译,作用域链 你:秒了

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

在上一篇文章中,我们学习了作用域的基础和调用栈的一些知识,大家还记得有哪些作用域吗?哈哈不记得的同学乖乖回去复习一遍再来。在上一篇的末尾,我们阐述了作用域和调用栈的关系,当函数被调用并添加到调用栈时,该函数会创建它自己的作用域链,那么作用域链有什么作用呢?V8又是怎么编译代码的呢?这些疑问将在接下来的课程中一一揭晓

一,作用域链

作用域

我们再温习一下啥是作用域

所谓作用域呢就是函数身上的[[scope]]属性,每一个函数都是一个对象,这是一个我们不能访问的属性;他用于存储函数中的有效标识符,就是运行时的上下文对象的集合

理解基础

为了方便我们理解作用域链,我们来补充几个概念

GO对象

GO(Global Object)即全局变量对象。它包含了全局执行的上下文内容。在JavaScript中,全局变量对象是一个预定义的对象,它提供了全局的属性和方法。例如,在浏览器中,全局变量对象就是window对象。

AO对象

AO(Activation Object)即函数的活动对象。它包含了函数执行期的上下文内容,是一个即时的存储容器。当函数执行完毕后,AO会被销毁。简单来说,AO的主要作用是帮助JavaScript引擎在引用变量时能够顺利找到变量,并且与其他对象(如VO)之间的联系可以实现作用域链。

上下文对象

上下文对象是一种在特定环境或上下文中使用的对象,用于存储和管理与该环境或上下文相关的状态、属性或数据。上下文对象的主要目的是提供一种机制,以便在应用程序的不同部分之间共享和传递信息,而无需显式地在每个部分之间传递数据。

作用域链概念

作用域链是执行期上下文对象的集合,这种集合呈链式链接,我们把这种链式关系称之为作用域链

因为作用域链只能从内部指向外部,使用局部作用域可以访问外部,外部不可以访问内部

让我举个例子来帮助我们理解

function a() {
    function b() {
        b = 22
    }
    var a = 111
    b()
    console.log(a);
}
var glob = 100
a()

让我们思考一下他打印的结果会是什么?

面试题:预编译,作用域链  你:秒了

现在我们再来分析一下这题,首先对于window来说存在一个全局上下文对象GO,变量a也在全局被声明;当a被调用时,产生了一个a的上下文对象,b被调用时产生了b的上下文对象。现在,a,b的作用域链条开始了指向,他们作用域的0号位都指向了自己,先在自己的内部找参数,再访问外部作用域。如图所示形成一个个链条,确定了编译访问的顺序。 所以答案也就呼之欲出了,打印a的值为111嘛,简简单单~

总结

上下文对象记录有效标识符,当函数被调用时,他的上下文被创建;他的作用域0号位指向自己的上下文对象,1号位指向外部作用域。作用域是执行期上下文对象的集合,这种集合呈链式链接,我们把这种链式关系称之为作用域链。

也正是因为作用域链只能从内部指向外部,所以局部作用域可以访问外部,而外部作用域不可以访问内部

二,预编译

作用域链的形成为预编译指明了道路,读懂预编译我们就能化身V8引擎,机智的处理各种代码执行问题,预编译作为面试的常考题更是十分重要,所以我们废话少说,直接看题

基础知识

声明提升问题

console.log(a);
var a = 1
var a
console.log(a);
a = 1

在函数出现后函数才能被调用,使用xx()调用函数也得先声明函数吧?哎,不用! 你看,为什么下面这串代码也没报错呢?

test()
function test(){
    var a = 123
    console.log(a);
}

哈哈,其实是因为函数的声明提升问题

function test(){
    var a = 123
    console.log(a);
}
test()

记住:变量声明,声明提升;函数声明,函数体整体提升

关键字覆盖问题

var obj = {
    a: 0
}

obj.a = 1
obj.a = 2
console.log(obj)

对象中的属性名key不会重复,第二个出现的key会把第一个覆盖

预编译的过程 面试考点

现在我们直接进入到预编译的过程中来,看下面这题

var a = 1
function fn(a) {
    var a = 2
    function a() { }

    console.log(a);
}
fn(3);

这题首先在全局声明了一个变量a,一个函数fn并且在最后调用了这个函数,传了一个实参a到fn函数;再看局部作用域fn中,声明了一个var a并将a赋值为2,声明了一个函数但是并没有调用他,最后在这个局部函数fn中打印了a。

好,题目看完,相信佬们早就有了答案,这不就打印a的值为2吗,瞧不起谁呢!哈哈stop,但如果我说这是一道面试题,需要你好好解释一番a的值为什么为2,你又该怎么回答呢?

这时一定别慌,你只需要牢记下面这些步骤,无论再长再复杂的编程块你都能准确的读准其中参数的值

重要点

  • 当预编译发生在全局时
  1. 创建GO对象
  2. 找变量声明,将函数名作为GO的属性名,值为 undefined
  3. 在全局找函数声明,将函数名作为GO的属性名,值为该函数体
  • 当预编译发生在函数体内
  1. 创建一个AO对象
  2. 找形参和变量声明,将形参和变量名作为AO的属性名,值为 undefined
  3. 形参和实参统一
  4. 在函数体内找函数声明,将函数名作为AO的属性名,值为该函数体

来两道题运用一下这个重要的方法

1,

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)

console.log(fn)

我们从全局开始分析,当预编译开始时,首先创建了一个GO对象,然后我们发现,在全局作用域中只有一个函数声明和fn这个函数的调用,我们把fn赋值为function,然后全局执行上下文,触发fn的调用。如果fn要调用的话就必须访问fn的作用域,预编译开始编译函数体内的代码,这时fn的AO对象被创建;我们从上往下寻找形参和变量声明,把他们都赋值为undefined,然后统一实参,把1赋值给a的undefined,覆盖住他,最后我们在函数体内找函数声明;在这里有一个需要注意的点函数声明为function x(){}的形式,var b = function(){} 这是一个函数表达式,不赋值;最后我们开始执行代码,这里将123赋值给了a,把a赋值给了c,所以最后a = 123,b = undefined,c = 123。在本道题目中考察的是代码在运行过程中的赋值情况,所以我们回到代码执行前一步;代码从上到下运行最后输出结果。详细的图示如下:

面试题:预编译,作用域链  你:秒了 最后打印结果如图: 面试题:预编译,作用域链  你:秒了

2.熟悉了第一题的步骤相信下面这道题佬们必定也是得心应手

function test(a, b) {
    console.log(a);
    c = 0
    var c;
    a = 3
    b = 2
    console.log(b);
    function b() { }
    console.log(b);
}
test(1)

同样,先创建一个GO对象,找到 test函数名赋值为函数体function,出现了test()的调用,创建一个test 的AO对象;变量a b c 第一步被赋值为undefined,第二步形参和实参统一时 a被赋值为1,b被赋值为函数体,最后执行

面试题:预编译,作用域链  你:秒了

结语

好啦,以上就是我们本期的所有内容,千万不要忘了我们这几天学习的知识喔!!还记的啥是对象吗?函数怎么构建?有哪几种创造对象的方法?基本数据类型有哪些?JS的解释器在浏览器中起到了哪些作用?啥是编译器啥是解释器?基本的作用域类型有哪些?let和var的区别? 这些问题的答案都在我们往期的文章中,请白们多多消化。在下篇文章中我们将据徐解解析生剖浏览器中的JS执行机制,让我们准备好大脑迎接下一场洗礼!

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