Javascript 函數(shù)綁定

2023-02-17 10:51 更新

當(dāng)將對(duì)象方法作為回調(diào)進(jìn)行傳遞,例如傳遞給 ?setTimeout?,這兒會(huì)存在一個(gè)常見的問題:“丟失 ?this?”。

在本章中,我們會(huì)學(xué)習(xí)如何去解決這個(gè)問題。

丟失 “this”

我們已經(jīng)看到了丟失 this 的例子。一旦方法被傳遞到與對(duì)象分開的某個(gè)地方 —— this 就丟失。

下面是使用 setTimeout 時(shí) this 是如何丟失的:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(user.sayHi, 1000); // Hello, undefined!

正如我們所看到的,輸出沒有像 this.firstName 那樣顯示 “John”,而顯示了 undefined!

這是因?yàn)?nbsp;setTimeout 獲取到了函數(shù) user.sayHi,但它和對(duì)象分離開了。最后一行可以被重寫為:

let f = user.sayHi;
setTimeout(f, 1000); // 丟失了 user 上下文

瀏覽器中的 setTimeout 方法有些特殊:它為函數(shù)調(diào)用設(shè)定了 this=window(對(duì)于 Node.js,this 則會(huì)變?yōu)橛?jì)時(shí)器(timer)對(duì)象,但在這兒并不重要)。所以對(duì)于 this.firstName,它其實(shí)試圖獲取的是 window.firstName,這個(gè)變量并不存在。在其他類似的情況下,通常 this 會(huì)變?yōu)?nbsp;undefined

這個(gè)需求很典型 —— 我們想將一個(gè)對(duì)象方法傳遞到別的地方(這里 —— 傳遞到調(diào)度程序),然后在該位置調(diào)用它。如何確保在正確的上下文中調(diào)用它?

解決方案 1:包裝器

最簡單的解決方案是使用一個(gè)包裝函數(shù):

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000);

現(xiàn)在它可以正常工作了,因?yàn)樗鼜耐獠吭~法環(huán)境中獲取到了 user,就可以正常地調(diào)用方法了。

相同的功能,但是更簡短:

setTimeout(() => user.sayHi(), 1000); // Hello, John!

看起來不錯(cuò),但是我們的代碼結(jié)構(gòu)中出現(xiàn)了一個(gè)小漏洞。

如果在 setTimeout 觸發(fā)之前(有一秒的延遲?。?code>user 的值改變了怎么辦?那么,突然間,它將調(diào)用錯(cuò)誤的對(duì)象!

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// ……user 的值在不到 1 秒的時(shí)間內(nèi)發(fā)生了改變
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

// Another user in setTimeout!

下一個(gè)解決方案保證了這樣的事情不會(huì)發(fā)生。

解決方案 2:bind

函數(shù)提供了一個(gè)內(nèi)建方法 bind,它可以綁定 ?this?。

基本的語法是:

// 稍后將會(huì)有更復(fù)雜的語法
let boundFunc = func.bind(context);

func.bind(context) 的結(jié)果是一個(gè)特殊的類似于函數(shù)的“外來對(duì)象(exotic object)”,它可以像函數(shù)一樣被調(diào)用,并且透明地(transparently)將調(diào)用傳遞給 func 并設(shè)定 this=context。

換句話說,boundFunc 調(diào)用就像綁定了 this 的 func。

舉個(gè)例子,這里的 funcUser 將調(diào)用傳遞給了 func 同時(shí) 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)在我們來嘗試一個(gè)對(duì)象方法:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user); // (*)

// 可以在沒有對(duì)象(譯注:與對(duì)象分離)的情況下運(yùn)行它
sayHi(); // Hello, John!

setTimeout(sayHi, 1000); // Hello, John!

// 即使 user 的值在不到 1 秒內(nèi)發(fā)生了改變
// sayHi 還是會(huì)使用預(yù)先綁定(pre-bound)的值,該值是對(duì)舊的 user 對(duì)象的引用
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

