JavaScript手撕bind方法?

首先,它是函數的一個方法,咱們須要將其--

1.掛載到Function的原型鏈上

Function.prototype.mybind =...
//這樣,全部繼承自Function的函數就可以使用.操做符來訪問mybind了!
//PS:由於JS原型式繼承
而後,讓咱們先看看原生JS的bind方法有哪些行爲--

2.調用函數時改變this指向

讓調用該方法的函數的this指向傳入的第一個參數

咱們能夠藉助apply方法實現javascript

Function.prototype.mybind = function (context) {
  this.apply(context);
};
let obj = {
  name: "Crushdada",
};
let fn = function (params) {
  console.log(this.name);
};
fn.mybind(obj); //Crushdada

3.返回一個匿名的綁定函數

注意兩點:
  • 因爲咱們返回了一個綁定函數(匿名函數),則在調用時須要在調用語句後面再加一個圓括號
  • 與此同時,因爲匿名函數中的this指向window/global,咱們須要使用箭頭函數或者手動保存一下指向mybind中指向調用者fn的thisjava

    • 此處使用箭頭函數
    Function.prototype.mybind = function (context) {
      return () => this.apply(context);
    };
    let obj = {
      name: "Crushdada",
    };
    let fn = function (params) {
      console.log(this.name);
    };
    fn.mybind(obj)(); //Crushdada

    4.支持柯里化傳遞參數

我的理解:相比「容許傳入參數」這種說法,形容爲「傳遞參數」更貼切,bind方法做爲一箇中間方法,會代收參數後再傳遞給它返回的匿名綁定函數,其返回一個匿名函數這一點,自然支持柯里化(多是ES6引入它的初衷之一),由於這樣就容許咱們在調用bind時傳入一部分參數,在調用其綁定函數時再傳入剩下的參數。而後它會在接收完第二次傳參後再apply執行調用bind的那個方法segmentfault

  • 實現柯里化的邏輯很簡單,僅僅須要在mybind中接收一次參數,而後在綁定函數中接收一次參數,並將兩者拼接後一塊兒傳給mybind的調用方法使用便可數組

    下面,實現傳參&柯里化!
  • 若使用的是普通函數,要處理參數,因爲arguments爲類數組,slice爲Array方法,故先在原型鏈上調用而後call一下app

    • 第一個參數爲this新的指向,不是屬性,故slice掉它
    使用箭頭函數能極大簡化代碼
    下面咱們改億點點細節!
  • 使用箭頭函數(Array Function)沒有arguments屬性,所以使用rest運算符替代處理
  • 在拼接args和bindArgs時使用擴展運算符替代concat
  • 不得不說ES6引入的rest運算符、擴展運算符在處理參數這一點上提供了極大的便利函數

    Function.prototype.mybind = function (context, ...args) {
      return (...bindArgs) => {
      //拼接柯里化的兩次傳參
        let all_args = [...args, ...bindArgs]; 
      //執行調用bind方法的那個函數
        let call_fn = this.apply(context, all_args); 
        return call_fn;
      };
    };
    let person = {
      name: "Crushdada",
    };
    let getInfo = function (like, fav) {
      let info = `${this.name} likes ${like},but his favorite is ${fav}`;
      return info;
    };
    //anonymous_bind:mybind返回的那個匿名的綁定函數
    let anonymous_bind = getInfo.mybind(person, "南瓜子豆腐");
    let info = anonymous_bind("皁角仁甜菜"); //執行綁定函數
    console.log(info);
    //Crushdada likes 南瓜子豆腐,but his favorite is 皁角仁甜菜

    箭頭函數不能做爲構造函數!

須要用普通函數重寫mybind

寫到支持柯里化這一步,bind方法仍是可使用箭頭函數實現的,並且比普通函數更加簡潔post

可是想要繼續完善它的的行爲,就不能用繼續用Arrow Function了,由於箭頭函數不能被new!,要是嘗試去new它會報錯:性能

anonymous_bind is not a constructor
筆者也是寫到這纔想起箭頭函數這個機制的。那麼下面咱們須要用普通函數重寫mybind
不過也很簡單,只須要手動保存一下this便可。就再也不貼出改動後的代碼了。直接看下一步

5.支持new綁定函數

bind的一個隱式行爲:ui

  • 它返回的綁定函數容許被new 關鍵字調用,可是,實際被做爲構造器的是調用bind的那個函數!!!
  • 且new調用時傳入的參數照常被傳遞給調用函數。this

    邏輯

