一道面试题带你全面认识js预编译底层逻辑

考生请听题,请问下面的题目输出结果是什么?并给出分析过程。

css 复制代码
var a = 1
function foo(a){
    var a = 2
    function a(){}
    var b = a
    console.log(a)
}
foo(3)

啊? 这是认真的吗,还会有人这样写代码?好好好,这么玩是吧

当我们拿到这样的面试题的时候,我们需要有一个深入理解js预编译底层逻辑的能力,这道题目有点难,但是接下来我将会详细给你慢慢介绍预编译的全部步骤。

首先给出我压箱底的预编译过程,句句重点!

函数体的预编译

  1. 创建AO(Action Object)对象
  2. 找到有效标识符(形参和变量声明),找到后将有效标识符作为AO对象的属性名(key),值(value)赋为undefined
  3. 将形参和实参值统一
  4. 在函数体内找函数声明,将函数名作为AO对象的key,值赋为函数体

全局的预编译

  1. 创建GO(Global Object)对象
  2. 找变量声明(全局没有形参),将变量声明作为GO的key,值赋为undefined
  3. 在全局找函数声明,将函数名作为GO对象的key,值赋为函数体

这道面试题我们先放在这里,待讲完所有的例子和规则的时候我们再来公布答案,看看你是否真正学会

题一

javascript 复制代码
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)

输出结果

csharp 复制代码
[Function: a]
123
123
[Function: b]
123

这道题是针对函数体内的预编译,我们就只需要看函数体的预编译

步骤一:创建AO对象

markdown 复制代码
AO{
	
}

步骤二:找到形参与变量声明后并赋值为undefined

javascript 复制代码
AO{
	a: undefined -> undefined
	b: undefined
	d: undefined
}

这里需要解释下a这个key,从上到下,先是因为fn(a)形参的原因赋值为undefined,后是因为var a的原因再次赋值为undefined,两次重复声明,取最新的,最后还是undefined

步骤三:将形参与实参值统一

javascript 复制代码
AO{
	a: undefined -> 1
	b: undefined
	d: undefined
}

步骤四:在函数体内找到函数声明并赋为函数体

yaml 复制代码
AO{
	a: 1 -> function: a
	b: undefined
	d: undefined ->function: d
}

这里需要注意,像function a(){}这样的才是函数声明,有=叫做函数表达式,比如var b = function(){}

好,这里我们的预编译(编译)工作已经完成

现在开始执行

yaml 复制代码
AO{
	a: function: a 输出 -> 123 输出 输出
	b: undefined -> function: b 输出
	d: function: d -> 123 输出
}

执行的时候是执行带有=的赋值语句和console.log等执行语句,千万不能漏掉函数表达式!

题二

javascript 复制代码
function foo(a,b){
    console.log(a)
    c = 0 
    var c 
    a = 3
    b = 2
    console.log(b)
    function b(){}
    function d(){}
    console.log(b)
}
foo(1)

输出结果

复制代码
1
2
2

这道题跟题一不同就在于有直接赋值语句,比如c = 0,像这样在函数体内赋值的会默认跑到全局作用域中声明,因此我们在执行步骤二的时候直接跳过即可

步骤一:创建AO对象

复制代码
AO{

}

步骤二:找到形参与变量声明后并赋值为undefined

javascript 复制代码
AO{
	a: undefined
	b: undefined
	c: undefined
}

步骤三:将形参与实参值统一

javascript 复制代码
AO{
	a: undefined -> 1
	b: undefined
	c: undefined
}

步骤四:在函数体内找到函数声明并赋为函数体

yaml 复制代码
AO{
	a: 1 
	b: undefined -> function: b
	c: undefined
	d: function: d
}

现在开始执行

yaml 复制代码
AO{
	a: 1 输出 -> 3
	b: function: b -> 2 输出 输出
	c: undefined -> 0
	d: function: d
}

题三

javascript 复制代码
var glogal = 100
function fn(){
    console.log(glogal)
}

输出结果

复制代码
100

