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