序言
在之前的文章我们浅聊了关于JS作用域,然后对于预编译的说明是代码编译总是发生在执行前的,以及变量的查找是可以从内部作用域往外部作用域查找的,不能由外到内查找等等,然而这些对于小厂面试或许足够,但是大厂的需求更复杂,要求也随之以往更加高,如果你面试时被问及JS预编译的底层逻辑你是否思路清晰,先将问题抛掷脑后,让我们今天一起进入JS预编译的真实世界....
新手可以先看上一个文章 专门针对小白 聊聊关于作用域的那点事儿
一、声明提升和函数整体提升
JavaScript代码在执行之前,会经过一个预编译(Hoisting)的过程。这个过程会将变量声明和函数声明提升到作用域的顶部,以便在代码执行时可以正确地引用它们。
变量和函数提升的概念:在预编译阶段,JavaScript引擎会先扫描整个代码,找出所有的变量声明(使用var
、let
或const
关键字)和函数声明,并将它们添加到相应的作用域中。这个过程被称为变量和函数的提升(hoisting)。提升使得我们可以在声明之前使用变量和函数,但是它们的赋值或定义将在实际执行时发生。
1. 变量的提升会将变量声明移动到作用域的顶部,但不会将赋值操作提升
js
console.log(x); // undefined
var x = 10;
上述代码打印的结果就是 undefined
原因就是用var
声明变量的时候 声明语句会提升到当前域的最高处而赋值语句不会 结果就是
js
var x
console.log(x); // undefined
x = 10;
所以建议大家声明语句的时候使用
let
或者const
2. 函数的提升则会将整个函数体移动到作用域的顶部
js
foo(); // "Hello, world!"
function foo() {
console.log("Hello, world!");
}
在上面的例子中,函数
foo
被提升到作用域的顶部,所以我们可以在函数声明之前调用它。 需要注意的是,使用let
和const
关键字声明的变量不会被提升到作用域的顶部,而是在代码中的实际位置进行处理。这被称为暂时性死区(Temporal Dead Zone,TDZ),在TDZ中访问变量会抛出错误。
js
var obj = {
name : '老王',
age : 18,
sex : 'boy'
}
obj.sex = 'girl'
obj.sex = 'boy'
console.log(obj); //boy
我们先引入一个例子,在上述代码中我们定义了一个obj对象(有三个键值对)对对象的sex属性值进行了两次修改,最后执行结果肯定就是boy,这里其实就是重写覆盖了两次sex属性的键值。
二、预编译发生在函数执行之前的四部曲
- 创建一个AO对象 (Action Object)
- 找有效标识符(形参和变量声明),将变量声明和形参作为AO的属性名,值为undefined
- 将实参和形参值统一
- 在函数体内找函数声明 将函数名作为AO对象的属性名,值赋予函数体
js
var a = 1
function foo(a){
var a = 2
function a(){}
var b = a
console.log(a);
}
foo(3)
我们再看上述代码,是不是觉得相比之前没有那么容易了,但是我们仔细想了想似乎还是能想到答案,首先在全局var语句声明了一个变量a,然后执行foo函数并且为形参a传入一个实参3,然后v8引擎开始执行第一个赋值语句:给a赋值1,然后执行 foo函数体内,编译器又开始编译,声明了一个变量a值为undefined,还有一个函数名为a的函数,声明了一个变量b值为undefined,最后v8再次进行执行将2赋值给a然后将a的值(2)赋值给b,最后打印出a的值:2
这里其实我们就是对a的值进行了三次修改,如果这样分析属实有点麻烦,而且要注意的变量只被声明后值为undefined。
我们再来分析一个例子
js
function fn(a){
console.log(a);
var a = 123
console.log(a);
var b = function(){}
console.log(a);
var b = function(){}
console.log(b);
function d(){}
var d = a
console.log(d);
}
fn(1)
上述代码的输出结果为:
接下来我们开始解释JS在函数执行前预编译的逻辑思路
- 编译器会创建一个AO(Action Object)对象:
js
AO: {
}
- 找有效标识符(形参和变量声明),将变量声明和形参作为AO的属性名,值为undefined:
js
AO: {
a:undefined
b:undefined
d:undefined
}
- 将实参和形参值统一
js
AO: {
a:undefined -> 1
b:undefined
d:undefined
}
- 在函数体内找函数声明 将函数名作为AO对象的属性名,值赋予函数体
js
AO: {
a:1
b:undefined
d:undefined -> function d(){}
}
那么到这里我们的预编译就结束了,现在的工作就是将由我们的引擎来执行
js
function fn(a){
console.log(a); //1
var a = 123
console.log(a);//123
var b = function(){}
console.log(a);//123
var b = function(){}
console.log(b);//function(){}
function d(){}
var d = a
console.log(d);//123
}
fn(1)
我们再来一个例子加深印象 检验成果
js
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)
此时你是不是会想谁能写出这么ugly的代码,如果就在你旁边你肯定会忍不住拔刀相见吧,可是在我们国家法律也没规定这样写代码犯法,不用担心,接下来我们将从JS预编译的底层逻辑
出发,轻松准确解决问题。
我们再按照之前的思路一步步来 最后AO对象中每个变量的值重写情况就是如下
js
AO: {
a:undefined 1 3
b:undefined function b(){} 2
c:undefined 0
d:function d(){}
}
解释
:首先形参a,b值为undefined,我们可以发现与之前不同的是这段代码中多了很多直接赋值的语句,而这种直接赋值语句会导致语句直接提升到全局作用域中,于是我们代码就变成了这样
js
c = 0
a = 3
b = 2
function foo (a,b){
var c
console.log(a); //1
console.log(b);//2
function b(){}
function d(){}
console.log(b);//2
}
foo(1)
继续解释
:这里c会被赋值成undefined可以理解了吧,然后就是变量b和变量d的值分别重写function b(){}和function d(){}
三、预编译发生在全局的三部曲
- 创建 GO 对象 (Global Object)
- 找变量声明,将变量声明作为GO的属性名,值为undefined
- 在全局找函数声明 将函数名作为GO对象的属性名,值赋予函数体
话不多说直接上栗子
js
var global = 100
function fn(){
console.log(global);
}
fn()
- 首先创建一个GO对象
js
GO:{
}
- 找变量声明,将变量声明作为GO的属性名,值为undefined
js
GO:{
global:undefined
fn:undefined
}
- 在全局找函数声明 将函数名作为GO对象的属性名,值赋予函数体
js
GO:{
global:undefined
fn:undefined -> function fn(){}
}
- 创建一个AO对象
js
AO:{
}
因为在函数体内没有有有效标识符和函数声明 所以最后结果就是
js
// GO:{
// global:undefined 100
// fn:undefined functionfn(){}
// }
var global = 100
function fn(){
console.log(global);
}
// AO:{
// }
fn()
打印结果为
再看一个例子
js
global = 100
function fn(){
console.log(global); //undefined
global = 200
console.log(global); // 200
var global = 300
}
fn()
var global
按照之前的套路继续往前走
- 创建GO对象
js
GO:{
}
- 找变量声明,将变量声明作为GO的属性名,值为undefined
js
GO:{
global:undefined
fn:undefined
}
- 在全局找函数声明 将函数名作为GO对象的属性名,值赋予函数体
js
GO:{
global:undefined
fn:undefined -> function(){}
}
4.创建AO对象
js
AO:{
}
5.找有效标识符(形参和变量声明),将变量声明和形参作为AO的属性名,值为undefined
js
AO:{
global:undefined
}
因为在fn函数内没有形参和实参以及函数声明 引擎直接继续执行
js
// GO:{
// global:undefined 100
// fn:function(){}
// }
global = 100
function fn(){
console.log(global); //undefined
global = 200
console.log(global); // 200
var global = 300
}
// AO:{
// global:undefined 200 300
// }
fn()
var global
最终的打印结果就是:
四、调用栈
1. 栈的概念
在JS中你可以理解成栈就是一个失去了部分功能的数组,栈就像是一口井,当你往这口井中添加对象时,对象会在井的最底部,然后依次向上堆积,如果你要从这口井中取出对象时也只能从最上面取,不能如数组样任意位置存取。
这就是栈遵循"后进先出"(Last-In-First-Out, LIFO)的原则
2. 调用栈
JavaScript调用栈(Call Stack)是一种用于跟踪函数调用的栈数据结构,而在预编译的时候JS引擎会创建执行上下文 ,如果JS代码既即存在全局作用域也存在函数作用域,那么JS引擎会创建全局执行上下文 和函数执行上下文,并将其推入调用栈的底部。然后,逐行执行代码,遇到函数调用时,会将该函数的执行上下文推入调用栈的顶部,开始执行函数内部的代码。如果函数内部又调用了其他函数,那么新函数的执行上下文会被推入调用栈的顶部,形成一个嵌套的结构。
当函数执行完毕后,它的执行上下文会从调用栈中弹出,控制权回到调用该函数的地方。如果调用栈为空,表示整个程序执行完毕。
可能你读着有些难以理解,我们如果借助图片的话就比较容易了
五、总结
在JS中,如果我们要想更通透地理解清楚代码,我们需要理解JS预编译的底层原理,而在大厂的面试中这也是很热门的一个考点,当面试官问你怎么理解我们上面代码的运行顺序时,不仅仅是简单的函数和声明提升,通过以上的学习我们就能够解释为什么在内部作用域中可以访问到外部作用域的变量,在外部作用域中无法访问到内部作用域的变量。
感谢大家的阅读,点点赞吧♥
如果想了解更多面试知识以及有用的干货,点赞+收藏
开源Git仓库: gitee.com/cheng-bingw...