由ES規范學JavaScript(三):深入理解this
一. this的來源
this 是JavaScript的關鍵字,它最初應該是從Java、C++等面向對象的語言中借鑒來的。
比如,在Java中沒有函數只有方法,this只能用在類的成員方法或構造方法中,表示當前實例對象。所以在Java中this的含義很明確,在其他語言中也類似。
然而到了JavaScript中,this變得復雜了起來:不僅函數內可以用,在所有函數外(全局上下文中)也可以用;函數中的this的含義在函數聲明時無法確定,要到運行期才能確定,而且與調用函數的方式有關;代碼是否是嚴格模式也會影響this的取值。
二. this到底是什么
在ES5.1中,有一個所謂 執行上下文(Execution Context,EC) 的概念,簡單的說就是JS引擎的執行進入到某塊代碼區域時,為該代碼區域建立的上下文對象,主要用來記錄該區域中聲明的變量、函數等。
EC有三個重要組成部分: VE 、 LE 和 ThisBinding 。前兩個是詞法環境,暫且不管。第三個 ThisBinding 就是指在該代碼區域的this的值。
可見,this是跟某塊代碼區域關聯的。而在JS中,代碼區域有三種:
-
global代碼
-
function代碼
-
eval代碼。
此文中主要討論前兩種。
三. 全局代碼區域中的this
全局是所有函數之外的代碼區域。在此區域中的this就是指全局對象 window (在Node.js中是 global )。
參考: http://es5.github.io/#x10.4.1.1
四. 函數代碼區域中的this
函數代碼區域是指某個函數內的代碼,但是不包括它所嵌套的函數內的代碼。從我們可以看出:
this是與包裹它的且離它最近的函數相關的,this既不能穿透到外部的函數,也不能穿透進內部的函數。
舉例來說:
btn.addListener('click', function() { var that = this; dosth(function() { console.log(that.name); }); });
通常每個函數中的this是不同的,內部函數可以引用外部函數的局部變量,但是不能直接引用外部函數的this。通過將外部函數的this賦值給一個局部變量可以解決這個問題。
函數內的this的具體函數比較復雜,主要與調用這個函數的方式有關。主要包括以下情況:
1. 直接調用時
示例:
fn();
直接調用函數時,如果是在嚴格模式下,this會被設為 undefined ;如果是在嚴格模式下,this會被設為全局對象 window 。
2. 作為方法調用時
示例:
var student = { name: 'Tom', saySth: function() { console. log(this.name); }; };student.saySth(); // 輸出Tom</pre>
作為方法調用時,this指方法所屬的對象。
參考: http://es5.github.io/#x10.4.3 和 http://es5.github.io/#x11.2.3
3. call和apply方法:調用時指定this
除了上述兩種固定的情況外,Javascript提供了一種可以隨心所欲地根據需要更改函數中this方法。即使用函數對象的 call 或 apply 方法來調用函數,顯然這種方式給編程帶來了極大的靈活性。
示例:
function fn() { var args = Array. prototype. slice.call(arguments, 1); console.log(args); }fn(1, 2, 3); // 輸出[2, 3]</pre>
這種方法常用的場景就是:把一個對象的方法"借"給另一個具有類似結構的對象使用。
4. bind方法:重新綁定函數的this
與call和apply不同,bind方法是在調用前就把函數內的this綁定了,而且一旦綁定就不能再改變。實際上bind方法返回了一個原函數的新版本。
示例:
function fn() { console.log(this.age); }var fn2 = fn.bind({age: 18}); fn2() // 輸出18 fn2.call({age: 25}) // 輸出18</pre>
通過bind得到的函數,不論用哪種方式調用,它的this都是相同的。
參考: http://es5.github.io/#x15.3.4.5
5. 構造函數中的this
當構造函數通過new操作符來調用時,this表示正在創建的對象。
參考: http://es5.github.io/#x11.2.2
6. 回調函數的this
回調函數也只不過是函數的一種,實際上這種情況已經包含在了前面提到的情況中。但是由于回調函數的調用者往往不是我們自己,而是回調函數的接收者,即某個庫或框架、甚至是JS運行時環境。這樣一來,回調函數在中的this是什么就與對方的調用方式有關了,因此變得比較復雜,所以單獨拿出來討論一下。
情況1:沒有明確作用對象的情況下,通常this為全局對象
例如 setTimeout 函數的回調函數,它的this就是全局對象。你如果希望自己指定this,可以通過bind函數等方法。
情況2:某個事件的監聽器回調函數,通常this就是事件源對象
例如: button.addEventListener('click', fn) 中,fn的中的this就是button對象。
情況3:某些API會專門提供一個參數,用來指定回調函數中的this
例如,我們可以重新設計一個可以指定this的setTimeout:
function setTimeoutExt(cb, period, thisArg) { setTimeout(function() { cb.call(thisArg); }, period); }另外,在ExtJS中也大量使用了可以指定this的接口。
五. 重新審視
this,除了面向對象語言中通用的那兩種情況(方法和構造函數)外,在JavaScript 中還提供了更多的使用方式,雖然這讓JS中的this變得相對難以掌握,但是它使得JS更加豐富更加靈活。我們可以把this看成函數的一個特殊的隱含的參數,這個參數代表函數正在操作的主體。
注:時間比較倉促,有些地方沒有太深入,代碼實例也比較簡單。后面繼續完善。