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