首页
Javascript
Html
Css
Node.js
Electron
移动开发
小程序
工具类
服务端
浏览器相关
前端收藏
其他
关于
公司注册

ECMA-262-3 in detail——第三章:this关键字

2015年03月25日 发布 阅读(1859) 作者:Jerman

介绍 | Introduction

在这篇文章中我们讨论一个与执行上下文(execution context)相关的细节:This关键字。

从实际情况来看,这个主题是困难的,而且在不同的执行上下文中判断this的值常常产生问题。

许多程序员习惯于认为在编程语言中,this关键字是与面向对象编程紧密相关的,而且引用的是由构造式最新创建的对象。在ECMAScript中,这个概念也被实现了,然而我们将看到,在这里它不仅仅指向已创建的对象。

让我们来详细地看下在ECMAScript中this的值究竟是什么。

定义 | Definitions

this是执行上下文对象的一个属性:

  1. activeExecutionContext = {
  2. VO: {...},
  3. this: thisValue
  4. }

这里的VO是指变量对象(variable object)。

this 与上下文的可执行代码的类型(type of executable code)直接相关。它的值在进入上下文时确定,并且代码在相同上下文中执行时不会改变。让我们来更详细地讨论这些情况。

全局代码中this的值 | This value in the global code

这里面十分简单。在全局代码中,this的值始终是全局对象(global object)本身。所以,可以直接引用它。

  1. this.a = 10; // global.a = 10
  2. a; // 10
  3. b = 20; // global.b = 20
  4. this.b; // 20
  5. var c = 30; // global.c = 30
  6. /* tips:和上面的区别是全局代码中通过变量声明的不能用delete删除 */
  7. this.c; // 30

函数代码中this的值 | This value in the function code

当在函数代码中使用this时事情就变得有趣的多了。这是最困难的情况而且常常产生问题。

关于在函数代码中的this的值的第一个(可能也是主要的一个)特性就是:它并不固定地绑定到某个函数。

就像上面提到的那样,this的值在进入上下文时确定,而在函数代码的情况下,这个值可以在每次函数调用时都完全不同。

然而,在代码运行时this的值是不可改变的,也就是说,你无法赋一个新值,因为它不是一个变量(在Python中则相反,它所定义的self对象在运行时可以重复改变)。

  1. var foo = {x: 10};
  2. var bar = {
  3. x: 20,
  4. test: function() {
  5. alert(this === bar);
  6. alert(this.x);
  7. this = foo; // error, 不能修改this的值
  8. }
  9. };
  10. bar.test(); // true, 20
  11. foo.test = bar.test;
  12. foo.test(); // false, 10

那么是什么导致了this的值在函数代码中的变化?原因包括若干个:

首先,在通常的函数调用中,this由激活上下文代码的caller(也就是调用函数的父上下文)提供。而this的值则由调用表达式的格式(the form of a call expression)决定(换句话说,由函数是如何调用的语法格式决定)。

为了在任何上下文中不出错地判断this的值,理解和记住这一点是十分重要的。正是调用表达式的格式,或者说调用函数的方式,影响了调用上下文中this的值。

(我们常常在一些关于javascript的文章甚至书中看到这样的说法——“this的值取决于函数是如何定义的:如果是全局函数,那么this指向全局对象,如果函数是对象的方法,那么this的值总是指向那个对象”——这个一个错误的描述。)下面我们将看到,一个普通的全局函数也可以通过不同格式的调用表达式而导致this的值的不同。

  1. function foo() {
  2. alert(this);
  3. };
  4. foo(); // object window
  5. alert(foo === foo.prototype.constructor); // true;
  6. //但当通过上面这种形式调用foo时,this值就改变了
  7. foo.prototype.constructor(); //object foo{} 即foo.prototype
  8. 同样的,可以调用一个对象的方法函数,而函数内的this的值不指向这个对象。
  9. var foo = {
  10. bar: function(){
  11. alert(this);
  12. alert(this === foo);
  13. }
  14. };
  15. foo.bar(); // foo, true
  16. var fn = foo.bar;
  17. alert(fn === foo.bar); // true
  18. fn(); // global, false

那么,调用表达式的格式是如何影响this的值的?为了完全理解this值的决定者,我们有必要考虑一个内部类型的细节——引用类型(Reference type)。

引用类型 | Reference type

通过伪代码,引用类型的值可以看做为一个拥有两个属性的对象:base(也就是属性归属的对象)和这个base的属性名称(propertyName)。

  1. var valueOfReferenceType = {
  2. base: <base object>,
  3. propertyName: <property name>
  4. }

引用类型的值只可能有两种情况:

当我们处理一个标示符(identifier)

当我们处理一个属性访问器(property accessor)

