W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
當(dāng)將對象方法作為回調(diào)進行傳遞,例如傳遞給 ?setTimeout
?,這兒會存在一個常見的問題:“丟失 ?this
?”。
在本章中,我們會學(xué)習(xí)如何去解決這個問題。
我們已經(jīng)看到了丟失 this
的例子。一旦方法被傳遞到與對象分開的某個地方 —— this
就丟失。
下面是使用 setTimeout
時 this
是如何丟失的:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
正如我們所看到的,輸出沒有像 this.firstName
那樣顯示 “John”,而顯示了 undefined
!
這是因為 setTimeout
獲取到了函數(shù) user.sayHi
,但它和對象分離開了。最后一行可以被重寫為:
let f = user.sayHi;
setTimeout(f, 1000); // 丟失了 user 上下文
瀏覽器中的 setTimeout
方法有些特殊:它為函數(shù)調(diào)用設(shè)定了 this=window
(對于 Node.js,this
則會變?yōu)橛嫊r器(timer)對象,但在這兒并不重要)。所以對于 this.firstName
,它其實試圖獲取的是 window.firstName
,這個變量并不存在。在其他類似的情況下,通常 this
會變?yōu)?nbsp;undefined
。
這個需求很典型 —— 我們想將一個對象方法傳遞到別的地方(這里 —— 傳遞到調(diào)度程序),然后在該位置調(diào)用它。如何確保在正確的上下文中調(diào)用它?
最簡單的解決方案是使用一個包裝函數(shù):
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
現(xiàn)在它可以正常工作了,因為它從外部詞法環(huán)境中獲取到了 user
,就可以正常地調(diào)用方法了。
相同的功能,但是更簡短:
setTimeout(() => user.sayHi(), 1000); // Hello, John!
看起來不錯,但是我們的代碼結(jié)構(gòu)中出現(xiàn)了一個小漏洞。
如果在 setTimeout
觸發(fā)之前(有一秒的延遲?。?code>user 的值改變了怎么辦?那么,突然間,它將調(diào)用錯誤的對象!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ……user 的值在不到 1 秒的時間內(nèi)發(fā)生了改變
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Another user in setTimeout!
下一個解決方案保證了這樣的事情不會發(fā)生。
函數(shù)提供了一個內(nèi)建方法 bind,它可以綁定 ?this
?。
基本的語法是:
// 稍后將會有更復(fù)雜的語法
let boundFunc = func.bind(context);
func.bind(context)
的結(jié)果是一個特殊的類似于函數(shù)的“外來對象(exotic object)”,它可以像函數(shù)一樣被調(diào)用,并且透明地(transparently)將調(diào)用傳遞給 func
并設(shè)定 this=context
。
換句話說,boundFunc
調(diào)用就像綁定了 this
的 func
。
舉個例子,這里的 funcUser
將調(diào)用傳遞給了 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
。
所有的參數(shù)(arguments)都被“原樣”傳遞給了初始的 func
,例如:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// 將 this 綁定到 user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John(參數(shù) "Hello" 被傳遞,并且 this=user)
現(xiàn)在我們來嘗試一個對象方法:
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 秒內(nèi)發(fā)生了改變
// sayHi 還是會使用預(yù)先綁定(pre-bound)的值,該值是對舊的 user 對象的引用
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
在 (*)
行,我們?nèi)×朔椒?nbsp;user.sayHi
并將其綁定到 user
。sayHi
是一個“綁定后(bound)”的方法,它可以被單獨調(diào)用,也可以被傳遞給 setTimeout
—— 都沒關(guān)系,函數(shù)上下文都會是正確的。
這里我們能夠看到參數(shù)(arguments)都被“原樣”傳遞了,只是 this
被 bind
綁定了:
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John!(參數(shù) "Hello" 被傳遞給了 say)
say("Bye"); // Bye, John!(參數(shù) "Bye" 被傳遞給了 say)
便捷方法:?
bindAll
?如果一個對象有很多方法,并且我們都打算將它們都傳遞出去,那么我們可以在一個循環(huán)中完成所有方法的綁定:
for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } }
JavaScript 庫還提供了方便批量綁定的函數(shù),例如 lodash 中的 _.bindAll(object, methodNames)。
到現(xiàn)在為止,我們只在談?wù)摻壎?nbsp;this
。讓我們再深入一步。
我們不僅可以綁定 this
,還可以綁定參數(shù)(arguments)。雖然很少這么做,但有時它可以派上用場。
bind
的完整語法如下:
let bound = func.bind(context, [arg1], [arg2], ...);
它允許將上下文綁定為 this
,以及綁定函數(shù)的起始參數(shù)。
例如,我們有一個乘法函數(shù) mul(a, b)
:
function mul(a, b) {
return a * b;
}
讓我們使用 bind
在該函數(shù)基礎(chǔ)上創(chuàng)建一個 double
函數(shù):
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)
的調(diào)用創(chuàng)建了一個新函數(shù) double
,它將調(diào)用傳遞到 mul
,將 null
綁定為上下文,并將 2
綁定為第一個參數(shù)。并且,參數(shù)(arguments)均被“原樣”傳遞。
它被稱為 偏函數(shù)應(yīng)用程序(partial function application) —— 我們通過綁定先有函數(shù)的一些參數(shù)來創(chuàng)建一個新函數(shù)。
請注意,這里我們實際上沒有用到 this
。但是 bind
需要它,所以我們必須傳入 null
之類的東西。
下面這段代碼中的 triple
函數(shù)將值乘了三倍:
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
為什么我們通常會創(chuàng)建一個偏函數(shù)?
好處是我們可以創(chuàng)建一個具有可讀性高的名字(double
,triple
)的獨立函數(shù)。我們可以使用它,并且不必每次都提供一個參數(shù),因為參數(shù)是被綁定了的。
另一方面,當(dāng)我們有一個非常通用的函數(shù),并希望有一個通用型更低的該函數(shù)的變體時,偏函數(shù)會非常有用。
例如,我們有一個函數(shù) send(from, to, text)
。然后,在一個 user
對象的內(nèi)部,我們可能希望對它使用 send
的偏函數(shù)變體:從當(dāng)前 user 發(fā)送 sendTo(to, text)
。
當(dāng)我們想綁定一些參數(shù)(arguments),但是這里沒有上下文 this
,應(yīng)該怎么辦?例如,對于一個對象方法。
原生的 bind
不允許這種情況。我們不可以省略上下文直接跳到參數(shù)(arguments)。
幸運的是,僅綁定參數(shù)(arguments)的函數(shù) partial
比較容易實現(xiàn)。
像這樣:
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");
// 類似于這樣的一些內(nèi)容:
// [10:00] John: Hello!
partial(func[, arg1, arg2...])
調(diào)用的結(jié)果是一個包裝器 (*)
,它調(diào)用 func
并具有以下內(nèi)容:
this
?(對于 ?user.sayNow
? 調(diào)用來說,它是 ?user
?)...argsBound
? —— 來自于 ?partial
? 調(diào)用的參數(shù)(?"10:00"
?)...args
? —— 給包裝器的參數(shù)(?"Hello"
?)使用 spread 可以很容易實現(xiàn)這些操作,對吧?
此外,還有來自 lodash 庫的現(xiàn)成的 _.partial 實現(xiàn)。
方法 func.bind(context, ...args)
返回函數(shù) func
的“綁定的(bound)變體”,它綁定了上下文 this
和第一個參數(shù)(如果給定了)。
通常我們應(yīng)用 bind
來綁定對象方法的 this
,這樣我們就可以把它們傳遞到其他地方使用。例如,傳遞給 setTimeout
。
當(dāng)我們綁定一個現(xiàn)有的函數(shù)的某些參數(shù)時,綁定后的(不太通用的)函數(shù)被稱為 partially applied 或 partial。
當(dāng)我們不想一遍又一遍地重復(fù)相同的參數(shù)時,partial 非常有用。就像我們有一個 send(from, to)
函數(shù),并且對于我們的任務(wù)來說,from
應(yīng)該總是一樣的,那么我們就可以搞一個 partial 并使用它。
輸出將會是什么?
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();
綁定函數(shù)的上下文是硬綁定(hard-fixed)的。沒有辦法再修改它。
所以即使我們執(zhí)行 user.g()
,源方法調(diào)用時還是 this=null
。
我們可以通過額外的綁定改變 ?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)綁定函數(shù) 對象僅在創(chuàng)建的時候記憶上下文(以及參數(shù),如果提供了的話)。
一個函數(shù)不能被重綁定(re-bound)。
函數(shù)的屬性中有一個值。?bind
? 之后它會改變嗎?為什么,闡述一下?
function sayHi() {
alert( this.name );
}
sayHi.test = 5;
let bound = sayHi.bind({
name: "John"
});
alert( bound.test ); // 輸出將會是什么?為什么?
答案:undefined
。
bind
的結(jié)果是另一個對象。它并沒有 test
屬性。
下面代碼中對 ?askPassword()
? 的調(diào)用將會檢查 password,然后基于結(jié)果調(diào)用 ?user.loginOk/loginFail
?。
但是它導(dǎo)致了一個錯誤。為什么?
修改最后一行,以使所有內(nèi)容都能正常工作(其它行不用修改)。
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);
發(fā)生了錯誤是因為 ask
獲得的是沒有綁定對象的 loginOk/loginFail
函數(shù)。
當(dāng) ask
調(diào)用這兩個函數(shù)時,它們自然會認(rèn)定 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));
現(xiàn)在它能正常工作了。
另一個可替換解決方案是:
//...
askPassword(() => user.loginOk(), () => user.loginFail());
通常這也能正常工作,也看起來挺好的。
但是可能會在更復(fù)雜的場景下失效,例如變量 user
在調(diào)用 askPassword
之后但在訪問者應(yīng)答和調(diào)用 () => user.loginOk()
之前被修改。
這個任務(wù)是比 修復(fù)丟失了 "this" 的函數(shù) 略微復(fù)雜的變體。
user
對象被修改了?,F(xiàn)在不是兩個函數(shù) loginOk/loginFail
,現(xiàn)在只有一個函數(shù) user.login(true/false)
。
在下面的代碼中,我們應(yīng)該向 askPassword
傳入什么參數(shù),以使得 user.login(true)
結(jié)果是 ok
,user.login(fasle)
結(jié)果是 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(?, ?); // ?
你只能修改最后一行的代碼。
askPassword(() => user.login(true), () => user.login(false));
現(xiàn)在它從外部變量中獲得了 user
,然后以常規(guī)方式運行它。
user.login
創(chuàng)建一個偏函數(shù),該函數(shù)使用 user
作為上下文,并具有正確的第一個參數(shù):askPassword(user.login.bind(user, true), user.login.bind(user, false));
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: