當將物件方法傳遞為回呼函式時,例如傳遞給 setTimeout,有一個已知問題:「失去 this」。
在本章中,我們將了解解決此問題的方法。
失去「this」
我們已經看過失去 this 的範例。一旦方法從物件中分開傳遞到某個地方,this 就會遺失。
以下是可能發生在 setTimeout 的情況
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
正如我們所見,輸出顯示的不是 this.firstName 中的「John」,而是 undefined!
這是因為 setTimeout 取得了函式 user.sayHi,而這個函式是從物件中分開取得的。最後一行可以改寫成
let f = user.sayHi;
setTimeout(f, 1000); // lost user context
瀏覽器中的 setTimeout 方法有點特別:它會為函式呼叫設定 this=window(對於 Node.js,this 會變成計時器物件,但這在這裡並不重要)。因此,對於 this.firstName,它會嘗試取得 window.firstName,但這個值並不存在。在其他類似的案例中,this 通常只會變成 undefined。
這個任務相當典型,我們想要將物件方法傳遞到其他地方(這裡是排程器),並在那裡呼叫它。如何確保它會在正確的內容中被呼叫?
解決方案 1:包裝器
最簡單的解決方案是使用包裝函式
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
現在它可以運作,因為它從外部詞彙環境中接收 user,然後正常呼叫方法。
相同,但更簡短
setTimeout(() => user.sayHi(), 1000); // Hello, John!
看起來很好,但我們的程式碼結構中出現了一個小漏洞。
如果在 setTimeout 觸發之前(有一秒的延遲!)user 的值發生了變化,會怎樣?那麼,它會突然呼叫錯誤的物件!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...the value of user changes within 1 second
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Another user in setTimeout!
下一個解決方案可以保證這種情況不會發生。
解決方案 2:繫結
函式提供了一個內建方法 bind,允許修正 this。
基本語法是
// more complex syntax will come a little later
let boundFunc = func.bind(context);
func.bind(context) 的結果是一個特殊的類函式「異國物件」,它可以像函式一樣被呼叫,並將呼叫透明地傳遞給 func,設定 this=context。
換句話說,呼叫 boundFunc 就如同具有固定 this 的 func。
例如,這裡的 funcUser 會傳遞一個呼叫給 func,其中 this=user
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
這裡 func.bind(user) 作為 func 的「綁定變異」,固定 this=user。
所有參數都「原樣」傳遞給原始 func,例如
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// bind this to user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user)
現在讓我們使用物件方法試試看
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// can run it without an object
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// even if the value of user changes within 1 second
// sayHi uses the pre-bound value which is reference to the old user object
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
在 (*) 行中,我們取得 user.sayHi 方法並將其綁定到 user。sayHi 是「綁定」函式,可以單獨呼叫或傳遞給 setTimeout,沒關係,內容會是正確的。
這裡我們可以看到參數「原樣」傳遞,只有 this 由 bind 固定
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John! ("Hello" argument is passed to say)
say("Bye"); // Bye, John! ("Bye" is passed to say)
bindAll如果一個物件有許多方法,而且我們計畫積極傳遞它,那麼我們可以在迴圈中將它們全部綁定
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
JavaScript 函式庫也提供用於方便大量綁定的函式,例如 _.bindAll(object, methodNames) 在 lodash 中。
部分函式
到目前為止,我們只討論過綁定 this。讓我們更進一步。
我們不僅可以綁定 this,還可以綁定參數。這很少見,但有時會很方便。
bind 的完整語法
let bound = func.bind(context, [arg1], [arg2], ...);
它允許將內容綁定為 this,並作為函式的起始參數。
例如,我們有一個乘法函式 mul(a, b)
function mul(a, b) {
return a * b;
}
讓我們使用 bind 在其基礎上建立一個函式 double
function mul(a, b) {
return a * b;
}
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
呼叫 mul.bind(null, 2) 會建立一個新函式 double,它會傳遞呼叫給 mul,並將 null 固定為內容,2 固定為第一個參數。其他參數「原樣」傳遞。
這稱為 部分函式應用,我們透過固定現有函式的某些參數來建立一個新函式。
請注意,我們實際上沒有在這裡使用 this。但 bind 需要它,所以我們必須輸入一些東西,例如 null。
以下程式碼中的 triple 函式將值加倍
function mul(a, b) {
return a * b;
}
let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
我們通常為什麼要建立部分函式?
好處是我們可以建立一個具有可讀名稱(double、triple)的獨立函式。我們可以使用它,而不必每次都提供第一個參數,因為它已透過 bind 固定。
在其他情況下,當我們有一個非常通用的函式,並希望有一個較不通用的變異以方便使用時,部分應用會很有用。
例如,我們有一個函式 send(from, to, text)。然後,在 user 物件中,我們可能希望使用它的部分變異:sendTo(to, text),它從目前的使用者傳送。
在沒有內容的情況下進行部分應用
如果我們想要修正一些參數,但不想修正內容 this 呢?例如,對於物件方法。
原生 bind 不允許這樣做。我們不能只省略內容並跳到參數。
幸運的是,一個只用於繫結參數的函式 partial 可以輕鬆實作。
像這樣
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// Usage:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// add a partial method with fixed time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!
呼叫 partial(func[, arg1, arg2...]) 的結果是一個包裝器 (*),它會呼叫 func,並帶有
- 與它取得相同的
this(對於user.sayNow呼叫,它是user) - 然後給它
...argsBound– 來自partial呼叫的參數("10:00") - 然後給它
...args– 傳遞給包裝器的參數("Hello")
使用擴充語法這麼做很容易,對吧?
另外,還有一個來自 lodash 函式庫的現成實作 _.partial。
摘要
方法 func.bind(context, ...args) 會傳回函式 func 的「繫結變體」,它會修正內容 this 和第一個參數(如果已提供)。
我們通常會套用 bind 來修正物件方法的 this,以便我們可以將它傳遞到某個地方。例如,傳遞到 setTimeout。
當我們修正現有函式的某些參數時,產生的(較不通用)函式稱為部分套用或部分。
當我們不想重複使用相同的參數時,部分會很方便。例如,如果我們有一個 send(from, to) 函式,而 from 對於我們的任務應該總是相同的,我們可以取得一個部分並繼續使用它。
留言
<code>標籤,要插入多行程式碼,請將它們包在<pre>標籤中,要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)