在 JavaScript 中找出 this
的值可能會很棘手,以下是如何操作:
JavaScript 的 this
經常成為笑柄,原因在於它實在太複雜。不過,我曾見過開發人員為了避免處理這個 this
,而採取更複雜的做法,如果您不確定 this
的用途,希望以下說明對您有所幫助。這是我的 this
指南。
我會從最具體的情況開始,然後結束最不具體的情況。本文有點像是大型 if (…) … else if () … else if (…) …
,您可以直接前往與所查看程式碼相符的第一個部分。
- 如果函式定義為箭頭函式
- 如果函式/類別是使用
new
呼叫 - 否則,如果函式具有「已繫結」
this
值 - 否則,如果
this
是在呼叫時設定 - 如果函式是透過父項物件 (
parent.func()
) 呼叫 - 否則,如果函式或父項範圍處於嚴格模式
- 其他情況
如果函式定義為箭頭函式:
const arrowFunction = () => {
console.log(this);
};
在這種情況下,this
的值「一律」會與父範圍中的 this
相同:
const outerThis = this;
const arrowFunction = () => {
// Always logs `true`:
console.log(this === outerThis);
};
箭頭函式非常實用,因為 this
的內部值無法變更,總是與外部 this
相同。
其他範例
使用箭頭函式時,無法使用 bind
變更 this
的值:
// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();
使用箭頭函式時,this
的值無法使用 call
或 apply
變更:
// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});
使用箭頭函式時,如果以其他物件的成員身分呼叫函式,this
的值就無法變更:
const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();
使用箭頭函式時,您可以呼叫函式做為建構函式,但無法變更 this
的值:
// TypeError: arrowFunction is not a constructor
new arrowFunction();
「Bound」例項方法
使用例項方法時,如果您想確保 this
始終參照類別例項,最佳做法是使用箭頭函式和類別欄位:
class Whatever {
someMethod = () => {
// Always the instance of Whatever:
console.log(this);
};
}
在元件 (例如 React 元件或網頁元件) 中使用例項方法做為事件監聽器時,這個模式非常實用。
上述做法可能會違反「this
會與父級範圍中的 this
相同」規則,但如果您將類別欄位視為在建構函式中設定內容的語法糖衣,就會開始覺得有道理:
class Whatever {
someMethod = (() => {
const outerThis = this;
return () => {
// Always logs `true`:
console.log(this === outerThis);
};
})();
}
// …is roughly equivalent to:
class Whatever {
constructor() {
const outerThis = this;
this.someMethod = () => {
// Always logs `true`:
console.log(this === outerThis);
};
}
}
其他模式包括在建構函式中繫結現有函式,或在建構函式中指派函式。如果您因為某些原因無法使用類別欄位,在建構函式中指派函式是合理的替代做法:
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
否則,如果函式/類別是使用 new
呼叫,則:
new Whatever();
上述程式碼會呼叫 Whatever
(或其建構函式,如果是類別的話),並將 this
設為 Object.create(Whatever.prototype)
的結果。
class MyClass {
constructor() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
}
// Logs `true`:
new MyClass();
舊版建構函式也是如此:
function MyClass() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
// Logs `true`:
new MyClass();
其他範例
使用 new
呼叫時,this
的值無法使用 bind
變更:
const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();
使用 new
呼叫時,如果以其他物件的成員身分呼叫函式,this
的值就無法變更:
const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();
否則,如果函式含有「已繫結」this
值:
function someFunction() {
return this;
}
const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);
每次呼叫 boundFunction
時,其 this
值都會是傳遞至 bind
(boundObject
) 的物件。
// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);
其他範例
呼叫已繫結的函式時,this
的值無法使用 call
或 apply
變更:
// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
呼叫已繫結的函式時,如果以其他物件的成員身分呼叫函式,this
的值就無法變更:
const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);
否則,如果 this
是在呼叫時設定:
function someFunction() {
return this;
}
const someObject = {hello: 'world'};
// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);
this
的值是傳遞至 call
/apply
的物件。
不幸的是,this
會由 DOM 事件監聽器等項目設為其他值,因此使用 this
可能會導致難以理解的程式碼:
element.addEventListener('click', function (event) { // Logs `element`, since the DOM spec sets `this` to // the element the handler is attached to. console.log(this); });
在上述情況下,我會避免使用 this
,而是改為:
element.addEventListener('click', (event) => { // Ideally, grab it from a parent scope: console.log(element); // But if you can't do that, get it from the event object: console.log(event.currentTarget); });
否則,如果函式是透過父項物件 (parent.func()
) 呼叫,則:
const obj = {
someMethod() {
return this;
},
};
// Logs `true`:
console.log(obj.someMethod() === obj);
在這種情況下,函式會以 obj
的成員身分呼叫,因此 this
會是 obj
。這會在呼叫時發生,因此如果函式在沒有父項物件或使用不同父項物件時呼叫,連結就會中斷:
const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);
const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);
someMethod() === obj
為 false,因為 someMethod
並未以 obj
成員身分呼叫。您可能在嘗試以下操作時遇到這個問題:
const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');
這是因為 querySelector
的實作會查看自己的 this
值,並預期該值為某種 DOM 節點,而上述做法會中斷該連線。如要正確達成上述目標,請按照下列步驟操作:
const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);
有趣的事實:並非所有 API 在內部都使用 this
。console.log
等控制台方法已變更,以避免 this
參照,因此 log
不必繫結至 console
。
否則,如果函式或父項範圍處於嚴格模式,則:
function someFunction() {
'use strict';
return this;
}
// Logs `true`:
console.log(someFunction() === undefined);
在這種情況下,this
的值為未定義。如果父範圍處於嚴格模式 (且所有模組都處於嚴格模式),則函式中不需要 'use strict'
。
如果是其他情況:
function someFunction() {
return this;
}
// Logs `true`:
console.log(someFunction() === globalThis);
在這種情況下,this
的值與 globalThis
相同。
呼!
大功告成!以上就是我對 this
的瞭解。有疑問我漏掉什麼了嗎?歡迎在 Twitter 上向我發文。
感謝 Mathias Bynens、Ingvar Stepanyan 和 Thomas Steiner 審查。