# 第一章:?`this`?是什么?
JavaScript 中最令人困惑的機制之一就是?`this`?關鍵字。它是一個在每個函數作用域中自動定義的特殊標識符關鍵字,但即便是一些老練的 JavaScript 開發者也對它到底指向什么感到困擾。
> 任何足夠?*先進*?的技術都跟魔法沒有區別。-- Arthur C. Clarke
JavaScript 的?`this`?機制實際上沒有?*那么*?先進,但是開發者們總是在大腦中插入“復雜”和“混亂”來解釋這句話,毫無疑問,如果沒有清晰的理解,在?*你的*?困惑中?`this`?可能看起來就是徹頭徹尾的魔法。
注意:?“this”這個詞是在一般的論述中極常用的代詞。所以,特別是在口頭論述中,很難確定我們是在將“this”作為一個代詞使用,還是在將它作為一個實際的關鍵字標識符使用。為了表意清晰,我會總是使用?`this`?來代表特殊的關鍵字,而在其他情況下使用“this”或?*this*?或 this。
## 為什么要用?`this`?
如果對于那些老練的 JavaScript 開發者來說?`this`?機制都是如此的令人費解,那么有人會問為什么這種機制會有用?它帶來的麻煩不是比好處多嗎?在講解?*如何*?有用之前,我們應當先來看看?*為什么*?有用。
讓我們試著展示一下?`this`?的動機和用途:
```source-js
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()`?函數對多個?*環境*?對象(`me`?和?`you`)進行復用,而不是針對每個對象定義函數的分離版本。
與使用?`this`?相反地,你可以明確地將環境對象傳遞給?`identify()`?和?`speak()`。
```source-js
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`?機制提供了更優雅的方式來隱含地“傳遞”一個對象引用,導致更加干凈的API設計和更容易的復用。
你的使用模式越復雜,你就會越清晰地看到:將執行環境作為一個明確參數傳遞,通常比傳遞?`this`?執行環境要亂。當我們探索對象和原型時,你將會看到一組可以自動引用恰當執行環境對象的函數是多么有用。
## 困惑
我們很快就要開始講解?`this`?是如何?*實際*?工作的,但我們首先要摒棄一些誤解——它實際上?*不是*?如何工作的。
在開發者們用太過于字面的方式考慮“this”這個名字時就會產生困惑。這通常會產生兩種臆測,但都是不對的。
### 它自己
第一種常見的傾向是認為?`this`?指向函數自己。至少,這是一種語法上的合理推測。
為什么你想要在函數內部引用它自己?最常見的理由是遞歸(在函數內部調用它自己)這樣的情形,或者是一個在第一次被調用時會解除自己綁定的事件處理器。
初次接觸 JS 機制的開發者們通常認為,將函數作為一個對象(JavaScript 中所有的函數都是對象!),可以讓你在方法調用之間儲存?*狀態*(屬性中的值)。這當然是可能的,而且有一些有限的用處,但這本書的其余部分將會闡述許多其他的模式,提供比函數對象?*更好*?的地方來存儲狀態。
過一會兒我們將探索一個模式,來展示?`this`?是如何不讓一個函數像我們可能假設的那樣,得到它自身的引用的。
考慮下面的代碼,我們試圖追蹤函數(`foo`)被調用了多少次:
```source-js
function foo(num) {
console.log( "foo: " + num );
// 追蹤 `foo` 被調用了多少次
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` 被調用了多少次?
console.log( foo.count ); // 0 -- 這他媽怎么回事……?
```
`foo.count`?*依然*?是?`0`, 即便四個?`console.log`?語句明明告訴我們?`foo(..)`?實際上被調用了四次。這種挫敗來源于對于?`this`(在?`this.count++`?中)的含義進行了?*過于字面化*?的解釋。
當代碼執行?`foo.count = 0`?時,它確實向函數對象?`foo`?添加了一個?`count`?屬性。但是對于函數內部的?`this.count`?引用,`this`?其實?*根本就不*?指向那個函數對象,即便屬性名稱一樣,但根對象也不同,因而產生了混淆。
注意:?一個負責任的開發者?*應當*?在這里提出一個問題:“如果我遞增的?`count`?屬性不是我以為的那個,那是哪個?`count`?被我遞增了?”。實際上,如果他再挖的深一些,他會發現自己不小心創建了一個全局變量?`count`(第二章解釋了這是?*如何*?發生的!),而且它當前的值是?`NaN`。當然,一旦他發現這個不尋常的結果后,他會有一堆其他的問題:“它怎么是全局的?為什么它是?`NaN`?而不是某個正確的計數值?”。(見第二章)
與停在這里來深究為什么?`this`?引用看起來不是如我們?*期待*?的那樣工作,并且回答那些尖銳且重要的問題相反,許多開發者簡單地完全回避這個問題,轉向一些其他的另類解決方法,比如創建另一個對象來持有?`count`?屬性:
```source-js
function foo(num) {
console.log( "foo: " + num );
// 追蹤 `foo` 被調用了多少次
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` 被調用了多少次?
console.log( data.count ); // 4
```
雖然這種方式“解決”了問題是事實,但不幸的是它簡單地忽略了真正的問題 —— 缺乏對于?`this`?的含義和其工作方式上的理解 —— 反而退回到了一個他更加熟悉的機制的舒適區:詞法作用域。
注意:?詞法作用域是一個完善且有用的機制;我不是在用任何方式貶低它的作用(參見本系列的?*"作用域與閉包"*)。但在如何使用?`this`?這個問題上總是靠?*猜*,而且通常都猜?*錯*,并不是一個退回到詞法作用域,而且從不學習?*為什么*?`this`?不跟你合作的好理由。
為了從函數對象內部引用它自己,一般來說通過?`this`?是不夠的。你通常需要通過一個指向它的詞法標識符(變量)得到函數對象的引用。
考慮這兩個函數:
```source-js
function foo() {
foo.count = 4; // `foo` 引用它自己
}
setTimeout( function(){
// 匿名函數(沒有名字)不能引用它自己
}, 10 );
```
第一個函數,稱為“命名函數”,`foo`?是一個引用,可以用于在它內部引用自己。
但是在第二個例子中,傳遞給?`setTimeout(..)`?的回調函數沒有名稱標識符(所以被稱為“匿名函數”),所以沒有合適的辦法引用函數對象自己。
注意:?在函數中有一個老牌兒但是現在被廢棄的,而且令人皺眉頭的?`arguments.callee`?引用?*也*?指向當前正在執行的函數的函數對象。這個引用通常是匿名函數在自己內部訪問函數對象的唯一方法。然而,最佳的辦法是完全避免使用匿名函數,至少是對于那些需要自引用的函數,而使用命名函數(表達式)。`arguments.callee`?已經被廢棄而且不應該再使用。
對于當前我們的例子來說,另一個?*好用的*?解決方案是在每一個地方都使用?`foo`?標識符作為函數對象的引用,而根本不用`this`:
```source-js
function foo(num) {
console.log( "foo: " + num );
// 追蹤 `foo` 被調用了多少次
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` 被調用了多少次?
console.log( foo.count ); // 4
```
然而,這種方法也類似地回避了對?`this`?的?*真正*?理解,而且完全依靠變量?`foo`?的詞法作用域。
另一種解決這個問題的方法是強迫?`this`?指向?`foo`?函數對象:
```source-js
function foo(num) {
console.log( "foo: " + num );
// 追蹤 `foo` 被調用了多少次
// 注意:由于 `foo` 的被調用方式(見下方),`this` 現在確實是 `foo`
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 `call(..)`,我們可以保證 `this` 指向函數對象(`foo`)
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo` 被調用了多少次?
console.log( foo.count ); // 4
```
與回避?`this`?相反,我們接受它。?我們馬上將會更完整地講解這樣的技術?*如何*?工作,所以如果你依然有點兒糊涂,不要擔心!
### 它的作用域
對?`this`?的含義第二常見的誤解,是它不知怎的指向了函數的作用域。這是一個刁鉆的問題,因為在某一種意義上它有正確的部分,而在另外一種意義上,它是嚴重的誤導。
明確地說,`this`?不會以任何方式指向函數的?詞法作用域。作用域好像是一個將所有可用標識符作為屬性的對象,這從內部來說是對的。但是 JavasScript 代碼不能訪問作用域“對象”。它是?*引擎*?的內部實現。
考慮下面代碼,它(失敗的)企圖跨越這個邊界,用?`this`?來隱含地引用函數的詞法作用域:
```source-js
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //undefined
```
這個代碼段里不只有一個錯誤。雖然它看起來是在故意瞎搞,但你看到的這段代碼,提取自在公共社區的幫助論壇中被交換的真實代碼。真是難以想象對?`this`?的臆想是多么的誤導人。
首先,試圖通過?`this.bar()`?來引用?`bar()`?函數。它幾乎可以說是?*碰巧*?能夠工作,我們過一會兒再解釋它是?*如何*?工作的。調用?`bar()`?最自然的方式是省略開頭的?`this.`,而僅使用標識符進行詞法引用。
然而,寫下這段代碼的開發者試圖用?`this`?在?`foo()`?和?`bar()`?的詞法作用域間建立一座橋,使得`bar()`?可以訪問?`foo()`內部作用域的變量?`a`。這樣的橋是不可能的。?你不能使用?`this`?引用在詞法作用域中查找東西。這是不可能的。
每當你感覺自己正在試圖使用?`this`?來進行詞法作用域的查詢時,提醒你自己:*這里沒有橋*。
## 什么是?`this`?
我們已經列舉了各種不正確的臆想,現在讓我們把注意力轉移到?`this`?機制是如何真正工作的。
我們早先說過,`this`?不是編寫時綁定,而是運行時綁定。它依賴于函數調用的上下文條件。`this`?綁定與函數聲明的位置沒有任何關系,而與函數被調用的方式緊密相連。
當一個函數被調用時,會建立一個稱為執行環境的活動記錄。這個記錄包含函數是從何處(調用棧 —— call-stack)被調用的,函數是?*如何*?被調用的,被傳遞了什么參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的?`this`?引用。
下一章中,我們將會學習尋找函數的?調用點(call-site)?來判定它的執行如何綁定?`this`。
## 復習
對于那些沒有花時間學習?`this`?綁定機制如何工作的 JavaScript 開發者來說,`this`?綁定一直是困惑的根源。對于?`this`?這么重要的機制來說,猜測、試錯、或者盲目地從 Stack Overflow 的回答中復制粘貼,都不是有效或正確利用它的方法。
為了學習?`this`,你必須首先學習?`this`*不是*?什么,不論是哪種把你誤導至何處的臆測或誤解。`this`?既不是函數自身的引用,也不是函數?*詞法*?作用域的引用。
`this`?實際上是在函數被調用時建立的一個綁定,它指向?*什么*?是完全由函數被調用的調用點來決定的。