面試常見問題之實現bind函式

木魚木心發表於2018-05-20

前言:

原文首發於我的部落格,說實話,這半年來在各大社群看別人分享的面試題中 bind 函式已經出現 n 多次了,這次準備詳細探究下

首先讓我們看看 mdn 對於 bind 函式的描述是什麼

語法

fun.bind(thisArg[, arg1[, arg2[, ...]]])

引數

  • thisArg   當繫結函式被呼叫時,該引數會作為原函式執行時的 this 指向。當使用 new 操作符呼叫繫結函式時,該引數無效。
  • arg1, arg2, ...   當繫結函式被呼叫時,這些引數將置於實參之前傳遞給被繫結的方法。

返回值

  返回由指定的 this 值和初始化引數改造的原函式拷貝


當程式碼 new Foo(...) 執行時,會發生以下事情: 1、一個繼承自 Foo.prototype 的新物件被建立。 2、使用指定的引數呼叫建構函式 Foo ,並將 this 繫結到新建立的物件。new Foo 等同於 new Foo(),也就是沒有指定引數列表,Foo 不帶任何引數呼叫的情況。 3、由建構函式返回的物件就是 new 表示式的結果。如果建構函式沒有顯式返回一個物件,則使用步驟 1 建立的物件。(一般情況下,建構函式不返回值,但是使用者可以選擇主動返回物件,來覆蓋正常的物件建立步驟)如果你看不懂這段話,沒關係,看完下面這段程式碼你就清楚了

function Foo(){

}
下面程式碼就是執行new Foo()時的簡單實現
let obj = {};
obj.__proto__  = Foo.prototype
return Foo.call(obj)
複製程式碼

對於new的完整實現可以參考這位大神的部落格


實現

乞丐版, 無法預先填入引數,僅實現執行時改變 this 指向

let obj = {
  ll: 'seve'
};

Function.prototype.bind = function(that) {
  var self = this;
  return function() {
    return self.apply(that, arguments);
  };
};
let func0 = function(a, b, c) {
  console.log(this.ll);
  console.log([a, b, c]);
}.bind(obj, 1, 2);

func0(3); // seve
// [ 3, undefined, undefined ] 發現1,2並沒有填入
複製程式碼

乞丐版也太 low 了對吧,所以我們繼續完善

es6 進階版

es6 提供了結構運算子,可以很方便的利用其功能實現 bind

Function.prototype.bind = function(that, ...argv) {
  if (typeof this !== 'function') {
    throw new TypeError(`${this} is not callable`);
  }
  // 儲存原函式
  let self = this;
  // 獲取bind後函式傳入的引數
  return function(...argu) {
    return self.apply(that, [...argv, ...argu]);
  };
};
let func1 = function(a, b, c) {
  console.log(this.ll);
  console.log([a, b, c]);
}.bind(obj, 1, 2);

func1(3); // seve
// [ 1, 2, 3 ]
複製程式碼

es6 版實現很簡單對吧,但是面試官說我們的執行環境是 es5,這時你心中竊喜,bable 大法好,但是你可千萬不要說有 babel,因為面試官的意圖不太可能是問你 es6 如何轉換成 es5,而是考察你其他知識點,比如下面的類陣列如何轉換為真正的陣列

es5 進階版

Function.prototype.bind = function() {
  if (typeof this !== 'function') {
    throw new TypeError(`${this} is not callable`);
  }
  var self = this;
  var slice = [].slice;
  // 模擬es6的解構效果
  var that = arguments[0];
  var argv = slice.call(arguments, 1);
  return function() {
    // slice.call(arguments, 0)將類陣列轉換為陣列
    return self.apply(that, argv.concat(slice.call(arguments, 0)));
  };
};
let func2 = function(a, b, c) {
  console.log(this.ll);
  console.log([a, b, c]);
}.bind(obj, 1, 2);

func2(3); // seve
// [ 1, 2, 3 ]
複製程式碼

當然,寫到這裡,對於絕大部分面試,這份程式碼都是一份不錯的答案,但是為了給面試官留下更好的印象,我們需要上終極版 實現完整的bind函式,這樣還可以跟面試官吹一波

終極版

為了當使用new操作符時,bind後的函式不丟失this。我們需要把bind前的函式的原型掛載到bind後函式的原型上

但是為了修改bind後函式的原型而對bind前的原型不產生影響,都是物件惹的禍。。。直接賦值只是賦值物件在堆中的地址 所以需要把原型繼承給bind後的函式,而不是直接賦值,我有在一些地方看到說Object.crate可以實現同樣的效果,有興趣的可以瞭解一下,但是我自己試了下,發現效果並不好,new 操作時this指向錯了(可能是我使用姿勢錯了)

通過直接賦值的效果

Function.prototype.bind = function(that, ...argv) {
  if (typeof this !== 'function') {
    throw new TypeError(`${this} is not callable`);
  }
  // 儲存原函式
  let self = this;
  let func = function() {};
  // 獲取bind後函式傳入的引數
  let bindfunc = function(...arguments) {
    return self.apply(this instanceof func ? this : that, [...argv, ...arguments]);
  };
  // 把this原型上的東西掛載到func原型上面
  // func.prototype = self.prototype;
  // 為了避免func影響到this,通過new 操作符進行復制原型上面的東西
  bindfunc.prototype = self.prototype;

  return bindfunc;
};

function bar() {
  console.log(this.ll);
  console.log([...arguments]);
}
let func3 = bar.bind(null);

func3.prototype.value = 1;

console.log(bar.prototype.value) // 1    可以看到bind後的原型對bind前的原型產生的同樣的影響
複製程式碼

通過繼承賦值的效果

Function.prototype.bind = function(that, ...argv) {
  if (typeof this !== 'function') {
    throw new TypeError(`${this} is not callable`);
  }
  // 儲存原函式
  let self = this;
  let func = function() {};
  // 獲取bind後函式傳入的引數
  let bindfunc = function(...argu) {
    return self.apply(this instanceof func ? this : that, [...argv, ...argu]);
  };
  // 把this原型上的東西掛載到func原型上面
  func.prototype = self.prototype;
  // 為了避免func影響到this,通過new 操作符進行復制原型上面的東西
  bindfunc.prototype = new func();

  return bindfunc;
};

function bar() {
  console.log(this.ll);
  console.log([...arguments]);
}
let func3 = bar.bind(null);

func3.prototype.value = 1;

console.log(bar.prototype.value) // undefined   可以看到bind後的原型對bind前的原型不產生影響

func3(5);     // seve
              // [ 5 ]
new func3(5); // undefined
              // [ 5 ]
複製程式碼

以上程式碼或者表述如有錯誤或者不嚴謹的地方,歡迎指出,或者在評論區討論,覺得我的文章有用的話,可以訂閱或者star支援我的部落格

下系列文章我打算寫關於koa框架的實現,第一篇我會帶大家探究Object.create的效果及實現

相關文章