原文:你不知道的js系列
JavaScript 的 this 機制並沒有那么復雜
為什么會有 this?
在如何使用 this 之前,我們要搞清楚一個問題,為什么要使用 this。
下面的代碼嘗試去說明 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() 可以在多個上下文(me 和 you)對象中重用,不用給每個對象分別創建函數。
如果不用 this,你也可以將上下文對象直接傳入函數:
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 的字面意思。
引用自身 Itself
一種普遍的錯誤是認為 this 指代這個函數自身。
為什么你會想從一個函數內部引用它自己呢,通常的原因是遞歸,或者事件回調函數在被調用之后解除綁定。
JS 新手會認為將函數作為對象引用可以在函數調用期間存儲狀態(屬性的值)。這確實是可以的但是用處有限,后面會介紹其它模式,除了函數對象本身還有更好的存儲狀態的地方。
下面的代碼會說明,this並不會像我們以為的那樣讓函數得到對自身的引用:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called 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 // how many times was `foo` called? console.log( foo.count ); // 0 -- WTF?
foo.count 還是 0 ,循環確實執行了 4 次,console.log 也確實被調用了 4 次。
foo.count = 0 執行之后,實際上給函數對象 foo 添加了一個屬性 count。
但是在函數內部的 this.count 中,this 實際上並不指向這個函數對象,即使這個屬性名字是一樣的,但屬性所在的對象是不同的。
如果 foo 的屬性 count 的值沒有改變,那么我們改變的究竟是什么。實際上,如果你再深究一下,就會發現,這段代碼意外地創建了一個全局變量 count,而且當時會有一個值 NaN(具體看這個系列的第二節)。
很多開發者就會通過別的方式避免這個問題,比如創建另外一個對象儲存這個屬性 count:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called 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 // how many times was `foo` called? console.log( data.count ); // 4
這確實解決了問題,但是很遺憾這忽略了真正的問題——不理解 this 的含義和用法,只是回到熟悉的詞法作用域機制。
如果想在一個函數對象內部引用自身,this 是不夠的,你需要一個標識符:
function foo() { foo.count = 4; // `foo` refers to itself } setTimeout( function(){ // anonymous function (no name), cannot // refer to itself }, 10 );
在第一個函數中,函數被命名為 foo,這個標識符 foo 就可以用來指代這個函數對象自身。
但在第二段中,回調函數沒有名字,所以沒辦法引用自己。
注:老派的已經被廢棄的 arguments.callee 在函數中可以用來指代正在執行的函數對象。這是在匿名函數內部訪問函數對象的唯一方式。
當然最好的方式還是避免匿名函數的使用。
另外一種解決辦法就是使用 foo 標識符,不使用 this:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called 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 // how many times was `foo` called? console.log( foo.count ); // 4
然而這種方法同樣回避了對 this 的理解。
另外一種解決這個問題的方式是,將 this 強制綁定到 foo 這個函數對象上:
function foo(num) { console.log( "foo: " + num ); // keep track of how many times `foo` is called // Note: `this` IS actually `foo` now, based on // how `foo` is called (see below) this.count++; } foo.count = 0; var i; for (i=0; i<10; i++) { if (i > 5) { // using `call(..)`, we ensure the `this` // points at the function object (`foo`) itself foo.call( foo, i ); } } // foo: 6 // foo: 7 // foo: 8 // foo: 9 // how many times was `foo` called? console.log( foo.count ); // 4
作用域的引用 Its Scope
第二個常見的關於 this 的錯誤理解是,this 指向這個函數的作用域。這是一個有點狡猾的問題,因為在某種意義上這種說法是有些正確的,但在另一種意義上,這又是被誤導的。
首先,this 並沒有指向函數的詞法作用域。作用域確實就像是一個包含所有標識符屬性的對象,但是這個作用域 “對象” 是無法被代碼直接訪問的,這是引擎內部實現的。
所以下面的代碼是錯誤的:
function foo() { var a = 2; this.bar(); } function bar() { console.log( this.a ); } foo(); //undefined
你可能覺得這段代碼很做作,但這是摘自一些幫助論壇里的真實代碼。
首先,這段代碼試圖通過 this.bar() 引用函數 bar(),能運行起來也是巧合。調用 bar() 最自然的方式就是直接使用標識符引用,去掉前面的 this。
然而,寫這段代碼的開發者其實是想讓 bar() 訪問 foo() 內部的變量 a,但 this 不能被用來查詢詞法作用域的。
this 到底是什么
前面講到過,this 是在運行時綁定的,它的上下文環境取決於函數調用的條件。this 的綁定和函數聲明的位置沒有關系,和函數調用的位置有關。
當一個函數被調用時,一個執行上下文被創建。這個上下文記錄包含函數調用的位置,函數調用的方式以及傳入的參數這些信息。this 的引用就是在這個時候決定的。
在下一節中,會介紹根據一個函數的調用位置確定它執行過程中將如何綁定this。
小結:
- this 既不指代函數本身,也不指代函數的詞法作用域。
- this 是在函數調用的時候綁定的,它引用的內容完全取決於函數調用的位置。