标示符是在标示符识别(identifier resolution)的过程中进行处理的(这一过程的讨论见 作用域链)。这里我们只需要注意到:当这个算法返回时总会有一个引用类型的值(这对this的值很重要)。

标示符包括:变量名,函数名,函数参数名,以及全局对象的非标准属性的名称。例如,对于下面的标示符:

  1. var foo = 10;
  2. function bar() {};
  3. 在操作的中间结果中,相应的引用对象的值如下:
  4. var fooReference = {
  5. base: global,
  6. propertyName: 'foo'
  7. };
  8. var barRerence = {
  9. base: global,
  10. propertyName: 'bar'
  11. };

从一个引用类型的值中获得一个对象的实际值(real value)是通过一个GetValue的方法来完成的。这个方法用伪代码可以表示为:

  1. function GetValue() {
  2. if (Type(value) != Reference) {
  3. return value;
  4. }
  5. var base = GetBase(value);
  6. if(base === null) {
  7. throw new RerenceError;
  8. }
  9. return base[[Get]](GetPropertyName(value));
  10. };

在这里,内部的Get方法返回对象属性的实际值,包括了对从原型链中继承的属性的分析。

  1. GetValue(fooRerence); // 10
  2. GetValue(barRerence); // function object "bar"

属性访问器(property accessors)也是确定的。有两种变体:点符号(dot notation)(当属性名是正确且已知的标示符)和括号符号(bracket notation)。

  1. foo.bar();
  2. foo['bar']();

从中间计算的返回值中我们也能得到引用类型的值。

  1. var foobarReference = {
  2. base: foo,
  3. propertyName: 'bar'
  4. };
  5. GetValue(foobarRerence); // function object "bar"

那么,引用类型的值和函数代码中this的值的关系是什么呢?——下面是这篇文章中最重要的部分。决定函数上下文中this的值的一般准则如下:

一个函数上下文中的this的值由调用者(caller)提供,并由当前调用表达式的格式(函数调用在语法上的写法)决定。

The value of this in a function context is provided by the caller and determined by the current form of a call expression (how the function call is written syntactically).

如果调用括号“(…)”的左边有一个引用类型的值,那么this的值指向引用类型值的base属性(对象)。

If on the left hand side from the call parentheses ( … ), there is a value of Reference type then this value is set to the base object of this value of Reference type.

在所有其他的情况下(换句话说,所有区别于引用类型的其他值类型的情况下),this的值总是设成null。但由于对于this的值而言null没有实际意义,因此它隐式转换成了global对象。

In all other cases (i.e. with any other value type which is distinct from the Reference type), this value is always set to null. But since there is no any sense in null for this value, it is implicitly converted to global object.

让我们用示例来说明:

  1. function foo() {
  2. return this;
  3. };
  4. foo(); // global

我们看到调用括号的左边有一个引用类型的值(因为foo是一个标示符):

  1. var fooReference = {
  2. base: global,
  3. propertyName: 'foo'
  4. }

因此,this的值指向了引用类型的值中的base对象,也就是全局对象。

属性访问器的情况也是类似的:

  1. var foo = {
  2. bar: function() {
  3. return this;
  4. }
  5. };
  6. foo.bar(); // foo

同样,我们得到一个引用类型的值,其中的base为foo对象并在后面bar函数激活时作为this的值。

  1. var foobarReference = {
  2. base: foo,
  3. propertyName: 'bar'
  4. };

然而,当以另一种调用表达式的格式去激活相同函数时,我们就已经改变了this的值:

  1. var test = foo.bar;
  2. test(); // global

因为test作为标示符,产生了另一个引用类型的值,其中的base(全局对象)成为this的值:

  1. var testReference = {
  2. base: global,
  3. propertyName: 'test'
  4. };

注意:在ES5的严格模式中,this的值不再强制为全局对象,而是设为了undefined
现在我们可以准确地说明,为什么相同函数通过不同调用表达式的格式激活时this的值也不同了——答案就是由于中间引用类型的值的不同:

  1. function foo() {
  2. alert(this);
  3. }
  4. foo(); // global
  5. var fooReference = {
  6. base: global,
  7. propertyName: 'foo'
  8. };
  9. alert(foo === foo.prototype.constructor); // true
  10. //另一种调用格式
  11. foo.prototype.constructor(); // foo.prototype, 因为:
  12. var fooPrototypeConstructorReference = {
  13. base: foo.prototype,
  14. propertyName: 'constructor'
  15. };

另一个关于不同格式的调用表达式动态决定this的值的经典例子如下:

  1. function foo() {
  2. alert(this.bar);
  3. };
  4. var x = {bar: 10};
  5. var y = {bar: 20};
  6. x.test = foo;
  7. y.test = foo;
  8. x.test(); // 10
  9. y.test(); // 20

函数调用和非引用类型 | Function call and non-Reference type

