在這篇文章里,我們將討論跟執(zhí)行上下文直接相關(guān)的更多細(xì)節(jié)。討論的主題就是 this 關(guān)鍵字。實(shí)踐證明,這個(gè)主題很難,在不同執(zhí)行上下文中 this 的確定經(jīng)常會(huì)發(fā)生問題。
許多程序員習(xí)慣的認(rèn)為,在程序語言中,this 關(guān)鍵字與面向?qū)ο蟪绦蜷_發(fā)緊密相關(guān),其完全指向由構(gòu)造器新創(chuàng)建的對(duì)象。在 ECMAScript 規(guī)范中也是這樣實(shí)現(xiàn)的,但正如我們將看到那樣,在 ECMAScript 中,this 并不限于只用來指向新創(chuàng)建的對(duì)象。
讓我們更詳細(xì)的了解一下,在 ECMAScript 中 this 到底是什么?
this 是執(zhí)行上下文中的一個(gè)屬性:
activeExecutionContext = {
VO: {...},
this: thisValue
};
這里 VO 是我們前一章討論的變量對(duì)象。
this 與上下文中可執(zhí)行代碼的類型有直接關(guān)系,this 值在進(jìn)入上下文時(shí)確定,并且在上下文運(yùn)行期間永久不變。
下面讓我們更詳細(xì)研究這些案例:
在這里一切都簡(jiǎn)單。在全局代碼中,this 始終是全局對(duì)象本身,這樣就有可能間接的引用到它了。
// 顯示定義全局對(duì)象的屬性
this.a = 10; // global.a = 10
alert(a); // 10
// 通過賦值給一個(gè)無標(biāo)示符隱式
b = 20;
alert(this.b); // 20
// 也是通過變量聲明隱式聲明的
// 因?yàn)槿稚舷挛牡淖兞繉?duì)象是全局對(duì)象自身
var c = 30;
alert(this.c); // 30
在函數(shù)代碼中使用 this 時(shí)很有趣,這種情況很難且會(huì)導(dǎo)致很多問題。
這種類型的代碼中,this 值的首要特點(diǎn)(或許是最主要的)是它不是靜態(tài)的綁定到一個(gè)函數(shù)。
正如我們上面曾提到的那樣,this 是進(jìn)入上下文時(shí)確定,在一個(gè)函數(shù)代碼中,這個(gè)值在每一次完全不同。
不管怎樣,在代碼運(yùn)行時(shí)的 this 值是不變的,也就是說,因?yàn)樗皇且粋€(gè)變量,就不可能為其分配一個(gè)新值(相反,在 Python 編程語言中,它明確的定義為對(duì)象本身,在運(yùn)行期間可以不斷改變)。
var foo = {x: 10};
var bar = {
x: 20,
test: function () {
alert(this === bar); // true
alert(this.x); // 20
this = foo; // 錯(cuò)誤,任何時(shí)候不能改變this的值
alert(this.x); // 如果不出錯(cuò)的話,應(yīng)該是10,而不是20
}
};
// 在進(jìn)入上下文的時(shí)候
// this被當(dāng)成bar對(duì)象
// determined as "bar" object; why so - will
// be discussed below in detail
bar.test(); // true, 20
foo.test = bar.test;
// 不過,這里this依然不會(huì)是foo
// 盡管調(diào)用的是相同的function
foo.test(); // false, 10
那么,影響了函數(shù)代碼中 this 值的變化有幾個(gè)因素:
首先,在通常的函數(shù)調(diào)用中,this 是由激活上下文代碼的調(diào)用者來提供的,即調(diào)用函數(shù)的父上下文(parent context )。this 取決于調(diào)用函數(shù)的方式。
為了在任何情況下準(zhǔn)確無誤的確定 this 值,有必要理解和記住這重要的一點(diǎn)。正是調(diào)用函數(shù)的方式影響了調(diào)用的上下文中的 this 值,沒有別的什么(我們可以在一些文章,甚至是在關(guān)于 javascript 的書籍中看到,它們聲稱:“this 值取決于函數(shù)如何定義,如果它是全局函數(shù),this 設(shè)置為全局對(duì)象,如果函數(shù)是一個(gè)對(duì)象的方法,this 將總是指向這個(gè)對(duì)象。–這絕對(duì)不正確”)。繼續(xù)我們的話題,可以看到,即使是正常的全局函數(shù)也會(huì)被調(diào)用方式的不同形式激活,這些不同的調(diào)用方式導(dǎo)致了不同的 this 值。
function foo() {
alert(this);
}
foo(); // global
alert(foo === foo.prototype.constructor); // true
// 但是同一個(gè)function的不同的調(diào)用表達(dá)式,this是不同的
foo.prototype.constructor(); // foo.prototype
有可能作為一些對(duì)象定義的方法來調(diào)用函數(shù),但是 this 將不會(huì)設(shè)置為這個(gè)對(duì)象。
var foo = {
bar: function () {
alert(this);
alert(this === foo);
}
};
foo.bar(); // foo, true
var exampleFunc = foo.bar;
alert(exampleFunc === foo.bar); // true
// 再一次,同一個(gè) function 的不同的調(diào)用表達(dá)式,this 是不同的
exampleFunc(); // global, false
那么,調(diào)用函數(shù)的方式如何影響 this 值?為了充分理解 this 值的確定,需要詳細(xì)分析其內(nèi)部類型之一——引用類型(Reference type)。
使用偽代碼我們可以將引用類型的值可以表示為擁有兩個(gè)屬性的對(duì)象——base(即擁有屬性的那個(gè)對(duì)象),和 base 中的 propertyName 。
var valueOfReferenceType = {
base: <base object>,
propertyName: <property name>
};
引用類型的值只有兩種情況:
標(biāo)示符的處理過程在下一篇文章里詳細(xì)討論,在這里我們只需要知道,在該算法的返回值中,總是一個(gè)引用類型的值(這對(duì) this 來說很重要)。
標(biāo)識(shí)符是變量名,函數(shù)名,函數(shù)參數(shù)名和全局對(duì)象中未識(shí)別的屬性名。例如,下面標(biāo)識(shí)符的值:
var foo = 10;
function bar() {}
在操作的中間結(jié)果中,引用類型對(duì)應(yīng)的值如下:
var fooReference = {
base: global,
propertyName: 'foo'
};
var barReference = {
base: global,
propertyName: 'bar'
};
為了從引用類型中得到一個(gè)對(duì)象真正的值,偽代碼中的 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));
}
內(nèi)部的[[Get]]方法返回對(duì)象屬性真正的值,包括對(duì)原型鏈中繼承的屬性分析。
GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"
屬性訪問器都應(yīng)該熟悉。它有兩種變體:點(diǎn)(.)語法(此時(shí)屬性名是正確的標(biāo)示符,且事先知道),或括號(hào)語法([])。
foo.bar();
foo['bar']();
在中間計(jì)算的返回值中,我們有了引用類型的值。
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
GetValue(fooBarReference); // function object "bar"
引用類型的值與函數(shù)上下文中的 this 值如何相關(guān)?——從最重要的意義上來說。 這個(gè)關(guān)聯(lián)的過程是這篇文章的核心。 一個(gè)函數(shù)上下文中確定 this 值的通用規(guī)則如下:
在一個(gè)函數(shù)上下文中, this 由調(diào)用者提供,由調(diào)用函數(shù)的方式來決定。如果調(diào)用括號(hào)()的左邊是引用類型的值,this 將設(shè)為引用類型值的 base 對(duì)象(base object),在其他情況下(與引用類型不同的任何其它屬性),這個(gè)值為 null。不過,實(shí)際不存在 this 的值為 null 的情況,因?yàn)楫?dāng) this 的值為 null 的時(shí)候,其值會(huì)被隱式轉(zhuǎn)換為全局對(duì)象。*注:第 5 版的 ECMAScript 中,已經(jīng)不強(qiáng)迫轉(zhuǎn)換成全局變量了,而是賦值為 undefined。*
我們看看這個(gè)例子中的表現(xiàn):
function foo() {
return this;
}
foo(); // global
我們看到在調(diào)用括號(hào)的左邊是一個(gè)引用類型值(因?yàn)?foo 是一個(gè)標(biāo)示符)。
var fooReference = {
base: global,
propertyName: 'foo'
};
相應(yīng)地,this 也設(shè)置為引用類型的 base 對(duì)象。即全局對(duì)象。
同樣,使用屬性訪問器:
var foo = {
bar: function () {
return this;
}
};
foo.bar(); // foo
我們?cè)俅螕碛幸粋€(gè)引用類型,其 base 是 foo 對(duì)象,在函數(shù) bar 激活時(shí)用作 this。
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
但是,用另外一種形式激活相同的函數(shù),我們得到其它的 this 值。
var test = foo.bar;
test(); // global
因?yàn)?test 作為標(biāo)示符,生成了引用類型的其他值,其 base(全局對(duì)象)用作 this 值。
var testReference = {
base: global,
propertyName: 'test'
};
現(xiàn)在,我們可以很明確的告訴你,為什么用表達(dá)式的不同形式激活同一個(gè)函數(shù)會(huì)不同的 this 值,答案在于引用類型(type Reference)不同的中間值。
function foo() {
alert(this);
}
foo(); // global, because
var fooReference = {
base: global,
propertyName: 'foo'
};
alert(foo === foo.prototype.constructor); // true
// 另外一種形式的調(diào)用表達(dá)式
foo.prototype.constructor(); // foo.prototype, because
var fooPrototypeConstructorReference = {
base: foo.prototype,
propertyName: 'constructor'
};
另外一個(gè)通過調(diào)用方式動(dòng)態(tài)確定 this 值的經(jīng)典例子:
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
因此,正如我們已經(jīng)指出,當(dāng)調(diào)用括號(hào)的左邊不是引用類型而是其它類型,這個(gè)值自動(dòng)設(shè)置為 null,結(jié)果為全局對(duì)象。
讓我們?cè)偎伎歼@種表達(dá)式:
(function () {
alert(this); // null => global
})();
在這個(gè)例子中,我們有一個(gè)函數(shù)對(duì)象但不是引用類型的對(duì)象(它不是標(biāo)示符,也不是屬性訪問器),相應(yīng)地,this 值最終設(shè)為全局對(duì)象。
更多復(fù)雜的例子:
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?
為什么我們有一個(gè)屬性訪問器,它的中間值應(yīng)該為引用類型的值,在某些調(diào)用中我們得到的 this 值不是 base 對(duì)象,而是 global 對(duì)象?
問題在于后面的三個(gè)調(diào)用,在應(yīng)用一定的運(yùn)算操作之后,在調(diào)用括號(hào)的左邊的值不在是引用類型。
有一種情況是這樣的:當(dāng)調(diào)用表達(dá)式限定了 call 括號(hào)左邊的引用類型的值, 盡管 this 被設(shè)定為 null,但結(jié)果被隱式轉(zhuǎn)化成 global。當(dāng)引用類型值的 base 對(duì)象是被活動(dòng)對(duì)象時(shí),這種情況就會(huì)出現(xiàn)。
下面的實(shí)例中,內(nèi)部函數(shù)被父函數(shù)調(diào)用,此時(shí)我們就能夠看到上面說的那種特殊情況。正如我們?cè)诘?12 章知道的一樣,局部變量、內(nèi)部函數(shù)、形式參數(shù)儲(chǔ)存在給定函數(shù)的激活對(duì)象中。
function foo() {
function bar() {
alert(this); // global
}
bar(); // the same as AO.bar()
}
活動(dòng)對(duì)象總是作為 this 返回,值為 null——(即偽代碼的 AO.bar()相當(dāng)于 null.bar())。這里我們?cè)俅位氐缴厦婷枋龅睦?,this 設(shè)置為全局對(duì)象。
有一種情況除外:如果 with 對(duì)象包含一個(gè)函數(shù)名屬性,在 with 語句的內(nèi)部塊中調(diào)用函數(shù)。With 語句添加到該對(duì)象作用域的最前端,即在活動(dòng)對(duì)象的前面。相應(yīng)地,也就有了引用類型(通過標(biāo)示符或?qū)傩栽L問器), 其 base 對(duì)象不再是活動(dòng)對(duì)象,而是 with 語句的對(duì)象。順便提一句,它不僅與內(nèi)部函數(shù)相關(guān),也與全局函數(shù)相關(guān),因?yàn)?with 對(duì)象比作用域鏈里的最前端的對(duì)象(全局對(duì)象或一個(gè)活動(dòng)對(duì)象)還要靠前。
var x = 10;
with ({
foo: function () {
alert(this.x);
},
x: 20
}) {
foo(); // 20
}
// because
var fooReference = {
base: __withObject,
propertyName: 'foo'
};
同樣的情況出現(xiàn)在 catch 語句的實(shí)際參數(shù)中函數(shù)調(diào)用:在這種情況下,catch 對(duì)象添加到作用域的最前端,即在活動(dòng)對(duì)象或全局對(duì)象的前面。但是,這個(gè)特定的行為被確認(rèn)為 ECMA-262-3 的一個(gè) bug,這個(gè)在新版的 ECMA-262-5 中修復(fù)了。這樣,在特定的活動(dòng)對(duì)象中,this 指向全局對(duì)象。而不是 catch 對(duì)象。
try {
throw function () {
alert(this);
};
} catch (e) {
e(); // ES3標(biāo)準(zhǔn)里是__catchObject, ES5標(biāo)準(zhǔn)里是global
}
// on idea
var eReference = {
base: __catchObject,
propertyName: 'e'
};
// ES5新標(biāo)準(zhǔn)里已經(jīng)fix了這個(gè)bug,
// 所以this就是全局對(duì)象了
var eReference = {
base: global,
propertyName: 'e'
};
同樣的情況出現(xiàn)在命名函數(shù)(函數(shù)的更對(duì)細(xì)節(jié)參考第 15 章 Functions)的遞歸調(diào)用中。在函數(shù)的第一次調(diào)用中,base 對(duì)象是父活動(dòng)對(duì)象(或全局對(duì)象),在遞歸調(diào)用中,base 對(duì)象應(yīng)該是存儲(chǔ)著函數(shù)表達(dá)式可選名稱的特定對(duì)象。但是,在這種情況下,this 總是指向全局對(duì)象。
(function foo(bar) {
alert(this);
!bar && foo(1); // "should" be special object, but always (correct) global
})(); // global
還有一個(gè)與 this 值相關(guān)的情況是在函數(shù)的上下文中,這是一個(gè)構(gòu)造函數(shù)的調(diào)用。
function A() {
alert(this); // "a"對(duì)象下創(chuàng)建一個(gè)新屬性
this.x = 10;
}
var a = new A();
alert(a.x); // 10
在這個(gè)例子中,new 運(yùn)算符調(diào)用“A”函數(shù)的內(nèi)部的[[Construct]] 方法,接著,在對(duì)象創(chuàng)建后,調(diào)用內(nèi)部的[[Call]] 方法。 所有相同的函數(shù)“A”都將 this 的值設(shè)置為新創(chuàng)建的對(duì)象。
在函數(shù)原型中定義的兩個(gè)方法(因此所有的函數(shù)都可以訪問它)允許去手動(dòng)設(shè)置函數(shù)調(diào)用的 this 值。它們是 .apply 和 .call 方法。他們用接受的第一個(gè)參數(shù)作為 this 值,this 在調(diào)用的作用域中使用。這兩個(gè)方法的區(qū)別很小,對(duì)于 .apply,第二個(gè)參數(shù)必須是數(shù)組(或者是類似數(shù)組的對(duì)象,如 arguments,反過來,.call 能接受任何參數(shù)。兩個(gè)方法必須的參數(shù)是第一個(gè)——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 關(guān)鍵字的特征(對(duì)比于 C++ 和 Java,它們的確是特色)。我希望這篇文章有助于你準(zhǔn)確的理解 ECMAScript 中 this 關(guān)鍵字如何工作。