ECMAScript可执行程序和可执行上下文
ES5可执行程序
- 全局代码
- eval代码
- 函数代码
ES5可执行上下文
当ECMAScript引擎执行一段ECMAScript程序的时候,进入不同的可执行程序会创建不同的可执行上下文。比如进入全局代码,会创建全局上下文,进入函数代码,会创建函数上下文。其中函数上下文最具有代表性,进入一段函数代码会发生下面的事情:
引擎进入函数代码,执行前,建立函数上下文:
声明this指向,如果未给函数执行传入this参数,this指向global(浏览器下面就是 window)。
创建一个新的词法环境,将这个词法环境的外部词法环境指向函数的[[scope]]属性,而函数的[[scope]]属性是等于函数创建时所处环境的词法环境。
在这里解释一下词法环境(Lexical Environments ):
词法环境是一个规范类型(实现规范的内部类型,不是给ECMAScript程序使用的)。词法环境包含两个内容—— 环境记录对象 和 外部词法环境,其中环境记录保存着这个上下文中的变量存储情况,而外部词法环境连向外部的词法环境,在查找变量的适合形成链式结构,一步一步向上查找。
解释一下环境记录对象:
环境记录对象(Environment Records)有两种实现:声明式环境记录(declarative environment records,,下面简称为DER) 对象式环境记录 (object environment records, 下面简称为OER)。其中声明式环境记录会记录包括函数声明、var声明、catch语句等。对象式环境记录包括全局程序(window)和with语句。对象式环境记录可以理解为把变量等信息保存在自己的 binding object 上面,比如 window和with传入的object。
DER我们可以理解为保存着当前上下文所需要的变量,包括函数声明、var变量、arguments等。OER我们可以理解为保存着某个对象的变量,比如window对象和with使用的对象。
1
2
3
4
5
6
7//这一步我们创建了一个词法环境LE
newLE = {
DER: {
...//存储变量
},
outerLE: function.[[scope]] //指向外部词法环境形成链式结构
}
建立两个变量
LexicalEnvironment
和VariableEnvironment
都指向上面说的 LE。这里为什么需要两个变量指向同一个词法环境对象我们稍后再说,现在先假设他们就是一个东西,这个里面存储了当前函数需要的变量。
1
LE = newLE; VE = newLE;
上下文创建完毕之后,发生声明绑定实例化(Declaration Binding Instantiation)
声明绑定实例化就是把函数中的变量放在DER里面,放的顺序依次是 函数声明、形参和var变量。默认var变量都是 undefined
代码执行
代码执行阶段DER中存储的变量值会根据执行的代码被覆盖。其中涉及到标示符解析的过程。标示符解析就是变量查找的过程,它会找当前EC的LE,没有再找当前EC的outerEC的LE,一直向上便利。
LE和VE的区别:
LE和VE在一开始都等于newLE,就是新创建的词法环境对象。
VE用作变量存储,再绑定声明实例化过程中,VE的变量被赋值和存储,LE在大部分时间都等于VE。
with语句、catch语句等词法环境种,LE会被赋新的值,从而LE != VE。
所以LE是可变的,而VE从创建就一直指向同一个对象。
变量查找是找LE,但是变量存储是存在VE上面的。在with语句中,会插入新的LE,让with语句中的变量先查找withObj,但是退出with语句,LE和VE的值又相等了,这就是为什么要设计LE和VE。因为有些情况下,LE会被赋上新的值改变变量查找的情况。
函数声明解析的[[scope]] = 函数所处环境的VE;函数表达式解析的[[scope]] = 函数所处环境的LE;所以在with语句中,LE变化时,会影响到函数表达式的变量查找。并不会影响到函数声明的变量查找:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//在控制台执行这两段脚本
(function(){
var a = 2;
function test(){
console.log(a)
}
with({a:1}) {
test();
}
})();
//因为with语句的LE会变成 {a:1},函数声明的[[scope]]是VE不受影响,所以第一个console会
//输出2,但是下面那个LE会受影响,所以变成1
(function(){
var a = 2;
with({a:1}) {
var test = function(){
console.log(a)
}
test();
}
})();
实例分析
1 | //分析下面这段代码的执行过程 |
1 进入全局代码,创建全局上下文:
1 | GEC = { |
2 全局代码声明绑定实例化:
1 | GEC.VE = { |
3 依次执行代码:
1 | console.log(a);//查找GEC.LE.OER.a , 输出 undefined |
创建test函数上下文:
1
2
3
4
5
6testEC = {
VE: {
DER: {},
outer: testEC.[[scope]] //testEC.[[scope]] = GEC
}
}
test函数声明绑定实例化:
1
2
3
4
5
6
7
8testEC = {
VE: {
DER{
a: function a(){}, //根据函数表达式>形参>var 的优先级关系
}
outer: testEC.[[scope]] //testEC.[[scope]] = GEC
}
}
test函数执行:
1
2console.log(a); //输出testEC.VE.DER.a
a = function(){}; //改变a的值
- test函数执行完毕,可执行上下文回退到GEC
4 执行b()
创建b函数上下文:
1
2
3
4
5
6
7
8bEC = {
VE: {
DER: {
//为空,没有变量
},
outer: bEC.[[scope]] // = GEC
}
}
b函数声明绑定实例化,由于没有变量,跳过此步骤
b函数执行console.log(a); 查找
bEC.VE.DER.a
没有,然后找bEC.VE.outer , 就是GEC, GEC有变量a = 1; 所以输出 a = 1;