解析 Bind 原理,并手寫 Bind 實(shí)現(xiàn)
bind()
bind() 方法創(chuàng)建一個(gè)新的函數(shù),在 bind() 被調(diào)用時(shí),這個(gè)新函數(shù)的 this 被指定為 bind() 的第一個(gè)參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù),供調(diào)用時(shí)使用。
— MDN
bind 方法與 call / apply 最大的不同就是前者返回一個(gè)綁定上下文的函數(shù),而后兩者是直接執(zhí)行了函數(shù)。
來(lái)個(gè)例子說(shuō)明下:
- let value = 2;
- let foo = {
- value: 1
- };
- function bar(name, age) {
- return {
- value: this.value,
- name: name,
- age: age
- }
- };
- bar.call(foo, "Jack", 20); // 直接執(zhí)行了函數(shù)
- // {value: 1, name: "Jack", age: 20}
- let bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一個(gè)函數(shù)
- bindFoo1();
- // {value: 1, name: "Jack", age: 20}
- let bindFoo2 = bar.bind(foo, "Jack"); // 返回一個(gè)函數(shù)
- bindFoo2(20);
- // {value: 1, name: "Jack", age: 20}
通過(guò)上述代碼可以看出 bind 有如下特性:
1、指定 this
2、傳入?yún)?shù)
3、返回一個(gè)函數(shù)
4、柯里化
模擬實(shí)現(xiàn):
- Function.prototype.bind = function (context) {
- // 調(diào)用 bind 的不是函數(shù),需要拋出異常
- if (typeof this !== "function") {
- throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
- }
- // this 指向調(diào)用者
- var self = this;
- // 實(shí)現(xiàn)第2點(diǎn),因?yàn)榈?個(gè)參數(shù)是指定的this,所以只截取第1個(gè)之后的參數(shù)
- var args = Array.prototype.slice.call(arguments, 1);
- // 實(shí)現(xiàn)第3點(diǎn),返回一個(gè)函數(shù)
- return function () {
- // 實(shí)現(xiàn)第4點(diǎn),這時(shí)的arguments是指bind返回的函數(shù)傳入的參數(shù)
- // 即 return function 的參數(shù)
- var bindArgs = Array.prototype.slice.call(arguments);
- // 實(shí)現(xiàn)第1點(diǎn)
- return self.apply( context, args.concat(bindArgs) );
- }
- }
但還有一個(gè)問題,bind 有以下一個(gè)特性:
一個(gè)綁定函數(shù)也能使用 new 操作符創(chuàng)建對(duì)象:這種行為就像把原函數(shù)當(dāng)成構(gòu)造器,提供的 this 值被忽略,同時(shí)調(diào)用時(shí)的參數(shù)被提供給模擬函數(shù)。
來(lái)個(gè)例子說(shuō)明下:
- let value = 2;
- let foo = {
- value: 1
- };
- function bar(name, age) {
- this.habit = 'shopping';
- console.log(this.value);
- console.log(name);
- console.log(age);
- }
- bar.prototype.friend = 'kevin';
- let bindFoo = bar.bind(foo, 'Jack');
- let obj = new bindFoo(20);
- // undefined
- // Jack
- // 20
- obj.habit;
- // shopping
- obj.friend;
- // kevin
上面例子中,運(yùn)行結(jié)果 this.value 輸出為 undefined ,這不是全局 value 也不是 foo 對(duì)象中的 value ,這說(shuō)明 bind 的 this 對(duì)象失效了,new 的實(shí)現(xiàn)中生成一個(gè)新的對(duì)象,這個(gè)時(shí)候的 this 指向的是 obj 。
這個(gè)可以通過(guò)修改返回函數(shù)的原型來(lái)實(shí)現(xiàn),代碼如下:
- Function.prototype.bind = function (context) {
- // 調(diào)用 bind 的不是函數(shù),需要拋出異常
- if (typeof this !== "function") {
- throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
- }
- // this 指向調(diào)用者
- var self = this;
- // 實(shí)現(xiàn)第2點(diǎn),因?yàn)榈?個(gè)參數(shù)是指定的this,所以只截取第1個(gè)之后的參數(shù)
- var args = Array.prototype.slice.call(arguments, 1);
- // 創(chuàng)建一個(gè)空對(duì)象
- var fNOP = function () {};
- // 實(shí)現(xiàn)第3點(diǎn),返回一個(gè)函數(shù)
- var fBound = function () {
- // 實(shí)現(xiàn)第4點(diǎn),獲取 bind 返回函數(shù)的參數(shù)
- var bindArgs = Array.prototype.slice.call(arguments);
- // 然后同傳入?yún)?shù)合并成一個(gè)參數(shù)數(shù)組,并作為 self.apply() 的第二個(gè)參數(shù)
- return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
- // 注釋1
- }
- // 注釋2
- // 空對(duì)象的原型指向綁定函數(shù)的原型
- fNOP.prototype = this.prototype;
- // 空對(duì)象的實(shí)例賦值給 fBound.prototype
- fBound.prototype = new fNOP();
- return fBound;
- }
注釋1 :
- 當(dāng)作為構(gòu)造函數(shù)時(shí),this 指向?qū)嵗?,此時(shí) this instanceof fBound 結(jié)果為 true ,可以讓實(shí)例獲得來(lái)自綁定函數(shù)的值,即上例中實(shí)例會(huì)具有 habit 屬性。
- 當(dāng)作為普通函數(shù)時(shí),this 指向 window ,此時(shí)結(jié)果為 false ,將綁定函數(shù)的 this 指向 context
注釋2 :
- 修改返回函數(shù)的 prototype 為綁定函數(shù)的 prototype,實(shí)例就可以繼承綁定函數(shù)的原型中的值,即上例中 obj 可以獲取到 bar 原型上的 friend
- 至于為什么使用一個(gè)空對(duì)象 fNOP 作為中介,把 fBound.prototype 賦值為空對(duì)象的實(shí)例(原型式繼承),這是因?yàn)橹苯? fBound.prototype = this.prototype 有一個(gè)缺點(diǎn),修改 fBound.prototype 的時(shí)候,也會(huì)直接修改 this.prototype ;其實(shí)也可以直接使用ES5的 Object.create() 方法生成一個(gè)新對(duì)象,但 bind 和 Object.create() 都是ES5方法,部分IE瀏覽器(IE < 9)并不支
注意: bind() 函數(shù)在 ES5 才被加入,所以并不是所有瀏覽器都支持,IE8 及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 來(lái)實(shí)現(xiàn)
詳情可前往 深度解析bind原理、使用場(chǎng)景及模擬實(shí)現(xiàn) 查看
補(bǔ)充:柯里化
在計(jì)算機(jī)科學(xué)中,柯里化(Currying)是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。這個(gè)技術(shù)由 Christopher Strachey 以邏輯學(xué)家 Haskell Curry 命名的,盡管它是 Moses Schnfinkel 和 Gottlob Frege 發(fā)明的。
- var add = function(x) {
- return function(y) {
- return x + y;
- };
- };
- var increment = add(1);
- var addTen = add(10);
- increment(2);
- // 3
- addTen(2);
- // 12
- add(1)(2);
- // 3
這里定義了一個(gè) add 函數(shù),它接受一個(gè)參數(shù)并返回一個(gè)新的函數(shù)。調(diào)用 add 之后,返回的函數(shù)就通過(guò)閉包的方式記住了 add 的第一個(gè)參數(shù)。所以說(shuō) bind 本身也是閉包的一種使用場(chǎng)景。
柯里化是將 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被調(diào)用的轉(zhuǎn)化。JavaScript 實(shí)現(xiàn)版本通常保留函數(shù)被正常調(diào)用和在參數(shù)數(shù)量不夠的情況下返回偏函數(shù)這兩個(gè)特性。