[JavaScript]ECMA-262-3 深入解析.第三章.this

介绍

在这篇文章里,我们将讨论跟执行上下文直接相关的更多细节。讨论的主题就是this关键字。

实践证明,这个主题很难,在不同执行上下文中确定this的值经常会发生问题。

许多程序员习惯的认为,在程序语言中,this关键字与面向对象程序开发紧密相关,其完全指向由构造器新创建的对象。在ECMAScript规范中也是这样实现的,但正如我们将看到那样,在ECMAScript中,this并不限于只用来指向新创建的对象。

下面让我们更详细的了解一下,在ECMAScript中this的值到底是什么?

定义

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

activeExecutionContext = {
  VO: {...},
  this: thisValue
};

这里VO是我们前一章讨论的变量对象。

this与上下文中可执行代码(的类型)直接相关。this的值在进入上下文时确定,并且在上下文运行代码期间不会改变this的值。

下面让我们更详细研究这些场景。

this在全局代码中的值

在这里一切都很简单。在全局代码中,this始终是全局对象本身,这样就有可能间接的引用到它了。

// explicit property definition of
// the global object
this.a = 10; // global.a = 10
alert(a); // 10
 
// implicit definition via assigning
// to unqualified identifier
b = 20;
alert(this.b); // 20
 
// also implicit via variable declaration
// because variable object of the global context
// is the global object itself
var c = 30;
alert(this.c); // 30

this在函数代码中的值

在函数代码中使用this时很有趣,这种应用场景很难且会导致很多问题。

在这种类型的代码中,this值的首要(也许是最主要的)特点是它没有静态绑定到一个函数。

正如我们上面曾提到的那样,this的值在进入上下文时确定,在函数代码中,this的值每一次(进入上下文时)可能完全不同。

不管怎样,在代码运行期间,this的值是不变的,也就是说,因为this不是一个变量,所以不可能为其分配一个新值。(相反,在Python编程语言中,它明确的定义为对象本身,在运行期间可以不断改变)。

var foo = {x: 10};
 
var bar = {
  x: 20,
  test: function () {
 
    alert(this === bar); // true
    alert(this.x); // 20
 
    this = foo; // error
 
    alert(this.x); // if there wasn't an error then 20, not 10
 
  }
 
};
 
// on entering the context this value is
// determined as "bar" object; why so - will
// be discussed below in detail
 
bar.test(); // true, 20
 
foo.test = bar.test;
 
// however here this value will now refer
// to "foo" – even though we're calling the same function
 
foo.test(); // false, 10

那么,在函数代码中,什么影响了this的值发生变化?有几个因素。

首先,在通常的函数调用中,this是由激活上下文代码的调用者来提供的,即调用函数的父上下文(parent context)。this取决于调用函数的方式。(译者注:参考这里)

为了在任何情况下准确无误的确定this值,有必要理解和记住这重要的一点:正是调用函数的方式影响了调用的上下文中this的值,没有别的什么(我们可以在一些文章,甚至是在关于javascript的书籍中看到,它们声称:“this的值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。–这绝对不正确”)。继续我们的话题,可以看到,即使是正常的全局函数也会因为不同调用方式而激活,这些不同调用方式产生了this不同的值。

function foo() {
  alert(this);
}
 
foo(); // global
 
alert(foo === foo.prototype.constructor); // true
 
// but with another form of the call expression
// of the same function, this value is different
 
foo.prototype.constructor(); // foo.prototype

有时可能将函数作为某些对象的一个方法来调用,此时this的值不会设置为这个对象。

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};
 
foo.bar(); // foo, true
 
var exampleFunc = foo.bar;
 
alert(exampleFunc === foo.bar); // true
 
// again with another form of the call expression
// of the same function, we have different this value
 
exampleFunc(); // global, false

那么,到底调用函数的方式如何影响this的值?为了充分理解this的值是如何确定的,我们需要详细分析一个内部类型(internal type)——引用类型(Reference type)。

引用类型

用伪代码可以把引用类型表示为拥有两个属性的对象——base(即拥有属性的那个对象),和base中的propertyName 。

var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
};

引用类型的值仅存在于两种情况中:

1. 当我们处理一个标示符时;(when we deal with an identifier;)

2. 或一个属性访问器;(or with a property accessor.)

