前言
相信学习前端知识的小伙伴们,一旦去考究JS底层的知识,都被一些变量声明提升、函数声明提升搞得醍醐灌顶。到最后,再去想一段JS代码究竟是什么是怎样的运行顺序?仍然是模糊不清的。接下来,我将带领大家用简单好理解的、具体的步骤去推敲和理解JS代码的运行顺序、每个阶段都干了什么?真正地打开JS世界的大门!
一起来思考
二话不说,先上代码。
var global = 100
function fn() {
console.log(global);
}
fn()
我们来假想一下,如果面试官问你:这段代码最后会输出什么?你大概率会觉得他在侮辱你,这不就是100吗;但如果面试官再问你,这个100怎么来的?你能回答的出来吗?
其实,面试官问你这个问题就是看你知不知道JS代码在运行时都发生了什么? 这个时候,很多小伙伴是不是已经开始想声明提升的问题了。用声明提升去思考代码也就是这样的顺序
var global //变量声明提升
function fn(){ //函数声明提升
console.log(global)
}
global = 100 //变量赋值
fn() //函数执行
看到这,很多小伙伴肯定会说不过如此。那么保持这个想法,看下面的代码
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() {}
var d = a
console.log(d); // 123
}
fn(1)
请问:这段代码是怎么的运行顺序?开始声明提升?现在,大家应该就知道我要说什么,通过声明提升、作用域去思考一段代码的运行顺序,如果代码简单还好说,一旦代码的声明操作、赋值操作一大堆,就会浪费大把的时间,而且出错率极高。
干货
函数体内
先以上面的代码为例,在一个函数体内的代码运行。其实JS运行可以分为编译阶段和执行阶段。那么这两个阶段分别发生了什么呢?
上述代码中
- 编译阶段
- 先创建一个AO(activation object)对象
- 然后去找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
- 再者,将实参和形参统一
- 最后,找函数声明,将函数名作为AO对象的属性名,值赋予函数体
带着这些,我们再来看这段代码
function fn(a) { //二、形参是a,值为undefined
console.log(a); // function() {}
var a = 123; //二、a变量声明,AO里已经有了,覆盖后还是一样的
console.log(a); // 123
function a() {}// 四、a 申明为一个函数
console.log(a); // 123
var b = function() {} //二、变量b声明,值为undefined
console.log(b); // function() {}
function d() {} // 四、d声明为一个函数
var d = a //二、变量d声明,值为undefined
console.log(d); // 123
}
//第一步在这、函数在执行前进行编译,创建AO对象
AO:{
//第二步,开始找形参和变量声明,并写入AO
a:undefined 1 function (){}
b:undefined
d:undefined function(){}
//第三步,就相当于把实参传给形参,所以a的值现在就变成了1
//第四步,找函数声明,写入AO
}
fn(1)
到最后编译阶段结束,AO对象中的属性和值是:
AO:{
a:function(){}
b:undefined
d:function(){}
}
现在,我们来通过看AO对象中,属性对应的值来执行整个函数。
- 从fn(1)开始执行函数
- 第一个log要a,去AO中找a,是function(){}。然后a被赋值为123,AO对象中的a随之更新
- 第二个log又要a,去AO中找a,现在输出的就是123了。然后变量b被赋值为一个函数,AO中也随之更新
- 第三个log要b,去AO中找b,输出的就是function(){},然后a的值被赋给了d,AO中a的值是123,所以d也变成123,AO中随之更新
- 第四个log要d,不就是123嘛 到这,这段代码就执行完了,不妨回过去看看,嗯?就这么简单?为了证明我不是在耍流氓,大家可以去跑一跑,对照一下。
看到这,聪明的朋友们马上会有疑问,这不过是函数体内的,那在全局上又是怎么样的。我只能说:更简单,接着看。。。
在全局下
在函数体中,整个编译阶段我总结成四部曲。在全局下,三步足以,多一步算我输。
- 第一步、创建一个GO(global object)对象
- 第二步、找变量声明,将变量声明作为GO的属性名,值为undefined
- 第三步、找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体 三步,结束了,老规矩,拿代码来说话:
GO:{
fn:function(){}
}
global = 100
function fn() {
console.log(global); // undefined
global = 200
console.log(global); // 200
var global = 300
}
AO:{
global:undefined
}
fn()
- 编译阶段
来,三步走起!
- 第一步、创建一个GO对象
- 第二步、找变量声明,并没有,全局中只有一个赋值语句和函数声明
- 第三步、找函数声明,所以GO对象中编译阶段只有fn 编译到这,全局就编译完了,接下来就是函数体内的编译了
- 第一步、创建一个AO对象
- 第二步、找形参和变量声明,这里没有形参,变量global声明,值为undefined。写入AO
- 第三步、传参,实参形参都没有
- 第四步、找函数声明,也没有 到这,整个编译就结束了,可以开始执行了。
- 执行阶段
- 先执行赋值语句,因为global在全局中没有定义,这里会强制声明一个global并赋值100,写入GO中
- 然后到fn()执行函数体。
- 第一个log要global,先去自己的AO找,找到了,值为undefined。所以输出undefined
- 进行赋值,将AO中的global值更新为200
- 第二个log还要global,这里已经变成200了,所以输出200
- 最后AO中global被赋值为300 看到这里,再回想一下,是不是感觉很通透了。下面稍微总结下上面说的方法
总结
-
函数体内的编译四步曲,发生在函数执行之前
- 创建AO对象 (activation object)
- 找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
- 将实参和形参统一
- 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体
-
全局下的编译三步曲,发生在代码最前面
- 创建GO对象
- 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
- 找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体
看到这里,小伙伴们应该通透了许多,但回顾整个文章,又会感觉怎么说得这么简单,会不会不够深奥?其实,这里才是重头戏。大家不妨试想,既然已经能够用这套方法去理解JS代码运行都了些什么,那么,结合这个知识,再去思考那些深奥的执行上下文、作用域、声明提升,这个时候,JS的大门就真正地为你打开了。你才会明白何为通透。 (觉得不错的小伙伴们,请把通透打在评论区!)
-
文章中如果存在问题,或者大佬们有更好的见解,可以在评论去讨论,笔者会进行回复并修改,共同进步!