发布日期 » 2017年11月4日 星期六

版权声明 » 帅华君原创文章,未经允许不得转载。

JavaScript代码执行上下文与this指向

执行上下文栈

ECMAScript中有三种代码类型: global codefunction codeeval code

每一种类型的代码都会在他们的执行上下文中进行运算。只有一个全局上下文,可有很多个函数执行上下文和eval执行上下文。

比如一个JavaScript脚本文件里又四处函数声明语句(或者函数定义表达式)和一处eval语句。

注意:执行上下文只会在代码执行时产生哦

对函数的每一次调用都会进入到函数的执行上下文中,并对其代码进行运算;对eval函数进行调用会进入到eval执行上下文堆栈中,并对其代码进行运算。

注意:一个函数可能会创建无数个上下文,因为对函数的每一次调用(即使函数递归的调用自己)都会生成一个具有新状态的上下文:

function foo(bar) {}

// 调用相同的函数
// 会在每次调用时产生三种不同的上下文
// 和不同的上下文状态("bar"参数的值每次都不同)。

foo(10);
foo(20);
foo(30);

一个执行上下文会触发另一个执行上下文,比如,一个函数调用另一个函数(或者在全局执行上下文中调用一个全局函数)。从逻辑上来说,这是一栈的形式实现的,它叫作执行上下文栈。

一个触发其他执行上下文的执行上下文叫做 caller 。被触发的执行上下文叫做 callee 。callee在同一时间可以是其他 callee 的 caller(比如,全局执行上下文中被调用的函数,又调用了其他的函数)。

当一个caller触发了一个callee,这个caller会暂缓自身的执行,然后吧控制权传递给callee。这个callee被pus到栈中,并成为一个运行中的执行上下文,在callee上下文结束后,他会他控制权返回给caller,然后caller的上下文继续执行(他可能接下来又触发了其他callee上下文)知道他结束,以此类推。callee可能简单的返回或者由于异常退出,一个抛出的但是没有被捕获的异常可能退出(从栈中pop)一个或多个上下文。

换句话说,所有ECMAScript程序的运行时可以用执行上下文(EC)栈来表示,栈顶是当前活跃的执行上下文:

当程序开始的时候他会进入全局执行上下文(Global EC),次上下文位于栈底,并且是栈中的第一个元素。然后全局代码进行一些初始化,创建需要的对象和函数。在全局上下文的执行过程中,他的代码可能触发其他(已经创建完成的)函数,这些函数将会进入他们自己的执行上下文(更准确的说是 函数执行上下文 ),向栈中push新的元素,以此类推。

当初始化完成之后,运行时系统(runtime system)就会等待一些事件(比如,鼠标事件,键盘事件),这些事件将会触发一些函数,随之进入新的执行上下文中。

在下个图中,拥有函数下执行上下文EC1、函数执行上下文EC2和全局上下文Global EC,当EC1进入和退出全局上下文的时候下面的栈将会发生变化:

这就是ECMAScript的运行时系统如何真正地管理代码运行的。

更多有关ECMAScript中执行上下文的信息可以在对应的 这篇文章中获取。

向我们所说的,栈中的每个执行上下文都可以用一个对象表示,让我们来看看它的结构以及一个上下文到底需要什么状态(哪些属性)来执行它的代码。

执行上下文

一个执行上下文可以抽象的表示为一个简单的对象。每一个执行上下文拥有一些属性(可以叫做上下文状态)用来跟踪和它相关的代码的执行过程,在下图中展示了一个上下文的结构:

除了这三个必要的属性(一个 变量对象(variable object) ,一个 this 值以及一个 作用域链(scope chain) )之外,执行上下文可以拥有任何附加的状态,这取决于宿主如何实现。

让我们详细看看上下文中的这些重要的属性。

变量对象

变量对象是与执行上下文相关的数据作用域。他是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。

注意, 函数表达式 (与 函数声明 相对)不包含在变量对象之中。

变量对象是一个抽象概念。对于不同的上下文类型(三种),在物理上,是使用不同的对象。比如,在全局上下文中变量对象就是全局对象本身,(这就是为什么我们可以通过全局对象的属性名来关联全局变量)。

