當將對象方法作爲回調進行傳遞,例如傳遞給 setTimeout
,這兒會存在壹個常見的問題:“丟失 this
”。
在本章中,我們會學習如何去解決這個問題。
我們已經看到了丟失 this
的例子。壹旦方法被傳遞到與對象分開的某個地方 —— this
就丟失。
下面是使用 setTimeout
時 this
是如何丟失的:
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); // 丟失了 user 上下文
浏覽器中的 setTimeout
方法有些特殊:它爲函數調用設定了 this=window
(對于 Node.js,this
則會變爲計時器(timer)對象,但在這兒並不重要)。所以對于 this.firstName
,它其實試圖獲取的是 window.firstName
,這個變量並不存在。在其他類似的情況下,通常 this
會變爲 undefined
。
這個需求很典型 —— 我們想將壹個對象方法傳遞到別的地方(這裏 —— 傳遞到調度程序),然後在該位置調用它。如何確保在正確的上下文中調用它?
最簡單的解決方案是使用壹個包裝函數:
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); // ……user 的值在不到 1 秒的時間內發生了改變 user = { sayHi() { alert("Another user in setTimeout!"); } }; // Another user in setTimeout!
下壹個解決方案保證了這樣的事情不會發生。
函數提供了壹個內建方法 bind,它可以綁定 this
。
基本的語法是:
// 稍後將會有更複雜的語法 let boundFunc = func.bind(context);
func.bind(context)
的結果是壹個特殊的類似于函數的“外來對象(exotic object)”,它可以像函數壹樣被調用,並且透明地(transparently)將調用傳遞給 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
的“綁定的(bound)變體”,綁定了 this=user
。
所有的參數(arguments)都被“原樣”傳遞給了初始的 func
,例如:
let user = { firstName: "John" }; function func(phrase) { alert(phrase + ', ' + this.firstName); } // 將 this 綁定到 user let funcUser = func.bind(user); funcUser("Hello"); // Hello, John(參數 "Hello" 被傳遞,並且 this=user)
現在我們來嘗試壹個對象方法:
let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); // (*) // 可以在沒有對象(譯注:與對象分離)的情況下運行它 sayHi(); // Hello, John! setTimeout(sayHi, 1000); // Hello, John! // 即使 user 的值在不到 1 秒內發生了改變 // sayHi 還是會使用預先綁定(pre-bound)的值,該值是對舊的 user 對象的引用 user = { sayHi() { alert("Another user in setTimeout!"); } };
在 (*)
行,我們取了方法 user.sayHi
並將其綁定到 user
。sayHi
是壹個“綁定後(bound)”的方法,它可以被單獨調用,也可以被傳遞給 setTimeout
—— 都沒關系,函數上下文都會是正確的。
這裏我們能夠看到參數(arguments)都被“原樣”傳遞了,只是 this
被 bind
綁定了:
let user = { firstName: "John", say(phrase) { alert(`${phrase}, ${this.firstName}!`); } }; let say = user.say.bind(user); say("Hello"); // Hello, John!(參數 "Hello" 被傳遞給了 say) say("Bye"); // Bye, John!(參數 "Bye" 被傳遞給了 say)
便捷方法:bindAll
如果壹個對象有很多方法,並且我們都打算將它們都傳遞出去,那麽我們可以在壹個循環中完成所有方法的綁定:
for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } }
JavaScript 庫還提供了方便批量綁定的函數,例如 lodash 中的 _.bindAll(object, methodNames)。
到現在爲止,我們只在談論綁定 this
。讓我們再深入壹步。
我們不僅可以綁定 this
,還可以綁定參數(arguments)。雖然很少這麽做,但有時它可以派上用場。
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
綁定爲第壹個參數。並且,參數(arguments)均被“原樣”傳遞。
它被稱爲 函數的部分應用(partial function application) —— 我們通過綁定先有函數的壹些參數來創建壹個新函數。
請注意,這裏我們實際上沒有用到 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
)的獨立函數。我們可以使用它,並且不必每次都提供壹個參數,因爲參數是被綁定了的。
另壹方面,當我們有壹個非常靈活的函數,並希望有壹個不那麽靈活的變型時,部分應用函數會非常有用。
例如,我們有壹個函數 send(from, to, text)
。然後,在壹個 user
對象的內部,我們可能希望對它使用 send
的部分應用函數變型:從當前 user 發送 sendTo(to, text)
。
當我們想綁定壹些參數(arguments),但是不想綁定上下文 this
,應該怎麽辦?例如,對于壹個對象方法。
原生的 bind
不允許這種情況。我們不可以省略上下文直接跳到參數(arguments)。
幸運的是,僅綁定參數(arguments)的函數 partial
比較容易實現。
像這樣:
function partial(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // 用法: let user = { firstName: "John", say(time, phrase) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // 添加壹個帶有綁定時間的 partial 方法 user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Hello"); // 類似于這樣的壹些內容: // [10:00] John: Hello!
partial(func[, arg1, arg2...])
調用的結果是壹個包裝器 (*)
,它調用 func
並具有以下內容:
與它獲得的函數具有相同的 this
(對于 user.sayNow
調用來說,它是 user
)
然後給它 ...argsBound
—— 來自于 partial
調用的參數("10:00"
)
然後給它 ...args
—— 給包裝器的參數("Hello"
)
使用 spread 可以很容易實現這些操作,對吧?
此外,還有來自 lodash 庫的現成的 _.partial 實現。
方法 func.bind(context, ...args)
返回函數 func
的“綁定的(bound)變體”,它綁定了上下文 this
和 ...args
參數。
通常我們應用 bind
來綁定對象方法的 this
,這樣我們就可以把它們傳遞到其他地方使用。例如,傳遞給 setTimeout
。
當我們綁定壹個現有的函數的某些參數時,綁定後的(不太通用的)函數被稱爲 partially applied 或 partial。
當我們不想壹遍又壹遍地重複相同的參數時,部分應用函數非常有用。就像我們有壹個 send(from, to)
函數,並且對于我們的任務來說,from
應該總是壹樣的,那麽我們就可以使用它的壹個部分應用函數。
重要程度: 5
輸出將會是什麽?
function f() { alert( this ); // ? } let user = { g: f.bind(null) }; user.g();
答案:null
。
function f() { alert( this ); // null } let user = { g: f.bind(null) }; user.g();
綁定函數的上下文是硬綁定(hard-fixed)的。沒有辦法再修改它。
所以即使我們執行 user.g()
,源方法調用時還是 this=null
。
重要程度: 5
我們可以通過額外的綁定改變 this
嗎?
輸出將會是什麽?
function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Ann" } ); f();
答案:John。
function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Pete"} ); f(); // John
f.bind(...)
返回的外來(exotic)綁定函數 對象僅在創建的時候記憶上下文(以及參數,如果提供了的話)。
壹個函數不能被重綁定(re-bound)。
重要程度: 5
函數的屬性中有壹個值。bind
之後它會改變嗎?爲什麽,闡述壹下?
function sayHi() { alert( this.name ); } sayHi.test = 5; let bound = sayHi.bind({ name: "John" }); alert( bound.test ); // 輸出將會是什麽?爲什麽?
答案:undefined
。
bind
的結果是另壹個對象。它並沒有 test
屬性。
重要程度: 5
下面代碼中對 askPassword()
的調用將會檢查 password,然後基于結果調用 user.loginOk/loginFail
。
但是它導致了壹個錯誤。爲什麽?
修改高亮的行,以使所有內容都能正常工作(其它行不用修改)。
function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', loginOk() { alert(`${this.name} logged in`); }, loginFail() { alert(`${this.name} failed to log in`); }, }; askPassword(user.loginOk, user.loginFail);
發生了錯誤是因爲 ask
獲得的是沒有綁定對象的 loginOk/loginFail
函數。
當 ask
調用這兩個函數時,它們自然會認定 this=undefined
。
讓我們 bind
上下文:
function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', loginOk() { alert(`${this.name} logged in`); }, loginFail() { alert(`${this.name} failed to log in`); }, }; askPassword(user.loginOk.bind(user), user.loginFail.bind(user));
現在它能正常工作了。
另壹個可替換解決方案是:
//... askPassword(() => user.loginOk(), () => user.loginFail());
通常這也能正常工作,也看起來挺好的。
但是可能會在更複雜的場景下失效,例如變量 user
在調用 askPassword
之後但在訪問者應答和調用 () => user.loginOk()
之前被修改。
重要程度: 5
這個任務是比 修複丟失了 "this" 的函數 略微複雜的變體。
user
對象被修改了。現在不是兩個函數 loginOk/loginFail
,現在只有壹個函數 user.login(true/false)
。
在下面的代碼中,我們應該向 askPassword
傳入什麽參數,以使得 user.login(true)
結果是 ok
,user.login(fasle)
結果是 fail
?
function askPassword(ok, fail) { let password = prompt("Password?", ''); if (password == "rockstar") ok(); else fail(); } let user = { name: 'John', login(result) { alert( this.name + (result ? ' logged in' : ' failed to log in') ); } }; askPassword(?, ?); // ?
妳只能修改高亮部分的代碼。
使用包裝(wapper)函數,箭頭函數很簡潔:
askPassword(() => user.login(true), () => user.login(false));
現在它從外部變量中獲得了 user
,然後以常規方式運行它。
或者從 user.login
創建壹個偏函數,該函數使用 user
作爲上下文,並具有正確的第壹個參數:
askPassword(user.login.bind(user, true), user.login.bind(user, false));