在 (*) 行,我們?nèi)×朔椒?nbsp;user.sayHi 并將其綁定到 user。sayHi 是一個(gè)“綁定后(bound)”的方法,它可以被單獨(dú)調(diào)用,也可以被傳遞給 setTimeout —— 都沒關(guān)系,函數(shù)上下文都會(huì)是正確的。

這里我們能夠看到參數(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?

如果一個(gè)對(duì)象有很多方法,并且我們都打算將它們都傳遞出去,那么我們可以在一個(gè)循環(huán)中完成所有方法的綁定:

for (let key in user) {
  if (typeof user[key] == 'function') {
    user[key] = user[key].bind(user);
  }
}

JavaScript 庫還提供了方便批量綁定的函數(shù),例如 lodash 中的 _.bindAll(object, methodNames)。

偏函數(shù)(Partial functions)

到現(xiàn)在為止,我們只在談?wù)摻壎?nbsp;this。讓我們?cè)偕钊胍徊健?

我們不僅可以綁定 this,還可以綁定參數(shù)(arguments)。雖然很少這么做,但有時(shí)它可以派上用場(chǎng)。

bind 的完整語法如下:

let bound = func.bind(context, [arg1], [arg2], ...);

它允許將上下文綁定為 this,以及綁定函數(shù)的起始參數(shù)。

例如,我們有一個(gè)乘法函數(shù) mul(a, b)

function mul(a, b) {
  return a * b;
}

讓我們使用 bind 在該函數(shù)基礎(chǔ)上創(chuàng)建一個(gè) 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

對(duì) mul.bind(null, 2) 的調(diào)用創(chuàng)建了一個(gè)新函數(shù) double,它將調(diào)用傳遞到 mul,將 null 綁定為上下文,并將 2 綁定為第一個(gè)參數(shù)。并且,參數(shù)(arguments)均被“原樣”傳遞。

它被稱為 偏函數(shù)應(yīng)用程序(partial function application) —— 我們通過綁定先有函數(shù)的一些參數(shù)來創(chuàng)建一個(gè)新函數(shù)。

請(qǐ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

為什么我們通常會(huì)創(chuàng)建一個(gè)偏函數(shù)?

好處是我們可以創(chuàng)建一個(gè)具有可讀性高的名字(double,triple)的獨(dú)立函數(shù)。我們可以使用它,并且不必每次都提供一個(gè)參數(shù),因?yàn)閰?shù)是被綁定了的。

另一方面,當(dāng)我們有一個(gè)非常通用的函數(shù),并希望有一個(gè)通用型更低的該函數(shù)的變體時(shí),偏函數(shù)會(huì)非常有用。

例如,我們有一個(gè)函數(shù) send(from, to, text)。然后,在一個(gè) user 對(duì)象的內(nèi)部,我們可能希望對(duì)它使用 send 的偏函數(shù)變體:從當(dāng)前 user 發(fā)送 sendTo(to, text)。

在沒有上下文情況下的 partial

當(dāng)我們想綁定一些參數(shù)(arguments),但是這里沒有上下文 this,應(yīng)該怎么辦?例如,對(duì)于一個(gè)對(duì)象方法。

原生的 bind 不允許這種情況。我們不可以省略上下文直接跳到參數(shù)(arguments)。

幸運(yùn)的是,僅綁定參數(shù)(arguments)的函數(shù) partial 比較容易實(shí)現(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}!`);
  }
};

// 添加一個(gè)帶有綁定時(shí)間的 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é)果是一個(gè)包裝器 (*),它調(diào)用 func 并具有以下內(nèi)容:

  • 與它獲得的函數(shù)具有相同的 ?this?(對(duì)于 ?user.sayNow? 調(diào)用來說,它是 ?user?)
  • 然后給它 ?...argsBound? —— 來自于 ?partial? 調(diào)用的參數(shù)(?"10:00"?)
  • 然后給它 ?...args? —— 給包裝器的參數(shù)(?"Hello"?)

使用 spread 可以很容易實(shí)現(xiàn)這些操作,對(duì)吧?

此外,還有來自 lodash 庫的現(xiàn)成的 _.partial 實(shí)現(xiàn)。

總結(jié)

方法 func.bind(context, ...args) 返回函數(shù) func 的“綁定的(bound)變體”,它綁定了上下文 this 和第一個(gè)參數(shù)(如果給定了)。

通常我們應(yīng)用 bind 來綁定對(duì)象方法的 this,這樣我們就可以把它們傳遞到其他地方使用。例如,傳遞給 setTimeout。

當(dāng)我們綁定一個(gè)現(xiàn)有的函數(shù)的某些參數(shù)時(shí),綁定后的(不太通用的)函數(shù)被稱為 partially applied 或 partial。

當(dāng)我們不想一遍又一遍地重復(fù)相同的參數(shù)時(shí),partial 非常有用。就像我們有一個(gè) send(from, to) 函數(shù),并且對(duì)于我們的任務(wù)來說,from 應(yīng)該總是一樣的,那么我們就可以搞一個(gè) partial 并使用它。

任務(wù)


作為方法的綁定函數(shù)

重要程度: 5

輸出將會(huì)是什么?

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)用時(shí)還是 this=null。


二次 bind

重要程度: 5

我們可以通過額外的綁定改變 ?this? 嗎?

輸出將會(huì)是什么?

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ù) 對(duì)象僅在創(chuàng)建的時(shí)候記憶上下文(以及參數(shù),如果提供了的話)。

一個(gè)函數(shù)不能被重綁定(re-bound)。


bind 后的函數(shù)屬性

重要程度: 5

函數(shù)的屬性中有一個(gè)值。?bind? 之后它會(huì)改變嗎?為什么,闡述一下?

function sayHi() {
  alert( this.name );
}
sayHi.test = 5;

let bound = sayHi.bind({
  name: "John"
});

alert( bound.test ); // 輸出將會(huì)是什么?為什么?

解決方案

答案:undefined。

bind 的結(jié)果是另一個(gè)對(duì)象。它并沒有 test 屬性。


修復(fù)丟失了 "this" 的函數(shù)

重要程度: 5

下面代碼中對(duì) ?askPassword()? 的調(diào)用將會(huì)檢查 password,然后基于結(jié)果調(diào)用 ?user.loginOk/loginFail?。

但是它導(dǎo)致了一個(gè)錯(cuò)誤。為什么?

修改最后一行,以使所有內(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ā)生了錯(cuò)誤是因?yàn)?nbsp;ask 獲得的是沒有綁定對(duì)象的 loginOk/loginFail 函數(shù)。

當(dāng) ask 調(diào)用這兩個(gè)函數(shù)時(shí),它們自然會(huì)認(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)在它能正常工作了。

另一個(gè)可替換解決方案是:

//...
askPassword(() => user.loginOk(), () => user.loginFail());

通常這也能正常工作,也看起來挺好的。

但是可能會(huì)在更復(fù)雜的場(chǎng)景下失效,例如變量 user 在調(diào)用 askPassword 之后但在訪問者應(yīng)答和調(diào)用 () => user.loginOk() 之前被修改。


偏函數(shù)在登錄中的應(yīng)用

重要程度: 5

這個(gè)任務(wù)是比 修復(fù)丟失了 "this" 的函數(shù) 略微復(fù)雜的變體。

user 對(duì)象被修改了?,F(xiàn)在不是兩個(gè)函數(shù) loginOk/loginFail,現(xiàn)在只有一個(gè)函數(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(?, ?); // ?

你只能修改最后一行的代碼。


解決方案

  1. 使用包裝(wapper)函數(shù),箭頭函數(shù)很簡潔:
  2. askPassword(() => user.login(true), () => user.login(false));

    現(xiàn)在它從外部變量中獲得了 user,然后以常規(guī)方式運(yùn)行它。

  3. 或者從 user.login 創(chuàng)建一個(gè)偏函數(shù),該函數(shù)使用 user 作為上下文,并具有正確的第一個(gè)參數(shù):
  4. askPassword(user.login.bind(user, true), user.login.bind(user, false));


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)