首頁 > 軟體

JS手寫bind之處理new的情況詳解

2022-07-11 10:01:13

大家好,我是前端西瓜哥。

之前寫了一篇關於 JS 中 bind 方法的實現的文章,並給出了實現:

Function.prototype.myBind = function(thisArg, ...prefixArgs) {
  const fn = this;
  return function(...args) {
    return fn.call(thisArg, ...prefixArgs, ...args);
  }
}

但沒有處理 通過 new 建立範例 的情況。

因為很少會遇到給 bind 返回的函數做 new 操作的場景,所以我沒去考慮這種特殊情況。

但面試中還是會涉及到的,我們還是實現一下相容 new 操作的 bind 寫法,順便學習一下 new 操作符。

因為存在一定上下文,在閱讀本文前,建議先閱讀前一篇文章:《前端面試題:手寫 bind》。

new

我們先學習一下 new 操作符。

new 用於通過函數來建立一個物件範例,在很多語言中都能看到。

JS 的函數,除了可以是普通函數,比如:

function sum(a, b) {
  return a + b;
}

還可以是建構函式,只需要在構造時在它前面加一個 new:

function Person(name, age) {
 this.name = name;
 this.age = age;
}

const person = new Person('前端西瓜哥', 100)
// Person {name: '前端西瓜哥', age: 100}

new 建立一個新物件,做了下面幾件事:

  • 建立一個空物件 {}
  • 空物件的原型屬性 __proto__ 指向建構函式的原型物件 Person.prototype
  • 函數中的 this 設定為這個空物件;
  • 如果該函數不返回一個物件,就返回這個 this,否則返回這個物件。

判斷函數是否通過 new 被呼叫

怎麼判斷一個函數正在被 new 操作符呼叫?

答案是 使用 instanceof 判斷 this 是否為當前函數的範例,即 this instanceof Fn 為 true,表示在通過 new 構建範例。

我們看一個例子:

function Person() {
  if (this instanceof Person) {
    console.log('通過 new 構建範例');
  } else {
    console.log('普通呼叫')
  }
}

Person()     // 輸出:普通呼叫
new Person() // 輸出:通過 new 構建範例

在 Vuejs 的原始碼,你會看到下面程式碼,這裡也用到了這個技巧。

function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

你在開發環境如果不通過 new 來使用 Vue 物件,會在控制檯提示你要通過 new 來呼叫 Vue。

new 和 bind

如果我們 new 的是 Function.prototype.bind 返回的新函數,會發生什麼事情?

function Person(name, age) {
 this.name = name;
 this.age = age;
}

const BoundPerson = Person.bind(null, '前端西瓜哥');
const boundPerson = new BoundPerson(100);
// Person {name: '前端西瓜哥', age: 100}

boundPerson.__proto__ === Person.prototype
// true

結果等價於直接去 new 原始函數。

不同的是,仍舊可以進行引數的預置。可以看到,建構函式的第一個引數,在呼叫 bind 的時候就提前設定為 '前端西瓜哥'。

實現完整的 bind

完整實現如下:

Function.prototype.myBind = function(thisArg, ...prefixArgs) {
  const fn = this;
  const boundFn = function(...args) {
    // 通過 new 使用當前函數
    if (this instanceof boundFn) {
      return new fn(...prefixArgs, ...args);
    }
    // 普通的方法呼叫當前函數
    return fn.call(thisArg, ...prefixArgs, ...args);
  }
  boundFn.prototype = fn.prototype;
  return boundFn;
}

這裡我通過 this instanceof boundFn 來判斷是否用了 new,如果是,就直接 new 原始函數然後返回,記得帶上 bind 預置好的引數。

其他保持原樣(具體見上文《前端面試題:手寫 bind》)。

boundFn.prototype = fn.prototype;  這個可寫可不寫,只是讓 bind 返回的新函數的 prototype 指向原函數的 prototype。

如果是原生 bind 返回的函數,它是沒有 protoype 屬性的,可以認為它是一種特別的函數,而我們實現的 bind 返回的卻是一個普通函數,所以並不能完全模擬的。

如果你 追求完美的實現,可以研讀一下  Function.prototype.bind 的標準:

然後再看看知名的 core.js 庫中對 bind 的實現:

其中核心實現為:

// `Function.prototype.bind` method implementation
// https://tc39.es/ecma262/
module.exports = Function.bind || function bind(that /* , ...args */) {
  var F = aCallable(this);
  var Prototype = F.prototype;
  var partArgs = arraySlice(arguments, 1);
  var boundFunction = function bound(/* args... */) {
    var args = concat(partArgs, arraySlice(arguments));
    return this instanceof boundFunction ? construct(F, args.length, args) : F.apply(that, args);
  };
  if (isObject(Prototype)) boundFunction.prototype = Prototype;
  return boundFunction;
};

這裡有更多的細節:

  • 這裡判斷了 this 是否為函數型別,不是函數會報錯;
  • F.prototype 需要是一個物件或函數,才能賦值給新函數;
  • 使用了普通函數和 arguments,這是為了相容 ES5。

結尾

手寫 bind,現在想來並不簡單,需要掌握好很多知識點:

  • bind 的詳盡用法:包括改變 this、預置引數、new 的表現;
  • 閉包的使用:儲存一些私有變數;
  • 通過原型鏈的方式(this instanceof boundFn)判斷是否通過 new 呼叫當前函數;
  • 使用 call 在執行時改變函數的 this 指向。

到此這篇關於JS手寫bind之處理new的情況詳解的文章就介紹到這了,更多相關JS處理new內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com