执行上下文和作用域链

执行上下文(Execution context)

执行上下文可以认为是 代码的执行环境。
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运行时系统就是这样管理代码的执行。
详细了解了这个过程之后,我们就可以对执行上下文总结一些结论了。

  1. 单线程
  2. 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待
  3. 全局上下文只有唯一的一个,它在浏览器关闭时出栈
  4. 函数的执行上下文的个数没有限制
  5. 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。
    执行上下文大概形式是这样的
ExecutionContext = {
   variableObject: {//或activation object
   },
   scopeChain: { ... },
   this: { ... }
}

下面我们分VO,scopeChain这三部分来说执行上下文里是什么。

创建VO/AO

全局上下文中的变量对象

首先,我们要给全局对象一个明确的定义:

全局对象(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:
};
之后会添加其他属性,最终,会包含这些属性函数的所有形参、所有函数声明、所有变量声明。
变量对象的创建,依次经历了以下几个过程。

  1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。

  2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。

  3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为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

创建全局执行上下文时,scopeChain指向一个链表,包含global的VO。
创建函数执行上下文时,scopeChain初始化为AO+该函数的[[scope]]
scopeChain的形式类似于下图
执行上下文和作用域链_第1张图片

定义this

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

使用call,apply显示指定this

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

new操作符会改变函数this的指向问题

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

你可能感兴趣的:(前端基本知识)