實現這一步的邏輯也較爲簡單,咱們類比一下和通常調用new時的區別--

  • new一個普通函數:按理來講生成的實例對象的構造函數是那個普通函數
  • new一個綁定函數:生成的實例對象的構造函數調用bind的那個函數

主要須要咱們寫的邏輯有:

  1. 判斷是不是new調用
  2. getInfo函數中的this指向--new中建立的實例對象obj

    1. 就是把getInfo函數裏的this換成obj,以使obj獲取到其中的屬性
    2. 能夠藉助apply方法
  3. 判斷getInfo函數是否返回一個對象,如果,則返回該對象,不然返回new生成的obj

    至於爲何這麼寫,就須要你先弄懂new關鍵字實現的機制了,個人筆記連接附在文末
    下面,實現它!
    Function.prototype.mybind= function (context, ...args) {
      let self = this;
      return function (...bindArgs) {
        //拼接柯里化的兩次傳參
        let all_args = [...args, ...bindArgs];
        // new.target 用來檢測是不是被 new 調用
        if (new.target !== undefined) {
          // 讓調用mybind的那個函數的this指向new中建立的空對象
          var result = self.apply(this, all_args);
          // 判斷調用mybind方法的那個實際的構造函數是否返回對象,沒有返回對象就返回new生成的實例對象obj
          return result instanceof Object ? result : this;
        }
        //若是不是 new 就原來的邏輯
        //執行調用bind方法的那個函數
        let call_fn = self.apply(context, all_args);
        return call_fn;
      };
    };
    let person = {
      name: "Crushdada",
    };
    let getInfo = function (like, fav) {
      this.dear = "Bravetata";
      let info = `${this.name} likes ${like},but his favorite is ${fav}`;
      return info;
    };
    //anonymous_bind:mybind返回的那個匿名的綁定函數
    let anonymous_bind = getInfo.mybind(person, "南瓜子豆腐");
    let obj = new anonymous_bind("皁角仁甜菜"); //執行綁定函數
    console.log(obj);       //{ dear: 'Bravetata' }
    console.log(obj.name);  //undefined
    解釋一下以上代碼:

第一個邏輯

  • new內部有相似這樣一條語句:Con.apply(obj, args)
  • 其中Con是new 的那個構造函數,obj是最後要返回的實例對象
  • 當咱們new上面mybind中return的那個綁定函數時
  • Con就是該綁定函數
  • 當Con.apply(obj, args)執行,
  • 調用綁定函數並將其中的this換成obj
  • 而後程序就進入到了該綁定函數中--
  • 判斷確實是new調用的
  • 執行self.apply(this, all_args);
  • 這條語句就至關於getInfo.apply(obj, all_args)
  • 這樣就達成咱們的目的了!--讓getInfo成爲new生成的實例對象的實際構造器

第二個邏輯

  • new關鍵字會判斷構造函數自己會不會返回一個對象
  • 若是會,則直接返回這個對象當作實例,不然正常是返回那個new生成的obj當作實例對象
  • 那麼--咱們在第一個邏輯裏已經調用了實際構造器--getInfo
  • 接下來咱們直接判斷一下調用的結果,即它是否return一個對象,而後return給new作最終的return便可

    此外:能夠看到,當new mybind返回的綁定函數時,obj沒有獲取到person.name屬性,爲undefined。也就是說--

    此時,bind改變this指向的行爲會失效

看個栗子,這樣清楚一點
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

儘管在全局和 foo 中都聲明瞭 value 值,最後依然返回了 undefind,說明綁定的this 失效了,

這是爲何呢?

若是你們瞭解 new 的模擬實現,就會知道了--

new是JS模擬面向對象的一個關鍵字,它的目的之一是實現繼承,它要去繼承構造函數(類)之中的屬性,那麼new關鍵字是怎樣去實現的呢?它在內部應用了相似這樣一條語句:

Con.apply(obj, args) //Con是new 的那個構造函數

new 關鍵字會先聲明一個空對象obj,而後將構造函數的this指向這個對象

這樣作會發生什麼--
  • 若是構造函數中設置了一些屬性,如:this.name = xx;
  • 那麼就至關於將this換成了obj,變成:obj.name = xx;
  • obj就繼承到了構造函數的屬性!!
  • obj就是最後會返回的實例對象

詳見:《JS中new操做符作了什麼?》--Crushdada's Notes

讓咱們回到爲何this會失效這一問題上
瞭解完new關鍵字的相關實現,咱們已經獲得答案了--

new完綁定函數後,綁定函數內部的this 已經指向了 obj,而obj中沒有value這個屬性,固然就返回undefined了