正如我们注意到的,当调用括号的左边不是一个引用类型的值而是任何其他类型的时候,this的值自动设为null,并且因此转换为全局对象。

让我们来看下这种情况的一个例子:

  1. (function() {
  2. alert(this); // null -> global
  3. }) ();

在这个例子中,有一个函数对象但是没有引用类型的对象(它既不是标示符也不是属性访问器),因此this的值设为global。

来看更复杂的例子:

  1. var foo = {
  2. bar: function() {
  3. alert(this);
  4. }
  5. };
  6. foo.bar(); // 引用,ok ->foo
  7. (foo.bar)(); // 引用,ok ->foo
  8. (foo.bar = foo.bar)(); // global?
  9. (false || foo.bar)(); // global?
  10. (foo.bar,foo.bar)(); // global?

那么,作为属性访问器,它的中间结果应该是一个引用类型的值,为什么在后面几种调用中返回的this的值却是global对象呢?

原因在于:在后面三种调用中,调用括号的左手边不是一个引用类型的值。

上面第一个种情况很清楚——存在明确的引用类型,因此this的值为相应的base对象,即foo。

在第二种情况中,左边是一个分组运算符(grouping operator),而这使得从引用类型的值获得对象实际值的方法(也就是GetValue方法)不会被应用(见规范11.1.6),因此,当分组操作返回结果时——我们还是得到一个引用类型的值,也因此,this的值再次指向base对象,即foo。

在第三种情况中,左边是一个赋值运算符(assignment operator),与分组运算符不同,它应用GetValue方法(见规范11.13.1),结果是返回一个已有的函数对象(但不是引用对象的值),这意味着this的值设为null而最终转换为全局对象。

第四第五种情况也是一样——逗号运算符和逻辑‘或’表达式,调用GetValue方法因此我们没有得到引用类型(type Reference)的值而是得到了函数类型(type function)的值,所以this的值再一次指向全局对象。

引用类型和this值null | Reference type and null this value

有一种情况是,调用括号左边的表达式是一个引用类型的值,但是this的值指向null,而最终指向全局对象。这种情况是当引用类型的值中的base对象是一个活化对象(activation object,译者按:函数体内的变量对象)时。

我们来看这种情况的一个例子,一个内部函数在它的外部函数内调用。正如我们从第二章中了解到的,局部变量、内部函数和形参都存储在相应外部函数的活化对象中。

  1. function foo() {
  2. function bar() {
  3. alert(this); // global
  4. };
  5. bar();
  6. };

活化对象始终返回this的值为null(也就是说,伪代码AO.bar()和null.bar()是相等的),于是就像上面描述过的那样,this的值再次指向全局对象。
一个例外的情况是,在with语句中,当with的对象包含一个函数属性时,语句中的函数调用。在作用域链(scope chain)中with语句会将它的对象(with的表达式)置于活化对象之前。因此,根据引用类型的值(通过标示符或属性访问器),我们得到的base对象不是活化对象,而是with语句的对象。顺便说一句,它不尽发生在内部函数中,全局函数也是一样,因为with对象覆盖(shadows)作用域链中更高的对象(全局或活化对象)。

  1. var x = 10;
  2. with({
  3. foo: function(){
  4. alert(this.x);
  5. },
  6. x: 20
  7. }){
  8. foo(); // 20
  9. };
  10. //因为
  11. var fooReference = {
  12. base: __withObject,
  13. propertyName: 'foo'
  14. };

类似的情况还发生在当函数是catch子句的实参而被调用时:在这种情况下,catch对象也被添加到作用域链的前端(即全局和活化对象前)。然而,这种情况被认为是Ecma-262-3的一个bug而在新版本262-5标准中被修复。在这个激活过程中的this的值应当是全局对象,而不是catch对象:

  1. try {
  2. throw function() {
  3. alert(this);
  4. };
  5. } catch (e) {
  6. e(); // __catchObject -in ES3, global - fixed in ES5
  7. };
  8. // in ES3
  9. var eReference = {
  10. base: __catchObject,
  11. propertyName: 'e'
  12. };
  13. // in ES5
  14. var eReference = {
  15. base: global,
  16. propertyName: 'e'
  17. };

同样的情况还包括已命名函数表达式的递归调用时(更多细节见第五章函数)。在函数的第一次调用时,是父活化对象或全局对象;而在递归调用时,base对象应当是储存该函数表达式可选名的对象,然而,在这种情况下this的值总是指向全局对象。

  1. (function foo(bar) {
  2. alert(this);
  3. !bar && foo(1); //
  4. })

作为构造式调用的函数中的this的值 | This value in function called as the constructor

关于函数上下文中this的值还有一种情况——函数作为构造式而调用:

  1. function A() {
  2. alert(this);
  3. this.x = 10;
  4. };
  5. var a = new A(); //alert返回当前创建的对象 a
  6. alert(a.x); // 10

