目标:
- 掌握执行上下文的相关知识
- 闭包的原理
知识点:
- 执行上下文栈
- 执行上下文的变量组成
- VO、AO
- 作用域链
- this
- 闭包的定义及原理
1、执行上下文
1.1、变量提升和函数提升
/* case1 */
var foo = function() {
console.log('foo1')
}
foo() //输出foo1
var foo = function() {
console.log('foo2')
}
foo() //输出foo2
/* case2 */
function foo() {
console.log('foo1')
}
foo() //输出foo2
function foo() {
console.log('foo2')
}
foo() //输出foo2
/* case3 */
console.log(add2(1,1)); //输出2
function add2(a,b){
return a+b;
}
console.log(add1(1,1)); //报错:add1 is not a function
var add1 = function(a,b){
return a+b;
}
通过以上3个代码实例,我们可以发现JavaScript中代码并不是一行一行的分析和执行,而是一段一段地分析执行。当执行一段代码的时候会进行一个“准备工作”。
JavaScript的可执行代码的类型就三种:全局代码、函数代码、eval代码,按这三种类型进行一段一段的拆分,而这里的“准备工作”就是执行上下文
代码中的var
和function
会进行变量提升和函数提升,但是有以下这些区别:
- var
var x = xxx
会将var x
对x
变量的声明提升到代码开始的地方,而赋值操作x = xxx
是执行到这一行时才执行 - function
function x() {}
用函数语句创建的函数x()
会把函数名和函数体都提升到最开始的地方。
所以上面的case2和case3相当于下面这段
/* case2 */
function foo() {
console.log('foo1')
}
function foo() {
console.log('foo2')
}
foo() // 输出foo2
foo() // 输出foo2
/* case3 */
function add2(a,b){
return a+b;
}
var add1
console.log(add2(1,1)); // 输出2
console.log(add1(1,1)); // 此时add1是undefined,所以会报错:add1 is not a function
add1 = function(a,b){
return a+b;
}
1.2、执行上下文栈
执行上下文是通过执行上下文栈(Execution context stack,ECS)来进行管理的,这里我们定义执行上下文栈为数组ECStack = []
来模拟执行上下文栈的行为,遵循后进先出的原则,执行到对应的代码的时候进行上下文压栈,执行完之后进行出栈。
开始执行JavaScript代码的时候,最先遇到的就是全局代码,所以先向ECStack
压入全局执行上下文,用globalContext
表示,整个应用程序结束的时候,globalContext
才会被推出。
来段简单的代码来举个例子:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
先推入globalContext
全局上下文,执行到fun1()
这行代码的时候开始压栈,先是推入fun1Context
,然后是fun2Context
,最后是fun3Context
,fun3
执行完之后将fun3Context
弹出,依次类推。这就是代码执行顺序,退出执行环境后推出globalContext
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中调用了fun2,创建fun2的执行上下文
ECStack.push(<fun2> functionContext);
// fun2还调用了fun3
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
在上一章节中提到的作用域面试真题的那两段代码的执行上下文是不一样的,在这里进行解析
// case 1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// 解析:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
// case 2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
// 解析:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
1.3、执行上下文
当JavaScript执行一段可执行代码时,都会创建对应的执行上下文
在上面我们已经知道了执行上下文栈是维护执行上下文的,那么执行上下文中又有什么属性呢?
对于每个执行上下文都有三个重要的属性:
- 变量对象 (Variable object,VO 或者 Activation object, AO)
- 作用域链 (Scope Chain)
- this
1.3.1、变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
全局上下文和函数上下文的变量对象的区别:
- 全局上下文
全局对象是预定义的对象,在JS中,全局对象就是Window对象
,可以访问其他所有预定义的对象、函数和属性;
在顶层JS代码中,可以用关键字this
引用全局对象也就是window
对象,且在这层代码中声明的所有变量都会成为全局对象的属性;
全局对象有window属性指向自身,即this.window.b === this.b
。 - 函数上下文
在函数上下文中,用活动对象(AO)来表示变量对象。AO其实就是VO,但是因为只有进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫活动对象。
而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象是在进入函数上下文时被创建的,它通过函数的argumens属性初始化。
执行上下文的代码会分成两个阶段进行处理:分析和执行
- 分析 —— 进入执行上下文
构造变量对象,它的值包括:- 函数的所有形参
- 函数声明
- 变量声明
function foo(a) { var b = 2; function c() {} var d = function() {}; b = 3; } foo(1); // 对应的AO: AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined }
- 执行 —— 代码执行
在这个阶段会顺序执行代码,根据代码修改变量对象的值,执行完成后,此时的AO为:AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: reference to function c(){}, d: reference to FunctionExpression "d" }
以上就是变量对象的创建过程,总结:
- 全局上下文的变量对象初始化是全局对象;
- 函数上下文的变量对象初始化只包括 Arguments 对象;
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
- 在代码执行阶段,会再次修改变量对象的属性值;
注:在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
1.3.2、作用域链
查找变量的时候,先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
JS采用静态作用域,函数的作用域在函数定义的时候就决定了,原理如下:
函数有一个内部属性[[scope]]
,当函数创建的时候就会保存所有父变量对象到其中,[[scope]]
就是所有父变量对象的层级链,但是它并不代表完整的作用域链。
function foo() {
function bar() {}
}
// 函数创建时各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
当函数激活,进入函数上下文,创建变量对象后,就会将活动对象添加到作用域链的前端,这时候执行上下文的作用域链Scope为Scope = [AO].concat([[Scope]])
。至此,作用域链创建完毕。
1.3.3、变量对象和作用域链创建过程总结
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
该段代码执行过程如下所示:
- checkscope函数被创建,保存作用域链到内部属性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
- 进入函数,创建函数执行上下文并压入执行上下文栈
ECStack = [ checkscopeContext, globalContext ];
- 分析函数,复制
[[scope]]
属性创建作用域链checkscopeContext = { Scope: checkscope.[[scope]], }
- 创建活动对象,初始化AO
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], }
- 将活动对象压入 checkscope 作用域链顶端
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] }
- 执行函数,修改属性值
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] }
- 查找到scope2的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [ globalContext ];
1.3.4、this
this
是规范类型中的标准规范,this
始终指向引用它的值
知道this
原理前需要了解的概念:
-
Reference
它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中,它由三部分组成:- base value
只能是属性所在的对象或者EnvironmentRecord
,就是string,number这些基本类型的值 - referenced name
属性名称 - strict reference
是否为严格模式,一般为false
var foo = 1; // foo对应的Reference是: var fooReference = { base: EnvironmentRecord, //相当于1 name: 'foo', strict: false }; var foo = { bar: function () { return this; } }; foo.bar(); // bar对应的Reference是: var BarReference = { base: foo, propertyName: 'bar', strict: false };
还有
GetBase
和IsPropertyReference
两个方法,作用分别是获取base value
的值和判断base value
是否为对象 - base value
-
MemberExpression
官方的MemberExpression有以下几种:- PrimaryExpression —— 原始表达式
- FunctionExpression —— 函数定义表达式
- MemberExpression [ Expression ] —— 属性访问表达式
- MemberExpression . IdentifierName —— 属性访问表达式
- new MemberExpression Arguments —— 对象创建表达式
function foo() { console.log(this) } foo(); // MemberExpression 是 foo function foo() { return function() { console.log(this) } } foo()(); // MemberExpression 是 foo() var foo = { bar: function () { return this; } } foo.bar(); // MemberExpression 是 foo.bar
所以简单理解
MemberExpression
其实就是()左边的部分
如何确定this
的值?
- 计算 MemberExpression 的结果赋值给 ref;
- 判断 ref 是不是一个 Reference 类型;
- 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
- 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
- 如果 ref 不是 Reference,那么 this 的值为 undefined,此时
this
就指向了window
概括一下就是:
判断最后一个括号左边的部分是不是Reference
,如果不是,那this
就是undefined
,指向window
;如果是,而且base value
是对象,那this
的值就是这个对象
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1
console.log(foo.bar()); //2
MemberExpression: foo.bar;
Reference: {
base: foo,
name: 'bar',
strict: false
}
// foo.bar是方法,所以ref是Reference;方法是对象,IsPropertyReference(ref) 是true;
// 综上:this是foo
//示例2
console.log((foo.bar)()); //2
MemberExpression: (foo.bar);
Reference: {
base: foo,
name: 'bar',
strict: false
}
// 与示例1同理
//示例3
console.log((foo.bar = foo.bar)()); //1
MemberExpression: (foo.bar = foo.bar);
// foo.bar = foo.bar是表达式,不是Reference,所以this是undefined
//示例4
console.log((false || foo.bar)()); //1
MemberExpression: (false || foo.bar);
// (false || foo.bar)不是Reference,所以this是undefined
//示例5
console.log((foo.bar, foo.bar)()); //1
MemberExpression: (foo.bar, foo.bar);
// (foo.bar, foo.bar)不是Reference,所以this是undefined
1.4、闭包
能够访问自由变量(在函数中使用,既不是函数的参数,也不是函数局部变量的变量)的函数称为闭包。
为什么会形成闭包呢?主要还是因为执行上下文中的作用域链,因为作用域链能够访问到上一级的活动变量。
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
再来解析一下这段代码的执行上下文的变化情况
- 进入全局代码,全局执行上下文压入执行上下文栈
ECStack = [ globalContext ]
- 全局执行上下文初始化
- 执行
checkscope
函数,创建函数执行上下文并压入执行上下文栈ECStack = [ checkscopeContext globalContext ]
checkscope
执行上下文初始化,创建变量对象、作用域链、this等checkscope
函数执行完毕,checkscope
执行上下文从执行上下文栈中弹出ECStack = [ globalContext ]
- 执行
f
函数,创建函数执行上下文并压入执行上下文栈ECStack = [ fContext globalContext ]
f
执行上下文初始化,创建变量对象、作用域链、this等fContext = { AO: {...}, Scope: [AO, checkscopeContext.AO, globalContext.VO] }
f
函数执行完毕,f
函数上下文从执行上下文栈中弹出
在第七步我们可以看到 f
函数的作用域链中存在checkscopeContext.AO
,所以即使 checkscopeContext
被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO
活在内存中,f
函数依然可以通过 f
函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。