让我们在全局执行上下文中考虑下边这个例子:

var foo = 10;

function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // ReferenceError, "baz" is not defined

之后,全局上下文的变量对象(variable object,简称VO)将会拥有如下的属性:

再看一遍,函数baz是一个函数表达式,没有被包含在变量对象之中,这就是为什么,当我们想要在函数自身以外访问他的时候会出现 ReferenceError 的异常。

注意,与其他语言(比如C/C++)相比,在ECMAScript中之后函数可以创建一个新的作用域。在函数作用于中所定义的变量和内部函数在函数外边是不能直接访问的,而且并不会污染全局变量对象。

使用 eval 我们也会进入一个新的(eval类型)的执行上下文中,无论如何, eval 使用全局的变量对象或者使用caller(比如eval被调用时所在的函数)的变量对象。

那么函数和它的变量对象是怎么的?在函数上下文中,变量对象是以活动对象(activation object)来表示的。

活动对象

当一个函数被caller所触发(被调用),一个特殊的对象,叫做活动对象(activation object)将会被创建。这个对象中包含形参和那个特殊的 arguments 对象(是对形参的一个映射,但是值是通过索引来获取)。 活动对象 之后会作为函数上下文的 变量对象 来使用。

换句话说,函数的变量对象也是一个同样简单的变量对象,但是除了变量和函数声明之外,他还存储了形参和 arguments 对象,并叫做活动对象。

考虑如下例子:

function foo(x, y) {
  var z = 30;
  function bar() {} // FD函数声明;
  (function baz() {}); // FE函数表达式;
}

foo(10, 20);

我们看下函数 foo 的上下文中的活动对象(activation object,简称AO):

并且函数表达式baz还是没有被包含在变量/活动对象中。

更详细的主题可以在 这篇文章 中找到。

This

this是一个与执行上下文相关的特殊对象,因此,它可以叫做上下文对象(也就是用来指明执行上下文是在哪个上下文中被触发的对象)。

任何对象都可以作为上下文中的this的值,我想再一次澄清,在一些对ECMAScript执行上下文和部分this的描述中的所产生的误解。this经常被错误的表述称是变量对象的一个属性。这类错误存在于比如像 这本书 中(即使如此,这本书的相关章节还是十分不错的。)再重复一次:

this是执行上下文的一个属性,而不是变量对象的一个属性

这个特性非常重要,因为与变量相反,this从不会参与到标识符解析过程。换句话说,在代码中当访问this的时候,它的值是直接从执行上下文中获取的,并不需要任何作用域链查找,this的值只在进入上下文的时候进行一词确定。

顺便说一下,与ECMAScript相反,比如,Python的方法都会拥有一个被当做简单变量的self参数,这个变量的值在各个方法中是相通的并且在执行过程中被更改层其他值。在ECMAScript中,给this赋一个新值是不可能的,因为,再重复一遍,他不是一个变量,他不是一个变量,并且不存在于变量对象中。

在全局上下文中,this就等于全局对象本身(这意味着,这里的this等于变量对象):

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

在函数上下文的情况下,对函数的每次调用,其中的this值可能是不同的,这个this值是通过函数调用表达式(也就是函数被调用的方式)的形式有caller所提供的。举个例子,下面的函数foo是一个callee,在全局上下文中被调用,次上下文位caller。让我们通过例子看一下,对于一个代码相同的函数,this值是如何在不同的调用中(函数触发的不同方式),有caller给出不同的结果的:

// the code of the "foo" function
// never changes, but the "this" value
// differs in every activation

function foo() {
  alert(this);
}

// caller activates "foo" (callee) and
// provides "this" for the callee

foo(); // global object
foo.prototype.constructor(); // foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // bar

(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // but here is global object
(bar.baz, bar.baz)(); // also global object
(false || bar.baz)(); // also global object

var otherFoo = bar.baz;
otherFoo(); // again global object

为了深入了解this为什么(并且更本质一些-如何)在每个函数调用中可能会发生变化,你可以阅读这篇文章


#相关链接