一、作用域Scope和上下文Context
在javascript中,作用域scope和上下文context是两个不同的概念。每个函数调用都会伴随着scope和context,从本质上来说,scope是和函数绑定的,而context是基于对象的。即scope用于在函数调用时提供变量访问,且每次函数调用时,都不同;而context始终是关键词this
的值,它指向当前执行代码所属的对象。
scope 作用域
在前一篇的“javascript变量”部分讨论了javascript的作用域,分为全局和局部,且javascript中不存在块作用域。
** 'this' context 上下文**
context 经常被函数所调用的方式所决定。(1)当函数被作为一个对象的方法调用时,this 被设置为该函数所属的对象。如
var obj = {
foo: function() {
return this;
}
};
obj.foo() === obj; // true。 this指向obj对象
(2)当使用new关键字去创建一个新的函数对象时,this的值也被设置为新创建的函数对象。比如
function foo() {
alert(this);
}
foo() // window
new foo() // foo
(3)当函数被普通调用时,this被为全局contex或者浏览器的window对象。比如
function foo() {
alert(this);
}
foo() // window
二、函数生命周期
函数生命周期可以分为创建和执行两个阶段。
在函数创建阶段,JS解析引擎进行预解析,会将函数声明提前,同时将该函数放到全局作用域中或当前函数的上一级函数的局部作用域中。
在函数执行阶段,JS解析引擎会将当前函数的局部变量和内部函数进行声明提前,然后再执行业务代码,当函数执行完退出时,释放该函数的执行上下文,并注销该函数的局部变量。
三、变量对象
VO 和 AO
VO (Variable Object)变量对象,对应的是函数创建阶段,JS解析引擎进行预解析时,所有变量和函数的声明(即在JS引擎的预解析阶段,就确定了VO的内容,只不过此时大部分属性的值都是undefined)。VO与执行上下文相关,知道自己的数据存储在哪里,并且知道如何访问。VO是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:
(1)变量 (var, 变量声明);
(2)函数声明 (FunctionDeclaration, 缩写为FD);
(3)函数的形参
function add(a,b){
var sum = a + b;
function say(){
alert(sum);
}
return sum;
}
// sum,say,a,b 组合的对象就是VO,不过该对象的值基本上都是undefined
AO(Activation Object)对应的是函数执行阶段,当函数被调用执行时,会创建一个执行上下文,该执行上下文包含了函数所需的所有变量,该变量共同组成了一个新的对象就是Activation Object。该对象包括了:
(1)函数的所有局部变量
(2)函数的所有命名参数声明(Function Declaration)
(3)函数的参数集合
function add(a,b){
var sum = a + b;
var x = 10;
function say(){
alert(sum);
}
return sum;
}
add(4,5);
// AO = {
// arguments : [4,5],
// a : 4,
// b : 5,
// x: undefined
// say : <reference to function>,
// sum : undefined
// }
更详细的关于变量对象VO的知识,请访问:http://www.cnblogs.com/TomXu/archive/2012/01/16/2309728.html
四、执行上下文
执行上下文(execution context)是ECMAScript规范中用来描述 JavaScript 代码执行的抽象概念。所有的 JavaScript 代码都是在某个执行上下文中运行的。在当前执行上下文中调用 function会进入一个新的执行上下文。该function调用结束后会返回到原来的执行上下文中。如果function在调用过程中抛出异常,并且没有将其捕获,有可能从多个执行上下文中退出。在function调用过程中,也可能调用其他的function,从而进入新的执行上下文,由此形成一个执行上下文栈。
每个执行上下文都与一个作用域链(scope chain)关联起来。该作用域链用来在function执行时求出标识符(identifier)的值。该链中包含多个对象,在对标识符进行求值的过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与标识符名称相同的属性。在每个对象中进行属性查找时,会使用该对象的prototype链。在一个执行上下文中,与其关联的作用域链只会被with语句和catch 子句影响。
执行上下文属性
每个执行上下文都有三个重要的属性,变量对象(Variable Object), 作用域链(Scope Chain)和this,当然还有一些其他属性。
![][3]
当一段javascript代码被执行的时候,javascript解释器会创建并使用Execution Context,这里有两个阶段:
(1)创建阶段(当函数被调用,但开始执行内部代码之前)
(a) 创建 Scope Chain
(b) 创建VO/AO (函数内部变量声明、函数声明、函数参数)
(c) 设置this值
(2)激活阶段/代码执行阶段
(a) 设置变量的值、函数的引用,然后解释/执行代码。
在阶段(1)(b)创建VO/AO这一步,解释器主要做了以下事情:
(1)根据函数的参数,创建并初始化参数列表
(2)扫描函数内部代码,查找函数声明。对于所有找到的内部函数声明,将函数名和函数引用存储 VO/AO中;如果 VO/AO中已经有同名的函数,那么就进行覆盖
(3)扫描函数内部代码,查找变量声明。对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为 undefined;如果变量名称和已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性(就是说变量无效)
比如以下代码:
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
在“创建阶段”,可以得到下面的 Execution Context object:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
在“激活/代码执行阶段”,Execution Context object 被更新为:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
函数在定义时就会确定它的作用域和作用域链(静态),只有在调用的时候才会创建一个执行上下文,(1)其中包含了调用时的形参,函数内的函数声明与变量,同时创建活动对象AO;(2)并将AO压入执行上下文的作用域链的最前端,执行上下文的作用域链是通过它正在调用的函数的[[scope]]属性得到的(动态);(3)执行上下文对象中也包含this的属性
五、作用域链 scope chain
每个运行上下文都有自己的变量对象,对于全局上下文,它是全局对象本身;对于函数,它是活动对象。作用域链是运行上下文所有变量对象(包括父变量对象)的列表。此链表用于查询标识符。
var x = 10;
function foo() {
var y = 20;
function bar() {
alert(x + y);
}
return bar;
}
foo()(); // 30
上面的例子中, bar 上下文的作用域链包括 AO(bar) --> AO(foo) -- > VO(global).
作用域链如何构造的
上面提到,作用域链Scope Chain是执行上下文Execution Context的一个属性。它是在函数被执行时,通过被执行函数的[[scope]]属性得到。
函数创建时:在javascript中,函数也是一个对象,它有一个属性[[scope]],该属性是在函数被创建时写入,它是该函数对象的所有父变量对象的层级链,它存在于函数这个对象中,直到函数销毁。
函数执行时:创建执行上下文Execution context, 执行上下文Execution context 把 AO 放在 函数[[scope]]最前面作为该执行上下文的Scope chain。
即 Scope chain(运行上下文的属性,动态) = AO|VO(运行上下文的属性,动态) + [[Scope]](函数的属性,静态)
一个例子
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
alert(x + y + z);
}
bar();
}
foo(); // 60
全局上下文的变量对象是:
globalContext.VO === Global = {
x: 10
foo: <reference to function>
};
在“foo”创建时,“foo”的[[scope]]属性是:
foo.[[Scope]] = [
globalContext.VO
];
在“foo”激活时(进入上下文),“foo”上下文的活动对象是:
fooContext.AO = {
y: 20,
bar: <reference to function>
};
“foo”上下文的作用域链为:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
内部函数“bar”创建时,其[[scope]]为:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
在“bar”激活时,“bar”上下文的活动对象为:
barContext.AO = {
z: 30
};
“bar”上下文的作用域链为:
barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];
对“x”、“y”、“z”的标识符解析如下:
"x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10"y"
-- barContext.AO // not found
-- fooContext.AO // found - 20"z"
-- barContext.AO // found - 30
基于作用域链的变量查询
在代码执行时需要访问某个变量值时,JS引擎会在Execution context的scope chain中从头到尾查找,直到在scope chain的某个元素中找到或者无法找到该变量。