查阅了大量文章文献,终于把js的执行顺序搞通了,我发觉这里面难搞的原因在于,很多文章只写了某一块知识点,或者某块知识点还没讲透彻导致有点懵,现在我总算是搞懂了,所以我想从头到尾彻彻底底的讲一遍。
1.JS的执行顺序
先简单讲下整个执行顺序
1.我们所写的js代码在执行阶段前,会有个预加载过程,目的是建立当前js代码的执行环境,而这个执行环境就是上下文(context)。
2.这个上下文包含了VO(Variable Object变量声明对象),AO(Active Object动态变量对象),scopeChain(作用域链)。
3.建立好上下文后,才会开始执行我们写的js代码。
1.1上下文(Execution Context)
通过上面我们可以直到,上下文就是每段代码的执行环境。
它分为三种类型:
1.全局级别的代码 - 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
2.函数级别的代码 - 当执行一个函数时,运行函数体中的代码。
3.Eval的代码 - 在Eval函数内运行的代码。
先讲函数级别的上下文(执行环境)。
上下文建立的阶段:
1.建立VO(Variable Object)
2.建立AO(Active Object)
3.建立 scopeChain
1.1.1 静态变量声明(Variable Object)
我们看下面的代码:
可以看到foot在声明前打印会显示“undefind”,但ball在声明前打印会报错显示 “ball is not defined”。两者的区别在于,后面foot被声明了,而ball至始都没有声明过。
那么,就抛出一个问题了,既然代码是从上到下执行,按道理来说两者都应该是“is not defined”,为什么前者会是undefind并且无报错执行。
答案就是在js代码执行前有个预加载阶段用来建立代码的执行环境(上下文),而建立上下文的第一个阶段就是建立当前环境声明过的变量Variable Object。
我们来看下面代码所生成的上下文对象
function fn(){
var foot = '脚';
var head = function(){
return '头';
}
function hand(){
return '手';
}
}
Variable Object :
// VO创建
fnExecutionContext = {//fn函数的上下文对象
VO: {
arguments: { .... },
hand:hand函数的引用地址,//函数声明
head: undefined, // 变量函数声明
foot: undefined, // 变量声明
this:Window,//this指向
},
scopeChain: {} //作用域链
}
我们可以看出,就跟我们刚才打印的一样,声明过的参数全部都被记录在了这个VO对象中,只不过var声明的参数值都为undefined。
也因此我们为什么刚才的打印会undefined了。
那么会有个疑问,为什么function声明的函数就有值呢,这个简称变量提升:
所谓的变量提升,就是在建立上下文是会把function声明的函数都提升到上面来,这样即时你把function写到下面,他依然会比你var声明的值更快获得实际的值。
那么什么时候var声明的变量才能有值呢,这个就是AO的原因了。
1.1.2 动态变量声明(Active Object)
所谓AO就是Active Object的简写,那么什么是AO呢,他其实就是VO,只不过在初步建立完VO后,就会开始执行我们的js代码。
js代码从上到下执行的过程中,如果遇到赋值的情况,那么就会更新VO变成了AO。我们来看看AO对象。
fnExecutionContext = {//fn函数的上下文对象
AO: {
arguments: { .... },
hand:hand函数的引用地址,//函数声明
head: head函数的引用地址, // 变量函数声明
foot: '脚', // 变量声明
},
scopeChain: {} //作用域
}
是不是一下子就懂了。
接下来我们看VOAO的arguments参数
1.1.3 Variable Object的arguments属性
每个函数的调用,都必然存在arguments对象,它存在于Variable对象当中,接下来我们来看看arguments对象里面到底有些什么东西。
function showargs() {
console.log( arguments );
}
showargs(1,2,3,4,5);
打印结果如下:
从上图可以明白,其实就是把调用这个函数时所传入的参数,通过一个类数组对象接收了。
类数组对象是什么意思呢,就是类似数组的对象,他没有数组的一些方法,但是他却是以{“0”:‘您好’,“1”:我好,“2”:大家好}
这种很像数组的方式存在。
除了传入的参数,我们通过arguments.callee还能获得当前函数的代码
function showcallee() {
var a = '这里是代码';
var b = '这是另一段代码';
var c = a + b;
console.log(arguments.callee);
return c;
}
showcallee();
到此,arguments对象我们就了解完毕了,他在上下文中起着,记录调用此函数时所传入的参数的作用。
1.1.4 scopeChain作用域链
在上下文对象中,我们看到了一个scopeChain作用域链,那么这个作用域链有什么用呢。
我们看如下代码:
function fn(){
var foot = '脚';
function footBall(){
var footBall = '足球';
console.log(footBall+ '需要:' +foot);
}
footBall();
}
打印结果如下:足球需要脚
从上面代码看出,函数footBall可以使用到函数fn所声明的变量foot。
如果没有scopeChain,这是做不到的,scopeChain就是充当这一角色。
JS权威指南指出”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”
ECMA262中所述 任何执行上下文时刻的作用域, 都是由作用域链(scope chain)来实现.在一个函数被定义的时候, 会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性.
作用域链其实就是所有内部上下文的变量对象的列表。用于变量查询。比如,在上述例子中,“footBall”上下文的作用域链包含了VO(footBall),VO(foot)和VO(global)。
看完上面的概念,我们可能会有点懵,接下来我们看下这个footBall的上下文对象。
footBallExecutionContext = {
VO: {
arguments: { .... },
footBall : undefined, // 变量声明
this: fn,
scopeChain: {
VO(footBall函数的VO),
VO1(fn函数的VO),
VOGlobal(全局的VO)
} //作用域链
}
.....
}
可以看出来footBall的VO对象是没有foot的变量声明,那为什么执行下来不会报错,而且还有值呢。
这就是scopeChain的意义了,如果在当前VO找不到相关的定义,便会通过scopeChain对象找父级的VO,找不到就再往上找,直到全局的VO。所以当footBall的VO对象里没有foot的定义时,便会去找父级的VO也就是fn函数的VO,在fn函数里有foot的定义。
如此下来,整个上下文就讲完啦。简单来说就是一个执行代码的环境,但细讲会发觉他有好多的属性,这都是为了在执行每一段js代码时都能有序,无误的执行。不得不赞叹,语言类的开发者真的要顾全很多的东西。
总结:
一个js的代码的执行顺序简单来讲是两步
1.建立当前所要执行的js代码的执行环境,简称上下文。
2.从上到下一次执行代码。而这篇文章主要讲的是建立环境的过程和顺序
下一篇我们会讲我们所写的代码的执行顺序,这里我们
参考文献:https://blog.51cto.com/enjoyjava/1124940
参考文献:https://www.tensweets.com/article/5ba71b42f0cb0c04f86b23dd
js代码的执行顺序参考文献:https://www.jb51.net/article/127025.htm
拓展
我们都知道,在es6多了let这个声明方式,虽然用是用的很多,也知道他的作用,但其实他和var的区别还是蛮大的。特别是在上下文环境建立的过程中。
比如最简单的:
console.log(k);//undifined
var k=2;
比如最简单的:
console.log(k);//报错
let k=2;
这里其实说的就是你定义了let的时候,在预编译阶段let定义的时候就会分配一个内存空间给这个变量,但分配后并不像var定义的那样,一旦分配内存空间,立刻定义值为undefined;用let定义的分配了内存,但这个内存并没有被分配任何值,为uninitialized状态。
executionContextObj={
‘scopeChain’: {VO},
‘variableObject’: {
k:uninitialized //uninitialized
},
‘this’: {window}
}
2.执行代码阶段(AO的建立阶段 VO=>AO)
执行第一行代码后,executionContextObj对象中的variableObject中的name:修改为jackiewillen.继续执行第二行,修改subName为kevin;这个时候你可能会有这样的疑问,如果这个subName只被let定义,而一直都未被赋值,那么值一直是uninitialized吗?
来看下面这样一段代码:
let k;
console.log(k);//the result is undefined;
那么为什么这里打印出了undefined呢,却没有报错呢,因为之前的console.log是放在let定义之前的,那个时候k的值是uninintialized;但过了预编译进入执行阶段程序运行到let k;时发现其未赋值,只是定义了这个变量,这个时候编译器才决定给k赋值为undefined。这个可以参见es6标准。
这也就是为什么let没有变量提升的原因。
进入到for循环,我们知道在es6中增加了块级作用域。也就是只要有{}都是一个块,在块外访问不到块内部的内容。
let声明的变量,事实上会另外开辟一个空间,叫词法环境,专门存储let声明的变量,只有在这个块中,才能使用。下面的代码blockObject就是所谓的此法环境。
如if(true){ let a=4;}
console.log(a);
这样就是访问不到这个a的值的。
接下来执行当for循环中的i值为1的时候,executionContextObj如下所示:
var name = 'name';
var myName = 'yin';
for(let i = 0; i<2;i++){
let yourName = 'herry'
console.log(i);
}
if(true){
let subName = 'kevin'
}
executionContextObj={
'scopeChain': {},
'variableObject': {
name:jackiewillen,
myName:yin, //因为对于var定义的变量,不存在块作用域
anonymous1:reference to function(){console.log(i)};
},/* function arguments / parameters, inner variable and function declarations */
'blockObject':{
subName:kevin
},
'blockObject':{
i:1,
yourName:herry //为什么这里在block中呢,而不像subName那样在variableObject中呢,因为用let定义的都在它所待的块定中
},
'this': {window}
}
注:scopeChain每次生成时都会检查有没有闭包Closure存在,然后再检查有没有Block的然后把上一个variableObject拿过来(除去其中已经是闭包的元素,和block的元素。其样式如同scopeChain={closure+vo+bo+vo+bo…} ,还有,只有在函数被激活也就是调用的那一刻,其内部声明的变量vo和bo才会加到scopeChain中去。打算再写一篇文章专门去写这个scopeChain
注:此时的blockObject是一个全新的blockObject因为前一个已经被销毁,当一对花括号运行过后,相应的block也就销毁了。
1.第一步,看看有没有闭包,发现没有。
'scopeChain': {}
2.第二步,看看有没有引用block块元素。发现引用了一个executionObject中的blockObject中的i,所以scopeChain为:
'scopeChain': {
blockObject:{
i:1
}
}
3.第三步,把区块链上的vo和bo都拿过来。
'scopeChain': { blockObject:{ i:1 }},
'variableObject': {
name:jackiewillen,
myName:yin, //因为对于var定义的变量,不存在块作用域
anonymous1:reference to function(){console.log(i)}; },
'blockObject':{
subName:kevin
},
}
这个就是匿名函数anynmous1中的scopeChain
当for当中的i运行到2的时候,executionObject如下所示:
executionContextObj={
'scopeChain': {},
'variableObject': {
name:jackiewillen,
myName:yin, //因为对于var定义的变量,不存在块作用域
anonymous1:reference to function(){console.log(i)};
anonymous2:reference to function(){console.log(i)};
},/* function arguments / parameters, inner variable and function declarations */
'blockObject':{
subName:kevin
},
'blockObject':{
i:2,
yourName:herry, //为什么这里在block中呢,而不像subName那样在 variableObject中呢,因为用let定义的都在它所待的块定中
},
'this': {window}
}
通过以上实验,我们知道let声明的变量会存放在一个单独开辟的词法环境当中,这个环境在这个{}花括号运行中才能拿到,直到这个花括号结束后,这个词法环境也会被销毁。