在大多數場景中,隨著函數的執行,就會產生一個 this 值,這個 this 儲存著呼叫該函數的物件的值。因此我們可以結論先行,在 JavaScript 中,this 指向的永遠是函數的呼叫者。
雖然結論只有簡簡單單的一句話,但是卻可以對映出多種場景,接下來就詳細分析不同場景中的 this 指向問題。
this 指向全域性物件
當函數沒有所屬物件而直接呼叫時,this 指向的是全域性物件,來看下面這段程式碼:
var value = 10;
var obj = {
value: 100,
method: function () {
var foo = function () {
console.log(this.value); // 10
console.log(this); // Window物件
};
foo();
return this.value;
}
};
obj.method();
首先我們定義一個全域性的 value 屬性為 10,相當於 window.value = 10。
然後定義一個 obj 物件,設定了 value 屬性值為 100,然後設定 method 屬性為一個函數,其 method 屬性中定義了一個函數表示式 foo,並執行了 foo() 函數,最終返回 value 屬性。
當我們呼叫 obj.method() 函數時,foo() 函數被執行,但是此時 foo() 函數的執行是沒有所屬物件的,因此 this 會指向全域性的 window 物件,在輸出 this.value 時,實際是輸出 window.value,因此輸出“10”。
而 method() 函數的返回值是 this.value,method() 函數的呼叫體是 obj 物件,此時 this 就指向 obj 物件,而 obj.value = 100,因此呼叫 obj.method() 函數後會返回“100”。
this 指向所屬物件
同樣沿用場景 1 中的程式碼,我們修改最後一行程式碼,輸出 obj.method() 函數的返回值。
console.log(obj.method()); // 100
obj.method() 函數的返回值是 this.value,method() 函數的呼叫體是 obj 物件,此時 this 就指向 obj 物件,而 obj.value = 100,因此會輸出“100”。
this 指向物件範例
當通過 new 操作符呼叫建構函式生成物件的範例時,this 指向該範例。
// 全域性變數
var number = 10;
function Person() {
// 複寫全域性變數
number = 20;
// 範例變數
this.number = 30;
}
// 原型函數
Person.prototype.getNumber = function () {
return this.number;
};
// 通過new操作符獲取物件的範例
var p = new Person();
console.log(p.getNumber()); // 30
在上面這段程式碼中,我們定義了全域性變數 number 和範例變數 number,通過 new 操作符生成 Person 物件的範例 p 後,在呼叫 getNumber() 操作時,其中的 this 就指向該範例 p,而範例 p 在初始化的時候被賦予 number 值為 30,因此最後會輸出“30”。
this 指向 call() 函數、apply() 函數、bind() 函數呼叫後重新系結的物件
我們都知道,通過 call() 函數、apply() 函數、bind() 函數可以改變函數執行的主體,如果函數中存在 this 關鍵字,則 this 也將會指向 call() 函數、apply() 函數、bind() 函數處理後的物件。
// 全域性變數
var value = 10;
var obj = {
value: 20
};
// 全域性函數
var method = function () {
console.log(this.value);
};
method(); // 10
method.call(obj); // 20
method.apply(obj); // 20
var newMethod = method.bind(obj);
newMethod(); // 20
在上面這段程式碼中,我們定義了全域性變數 value 和帶有 value 屬性的 obj 物件,即 window.value = 10、obj.value = 20,同時定義了一個全域性 method 匿名函數表示式,相當於 window.method = function(){}。
在直接呼叫 method() 函數時,沒有所屬的物件,method() 函數中的 this 指向的是全域性 window 物件,輸出 window.value 值,因此輸出“10”。
而在呼叫 method.call(obj) 時,將 method() 函數呼叫的主體改為 obj 物件,此時 this 指向的是 obj 物件,輸出 obj.value 值,因此輸出“20”。
apply() 函數和 bind() 函數都會產生同樣的效果,將函數指向的實體改為 obj 物件,因此後兩個輸出值也為“20”。
使用 call() 函數、apply() 函數、bind() 函數都會改變 this 的指向,但是在使用上還是有些許差異。
通過上面的程式碼也可以看出,call() 函數、apply() 函數在改變函數的執行主體後,會立即呼叫該函數;而 bind() 函數在改變函數的執行主體後,並沒有立即呼叫,而是可以在任何時候呼叫,上述範例中是通過手動執行 newMethod() 函數來進行呼叫的。
在處理 DOM 事件處理程式中的 this 時,call() 函數、apply() 函數、bind() 函數顯得尤為有用,我們以 bind() 函數為例進行說明。
var user = {
data: [
{name: "kingx1", age: 11},
{name: "kingx2", age: 12}
],
clickHandler: function (event) {
// 隨機生成整數0或1
var randomNum = ((Math.random() * 2 | 0) + 1) - 1;
// 從data陣列裡隨機獲取name屬性和age屬性,並輸出
console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
}
};
var button = document.getElementById('btn');
button.onclick = user.clickHandler;
我們建立一個包含 clickHandler() 函數的簡單物件 user,當頁面上的按鈕被單擊時可以使用。
在 clickHandler() 函數中,會隨機輸出 user 的 data 屬性中物件的 name 屬性和 age 屬性。
但是當我們單擊 button 按鈕時,卻會丟擲異常。
Uncaught TypeError: Cannot read property '1' of undefined
這是為什麼呢?
我們來看下異常資訊棧便可以很好理解產生這種情況的原因。我們呼叫了一個 undefined 物件屬性名為1的值,就是程式碼中 this.data[1] 的部分,間接可以表示出 data 為 undefined。
這是因為當我們單擊 button 按鈕,觸發 click 回撥函數時,clickHandler() 函數中的 this 指向的是 button 物件,而不是 user 物件,而 button 物件中是沒有 data 屬性的,因此 data 為 undefined,從而丟擲異常。
為了解決這個問題,我們需要將 click 回撥函數中的 this 指向改變為 user 物件,而通過 bind() 函數可以達到這個目的。
button.onclick = user.clickHandler.bind(user);
修改完成後,再次單擊 button 按鈕,控制檯會輸出對應的結果:
kingx2 43
kingx1 37
閉包中的 this
函數的 this 變數只能被自身存取,其內部函數無法存取。因此在遇到閉包時,閉包內部的 this 關鍵字無法存取到外部函數的 this 變數。
通過以下範例,我們來看看具體表現:
var user = {
sport: 'basketball',
data: [
{name: "kingx1", age: 11},
{name: "kingx2", age: 12}
],
clickHandler: function () {
// 此時的this指向的是user物件
this.data.forEach(function (person) {
console.log(this); // [object Window]
console.log(person.name + ' is playing ' + this.sport);
})
}
};
user.clickHandler();
在呼叫 user.clickHandler() 函數時,會執行到第 9 行程式碼,此時的 this 會指向 user 物件,因此可以存取到 data 屬性,並進行 forEach 迴圈。forEach 迴圈實際是一個匿名函數,用於接收一個 person 引數,表示每次遍歷的陣列中的值。
在執行到第 10 行程式碼時,輸出了 this,此時在一個匿名函數中輸出 this 時,它會指向全域性物件 window。
在執行到第 11 行程式碼時,輸出了 person.name 屬性和 this.sport 屬性,person 指向的是 data 數值中的物件,而 this 指向的依然是全域性物件 window,在 window 物件中沒有 sport 屬性,即為 undefined。
因此會輸出“undefined”:
kingx1 is playing undefined
kingx2 is playing undefined
如果我們希望 forEach 迴圈結果輸出的 sport 值為“basketball”,應該怎麼做呢?
可以使用臨時變數將 clickHandler() 函數的 this 提前進行儲存,對其使用 user 物件,而在匿名函數中,使用臨時變數存取 sport 屬性,而不是直接用 this 存取。
var user = {
sport: 'basketball',
data: [
{name: "kingx1", age: 11},
{name: "kingx2", age: 12}
],
clickHandler: function () {
// 使用臨時變數_this儲存this
var _this = this;
this.data.forEach(function (person) {
// 通過_this存取sport屬性
console.log(person.name + ' is playing ' + _this.sport);
})
}
};
user.clickHandler();
修改後輸出的結果如下所示:
kingx1 is playing basketball
kingx2 is playing basketball
接下來我們通過一道題加深對 this 的理解。
function f(k) {
this.m = k;
return this;
}
var m = f(1);
var n = f(2);
console.log(m.m);
console.log(n.m);
上面這道題的程式碼雖然短小,在理解的時候卻不是那麼容易,我們一步步來分析:
在執行 f(1) 的時候,因為 f() 函數的呼叫沒有所屬物件,所以 this 指向 window,然後 this.m=k 語句執行後,相當於 window.m = 1。
通過 return 語句返回“window”,而又將返回值“window”賦值給全域性變數 m,因此變成了 window.m = window,覆蓋前面的 window.m = 1。
在執行 f(2) 的時候,this 同樣指向 window,此時 window.m 已經變成 2,即 window.m = 2,覆蓋了 window.m = window。通過 return 語句將 window 物件返回並賦值給 n,此時 window.n=window。
先看 m.m 的輸出,m.m=(window.m).m,實際為 2.m,2 是一個數值型常數,並不存在 m 屬性,因此返回“undefined”。再看 n.m 的輸出,n.m=(window.n).m=window.m=2,因此輸出“2”。