感謝社區(qū)中各位的大力支持莺戒,譯者再次奉上一點點福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠急波,并抽取幸運大獎:點擊這里領(lǐng)取
JavaScript中最令人困惑的機制之一就是this
關(guān)鍵字从铲。它是一個在每個函數(shù)作用域中自動定義的特殊標識符關(guān)鍵字,但即便是一些老練的開發(fā)者也對它到底指向什么感到困擾幔崖。
任何足夠 先進 的技術(shù)都跟魔法沒有區(qū)別食店。-- Arthur C. Clarke
JavaScript的this
機制實際上沒有 那么 先進,但是開發(fā)者們總是在大腦中引用這句話來表達“復(fù)雜”和“混亂”赏寇,毫無疑問吉嫩,如果沒有清晰的理解,在 你的 困惑中this
可能看起來就是徹頭徹尾的魔法嗅定。
注意: “this”這個詞是在一般的論述中極常用的代詞自娩。所以,特別是在口頭論述中渠退,很難確定我們是在將“this”作為一個代詞使用忙迁,還是在將它作為一個實際的關(guān)鍵字識別符使用。為了表意清晰碎乃,我會總是使用this
來代表特殊的關(guān)鍵字姊扔,而在其他情況下使用“this”或 this 或this。
為什么用 this
梅誓?
如果對于那些老練的JavaScript開發(fā)者來說this
機制都是如此的令人費解恰梢,那么有人會問為什么這種機制會有用?它帶來的麻煩不是比好處多嗎梗掰?在講解 如何 有用之前嵌言,我們應(yīng)當先來看看 為什么 有用。
讓我們試著展示一下this
的動機和用途:
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER
如果這個代碼段 如何 工作讓你困惑及穗,不要擔心摧茴!我們很快就會講解它。只是簡要地將這些問題放在旁邊埂陆,以便于我們可以更清晰的探究 為什么苛白。
這個代碼片段允許identify()
和speak()
函數(shù)對多個 環(huán)境 對象(me
和you
)進行復(fù)用,而不是針對每個對象定義函數(shù)的分離版本焚虱。
與使用this
相反地购裙,你可以明確地將環(huán)境對象傳遞給identify()
和speak()
。
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); // Hello, I'm KYLE
然而著摔,this
機制提供了更優(yōu)雅的方式來隱含地“傳遞”一個對象引用缓窜,導(dǎo)致更加干凈的API設(shè)計和更容易的復(fù)用。
你的使用模式越復(fù)雜,你就會越清晰地看到:將執(zhí)行環(huán)境作為一個明確參數(shù)傳遞禾锤,通常比傳遞this
執(zhí)行環(huán)境要亂私股。當我們探索對象和原型時,你將會看到一組可以自動引用恰當執(zhí)行環(huán)境對象的函數(shù)是多么有用恩掷。
困惑
我們很快就要開始講解this
是如何 實際 工作的倡鲸,但我們首先要摒棄一些誤解——它實際上 不是 如何工作的。
在開發(fā)者們用太過于字面的方式考慮“this”這個名字時就會產(chǎn)生困惑黄娘。這通常會產(chǎn)生兩種臆測峭状,但都是不對的。
它自己
第一種常見的傾向是認為this
指向函數(shù)自己逼争。至少优床,這是一種語法上的合理推測。
為什么你想要在函數(shù)內(nèi)部引用它自己誓焦?最通常的理由是遞歸(在函數(shù)內(nèi)部調(diào)用它自己)這樣的情形胆敞,或者是一個在第一次被調(diào)用時會解除自己綁定的事件處理器。
初次接觸JS機制的開發(fā)者們通常認為杂伟,將函數(shù)作為一個對象(JavaScript中所有的函數(shù)都是對象R撇恪),可以讓你在方法調(diào)用之間儲存 狀態(tài)(屬性中的值)赫粥。這當然是可能的观话,而且有一些有限的用處,但這本書的其余部分將會闡述許多其他的模式越平,提供比函數(shù)對象 更好 的地方來存儲狀態(tài)频蛔。
過一會兒我們將探索一個模式,來展示this
是如何不讓一個函數(shù)像我們可能假設(shè)的那樣喧笔,得到它自身的引用的帽驯。
考慮下面的代碼龟再,我們試圖追蹤函數(shù)(foo
)被調(diào)用了多少次:
function foo(num) {
console.log( "foo: " + num );
// 追蹤`foo`被調(diào)用了多少次
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo`被調(diào)用了多少次书闸?
console.log( foo.count ); // 0 -- 這他媽怎么回事……?
foo.count
依然 是0
, 即便四個console.log
語句明明告訴我們foo(..)
實際上被調(diào)用了四次利凑。這種失敗來源于對于this
(在this.count++
中)的含義進行了 過于字面化 的解釋浆劲。
當代碼執(zhí)行foo.count = 0
時,它確實在函數(shù)對象foo
中加入了一個count
屬性哀澈。但是對于函數(shù)內(nèi)部的this.count
引用牌借,this
其實 根本就不 指向那個函數(shù)對象,即便屬性名稱一樣割按,但根對象也不同膨报,因而產(chǎn)生了混淆。
注意: 一個負責任的開發(fā)者 應(yīng)當 在這里提出一個問題:“如果我遞增的count
屬性不是我以為的那個,那是哪個count
被我遞增了现柠?”院领。實際上,如果他再挖的深一些够吩,他會發(fā)現(xiàn)自己不小心創(chuàng)建了一個全局變量count
(第二章解釋了這是 如何 發(fā)生的)比然,而且它當前的值是NaN
。當然周循,一旦他發(fā)現(xiàn)這個不尋常的結(jié)果后强法,他會有一堆其他的問題:“它怎么是全局的?為什么它是NaN
而不是某個正確的計數(shù)值湾笛?”饮怯。(見第二章)
與停在這里來深究為什么this
引用看起來不是如我們 期待 的那樣工作,并且回答那些尖銳且重要的問題相反嚎研,許多開發(fā)者簡單地完全回避這個問題硕淑,轉(zhuǎn)向一些其他的另類解決方法,比如創(chuàng)建另一個對象來持有count
屬性:
function foo(num) {
console.log( "foo: " + num );
// 追蹤foo被調(diào)用了多少次
data.count++;
}
var data = {
count: 0
};
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被調(diào)用了多少次嘉赎?
console.log( data.count ); // 4
雖然這種方式確實“解決”了問題置媳,但不幸的是它簡單地忽略了真正的問題——缺乏對于this
的含義和其工作方式上的理解——反而退回到了一個他更加熟悉的機制的舒適區(qū):詞法作用域。
注意: 詞法作用域是一個完善且有用的機制公条;我不是在用任何方式貶低它的作用(參見本系列的 "作用域與閉包")拇囊。但在如何使用this
這個問題上總是靠 猜,而且通常都犯 錯靶橱,并不是一個退回到詞法作用域寥袭,而且從不學(xué)習(xí) 為什么 this
不跟你合作的好理由。
為了從函數(shù)對象內(nèi)部引用它自己关霸,一般來說通過this
是不夠的传黄。你用通常需要通過一個指向它的詞法標識符(變量)得到函數(shù)對象的引用。
考慮這兩個函數(shù):
function foo() {
foo.count = 4; // `foo` 引用它自己
}
setTimeout( function(){
// 匿名函數(shù)(沒有名字)不能引用它自己
}, 10 );
第一個函數(shù)队寇,稱為“命名函數(shù)”膘掰,foo
是一個引用,可以用于在它內(nèi)部引用自己佳遣。
但是在第二個例子中识埋,傳遞給setTimeout(..)
的回調(diào)函數(shù)沒有名稱標識符(所以被稱為“匿名函數(shù)”),所以沒有恰當?shù)霓k法引用函數(shù)對象自己零渐。
注意: 在函數(shù)中有一個老牌兒但是現(xiàn)在被廢棄的窒舟,而且令人皺眉頭的arguments.callee
引用 也 指向當前正在執(zhí)行的函數(shù)的函數(shù)對象。這個引用通常是匿名函數(shù)在自己內(nèi)部訪問函數(shù)對象的唯一方法诵盼。然而惠豺,最佳的辦法是完全避免使用匿名函數(shù)银还,至少是對于那些需要自引用的函數(shù),而使用命名函數(shù)(表達式)洁墙。arguments.callee
已經(jīng)被廢棄而且不應(yīng)該再使用见剩。
對于當前我們的例子來說,另一個 好用的 解決方案是在每一個地方都使用foo
標識符作為函數(shù)對象的引用扫俺,而根本不用this
:
function foo(num) {
console.log( "foo: " + num );
// 追蹤`foo`被調(diào)用了多少次
foo.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo`被調(diào)用了多少次苍苞?
console.log( foo.count ); // 4
然而,這種方法也類似地回避了對this
的 真正 理解狼纬,而且完全依靠變量foo
的詞法作用域羹呵。
另一種解決問題的方法是強迫this
指向foo
函數(shù)對象:
function foo(num) {
console.log( "foo: " + num );
// 追蹤`foo`被調(diào)用了多少次
// 注意:由于`foo`的被調(diào)用方式(見下方),`this`現(xiàn)在確實是`foo`
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 `call(..)`疗琉,我們可以保證`this`指向函數(shù)對象(`foo`)
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo`被調(diào)用了多少次冈欢?
console.log( foo.count ); // 4
與回避this
相反,我們接受它盈简。 我們將會更完整地講解這樣的技術(shù) 如何 工作凑耻,所以如果你依然有點兒糊涂,不要擔心柠贤!
它的作用域
第二常見的對this
的含義的誤解香浩,是它不知怎的指向了函數(shù)的作用域。這是一個刁鉆的問題臼勉,因為在某一種意義上它有正確的部分邻吭,而在另外一種意義上,它是嚴重的誤導(dǎo)宴霸。
明確地說囱晴,this
不會以任何方式指向函數(shù)的 詞法作用域。作用域好像是一個將所有可用標識符作為屬性的對象瓢谢,這從內(nèi)部來說是對的畸写。但是JavasScript代碼不能訪問作用域“對象”。它是 引擎 的內(nèi)部實現(xiàn)氓扛。
考慮下面代碼枯芬,它(失敗的)企圖跨越這個邊界,用this
來隱含地引用函數(shù)的詞法作用域:
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //undefined
這個代碼段里不只有一個錯誤幢尚。雖然它看起來是在故意瞎搞破停,但你看到的這段代碼翅楼,是從公共的幫助論壇社區(qū)中被交換的真實代碼中提取出來的尉剩。真是難以想象對this
的臆想是多么的誤導(dǎo)人。
首先毅臊,試圖通過this.bar()
來引用bar()
函數(shù)理茎。它幾乎可以說是 碰巧 能夠工作,我們過一會兒再解釋它是 如何 工作的。調(diào)用bar()
最自然的方式是省略開頭的 this.
皂林,而僅對標識符進行詞法引用朗鸠。
然而,寫下這段代碼的開發(fā)者試圖用this
在foo()
和bar()
的詞法作用域間建立一座橋础倍,使得bar()
可以訪問foo()
內(nèi)部作用域的變量a
烛占。這樣的橋是不可能的。 你不能使用this
引用在詞法作用域中查找東西沟启。這是不可能的忆家。
每當你感覺自己正在試圖使用this
來進行詞法作用域的查詢時,提醒你自己:這里沒有橋德迹。
什么是this
芽卿?
我們已經(jīng)列舉了各種不正確的臆想,現(xiàn)在讓我們把注意力this
機制是如何真正工作的胳搞。
我們早先說過卸例,this
不是編寫時綁定,而是運行時綁定肌毅。它依賴于函數(shù)調(diào)用的上下文條件筷转。this
綁定和函數(shù)聲明的位置無關(guān),反而和函數(shù)被調(diào)用的方式有關(guān)悬而。
當一個函數(shù)被調(diào)用時旦装,會建立一個活動記錄,也稱為執(zhí)行環(huán)境摊滔。這個記錄包含函數(shù)是從何處(call-stack)被調(diào)用的阴绢,函數(shù)是 如何 被調(diào)用的,被傳遞了什么參數(shù)等信息艰躺。這個記錄的屬性之一呻袭,就是在函數(shù)執(zhí)行期間將被使用的this
引用。
下一章中腺兴,我們將會學(xué)習(xí)尋找函數(shù)的 調(diào)用點(call-site) 來判定它的執(zhí)行如何綁定this
左电。
復(fù)習(xí)
對于那些沒有花時間學(xué)習(xí)this
綁定機制如何工作的JavaScript開發(fā)者來說,this
綁定一直是困惑的根源页响。猜測篓足,試錯,或者盲目地從Stack Overflow的回答中復(fù)制粘貼闰蚕,都不是有效或正確利用this
這么重要的機制的方法栈拖。
為了學(xué)習(xí)this
,你必須首先學(xué)習(xí)this
不是 什么没陡,不論是哪種把你誤導(dǎo)至何處的臆測或誤解涩哟。this
既不是函數(shù)自身的引用索赏,也不是函數(shù)詞法作用域的引用。
this
實際上是在函數(shù)被調(diào)用時建立的一個綁定贴彼,它指向 什么 是完全由函數(shù)被調(diào)用的調(diào)用點來決定的潜腻。