导言
在这篇文章中我们将讨论一个与执行上下文直接相关的更多细节。讨论的主题就是this关键字。
实践表明,这个主题很难,在不同执行上下文中this值的确定经常导致问题。
许多程序员习惯的认为,在程序语言中,this关键字与面向对象的程序紧密相关,完全指向通过构造器创建的新的对象。在ECMAScript中也是这样执行的,但正如你看到的那样,这并不限于创建对象的定义。
让我们更详细的了解ECMAScript中真正的this值是什么?
定义
this是执行上下文中的一个属性。
1.
activeExecutionContext = {
2.
VO: {...},
3.
this
: thisValue
4.
};
这里VO是我们前一章讨论的变量对象。
this与上下文中可执行代码密切直接相关,this值在进入上下文时确定,代码在上下文运行期间一成不变
下面让我们更详细研究这些案例
全局代码中的this值
在这里一切都简单。在全局代码中,this始终是全局对象本身,这样有可能间接的引用它。
01.
// explicit property definition of
02.
// the global object
03.
this
.a = 10;
// global.a = 10
04.
alert(a);
// 10
05.
// implicit definition via assigning
06.
// to unqualified identifier
07.
b = 20;
08.
alert(
this
.b);
// 20
09.
// also implicit via variable declaration
10.
// because variable object of the global context
11.
// is the global object itself
12.
var
c = 30;
13.
alert(
this
.c);
// 30
函数代码中的this值
在函数代码中使用this 时很有趣,这种情况很难且会导致很多问题。
这种类型的代码中,this值的首要特点(或许是最主要的)是它不是静态的绑定到一个函数。
正如我们上面曾提到的那样,这个值进入上下文时确定,在一个函数代码中,这个值在每一次完全不同。
但是,在代码运行时的this值是不变的,也就是说,既然它不是一个变量,就不可能为其分配一个新值(相反,在Python编程语言中,它明确的定义为对象本身,在运行期间可以不断改变)。
01.
var
foo = {x: 10};
02.
var
bar = {
03.
x: 20,
04.
test:
function
() {
05.
alert(
this
=== bar);
// true
06.
alert(
this
.x);
// 20
07.
this
= foo;
// error
08.
alert(
this
.x);
// if there wasn't an error then 20, not 10
09.
10.
}
11.
};
12.
// on entering the context this value is
13.
// determined as "bar" object; why so - will
14.
// be discussed below in detail
15.
bar.test();
// true, 20
16.
foo.test = bar.test;
17.
// however here this value will now refer
18.
// to "foo" – even though we're calling the same function
19.
foo.test();
// false, 10
那么,是什么影响了函数代码中this值的变化。有几个因素:
首先,在通常的函数调用中,this是由激活上下文代码的调用者来提供的,即调用函数的父上下文(parent context )。this取决于调用函数的方式。
为了在任何情况下准确无误的确定this值,有必要理解和记住这重要的一点。正是调用函数的方式影响了调用的上下文中的this值,没有别的什么(我们可以在一些文章,甚至是在关于javascript的书籍中看到,它们声称:“this值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。–这绝对不正确”)。继续我们的话题,可以看到,即使是正常的全局函数也会被调用方式的不同形式激活,这些不同的调用方式导致了不同的this值。
1.
function
foo() {
2.
alert(
this
);
3.
}
4.
foo();
// global
5.
alert(foo === foo.prototype.constructor);
// true
6.
// but with another form of the call expression
7.
// of the same function, this value is different
8.
foo.prototype.constructor();
// foo.prototype
有可能作为一些对象定义的方法来调用函数,但是this将不会设置为这个对象。
01.
var
foo = {
02.
bar:
function
() {
03.
alert(
this
);
04.
alert(
this
=== foo);
05.
}
06.
};
07.
foo.bar();
// foo, true
08.
var
exampleFunc = foo.bar;
09.
alert(exampleFunc === foo.bar);
// true
10.
// again with another form of the call expression
11.
// of the same function, we have different this value
12.
exampleFunc();
// global, false
那么,调用函数的方式如何影响this值?为了充分理解this值的确定,需要详细分析其内部属性之一——引用类型(Reference type)。
引用类型
使用伪代码,引用类型的值可以表示为拥有两个属性的对象——base(即拥有属性的那个对象),和base中的propertyName 。
1.
var
valueOfReferenceType = {
2.
base: <base object>,
3.
propertyName: <property name>
4.
};
引用类型的值只有两种情况:
- 当我们处理一个标示符时
- 或一个属性访问器
标示符的处理过程在Chapter 4. Scope chain 中讨论,在这里我们只是看到,在该算法的返回值中,总是一个引用类型的值(这对this来说很重要)。
标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名。例如,下面标识符的值:
1.
var
foo = 10;
2.
function
bar() {}
在操作的中间结果中,引用类型对应的值如下:
01.
var
fooReference = {
02.
base: global,
03.
propertyName:
'foo'
04.
};
05.
06.
var
barReference = {
07.
base: global,
08.
propertyName:
'bar'
09.
};
为了从引用类型中得到一个对象真正的值,伪代码中的GetValue方法可以做如下描述:
01.
function
GetValue(value) {
02.
if
(Type(value) != Reference) {
03.
return
value;
04.
}
05.
var
base = GetBase(value);
06.
if
(base ===
null
) {
07.
throw
new
ReferenceError;
08.
}
09.
return
base.[[Get]](GetPropertyName(value));
10.
}
在那里内部的[[Get]]方法返回对象属性真正的值,包括对原型链中继承的属性分析。
1.
GetValue(fooReference);
// 10
2.
GetValue(barReference);
// function object "bar"
属性访问器应该熟悉。它有两种变体:点(.)语法(此时属性名是正确的标示符,且事先知道),或括号语法([])。
1.
foo.bar();
2.
foo[
'bar'
]();
在中间计算的返回值中,我们有了引用类型的值。
引用类型的值与函数上下文中的this值如何相关?——从最重要的意义上来说。 这个关联的过程是这篇文章的核心。 一个函数上下文中确定this值的通用规则如下:
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用括号()的左边是引用类型的值,this将设为引用类型值的base对象(base object),在其他情况下(与引用类型不同的任何其它属性),这个值为null。不过,实际不存在this的值为null的情况,因为当this的值为null的时候,其值会被隐式转换为全局对象。
我们看看这个例子中的表现:
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.
};
现在,我们可以很明确的告诉你,为什么用表达式的不同形式激活同一个函数会不同的this值,答案在于引用类型(type Reference)不同的中间值。
01.
function
foo() {
02.
alert(
this
);
03.
}
04.
05.
foo();
// global, because
06.
07.
var
fooReference = {
08.
base: global,
09.
propertyName:
'foo'
10.
};
11.
12.
alert(foo === foo.prototype.constructor);
// true
13.
14.
// another form of the call expression
15.
16.
foo.prototype.constructor();
// foo.prototype, because
17.
18.
var
fooPrototypeConstructorReference = {
19.
base: foo.prototype,
20.
propertyName:
'constructor'
21.
};
另外一个通过调用方式动态确定this值的经典例子:
01.
function
foo() {
02.
alert(
this
.bar);
03.
}
04.
05.
var
x = {bar: 10};
06.
var
y = {bar: 20};
07.
08.
x.test = foo;
09.
y.test = foo;
10.
11.
x.test();
// 10
12.
y.test();
// 20
函数调用和非引用类型
因此,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,这个值自动设置为null,结果为全局对象。
让我们再思考这种表达式:
1.
(
function
() {
2.
alert(
this
);
// null => global
3.
})();
在这个例子中,我们有一个函数对象但不是引用类型的对象(它不是标示符,也不是属性访问器),相应地,this值最终设为全局对象。
更多复杂的例子:
01.
var
foo = {
02.
bar:
function
() {
03.
alert(
this
);
04.
}
05.
};
06.
07.
foo.bar();
// Reference, OK => foo
08.
(foo.bar)();
// Reference, OK => foo
09.
10.
(foo.bar = foo.bar)();
// global?
11.
(
false
|| foo.bar)();
// global?
12.
(foo.bar, foo.bar)();
// global?
为什么我们有一个属性访问器,它的中间值应该为引用类型的值,在某些调用中我们得到的this值不是base对象,而是global对象?
问题在于后面的三个调用,在应用一定的运算操作之后,在调用括号的左边的值不在是引用类型。
第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。
在第二个例子中,组运算符并不适用,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如GetValue。相应的,在组运算的返回中———我们得到仍是一个引用类型。这就是this值为什么再次设为base对象,即foo。
第三个例子中,与组运算符不同,赋值运算符调用了GetValue方法。返回的结果是函数对象(但不是引用类型),这意味着this设为null,结果是global对象。
第四个和第五个也是一样——逗号运算符和逻辑运算符(OR)调用了GetValue 方法,相应地,我们失去了引用而得到了函数。并再次设为global。
引用类型和 this为null
有一种情况是这样的:当调用表达式限定了call括号左边的引用类型的值, 尽管this被设定为null,但结果是global。当引用类型值的base对象是被激活的对象时,这种情况就会出现。
下面的实例中,内部函数被父函数调用,此时我们就能够看到上面说的那种特殊情况。正如我们在第二章知道的一样,局部变量、内部函数、形式参数储存在给定函数的激活对象中。
1.
function
foo() {
2.
function
bar() {
3.
alert(
this
);
// global
4.
}
5.
bar();
// the same as AO.bar()
6.
}
激活对象总是作为this返回,值为null——(即伪代码的AO.bar()相当于null.bar())。这里我们再次回到上面描述的例子,this设置为全局对象。
有一种情况除外:如果with对象包含一个函数名属性,在with语句的内部块中调用函数。With语句添加到该对象作用域的最前端,即在激活对象的前面。相应地,也就有了引用类型(通过标示符或属性访问器), 其base对象不再是激活对象,而是with语句的对象。顺便提一句,它不仅与内部函数相关,也与全局函数相关。因为with对象使作用域最前端的对象(全局或激活对象)相形见绌。
01.
var
x = 10;
02.
with
({
03.
04.
foo:
function
() {
05.
alert(
this
.x);
06.
},
07.
x: 20
08.
09.
}) {
10.
11.
foo();
// 20
12.
13.
}
14.
15.
// because
16.
17.
var
fooReference = {
18.
base: __withObject,
19.
propertyName:
'foo'
20.
};
同样的情况出现在catch语句的实际参数中函数调用:在这种情况下,catch对象添加到作用域的最前端,即在激活对象或全局对象的前面。但是,这个特定的行为被确认为ECMA-262-3的一个bug,这个在新版的ECMA-262-5中修复了。这样,在特定的激活对象中,this指向全局对象。而不是catch对象。
01.
try
{
02.
throw
function
() {
03.
alert(
this
);
04.
};
05.
}
catch
(e) {
06.
e();
// __catchObject - in ES3, global - fixed in ES5
07.
}
08.
// on idea
09.
var
eReference = {
10.
base: __catchObject,
11.
propertyName:
'e'
12.
};
13.
// but, as this is a bug
14.
// then this value is forced to global
15.
// null => global
16.
var
eReference = {
17.
base: global,
18.
propertyName:
'e'
19.
};
同样的情况出现在命名函数(函数的更对细节参考Chapter 5. Functions)的递归调用中。在函数的第一次调用中,base对象是父激活对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this总是指向全局对象。
1.
(
function
foo(bar) {
2.
3.
alert(
this
);
4.
5.
!bar && foo(1);
// "should" be special object, but always (correct) global
6.
7.
})();
// global
作为构造器调用的函数的this
还有一个与this值相关的情况是在函数的上下文中,这是一个构造函数的调用。
1.
function
A() {
2.
alert(
this
);
// newly created object, below - "a" object
3.
this
.x = 10;
4.
}
5.
6.
var
a =
new
A();
7.
alert(a.x);
// 10
在这个例子中,new运算符调用“A”函数的内部的[[Construct]] 方法,接着,在对象创建后,调用内部的[[Call]] 方法。 所有相同的函数“A”都将this的值设置为新创建的对象。
手动设置一个函数调用的this
在函数原型中定义的两个方法(因此所有的函数都可以访问它)允许去手动设置函数调用的this值。它们是.apply和.call方法。他们用接受的第一个参数作为this值,this 在调用的作用域中使用。这两个方法的区别很小,对于.apply,第二个参数必须是数组(或者是类似数组的对象,如arguments,反过来,.call能接受任何参数。两个方法必须的参数是第一个——this。
例如:
01.
var
b = 10;
02.
function
a(c) {
03.
alert(
this
.b);
04.
alert(c);
05.
}
06.
07.
a(20);
// this === global, this.b == 10, c == 20
08.
09.
a.call({b: 20}, 30);
// this === {b: 20}, this.b == 20, c == 30
10.
a.apply({b: 30}, [40])
// this === {b: 30}, this.b == 30, c == 40
结论
在这篇文章中,我们讨论了ECMAScript中this关键字的特征(对比于C++ 和 Java,它们的确是特色)。我希望这篇文章有助于你准确的理解ECMAScript中this关键字如何工作。同样,我很乐意在评论中回到你的问题。