6.支持原型鏈繼承

實際上這一步是對綁定函數內重寫new方法的一個補充--

由於new方法原本就支持原型鏈繼承

邏輯

那麼咱們只須要--

讓new的實例對象obj的原型指向實際構造器getInfo的prototype便可

Object.setPrototypeOf(this, self.prototype);
規範化/嚴謹性

能夠爲mybind方法加上一個判斷,調用者必須是一個函數,不然拋出TypeError--

if (typeof this !== 'function' || Object.prototype.toString.call(this) !== '[object Function]') {
    throw new TypeError(this + ' must be a function');
  }
一個疑問?

咱們模擬實現bind方法,終歸是經過apply實現的。而它源碼是如何實現的,對於我來講就像一個黑盒。也就是說:不用apply,它是如何實現?

7.究極版本--不借助apply實現

百度的各類版本大都藉助apply實現的,不過很幸運在思否找到了答案--JS bind方法如何實現?

答者給出的替代apply的方法很簡單:

  • 調用bind方法的那個函數--即要改變this指向的那個函數:caller
  • 要讓caller的this指向:context

那麼咱們只須要--

  • 將caller做爲一個對象方法掛載到context上:context.callerFn = caller

    • 上面那句代碼中,屬性名"callerFn"是自定義的

這樣,當執行該句時,至關於context調用了caller函數,那麼caller函數中的this天然就指向其調用者context了。

以上,就替代了apply在本例中的核心功能--調用函數同時改變this指向

此外,爲提升代碼性能,用完callerFn後就刪掉它

context.__INTERNAL_SECRETS = func
  try {
  return context.__INTERNAL_SECRETS(...args)
} finally {
  delete context.__INTERNAL_SECRETS
}
將apply替換爲以上代碼,就獲得最終版了

最終版

FFunction.prototype.mybind = function (context, ...args) {
  if (
    typeof this !== "function" ||
    Object.prototype.toString.call(this) !== "[object Function]"
  ) {
    throw new TypeError(this + " must be a function");
  }
  let self = this; //這裏的this和self即:調用mybind的方法--fn()
  context.caller2 = self;
  return function (...bindArgs) {
    let all_args = [...args, ...bindArgs];
    //new調用時,this被換成new方法最後要返回的實例對象obj
    if (new.target !== undefined) {
      try {
        this.caller = self;
        var result = this.caller(...all_args);
      } finally {
        delete this.caller;
      }
      Object.setPrototypeOf(this, self.prototype);
      return result instanceof Object ? result : this;
    }
    //當不是new調用時,this指向global/window(由於匿名函數返回後由全局調用)
    try {
      var final_res = context.caller2(...all_args);
    } finally {
      delete context.caller2;
    }
    return final_res; //調用mybind的那個函數[可能]有返回
  };
};

其餘知識點:

Array.prototype.slice.call()

  • 接收一個字符串或有length屬性對象
  • 該方法可以將有length屬性對象或字符串轉換爲數組

    所以像是arguments對象這樣擁有length屬性的類數組就可使用該方法轉換爲真正的數組
    JS中,只有String和Array擁有.slice方法,對象沒有。
    let slice = (arrlike) => Array.prototype.slice.call(arrlike);
    var b = "123456";
    let arr = slice(b);
    console.log(arr);
    // ["1", "2", "3", "4", "5", "6"]

    arr.slice()方法

  • 返回一個新的數組對象,這一對象是一個由beginend決定的原數組的淺拷貝包括begin不包括end)。

    • 接收的參數--begin、end是數組index
    • 原始數組不會被改變。
    const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
    console.log(animals.slice(2));
    // expected output: Array ["camel", "duck", "elephant"]
    console.log(animals.slice(2, 4));
    // expected output: Array ["camel", "duck"]

    邏輯或「||」

a || b :

  • 若運算符前面的值爲false,則返回後面的值
  • 若true,返回前面的值

    js中邏輯值爲false的6種狀況

當一個函數擁有形參,但調用時沒有傳實參時,形參是undefined,會被按false處理
function name(params) {
  console.log(params); //undefined
}
name();
console.log(undefined == false);  //false
console.log(undefined || "undefined was reated as false");
//undefined was reated as false
會被邏輯或運算符當作false處理的總共6個--

0、null、""、false、undefined 或者 NaN

參考:

JavaScript深刻之bind的模擬實現--掘金

js 實現 bind 的這五層,你在第幾層?--藍色的秋風 | 思否

《JS中new操做符作了什麼?》--Crushdada's Notes

相關文章
相關標籤/搜索