执行上下文可以认为是 代码的执行环境。
1 当代码被载入的时候,js解释器 创建一个 全局的执行上下文。
2 当执行函数时,会创建一个 函数的执行上下文。
3 当执行 eval()的时候,创建 一个 eval 执行上下文。
当js解释器开始工作的时候:
1 首先创建一个 执行上下文栈(后进先出)
2 接着创建一个 全局的执行上下文,并放入执行上下文栈中。
3 当在全局上下文中调用一个函数的时候.
为函数创建一个 执行上下文,
压入执行上下文栈中。
4 当函数执行结束的时候,位于栈顶的 执行上下文 被弹出(并销毁)。继续执行 新的位于栈顶的执行上下文。
一个执行上下文可以激活另一个上下文,就好比一个函数调用了另一个函数(或者全局的上下文调用了一个全局函数),然后一层一层调用下去。逻辑上来说,这种实现方式是栈,我们可以称之为上下文堆栈。
激活其它上下文的某个上下文被称为 调用者(caller) 。被激活的上下文被称为被调用者(callee) 。被调用者同时也可能是调用者(比如一个在全局上下文中被调用的函数调用某些自身的内部方法)。
当一个caller激活了一个callee,那么这个caller就会暂停它自身的执行,然后将控制权交给这个callee. 于是这个callee被放入堆栈,称为进行中的上下文[running/active execution context]. 当这个callee的上下文结束之后,会把控制权再次交给它的caller,然后caller会在刚才暂停的地方继续执行。在这个caller结束之后,会继续触发其他的上下文。一个callee可以用返回(return)或者抛出异常(exception)来结束自身的上下文。
如下图,所有的ECMAScript的程序执行都可以看做是一个执行上下文堆栈[execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。
图 4. 执行上下文栈
当一段程序开始时,会先进入全局执行上下文环境[global execution context], 这个也是堆栈中最底部的元素。此全局程序会开始初始化,初始化生成必要的对象[objects]和函数[functions]. 在此全局上下文执行的过程中,它可能会激活一些方法(当然是已经初始化过的),然后进入他们的上下文环境,然后将新的元素压入堆栈。在这些初始化都结束之后,这个系统会等待一些事件(例如用户的鼠标点击等),会触发一些方法,然后进入一个新的上下文环境。
见图5,有一个函数上下文“EC1″和一个全局上下文“Global EC”,下图展现了从“Global EC”进入和退出“EC1″时栈的变化:
图 5. 执行上下文栈的变化
ECMAScript运行时系统就是这样管理代码的执行。
详细了解了这个过程之后,我们就可以对执行上下文总结一些结论了。
ExecutionContext = {
variableObject: {//或activation object
},
scopeChain: { ... },
this: { ... }
}
下面我们分VO,scopeChain这三部分来说执行上下文里是什么。
首先,我们要给全局对象一个明确的定义:
全局对象(Global object) 是在进入任何执行上下文之前就已经创建了的对象;
这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。
全局对象初始创建阶段将Math、String、Date、parseInt作为自身属性,等属性初始化,同样也可以有额外创建的其它对象作为属性(其可以指向到全局对象自身)。例如,在DOM中,全局对象的window属性就可以引用全局对象自身(当然,并不是所有的具体实现都是这样):
在下列例子中
让我们看一个例子:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
全局对象表现为
global = {
Math: <...>,
String: <...>
...
...
test:"test">
window: global //引用自身
};
在函数声明时,还会创建一个[[scope]]属性,指向一个链表,在这里test.[[scope]]=[global]。
全局上下文中的变量对象——在这里,变量对象就是全局对象自己:
VO(globalContext) === global;
在函数执行上下文中,未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。变量对象和活动对象其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。
VO(functionContext) === AO;
活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化(全局上下文中不存在)。arguments属性的值是Arguments对象:
AO = {
arguments:
};
之后会添加其他属性,最终,会包含这些属性函数的所有形参、所有函数声明、所有变量声明。
变量对象的创建,依次经历了以下几个过程。
建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
让我们看一个例子:
function test(a, b) {
console.log(c);//function
var c = 10;
function c() {}
console.log(c);//10
}
test(10); // call
当进入带有参数10的test函数上下文时,AO表现为如下:
第一步通过arguments新建
AO(test) = {
a: 10,
b: undefined
};
第二步检查函数声明
AO(test) = {
a: 10,
b: undefined,
c: <reference to FunctionDeclaration "c">
};
第三步检查变量声明
AO(test) = {
a: 10,
b: undefined,
c: to FunctionDeclaration "c">//同名属性直接跳过
};
当进入执行阶段
AO(test) = {
a: 10,
b: undefined,
c: 10//执行到c=10修改
};
也就是说实际的执行顺序类似于下面
function test(a, b) {
function c() {}
var c;//为防止已有属性被修改为undefined,不执行。
console.log(c);//function
c=10;
console.log(c);//10
}
同样在函数声明时,也会创建一个[[scope]]属性,指向一个链表,在这里d.[[scope]]=[AO(test),global]
创建全局执行上下文时,scopeChain指向一个链表,包含global的VO。
创建函数执行上下文时,scopeChain初始化为AO+该函数的[[scope]]
scopeChain的形式类似于下图
this的指向,是在函数被调用的时候确定的,也就是执行上下文被创建时确定的。
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
var a = 20;
function fn() {
function foo() {
console.log(this.a);//20
}
foo();
}
fn();
这里函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
console.log(obj.c);//40
console.log(obj.fn());//10
这里obj仍然在全局变量对象中,this=window,而fn是由obj这个对象调用的,因此this=obj;
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;//只赋值未执行
j();//执行,赋值this
这里j是window的一个变量,window调用了j(),因此this=window。
function foo() {
console.log(this);
}
foo(); // global
alert(foo === foo.prototype.constructor); // true
// 但是同一个function的不同的调用表达式,this是不同的
foo.prototype.constructor(); // foo.prototype
这里foo是通过foo.prototype这个对象来调用的,因此this=foo.prototype
JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有着两个方法。它们除了参数略有不同,其功能完全一样。它们的第一个参数都为this将要指向的对象。
如下例子所示。fn并非属于对象obj的方法,但是通过call,我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了。这就是call/apply的用法。
function fn() {
console.log(this.a);
}
var obj = {
a: 20
}
fn.call(obj);//20
function fn(num){
this.num = num;
}
var a = new fn(1);
console.log(a.num); //1
为什么this会指向a?首先new关键字会创建一个空的对象,然后会自动调用一个函数apply方法,将this指向这个空对象,这样的话函数内部的this就会被这个空的对象替代。
也就是
var a={};
fn.apply(a,[num]);
标识符可以理解为变量名称、函数声明和普通参数。例如,当一个函数在自身函数体内需要引用一个变量,但是这个变量并没有在函数内部声明(或者也不是某个参数名),那么这个变量就可以称为自由变量[free variable]。那么我们搜寻这些自由变量就需要用到作用域链。
作用域链会从两个维度来搜寻。
1. 首先在原本的作用域链
2. 每一个链接点的作用域的链(如果这个链接点是有prototype的话)
我们再看下面这个例子:
Object.prototype.x = 10;
var w = 20;
var y = 30;
// 在SpiderMonkey全局对象里
// 例如,全局上下文的变量对象是从"Object.prototype"继承到的
// 所以我们可以得到“没有声明的全局变量”
// 因为可以从原型链中获取
console.log(x); // 10
(function foo() {
// "foo" 是局部变量
var w = 40;
var x = 100;
// "x" 可以从"Object.prototype"得到,注意值是10哦
// 因为{z: 50}是从它那里继承的
with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}
// 在"with"对象从作用域链删除之后
// x又可以从foo的上下文中得到了,注意这次值又回到了100哦
// "w" 也是局部变量
console.log(x, w); // 100, 40
// 在浏览器里
// 我们可以通过如下语句来得到全局的w值
console.log(window.w); // 20
})();
我们就会有如下结构图示。这表示,在我们去搜寻parent之前,首先会去proto的链接中。
图 10. with增大的作用域链
作用域链实际是一个指向变量对象的指针链表,它只引用而不实际包含变量对象。
参考资料:http://blog.csdn.net/ymjring/article/details/41804973
http://zhangzhaoaaa.iteye.com/blog/2251426
http://www.cnblogs.com/wangfupeng1988/p/3989357.html
https://www.cnblogs.com/lin-js/p/5293418.html
https://www.cnblogs.com/TomXu/archive/2012/01/16/2309728.html
http://www.cnblogs.com/TomXu/archive/2012/01/12/2308594.html
http://www.jianshu.com/p/a6d37c77e8db
http://www.jianshu.com/p/d647aa6d1ae6
http://www.cnblogs.com/pssp/p/5216085.html