在这种情况下,new运算符调用了A函数的内部方法[[Construct]],而该方法在创建对象后调用了A函数的另一个内部方法[[Call]],将新创建的对象作为函数内this的值。

函数调用中手动设定this的值 | Manual setting of this value for a function call

在Function.prototype中定义两种方法(因此可以被所有函数访问),用于手动改变函数调用中this的值。这两种方法是call和apply。

这两种方法都以第一个传入参数作为调用上下文中this的值。这两种方法的区别不大:对于appy,第2个传入参数必须是数组(或类似数组的对象,例如:arguments);而call方法接受任何类型的参数(从第二个开始的参数依次作为实参项);这两种方法中都只有第一个参数是必须的——this的值。

  1. var b = 10;
  2. function a(c) {
  3. alert(this.b);
  4. alert(c);
  5. };
  6. a(20); // this === global, this.b == 10, c == 20
  7. a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
  8. a.apply({b: 30}, [40]); // this === {b: 30}, this.b == 30, c == 40

总结 | Conclusion

在这篇文章中,我们讨论了ECMAScript中this关键字的特点(这些特点相对于其他语言比如C++或Java来说确实是特殊的)。希望这篇文章能更有助于准确理解ECMAScript中this关键字是如何工作的。
(译者按:翻译这篇文章是起因于最近的回答错的一个问题,也正是从这篇文章开始我将会陆续翻译ECMAScript标准的其他文章作为一个专题放在上面,欢迎指正和讨论。)

版权声明:本站文章除特别声明外,均采用署名-非商业性使用-禁止演绎 4.0 国际 许可协议,如需转载,请注明出处
  • ECMA-262-3 in detail——第七章:OOP(第二部分:ECMAScript实现)

    这一章的第二部分是关于EMCAScript中的面向对象编程。在第一部分中我们讨论了OOP的基本理论并勾画出和ECMAScript的相似之处。在阅读第二部分之前,如果有必要,我还是建议首先阅读这一章的第一部分.基本理论,因为后面将会用到其中的一些术语。

    发布:2015-05-26 阅读(1949)

  • ECMA-262-3 in detail——第七章:OOP(第一部分:一般理论)

    这一章我们讨论ECMAScript中面向对象编程(object-oriented programming)的几个主要方面。由于这一主题已经在许多文章中谈论过,本章并不打算“老调重弹”,而是试图更多地着眼于这些过程内在的理论方面。尤其是,我们将研究对象创建的算法,看看对象间的关系(包括最基本的关系——继承)是如何实现的,并且给出一些讨论中将用到的准确定义(我希望这样能够打消一些术语和思路上的疑惑以及一些关于Javascript文章中OOP部分的常见的混淆)。

    发布:2015-05-06 阅读(1705)

  • ECMA-262-3 in detail——第六章:闭包

    在这一章中我们来谈谈Javascript中被讨论最多的话题之一——关于闭包(closures)。事实上这个主题并不是新鲜的。然而我们在这里将试着更多从理论的角度去分析和理解它,然后我们还会看一下ECMAScript内关于闭包的内容。

    发布:2015-04-28 阅读(1715)

  • ECMA-262-3 in detail——第五章:函数

    在这章里我们讨论ECMAScript中的一个基本对象——函数。我们将会看到不同类型的函数如何影响一个上下文中的变量对象,以及这些函数的作用域链中都包含什么。我们将会回答像下面这样经常被问到的问题:“下面这两种创建函数的方式有什么区别吗(如果有的话,区别是什么呢)?”

    发布:2015-04-17 阅读(1854)

  • ECMA-262-3 in detail——第四章:作用域链

    正如我们从第二章.变量对象中了解到的,执行上下文的数据(变量,函数声明,函数形参)以变量对象的属性的方式储存。

    发布:2015-04-07 阅读(1650)

  • ECMA-262-3 in detail——第三章:this关键字

    许多程序员习惯于认为在编程语言中,this关键字是与面向对象编程紧密相关的,而且引用的是由构造式最新创建的对象。在ECMAScript中,这个概念也被实现了,然而我们将看到,在这里它不仅仅指向已创建的对象。

    发布:2015-03-25 阅读(1859)

  • ECMA-262-3 in detail——第二章:变量对象

    在程序中我们总是声明变量和函数然后用它们来搭建我们的系统。但是解释器(interpreter)是在哪里和以什么方式来找到我们的数据(函数,变量)的呢?

    发布:2015-03-24 阅读(1963)

  • ECMA-262-3 in detail——第一章:执行上下文

    第1章:在这一章里,我们将会讨论ECMAScript中的执行上下文(execution context)以及与它们相关的可执行代码(executable code)的类型。

    发布:2015-01-10 阅读(1908)