面试题:预编译,作用域链 你:秒了
在上一篇文章中,我们学习了作用域的基础和调用栈的一些知识,大家还记得有哪些作用域吗?哈哈不记得的同学乖乖回去复习一遍再来。在上一篇的末尾,我们阐述了作用域和调用栈的关系,当函数被调用并添加到调用栈时,该函数会创建它自己的作用域链,那么作用域链有什么作用呢?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,你又该怎么回答呢?
这时一定别慌,你只需要牢记下面这些步骤,无论再长再复杂的编程块你都能准确的读准其中参数的值
重要点
- 当预编译发生在全局时
- 创建GO对象
- 找变量声明,将函数名作为GO的属性名,值为 undefined
- 在全局找函数声明,将函数名作为GO的属性名,值为该函数体
- 当预编译发生在函数体内
- 创建一个AO对象
- 找形参和变量声明,将形参和变量名作为AO的属性名,值为 undefined
- 形参和实参统一
- 在函数体内找函数声明,将函数名作为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