本文翻译于:http://dmitrysoshnikov.com/ecmascript/chapter-3-this/
部分难以翻译的句子参考:Justin's的翻译
注意:部分示例代码中的注释不进行翻译,考虑到平时写代码习惯注释也有可能是英文(简洁或者习惯),原汁原味的英文注释可能更加容易理解。
# 前言
我们知道,当 JavaScript 引擎执行一段可执行代码(executable code)时, 会创建对应的执行上下文(execution context)。
对于每一个执行上下文,都有三个重要的属性:
- 变量对象(Variable object, VO)
- 作用域链(Scope chain)
- this
这篇文章主要讲this
。
# 介绍
在这篇文章中,我们将讨论执行跟上下文直接相关的更多细节。讨论的主题就是this
关键词。
实践证明,这个主题很难,在不同的执行上下文中确定this
的值经常发生问题。
很多程序员习惯的认为在程序语言中this
关键词与面向对象程序开发紧密相关,其完全指向由构造函数新创建的对象。在 ECMAScript 中也是这样实现的,然而正如我们将看到,this
并不局限于只用来指向新创建的对象。
下面让我们更详细的了解一下,在 ECMAScript 中,this
的值到底是什么?
# 定义
this
是执行上下文中的一个属性。当一段代码执行时它是一个特殊的对象。
activeExecutionContext = {
VO: {...},
this: thisValue
}
2
3
4
这里的 VO 就是我们前一章讨论的变量对象。
this
与上下文中可执行代码的类型直接相关。this 的值在进入上下文期间确定,并且在上下文运行代码期间不会改变 this 的值。
让我们根据以下的场景去更详细的研究它。
# this 在全局代码中的值
在全局代码中,一切都很简单,this
的值始终是全局对象本身。这样就可能间接的引用到它。
// 显式定义属性到全局对象
this.a = 10; // global.a = 10
console.log(a); // 10
// 通过赋值给一个无标示符隐式定义
b = 20;
console.log(this.b); // 20
// 也是通过变量声明隐式声明
// 因为全局上下文变量对象就是全局对象自身
var c = 30;
console.log(c); // 30
2
3
4
5
6
7
8
9
10
11
12
# this 在函数中的值
在函数中使用 this
会更有趣。这种场景是最难的而且会导致很多问题。
this
值的第一个(或许是最主要的)特点是它没有静态绑定到一个函数中。
正如我们前面讨论那样,this
的值在进入上下文时确定并且在函数代码中,this
的值每一次(进入上下文时)可能完全不同。
然而,在代码运行期间,this
的值是不变的。也就是说,不可能分配一个新的值给this
因为this
不是一个变量。(相反,在 Python 中,this明确的定义为对象本身,可以在运行期间不断的修改。)
var foo = { x: 10 };
var bar = {
x: 20,
test: function () {
console.log(this === bar); // true
console.log(this.x); // 20
this = foo; // error, can't change this value
console.log(this.x); // if there wasn't an error, then would be 10, not 20
}
}
// 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
// 不过这里this的值就是foo, 尽管调用的是相同的function
foo.test(); // error, 10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
那么, 在函数代码中,什么影响了this
的值发生变化?
首先,在通常的函数调用中,this
是由激活上下文代码的调用者来提供的,即调用函数的上下文(parent context)。this
取决于调用函数的方式。
为了在任何情况下能准确无误的确定this
的值,有必要理解和记住重要的一点:正是调用函数的方式影响了调用的上下文中this
的值,没有别的什么。(我们可以在一些文章,甚至是在关于javascript的书籍中看到,他们声称:this
的值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。 -- 这绝对不正确)。继续我们的话题,可以看到,即使正常的全局函数也会因为不同的调用方式被激活,这些不同的调用方式产生了不同的this
值。
function foo() {
console.log(this);
}
foo(); // global
console.log(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
2
3
4
5
6
7
8
9
10
11
12
相似地,可能将函数作为某些对象的方法来调用,但是,this
的值不会设置为这个对象。
var foo = {
bar: function () {
console.log(this);
console.log(this === foo)
}
}
foo.bar(); // {bar: f}即是foo对象, true
var exampleFunc = foo.bar
console.log(exampleFunc === foo.bar); // true
// again with another form of the call expression
// of the same function, we have different this value
exampleFunc(); // global, false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
那么,到底调用函数的方式如何影响this
的值?为了充分理解this
的值是如何确定的,有必要详细分析一个内部类型(internal type)--- 引用类型。
# 引用类型
用伪代码可以把引用类型表示为拥有两个属性的对象 -- base(即拥有属性的那个对象),和 base 中的 propertyName。
var valueOfReferenceType = {
base: <base object>,
propertyName。: <property name>
}
2
3
4
注意:从ES5开始,一个引用类型也包含了名为
strict
的属性 -- 标识一个引用类型是否在strict
模式中处理。'use strict' // Access foo foo; //Reference for 'foo' const fooReference = { base: global, propertyName: 'foo', strict: true }
1
2
3
4
5
6
7
8
9
10
11
引用类型的值仅存在于两种情况中:
- 当我们处理一个标识符时;(when we deal with an identifier;)
- 或一个属性访问器。(or with a prototy accessor)
标识符的处理过程在[Chapter 4.Scope chain]中详细讨论;在这里我们只需要知道,使用这种处理方式的返回值总是一个引用类型的值(这对于this
来说很重要。
标识符是变量名,函数名,函数参数名和全局对象中未识别的变量名,例如,下面标识符的值:
var foo = 10;
function bar() {};
2
在操作的中间结果中,引用类型对应的值如下:
var fooReference = {
base: global,
propertyName: 'foo'
}
var barReference = {
base: global,
propertyName: 'bar'
}
2
3
4
5
6
7
8
9
为了从引用类型
中得到一个对象真正的值,在伪代码中可以用GetValue
方法来来表示,如下:
function GetValue(value) {
if (Type(value) != Reference) {
return value;
}
var base = GetBase(value);
if (base === null) {
throw new ReferenceError;
}
return base.[[Get]](GetPropertyName(value));
}
2
3
4
5
6
7
8
9
10
11
12
13
内部的[[Get]]
方法返回对象属性真正的值,包括对原型链中继承属性的分析。
GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"
2
属性访问器都应该熟悉。它有两种变体:点(.)语法(此时属性名是正确的标识符且事先知道),或括号语法([])。
foo.bar();
foo['bar']();
2
在计算中间的返回值中,引用类型对应的值如下:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
GetValue(fooBarReference); // function object "bar"
2
3
4
5
6
那么,从最重要的意义上来说,引用类型的值与函数上下文中的this
的值是如何关联起来的呢?这个关联过程是这篇文章的核心。 在一个函数上下文中确定this
的值的通用规则如下:
在一个函数上下文中,
this
的值由调用者提供,且由调用函数的方式决定。如果调用括号()的左边是引用类型的值,
this
将设为引用类型值的 base 对象。在其他情况下(即与引用类型不同的任何其他属性),
this
的值一直都为null
。不过,实际上不存在this
的值为 null 的情况,因为当this
的值为 null 的时候,其值会被隐式的转换为全局对象。注:第5版ECMAScript中,已经不强制转换为全局对象了,而是赋值为 undefined.
举个例子:
function foo() {
return this;
}
foo(); // global
2
3
4
我们看到在调用括号的左边是一个引用类型(因为 foo 是一个标识符)。
var fooReference = {
base: global,
propertyName: 'foo'
}
2
3
4
相应地,this
也设置为引用类型的 base 对象,即全局对象。
同样,使用属性访问器:
var foo = {
bar: function() {
console.log(this)
return this;
}
};
foo.bar(); // foo
2
3
4
5
6
7
8
我们再次拥有一个引用类型,其 base 是 foo 对象,在函数 bar 激活时用作 this
。
var fooBarReference = {
base: foo,
propertyName: 'bar'
}
2
3
4
但是,用另一种形式激活相同的函数,我们得到其它this
值。
var test = foo.bar
test() // global
2
因为 test 作为标识符,生成了引用类型的其他值,其 base (全局对象)作为 this
值:
var testReference = {
base: global,
propertyName: 'test'
}
2
3
4
现在,我们可以明确的告诉你,为什么用表达式的不同形式激活同一个函数会产生不同的 this 值,答案在于 引用类型不同的中间值。
function foo() {
console.log(this);
}
foo(); // global, because
var fooReference = {
base: global,
propertyName: 'foo'
}
console.log(foo === foo.prototype.constructor); // true
foo.prototype.constructor(); // foo.prototype, because
var fooPrototypeConstructorReference = {
base: foo.prototype,
propertyName: 'constructor'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
另外一个通过调用方式动态确定this
值的经典例子:
function foo() {
console.log(this.bar);
}
var x = {bar: 10};
var y = {bar: 20};
x.test = foo;
y.test = foo;
x.test(); //10
y.test(); //20
2
3
4
5
6
7
8
9
10
11
12
# 函数调用和非引用类型
因此,正如我们已经指出,如果调用括号左边不是引用类型而是其它类型,这个值自动设置为null
, 结果为全局对象。
思考这种表达式
(function() {
console.log(this); // null -> global
})()
2
3
在这个例子中,我们有一个函数对象但不是引用类型的对象(他不是标识符,也不是属性访问器),相应地,this
的值最终设为全局对象。
更多复杂的例子:
var foo = {
bar: function() {
console.log(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?
2
3
4
5
6
7
8
9
10
11
12
所以,为什么我们有一个属性访问器,它的中间值应该为引用类型的值,在某些调用中我们得到的this
的值不是 base 对象(即foo),而是 global 对象?
问题在于后面三个调用,在应用一定的运算操作之后,在调用括号的左边的值不在是引用类型。
- 第一个例子很明显 -- 明显的引用类型,结果是,
this
为 base 对象,即 foo。 - 在第二个例子中,组运算符并不适用,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如
GetValue
。相应地,在组运算的返回中 -- 我们得到仍是一个引用类型 。这也是this
的值为什么再次设为 base 对象,即 foo。 - 第三个例子中,与组运算符不同,赋值运算符调用了
GetValue
方法。返回的结果是函数对象(但不是引用类型),这意味着this
设为null,结果是 global 对象。 - 第四个和第五个例子相类似 -- 逗号运算符和逻辑运算符(OR)调用了
GetValue
方法,相应地,我们失去引用而得到了函数。并再次设为 global。
# 引用类型和this为null
有一种情况,如果调用方式确定了引用类型的值,不管怎样,只要this
的值被设置为 null, 其最终会被隐式转换成 global。这跟引用类型的值是激活对象相关。
下面的例子中,内部函数被父函数调用,此时我们就能看到上面说的那种特殊情况,正如我们在第二章知道的一样,局部变量,内部函数,形式参数都存储在给定的函数的激活对象中。
function foo() {
function bar() {
console.log(this); // global
}
bar();
}
2
3
4
5
6
激活对象总是作为this
的值返回 -- null(即伪代码AO.bar()
相当于null.bar()
)(译者注:如果不明白可以参考下原文)。这里我们再次回到上面描述的情况,this
的值还是被设置全局对象。
有一种情况除外,在with
语句中调用函数,且在 with 对象中包含函数名属性时,with语句将其对象添加到作用域的最前端,即在激活对象前面。
那么对应的,引用类型的值(通过的标识符或属性访问器),其 base 对象不再是激活对象,而是 with 语句对象。顺便提一句,这种情况不仅跟内部
函数相关,还跟全局函数相关,因为 with 对象比作用域链的最前端的对象(全局或者激活对象)还要靠前(because the with object shadows
higher object (global or an activation object) of the scope chain:)。
var x = 10;
with({
foo: function() {
console.log(this.x);
},
x: 20
}){
foo(); // 20
}
// because
var fooReference = {
base: __withObject,
propertyName: 'foo'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在 catch 语句的实际参数中的函数调用存在类似情况,在这种情况下,catch 对象被添加到作用域的最前端,即在激活对象或全局对象的前面。但是,这个特定
的行为被确认为是 ECMA-262-3 的一个 bug, 这个在新版本的 ECMA-262-5 被修复了,即this
指向全局对象,而不是 catch 对象。
try {
throw function () {
console.log(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'
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
同样的情况出现在命名函数(函数的更多细节参考Chapter 5. Functions)的递归调用中。在函数的第一次调用中,base对象是父激活对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this的值也总是被设置为global。
(function foo(bar) {
console.log(this);
!bar && foo(1); // "should" be special object, but always (correct) global
})(); // global
2
3
4
5
6
7
# 作为构造器调用的函数中的this值
还有一个在函数上下文中与this
的值相关的情况是:函数作为构造器调用时。
function A() {
console.log(this); // newly created object, below - "a" object
this.x = 10;
}
var a = new A();
console.log(a.x); // 10
2
3
4
5
6
7
在这个例子中,new 操作符调用A
函数内部的[[Construct]]方法,接着,在对象创建后,调用内部的[[Call]]方法,所有相同
的函数A
都将this
的值设置为新创建的对象。
# 手动设置一个函数调用的this值
在Function.prototype
中定义了两种方法(这两种方法全部函数都可以访问),允许手动设置函数调用时的this
值,他们就是apply
和call
方法。
它们用接受的第一个参数作为this
的值,这两种方法的区别不大,对于apply
,第二个参数必须是数组(或者是类数组的对象,比如arguments
), 相反,
call
方法能接受任何参数,两个方法必须的参数都是第一个 -- this
的值。
例如:
var b = 10;
function a(c) {
console.log(this.b);
console.log(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
2
3
4
5
6
7
8
9
10
11
# 译者注
到这里,对原文的翻译就已经结束了,原文结论部分不是很重要就没有翻译了。整体翻译完,不禁感概翻译一篇技术文章真的不容易, 向互联网翻译技术文章的技术大牛致敬。此文大概50%参考了Justin的翻译,剩下根据自己的理解翻译,因为学识有限难免会有纰漏,恳请指正。这篇文章 暂时告一段落,之后有新的看法会更新,感谢~~