标示符的处理过程在 Chapter 4. Scope chain中讨论;在这里我们只需要知道,使用这种处理方式的返回值总是一个引用类型的值(这对this来说很重要)。

标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名。例如,下面标识符的值:

var foo = 10;
function bar() {}

在操作的中间结果中,引用类型对应的值如下:

var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
var barReference = {
  base: global,
  propertyName: 'bar'
};

为了从引用类型中得到一个对象真正的值,在伪代码中可以用GetValue方法(译者注:11.1.6)来表示,如下:

function GetValue(value) {
 
  if (Type(value) != Reference) {
    return value;
  }
 
  var base = GetBase(value);
 
  if (base === null) {
    throw new ReferenceError;
  }
 
  return base.[[Get]](GetPropertyName(value));
 
}

内部的[[Get]]方法返回对象属性真正的值,包括对原型链中继承属性的分析。

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

属性访问器都应该熟悉。它有两种变体:点(.)语法(此时属性名是正确的标示符,且事先知道),或括号语法([])。

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

在计算中间的返回值中,引用类型对应的值如下:

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};
 
GetValue(fooBarReference); // function object "bar"

那么,从最重要的意义上来说,引用类型的值与函数上下文中的this的值是如何关联起来的呢?这个关联的过程是这篇文章的核心。(The given moment is the main of this article.) 在一个函数上下文中确定this的值的通用规则如下:

在一个函数上下文中,this的值由调用者提供,且由调用函数的方式决定。如果调用括号()的左边是引用类型的值,this将设为这个引用类型值的base对象,在其他情况下(与引用类型不同的任何其它属性),this的值都为null。不过,实际不存在this的值为null的情况,因为当this的值为null的时候,其值会被隐式转换为全局对象。

下面让我们看个例子:

function foo() {
  return this;
}
 
foo(); // global

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

var fooReference = {
  base: global,
  propertyName: 'foo'
};

相应地,this也设置为引用类型的base对象。即全局对象。

同样,使用属性访问器:

var foo = {
  bar: function () {
    return this;
  }
};
 
foo.bar(); // foo

同样,我们拥有一个引用类型的值,其base是foo对象,在函数bar激活时将base设置给this。

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

但是,如果用另一种方式激活相同的函数,this的值将不同。

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

因为test作为标识符,产生了其他引用类型的值,该值的base(全局对象)被设置为this的值。

var testReference = {
  base: global,
  propertyName: 'test'
};

现在,我们可以很明确的说明,为什么用不同的形式激活同一个函数会产生不同的this,答案在于不同的引用类型(type Reference)的中间值。

function foo() {
  alert(this);
}
 
foo(); // global, because
 
var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
alert(foo === foo.prototype.constructor); // true
 
// another form of the call expression
 
foo.prototype.constructor(); // foo.prototype, because
 
var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

另一个通过调用方式动态确定this的值的经典例子:

function foo() {
  alert(this.bar);
}
 
var x = {bar: 10};
var y = {bar: 20};
 
x.test = foo;
y.test = foo;
 
x.test(); // 10
y.test(); // 20

函数调用和非引用类型

那么,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,this的值自动设置为null,实际最终this的值被隐式转换为全局对象。

让我们思考下面这种函数表达式:

(function  () {
  alert(this); // null => global
})();

在这个例子中,我们有一个函数对象但不是引用类型的对象(因为它不是标示符,也不是属性访问器),相应地,this的值最终被设为全局对象。

更多复杂的例子:

var foo = {
  bar: function () {
    alert(this);
  }
};
 
foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo
 
