1 .脚本执行js引擎都做了什么呢?你是否知道?
- 语法分析
- 预编译(上下文创建)
- 解析执行(上下文执行)
语法分析就是检查你的语法有没有错误,然后预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数,我的理解这个过程中其实包括上下文的创建(当然你可以把全局script代码看成一个自调函数),后面会详细解释清楚,解析执行的意思就是执行代码了!
2.什么是执行栈?
执行栈,也叫调用栈,它是用来储存函数的执行上上文的,首先执行js代码时,之前我说过你可以把全局script代码看成一个自调函数,此时会创建一个全局执行上下文,然后把这个全局执行上下文推入(push)执行栈中,每当有新的函数调用就会产生新的函数执行上下文,不断的推入执行栈的顶端,根据后进先出(LIFO)的原理,当函数运行完毕后,就会把对应的上下文栈从执行栈中推出(pop),然后上下文控制权将移到当前执行栈的下一个执行上下文。
var a = '全局执行上下文';
function first() {
console.log('执行first函数')
second();
};
function second() {
console.log('执行second函数')
};
first();
3.什么是执行上下文?(这是es5的理解)
执行上下文包括变量对象,this指针,作用域链!
执行上下文有两个阶段:
- 创建阶段
- 执行阶段
1.创建阶段
- 确认this的指向
- LexicalEnvironment(词法环境) 组件被创建
- VariableEnvironment(变量环境) 组件被创建。
词法环境:
- 环境记录(储存变量和函数声明的位置)
- 对环境外部的引用(可以访问外面的词法环境)
词法环境类型:
- 全局环境(一个没有外部的词法环境的,所有它的外部词法环境为null,this也是指向这个全局对象)
- 函数环境(定义的变量和函数,arguments都储存在环境记录中,它的外部环境引用可以是全局环境也可以是父级的函数环境)!
变量环境也是词法环境的一种,词法环境指的储存是es6 let或者const声明的变量,变量环境一般指的是储存var声明的变量!
let a = '全局执行上下文';
function b(x) {
var c = 1;
console.log(x + c);
};
b(5);
上面代码修改下,来个简单点的好理解!下面是创建阶段的伪代码:
GlobalExectionContext = {//全局环境
ThisBinding: < Global Object > ,//this指向
LexicalEnvironment: {//词法环境
EnvironmentRecord: {//环境记录
Type: "Object",//全局的类型是Object
a: uninitialized,//储存a标识符
}
outer: < null >//全局没有外部环境引用
}
};
FunctionExectionContext = {//函数环境
ThisBinding: < Global Object > ,//this指向
LexicalEnvironment: {//词法环境
EnvironmentRecord: {//环境记录
Type: "Declarative",//函数的类型是Declarative
Arguments: { // 储存argumnets标识符
0: 5,
length: 1
},
},
outer: < GlobalLexicalEnvironment >//外部引用是全局环境
},
VariableEnvironment: {//变量环境
EnvironmentRecord: {//环境记录
Type: "Declarative",函数的类型是Declarative
c: undefined//储存c标识符
},
outer: < GlobalLexicalEnvironment >/外部引用是全局环境
}
}
这里可以清楚的看到var 和let的区别,let在创建阶段值是uninitialized,而var在创建阶段是underfined,这也是为什么var会存在变量提升,let会出现暂存死域的原理,其他就不解释了,注释写的很清楚了!
什么是作用域?
作用域是定义变量的区域,它有一套访问变量的规则,这套规则用来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找
什么是执行上下文?(这个es3的理解)
执行上下文有两个阶段:
- 进入执行上下文(es5的创建阶段 )
- 代码执行
进入执行上下文(编译阶段,代码还没执行),此时的变量对象会包括(如下顺序初始化):
- 所有函数没有实参的形参,值都为undefined!
- 函数声明如果存在相同名称,则直接覆盖这个属性!
- 变量声明如果存在相同名称,不会干扰已经存在的这类属性!
在函数上下文中,用活动对象(activation object, AO)来表示变量对象,变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问,当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问!
进入执行上下文:(建立阶段,函数被调用,但是还未执行函数中的代码)
- 创建变量对象(变量,参数,函数,arguments对象)
- 建立作用域链(scope chain)
- 确定this的值
上下文执行阶段(代码执行)
- 变量赋值,
- 函数引用
- 执行代码
[[Scope]]属性
[[Scope]]属性是函数创建时产生的,它是当前的作用域对象,也就是函数的标识符被创建的时候,我们所能够直接访问的那个作用域对象,它指向上级(父级的)作用域对象,当创建自己的活动对象|变量对象时,会把这个执行环境的[[scope]]按顺序复制到[[scope chain]]里,最后把这个活动对象推入到[[scope chain]]的顶部。这样[[scope chain]]就是一个有序的栈,这样保了对执行环境有权访问的所有变量和对象的有序访问,另外,[[scope]]中所存储执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫作用域链,下面来看下代码的创建阶段并附带伪代码
<script>
var a = 1;
function b(x) {
var c = 10;
function d() {
console.log('d函数执行');
};
d();
};
b(100);
</script>
首先创建一个全局的对象Global object(window),然后语法分析检查语法有没有错误,然后开始预编译(创建上下文),这个时候会创建活动对象!
//伪代码实现
//window.AO (window活动对象) document,navigator等属性我这里就不一一写了
global.AO: {
a: undefined,
b: function b(x) {
var c = 10;
function d() {
console.log('d函数执行');
};
d();
}
}
//global.[[scope]]===null
global.[[scope chain]]=[global.AO]//因为全局没有外部环境引用(直接把自己推入到[[scope chain]]的顶部)
创建完全局上下文之后就Push到当前的执行栈中,然后开始进行代码执行,执行过程中进行变量赋值,这个时候b函数还没有执行!
//伪代码实现
//执行阶段进行变量赋值
global.AO: {
a: 1,
b: function b(x) {
var c = 10;
function d() {
console.log('d函数执行');
};
d();
}
}
// 此时全局上下文 之前说过上下文包括变量对象,this指针,作用域链
//此时变量对象已经被激活成活动对象了(创建阶段就被激活了)
global.EC={
this:window,
AO: global.AO.//这里不重复写了,
scope chain:[ global.AO]// global.[[scope chain]]
}
然后执行到b函数,同样上面的类似操作,首先创建上下文阶段并压入执行栈!
//活动对象
b.AO = {
x:10//形参赋值
c: undefined,
d: function d() {
console.log('d函数执行');
}
}
执行阶段进行变量赋值!
b.AO = {
x:100
c: 10,
d: function d() {
console.log('d函数执行');
}
};
// b[[scope]]===global.[[scope chain]]
b.[[scope chain]]=[b.AO ,global.AO]//这里需要注意的是,先复制b[[scope]]===global.[[scope chain]],然后把b.AO 放在作用域链的最顶端,这个就是b的作用域链
此时b函数的上下文
// 此时全局上下文 之前说过上下文包括变量对象,this指针,作用域链
//此时变量对象已经被激活成活动对象了(创建阶段就被激活了)
b.EC={
this:window,//因为是window调用 b()===window.b();
AO: b.AO.//这里不重复写了,
scope chain:[b.AO ,global.AO] // b.[[scope chain]]
}
然后执行到d函数,同样上面的类似操作,首先创建上下文阶段并压入执行栈!然后再执行阶段,这里d函数里面没有变量声明和函数声明,我这里就不写了!这里我模拟下d的执行上下文!
d.context={
this:window,//因为是window调用 d()===window.d();
AO: d.AO.//这里不重复写了,
scope chain:[d.AO,b.AO ,global.AO]//先把b.[[scope]]===[b.AO ,global.AO] 复制过来,然后把d.AO放到作用域的最顶端!
}
执行完d函数之后,从执行栈中弹出(后进先出),上下文控制权将交给下一个执行上下文!然后不断的重复!最后只留下一个全局上下文,当浏览器关闭时候出栈!我这个例子比较简单!但是看完之后应该可以很清楚的了解上下文的执行,作用域和作用域链它们之前的关系!
总结:
-
创建一个全局执行上下文并Push到当前的执行栈中!
-
创建全局对象并将将全局对象压入作用域链!
-
执行全局上下文进行变量赋值,函数引用,执行代码等操作!
-
创建函数上下文并Push到当前的执行栈中!
-
创建函数活动对象,并用 arguments 创建活动对象!
-
为当前创建[[Scope]]属性,并将复制其上层(父级)作用域链保存到该属性!
-
将当前活动对象压入当前上下文中的作用域链最顶端!
-
执行函数,然后重复函数创建执行等操作!
-
函数执行完上下文从执行栈的顶端移除,并且变量对象随之销毁!
-
所有函数执行完之后执行栈中只剩下全局上下文,在应用关闭时销毁!
tip:
对于上下文的创建阶段,我说到了es6的概念和es3的概念,es6提到了确认this指向,词法环境,变量环境,es3提到this指向,创建变量对象,建立作用域链,es6的词法环境包括(储存变量和函数声明的位置)就是es3的创建变量对象的意思,对环境外部的引用(可以访问外面的词法环境)也是建立作用域链的意思,es6之所以多了个变量环境就是为了对var定义的变量的补充 ,所以两个版本的解释并不发生冲突!,所以你理解一个版本的概念就可以了!
参考文献:JavaScript深入理解之作用域链