这道题目大家可能会觉得过于简单,为什么我要来讲这道题呢?主要是因为面试官问你的时候可能就不会这么简单了,你知道输出100的原因是函数作用域没有global这个变量于是去全局作用域找,找到后输出100,这个答案并不是面试官想听到的,没有到点子上。你需要回答的是为什么遵循了一个从内到外的查找规则。

这个时候我们就要引入一个新名词---调用栈

调用栈

调用栈是一种栈,可以理解为被阉割后的数组,只能先进后出或者后进先出。当一个js文件有既有全局作用域又有函数作用域的时候我们会先将全局作用域放入栈底,这个时候我们换个叫法,全局执行上下文 ,函数作用域这里叫做函数执行上下文

全局执行上下文有两类,一类是变量环境,专门用于存放var变量的声明,另一类是词法环境,专门用来存放let和const变量,其实大家这里应该就猜到了,这里应该就是let和const不会声明提升的底层原因。没错,是这样的。

全局执行上下文放入栈底后开始执行操作,global = 100 fn的调用。之后函数执行上下文入栈函数执行上下文也有自己的变量环境,词法环境,存放内容与全局执行上下文一致,调用栈有个指针,指向当前在哪个上下文中执行,它会先在函数执行上下文中找词法环境,然后去到变量环境,找不到就下移,跑到全局执行上下文,先找词法环境,后找变量环境。这里可能文字讲解十分抽象,下面给出图。

哈哈哈,手绘的可能很丑,下次我用电脑画。

题四

javascript 复制代码
global = 100
function fn(){
    console.log(global)
    global = 200
    console.log(global)
    var global = 300
}
fn()
var global

输出结果

javascript 复制代码
undefined
200

这道题目既有全局又有函数,所以我们先对全局进行分析

步骤一:创建GO对象

复制代码
GO{

}

步骤二:找变量声明并赋为undefined

csharp 复制代码
GO{
	global: undefined
}

步骤三:在全局找函数声明并赋为函数体

php 复制代码
GO{
	global: undefined
	fn: function: fn
}

现在开始执行

rust 复制代码
GO{
	global: undefined -> 100
	fn: function: fn
}

执行函数的时候我们需要先对函数进行预编译

现在进入第二阶段:对函数体预编译

步骤一:创建AO对象

复制代码
AO{

}

步骤二:找有效标识符并赋为undefined

csharp 复制代码
AO{
	global: undefined
}

步骤三:将形参与实参值统一

csharp 复制代码
AO{
	global: undefined
}

步骤四:在函数体内找函数声明并赋为函数体

csharp 复制代码
AO{
	global: undefined
}

这里的步骤三、四都没有变化,因为函数体中没有形参和函数声明,现在开始直接执行函数

rust 复制代码
AO{
	global: undefined 输出 -> 200 输出 -> 300
}
GO{
	global: undefined -> 100
	fn: function: fn
}

所以这里的输出结果为undefined和200

相信大家到这里已经学会了如何去做这种题目,题四和开头的题目是一样的。

输出结果

复制代码
2

大家都答对了吗


如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]

相关推荐
Java水解8 分钟前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
大怪v26 分钟前
前端:人工智能?我也会啊!来个花活,😎😎😎“自动驾驶”整起!
前端·javascript·算法
洛小豆2 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
遂心_2 小时前
为什么 '1'.toString() 可以调用?深入理解 JavaScript 包装对象机制
前端·javascript
王同学QaQ3 小时前
Vue3对接UE,通过MQTT完成通讯
javascript·vue.js
程序员鱼皮3 小时前
刚刚 Java 25 炸裂发布!让 Java 再次伟大
java·javascript·计算机·程序员·编程·开发·代码
Asort4 小时前
JavaScript 从零开始(五):运算符和表达式——从零开始掌握算术、比较与逻辑运算
前端·javascript
一枚前端小能手4 小时前
🚀 缓存用错了网站更慢?前端缓存策略的5个致命误区
前端·javascript
闰五月4 小时前
JavaScript执行上下文详解
面试
艾小码4 小时前
为什么你的页面会闪烁?useLayoutEffect和useEffect的区别藏在这里!
前端·javascript·react.js