(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?

那么,为什么我们有一个属性访问器,它的中间值应该为引用类型的值,但是在某些调用中我们得到this的值不是base对象,而是global对象?

问题出现在后面的三个调用,在执行一定的操作运算之后,在调用括号的左边的值不再是引用类型。

第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。

在第二个例子中,分组操作符(译者注:这里的分组操作符就是指foo.bar外面的括号"()")没有实际意义,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如GetValue (参考11.1.6)。相应的,在分组操作的返回值中———我们得到的仍是一个引用类型。这就是this的值为什么再次被设为base对象,即 foo。

第三个例子中,与分组操作符不同,赋值操作符调用了GetValue方法(参考11.13.1的第三步)。返回的结果已经是函数对象(不是引用类型),这意味着this的值被设为null,实际最终结果是被设置为global对象。

第四个和第五个也是一样——逗号操作符和逻辑操作符(OR)调用了GetValue 方法,相应地,我们失去了引用类型的值而得到了函数类型的值,所以this的值再次被设为global对象。

引用类型和this为null

有一种情况,如果调用方式确定了引用类型的值(when call expression determinates on the left hand side of call brackets the value of Reference type。译者注,原文有点拖沓!),不管怎样,只要this的值被设置为null,其最终就会被隐式转换成global。当引用类型值的base对象是激活对象时,就会导致这种情况。

下面的实例中,内部函数被父函数调用,此时我们就能够看到上面说的那种特殊情况。正如我们在 第二章 学到的一样,局部变量、内部函数、形式参数都储存在给定函数的激活对象中。

function foo() {
  function bar() {
    alert(this); // global
  }
  bar(); // the same as AO.bar()
}

激活对象总是作为this的值返回——null(即伪代码AO.bar()相当于null.bar())。(译者注:不明白参考这里)这里我们再次回到上面描述的情况,this的值最终还是被设置为全局对象。

有一种情况除外:“在with语句中调用函数,且在with对象(译者注:即下面例子中的__withObject)中包含函数名属性时”。with语句将其对象添加在作用域链最前端,即在激活对象的前面。那么对应的,引用类型有值(通过标识符或属性访问器),其base对象不再是激活对象,而是with语句的对象。顺便提一句,这种情况不仅跟内部函数相关,还跟全局函数相关,因为with对象比作用域链里的最前端的对象(全局对象或一个激活对象)还要靠前。

var x = 10;
 
with ({
 
  foo: function () {
    alert(this.x);
  },
  x: 20
 
}) {
 
  foo(); // 20
 
}
 
// because
 
var  fooReference = {
  base: __withObject,
  propertyName: 'foo'
};

在catch语句的实际参数中的函数调用存在类似情况:在这种情况下,catch对象被添加到作用域的最前端,即在激活对象或全局对象的前面。但是,这个特定的行为被确认为是ECMA-262-3的一个bug,这个在新版的ECMA-262-5中修复了。修复后,在特定的激活对象中,this指向全局对象。而不是catch对象。

try {
  throw function () {
    alert(this);
  };
} catch (e) {
  e(); // __catchObject - in ES3, global - fixed in ES5
}
 
// on idea
 
var eReference = {
  base: __catchObject,
  propertyName: 'e'
};
 
// but, as this is a bug
// then this value is forced to global
// null => global
 
var eReference = {
  base: global,
  propertyName: 'e'
};

同样的情况出现在命名函数(函数的更多细节参考Chapter 5. Functions)的递归调用中。在函数的第一次调用中,base对象是父激活对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this的值也总是被设置为global。

(function  foo(bar) {
 
  alert(this);
 
  !bar && foo(1); // "should" be special object, but always (correct) global
 
})(); // global

this在作为构造器调用的函数中的值

还有一个在函数的上下文中与this的值相关的情况是:函数作为构造器调用时。

function A() {
  alert(this); // newly created object, below - "a" object
  this.x = 10;
}
 
var a = new A();
alert(a.x); // 10

在这个例子中,new操作符调用“A”函数内部的[[Construct]]方法,接着,在对象创建后,调用其内部的[[Call]]方法,所有相同的函数“A”都将this的值设置为新创建的对象。

手动设置一个函数调用的this

在Function.prototype中定义了两个方法允许手动设置函数调用时this的值,它们是.apply和.call方法(所有的函数都可以访问它们)。它们用接受的第一个参数作为this的值,this在调用的作用域中使用。这两个方法的区别不大,对于.apply,第二个参数必须是数组(或者是类似数组的对象,如arguments,相反,.call能接受任何参数。两个方法必须的参数都是第一个——this。

例如

var b = 10;
 
function a(c) {
  alert(this.b);
  alert(c);
}
 
a(20); // this === global, this.b == 10, c == 20
 
a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40

结论

在这篇文章中,我们讨论了ECMAScript中this关键字的特征(and they really are features, in contrast, say, with C++ or Java,译者注:这句话没什么大用,还不知道咋翻好,暂不翻了)。我希望这篇文章有助于你准确的理解ECMAScript中this关键字如何工作。同样,我很高兴在评论中回答您的问题。

其他参考

10.1.7 – This;
11.1.1 – The this keyword;
11.2.2 – The new operator;
11.2.3 – Function calls.

 

英文地址 : ECMA-262-3 in detail. Chapter 3. This.
中文地址 : [JavaScript]ECMA-262-3 深入解析.第三章.this

翻译声明:
1.因为Denis已经翻译过这篇文章,所以该篇译文在部分章节参考了他的译文,参考引用部分大概占整篇文章的30%左右,另外70%左右完全是重新翻译的。
2.在翻译过程中,跟原作者进行了充分的沟通,大家看译文的时候,可以多参考原文的留言列表。
3.再好的翻译也赶不上原汁原味的原文,所以推荐大家看过译文之后还是要再仔细看看原文。

作者:Justin
出处:http://justinw.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

posted on 2010-05-04 16:53 Justin 阅读(2746) 评论(18) 编辑 收藏

评论

#1楼  回复 引用 查看   

我现在基本不用 this用that哈哈,坑太多了,就算你对这个东西理解的很准确也要十分注意。一不小心就把自己埋进去了,~~
2010-05-04 21:36 | 在云端      

#2楼[楼主]  回复 引用 查看   

@在云端
该用还得用,逃避永远不是办法
2010-05-04 21:40 | Justin      

#3楼  回复 引用 查看   

累了啊,看不完了啊
2010-05-04 22:32 | 贺爱平      

#4楼[楼主]  回复 引用 查看   

@贺爱平
慢慢来,质量大于速度
2010-05-04 22:37 | Justin      

#5楼  回复 引用 查看   

参考这篇文章中的“函数调用和非引用类型”一节叙述的内容,再回头思考命名函数的递归调用中的在后几次调用过程中会将base设置为global的情况,会不会是因为不再将此命名函数作为引用类型而是作为函数类型来进行理解,参考文中提到的逗号操作符和逻辑操作符最终得到base为global的情况。
对于这种较特殊的情况,即使在原文当中也没有给出具体的说法。
一味地记住这种特殊情况,而又不能找到规律,这个对于初学者来说会比较辛苦,JS之所以高深,常常也是因为这种匪夷所思地近乎诡异的现象造成的,还记得刚入行时育碧游戏的一个加拿大牛人对我说过JS not for kid,看来的确是这样啊,我准备一方面读你翻译的文章一方面自己做读书笔记的文档,尽量做到简单易懂,以后讲授时,尽量把门槛降低。
2010-05-06 10:54 | 雨石      

#6楼[楼主]  回复 引用 查看   

@雨石
“再回头思考命名函数的递归调用中的在后几次调用过程中会将base设置为global的情况,”

写个代码的Demo来说明一下这种情况吧,我光看你的这一大段描述也有点晕呢,呵呵。

另外:这篇文章中作者已经给出规律了呀,就是“如果调用括号()的左边是引用类型的值,this将设为这个引用类型值的base对象,在其他情况下(与引用类型不同的任何其它属性),this的值都为 null。”,仔细品一品,我感觉他说的很清楚了。
2010-05-06 11:39 | Justin      

#7楼  回复 引用 查看   

引用Justin:
@雨石
“再回头思考命名函数的递归调用中的在后几次调用过程中会将base设置为global的情况,”

写个代码的Demo来说明一下这种情况吧,我光看你的这一大段描述也有点晕呢,呵呵。

另外:这篇文章中作者已经给出规律了呀,就是“如果调用括号()的左边是引用类型的值,this将设为这个引用类型值的base对象,在其他情况下(与引用类型不同的任何其它属性),this的值都为 null。”,仔细品一品,我感觉他说的很清楚了。


这个不是我说的
是原文这么写的:

同样的情况出现在命名函数(函数的更对细节参考Chapter 5. Functions)的递归调用中。在函数的第一次调用中,base对象是父激活对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this的值也总是被设置为global而不是其base对象
我就是想问,一个引用类型的函数,为什么在递归的后几次调用中,会将this的值设为global
2010-05-06 13:37 | 雨石      

#8楼[楼主]  回复 引用 查看   

@雨石
“我就是想问,一个引用类型的函数,为什么在递归的后几次调用中,会将this的值设为global”

这块原文是不太清楚,我目前的理解,作者的意思是:原因就是“在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this的值也总是被设置为global。”这句话,而这句话应该是直接或间接来自ECMA-262的,也就是说,要把这句话当成一个“规范”来遵守的固定的定理就行了。

2010-05-06 16:10 | Justin      

#9楼  回复 引用 查看   

嗯,谢谢你,我也觉得只能当做定理记,
我的猜测是:
因为不再将此命名函数作为引用类型而是作为函数类型来进行理解
呵呵,纯属瞎猜
期待你的新作。
2010-05-06 16:39 | 雨石      

#10楼[楼主]  回复 引用 查看   

@雨石
原文作者有些地方引用到ECMA-262,但是他并没有全部标示出来,所以我们如果没完整看过ECMA-262的话,有时理解起来会有困难,我翻译的时候也是遇到好多地方不明白,最后一问,原来就是直接引用的ECMA-262,死记硬背就行了。

“因为不再将此命名函数作为引用类型而是作为函数类型来进行理解”,因为命名函数是chapter5的内容,翻译完第五章再回来看这个问题可能就知道原因了,呵呵,不行我到时再跟原作者好好请教一下。

谢谢支持!
2010-05-06 16:58 | Justin      

#11楼  回复 引用   

太感动了。作者翻译得太好了,真的谢谢。
2010-09-09 01:09 | vinqon[未注册用户]

#12楼[楼主]  回复 引用 查看   

@vinqon
呵呵,别激动~
最近太忙,缓一缓会继续坚持翻译后续文章。
2010-09-09 16:35 | Justin      

#13楼  回复 引用   

(foo.bar)();
为什么是引用类型值的base : foo.
我一直看不懂?
2011-04-26 08:49 | winting[未注册用户]

#14楼  回复 引用   

function a() {
var b = 10;
function c() {
alert(this);
}
c();
alert(b);
}

a();
楼主能不能给分析下这种情况下:
激活对象总是作为this的值返回——null(即伪代码AO.bar()相当于null.bar())。(译者注:不明白参考这里)这里我们再次回到上面描述的情况,this的值最终还是被设置为全局对象。
按照第2章说的VO===AO, 变量b同样也是AO的一个属性,那么同理(即为代码AO.b相当于null.b),那么null的情况就变成了全局对象了,那么全局对象并没有b这个属性为什么可以alert出正确的值呢.
2011-09-01 22:47 | EricW[未注册用户]

#15楼  回复 引用 查看   

@Justin
对于this,可以使用对象定义来替代,this用多了不是一件好事情,更直观的对象定义会更好些
2011-09-30 00:38 | skyaspnet      

#16楼  回复 引用 查看   

In the second case there is a grouping operator which does not apply, considered above, method of getting the real value of an object from value of Reference type, i.e. GetValue (see note of 11.1.6).老大你这句话翻译不准确,,这句意思是分组操作符不会调用GetValue方法,而不是分组操作符没有意义,看下ECMA的定义:
11.1.6 The Grouping Operator

The production PrimaryExpression : ( Expression ) is evaluated as follows:

1. Evaluate Expression. This may be of type Reference.

2. Return Result(1).

NOTE
This algorithm does not apply GetValue to Result(1). The principal motivation for this is so that operators such as delete and typeof may be applied to parenthesised expressions.
2011-11-18 15:11 | ju350213      

#17楼  回复 引用 查看   

在这个例子中,new操作符调用“A”函数内部的[[Construct]]方法,接着,在对象创建后,调用其内部的[[Call]]方法,所有相同的函数“A”都将this的值设置为新创建的对象。 这句翻译有误:In this case, the new operator calls the internal [[Construct]] method of the “A” function which, in turn, after object creation, calls the internal [[Call]] method, all the same function “A”, having provided as this value newly created object. 原文意思是说:调用完internal [[Call]] method再调用同一个A函数(而不是翻译成所有相同的函数),使得刚创建的对象成为THIS的值
2011-11-18 16:02 | ju350213      

#18楼[楼主]  回复 引用 查看   

@ju350213
谢谢指正,暂时没时间深究,回头再仔细看看
2011-11-22 08:59 | Justin      
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1727295 Hg6wL3E0bFI=

导航

公告


2008.10-2009.10
版权声明: 本站采用创作共用许可署名,转载请保留出处

昵称:Justin
园龄:6年9个月
荣誉:推荐博客
粉丝:328
关注:35

搜索

 

随笔分类(273)

最新评论

阅读排行榜

评论排行榜