可执行代码
JS中分为3种:全局代码、函数代码、eval代码。
当执行到一个函数的时候,就会创建执行上下文(execution context)
执行上下文栈
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文我们定义执行上下文栈是一个数组:ECStack = [];
JS 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空。
我们来分析下面代码:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
// fun1()
ECStack.push(<fun1> functionContext);
// fun2
ECStack.push(<fun2> functionContext);
// fun3
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
变量对象中又可分为全局上下文下的变量对象
和函数上下文下的变量对象
。
- 全局上下文下的变量对象
全局上下文中的变量对象就是全局对象。
另外:
1.在客户端 JavaScript 中,全局对象就是 Window 对象,可以通过 this 引用。
console.log(this); // window
2.全局对象是由 Object 构造函数实例化的一个对象。
console.log(this instanceof Object);
3.作为全局变量的宿主。
var a = 1;
console.log(this.a);
5.客户端 JavaScript 中,全局对象有 window 属性指向自身。
var a = 1;
console.log(window.a);
this.window.b = 2;
console.log(this.b);
- 函数上下文下的变量对象
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JS 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object ,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
通过var声明的变量才被放入AO中,也会出现变量提升
执行过程
- 进入执行上下文
当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:
-
函数的所有形参 (如果是函数上下文)
*由名称和对应值组成的一个变量对象的属性被创建
*没有实参,属性值设为undefined
-
函数声明
*由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
*如果变量对象已经存在相同名称的属性,则完全替换这个属性 -
变量声明
*由名称和对应值(undefined
)组成一个变量对象的属性被创建;
*如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
举个例子:
function foo(a) {
console.log(b) //undefined
var b = 2;
function c() {} // 函数
var d = function() {}; // 变量
var c = 1 // 不影响c
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 对象
-
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
-
在代码执行阶段,会再次修改变量对象的属性值
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
上面说到:函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性 [[scope]],当函数创建
的时候,就会保存所有父变量对象到其中。当函数激活
时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。这时候执行上下文的作用域链,我们命名为 Scope。
函数执行上下文中作用域链和变量对象的创建过程:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
- checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
- 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
- checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
- 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
- 第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
- 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
- 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
在源代码中当定义(书写)一个函数的时候(并未调用),js引擎也能根据函数书写的位置,函数嵌套的位置,生成一个[[scope]],作为该函数的属性存在(这个属性属于函数的)。
然后进入函数执行阶段,生成执行上下文,执行上下文可以宏观的看成一个对象,(包含vo,scope,this),此时,执行上下文里的scope
(作用域链)和之前属于函数的那个[[scope]]不是同一个,执行上下文里的scope,是在之前函数的[[scope]]的基础上,又新增一个当前的AO对象构成的。
函数定义时候的[[scope]]和函数执行时候的scope,前者作为函数的属性,后者作为函数执行上下文的属性。
this
var obj = {
a: 1,
b: function(){console.log(this);}
}
1、作为对象调用时,指向该对象 obj.b();
// 指向obj
2、作为函数调用, var b = obj.b; b();
// 指向全局window
3、作为构造函数调用 var b = new Fun();
// this指向当前实例对象
4、作为call与apply调用 obj.b.apply(object, []);
// this指向当前的object