當將物件方法傳遞為回呼函式時,例如傳遞給 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…)