原文連結:A guide to this in JavaScript
原文作者:Ashay Mandwarya
譯者:JintNiu
推薦理由:this
一直是JavaScript
中的重難點,藉助這篇文章,重新認識並理解this
,加深印象。
this
無疑是 JavaScript
中使用最廣泛但又容易被誤解的關鍵字,今天我將會對其進行詳細的解釋。
當我們在學校學習英語代詞時:
Phelps is swimming fast because he wants to win the race.
這句話中,我們不直接使用 Phelps,而是使用代詞“he”來指代他。類似地,JavaScript
中使用 this
關鍵字指向引用上下文中的物件。
例:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
console.log(car.make + " " + car.model);
}
}
car.fullName();
// Lamborghini Huracán
// Lamborghini Huracán
複製程式碼
在上面的程式碼中,我們定義了一個具有屬性 make
,model
和 fullName
的物件 car
,其中 fullName
是一個函式,函式體內使用 2 種不同的方法輸出 make
和 model
。
- 使用
this
時,this.make+ " " +this.model
中的this
指的是在上下文中的物件,也就是car
,則this.make
為car.make
,this.model
為car.model
; - 使用點操作符時,我們可以直接訪問物件的屬性
car.make
和car.model
。
this
現在我們已經瞭解了什麼是 this
以及它 最基本的用法,為方便記憶,我們將列出一些場景,並分別舉例說明。
根據出現的位置,this
可分為以下幾種情況:
- 在方法內使用
- 在函式內使用
- 單獨存在
- 在事件中使用
call()
和apply()
1. 在方法內使用 this
當 this
在方法內使用時,指向其所屬的物件。
在物件內定義的函式稱為方法。再來看看汽車的例子:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
console.log(car.make + " " + car.model);
}
}
car.fullName();
複製程式碼
該例中,fullName()
即為方法,方法中的 this
指向物件 car
。
2. 在函式內使用 this
函式中的 this
就有些複雜了。與物件一樣,函式也具有屬性。函式每次執行時都會獲取 this
,它指向呼叫它的物件。
this
實際上只是“先行物件”的一種快捷引用,也就是物件呼叫。
— javascriptissexy.com
如果函式未被某物件呼叫,則函式內的 this
屬於全域性物件,該全域性物件被稱為 window
。在這種情況下,this
將指向全域性作用域中的變數。且看以下例子:
var make = "Mclaren";
var model = "720s"
function fullName() {
console.log(this.make + " " + this.model);
}
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
}
}
car.fullName(); // Lmborghini Huracán
window.fullName(); // Mclaren 720S
fullName(); // Mclaren 720S
複製程式碼
該例中,在全域性物件中定義了 make
, model
和 fullName
,物件 car
中也實現了 fullName
方法。當使用 car
呼叫該方法時,this
指向該物件內的變數;而使用另外兩種呼叫方式時,this
指向全域性變數。
3. 單獨使用 this
當單獨使用 this
,不依附於任何函式或者物件時,指向全域性物件。
這裡的 this
指向全域性變數 name
。
4. 在事件內使用 this
JS
中有很多種事件型別,但為了描述簡單,這裡我們以點選事件為例。
每當單擊按鈕並觸發一個事件時,可以呼叫另一個函式來去執行某個任務。如果在函式內使用 this
,則指向觸發事件中的元素。DOM 中,所有元素都以物件的形式儲存,也就是說網頁元素實際上就是 DOM 中的一個物件,因此每觸發一個事件時,this
就會指向該元素。
例:
<button onclick="this.style.display='none'">
Remove Me!
</button>
複製程式碼
5. call(), apply() & bind()
bind
:允許我們在方法中設定this
指向call
&apply
:允許我們藉助其他函式並在函式呼叫中改變this
的指向。
有關 call()
, apply()
和 bind()
的知識會在另一篇文章闡述。
難點
理解掌握了 this
會使工作變輕鬆很多,但實際情況往往不是那麼如意。請看以下例子。
例1:
var car = {
make: "Lamborghini",
model: "Huracán",
name: null,
fullName: function () {
this.name = this.make + " " + this.model;
console.log(this.name);
}
}
var anotherCar = {
make: "Ferrari",
model: "Italia",
name: null
}
anotherCar.name = car.fullName();
// Lamborghini Huracán
複製程式碼
結果並不是我們所期望的。分析其原因:當我們使用 this
呼叫另一個物件的方法時,只是為 anotherCar
分配了該方法,但實際呼叫者是 car
。因此返回的是 Lamborghini 而不是 Ferrari。
我們可以使用 call()
解決這個問題。
該例中利用 call()
方法使 anotherCar
物件呼叫 fullName()
,該物件中原本並沒有 fullName()
方法,但輸出了 Ferrari Italia
。
另外,當我們輸出 car.name
和 anotherCar.name
的值時,前者輸出 null
,而後者輸出了 Ferrari Italia
,也就是說 fullName()
函式確實被 anotherCar
呼叫了,而不是被 car
呼叫。
例2:
var cars = [
{ make: "Mclaren", model: "720s" },
{ make: "Ferrari", model: "Italia" }
]
var car = {
cars: [{ make: "Lamborghini", model: "Huracán" }],
fullName: function () {
console.log(this.cars[0].make + " " + this.cars[0].model);
}
}
var vehicle = car.fullName;
vehicle() // Mclaren 720s
複製程式碼
該例中,我們定義了一個全域性變數 cars
,並且在物件 car
中也定義了同名變數,接著將 fullName()
方法賦給變數 vehicle
,然後呼叫它。該變數屬於全域性變數,由於上下文的關係,this
指向的是全域性變數 cars
而不是區域性變數。
我們可以使用 bind()
解決這個問題。
bind
改變了this
的指向,使變數 vehicle
指向區域性變數 car
。也就是說,this
的指向取決於 car
的上下文環境。
例3:
var car = {
cars: [
{ make: "Lamborghini", model: "Huracán" },
{ make: "Mclaren", model: "720s" },
{ make: "Ferrari", model: "Italia" }
],
brand:"lamborghini",
fullName: function () {
this.cars.forEach(function(car){
console.log(car.model + " " + this.brand);
})
}
}
car.fullName();
// Huracán undefined
// 720s undefined
// Italia undefined
複製程式碼
在以上程式碼中,fullName()
使用 forEach
迭代陣列 cars
,每次迭代都產生一個沒有上下文的匿名函式,這類定義在函式內部的函式,稱之為閉包(closure
)。閉包在 JavaScript
中非常重要,而且被廣泛使用。
另一個重要的概念是作用域(scope
)。定義在函式內部的變數不能訪問其作用域以外的變數和屬性;匿名函式中的 this
不能訪問外部作用域,以至於 this
只能指向全域性物件。該例中,全域性物件中沒有定義 this
所要訪問的屬性 brand
,因此輸出 undefined
。
以上問題的解決方法是:我們可以在匿名函式外為 this
賦值,然後在函式內使用。
將 this
賦給變數 self
,並代替函式體內的 this
,輸出期望結果。
例4:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function (cars) {
cars.forEach(function (vehicle) {
console.log(vehicle + " " + this.model);
})
}
}
car.fullName(['lambo', 'ferrari', 'porsche']);
// lambo undefined
// ferrari undefined
// porsche undefined
複製程式碼
當無法使用 this
進行訪問時,可以使用變數 self
來儲存它(如例 3 ),但在該例中,也可以使用箭頭函式來解決:
可以看出,在 forEach()
中使用箭頭函式就可以解決該問題,而不是進行繫結或暫存 this
。這是由於箭頭函式繫結了上下文,this
實際上指向原始上下文或原始物件。
例5:
var car = {
make: "Lamborghini",
model: "Huracán",
fullName: function () {
console.log(this.make + " " + this.model);
}
}
var truck = {
make: "Tesla",
model: "Truck",
fullName: function (callback) {
console.log(this.make + " " + this.model);
callback();
}
}
truck.fullName(car.fullName);
// Tesla Truck
// undefined undefined
複製程式碼
上述程式碼中定義了兩個相同的物件,但其中一個包含回撥函式,回撥函式將作為引數傳入另一個函式,然後通過外部函式呼叫來完成某種操作。
該程式碼中物件 truck
的 fullName
方法包含一個回撥函式,並在方法中直接進行呼叫。當將 car.fullName
作為引數呼叫 truck.fullName()
時,輸出 Tesla Truck
和 undefined undefined
。
結果出乎意料。實際上,car.fullName
只是作為引數傳入,而不是由 truck
物件呼叫。換句話說,回撥函式呼叫了物件 car
的方法,但卻把 this
繫結到全域性作用域上,如下圖:
為便於觀察,我們輸出了 this
。可以看到回撥函式中的 this
指向了全域性作用域。繼續建立全域性變數 make
和 model
如下例:
顯而易見,回撥函式中的輸出了全域性變數 make
和 model
,再次證明了 this
指向全域性物件。
為得到期望結果,我們將使用 bind()
將 car
強制繫結到回撥函式中。如下:
完成!
毫無疑問,this
是非常有用的,但不容易理解。希望通過這篇文章你可以逐漸瞭解它的使用方法。
如果這篇文章對您有所幫助,點個贊?,加個關注? 吧~