apply/call/bind 自我實現

  • 2019 年 10 月 5 日
  • 筆記

call/apply/bind 日常編碼中被開發者用來實現 「對象冒充」,也即 「顯示綁定 this「。 https://github.com/ZengLingYong/Blog/issues/30

面試題:「call/apply/bind源碼實現」,事實上是對 JavaScript 基礎知識的一個綜合考核。

相關知識點:

  1. 作用域;
  2. this 指向;
  3. 函數柯里化;
  4. 原型與原型鏈;

call/apply/bind 的區別

  1. 三者都可用於顯示綁定 this;
  2. call/apply 的區別方式在於參數傳遞方式的不同;
    • fn.call(obj, arg1, arg2, ...), 傳參數列表,以逗號隔開;
    • fn.apply(obj, [arg1, arg2, ...]), 傳參數數組;
  3. bind 返回的是一個待執行函數,是函數柯里化的應用,而 call/apply 則是立即執行函數

思路初探

Function.prototype.myCall = function(context) {      // 原型中 this 指向的是實例對象,所以這裡指向 [Function: bar]      console.log(this);  // [Function: bar]      // 在傳入的上下文對象中,創建一個屬性,值指向方法 bar      context.fn = this;  // foo.fn = [Function: bar]      // 調用這個方法,此時調用者是 foo,this 指向 foo      context.fn();      // 執行後刪除它,僅使用一次,避免該屬性被其它地方使用(遍歷)      delete context.fn;  };    let foo = {      value: 2  };    function bar() {      console.log(this.value);  }  // bar 函數的聲明等同於:var bar = new Function("console.log(this.value)");    bar.call(foo);   // 2;

call 的源碼實現

初步思路有個大概,剩下的就是完善程式碼。

// ES6 版本  Function.prototype.myCall = function(context, ...params) {    // ES6 函數 Rest 參數,使其可指定一個對象,接收函數的剩餘參數,合成數組    if (typeof context === 'object') {      context = context || window;    } else {      context = Object.create(null);    }      // 用 Symbol 來作屬性 key 值,保持唯一性,避免衝突    let fn = Symbol();    context[fn] = this;    // 將參數數組展開,作為多個參數傳入    const result = context[fn](...params);    // 刪除避免永久存在    delete(context[fn]);    // 函數可以有返回值    return result;  }    // 測試  var mine = {      name: '以樂之名'  }    var person = {    name: '無名氏',    sayHi: function(msg) {      console.log('我的名字:' + this.name + ',', msg);    }  }    person.sayHi.myCall(mine, '很高興認識你!');  // 我的名字:以樂之名,很高興認識你!

知識點補充:

  1. ES6 新的原始數據類型 Symbol,表示獨一無二的值;
  2. Object.create(null) 創建一個空對象
// 創建一個空對象的方式    // eg.A  let emptyObj = {};    // eg.B  let emptyObj = new Object();    // eg.C  let emptyObj = Object.create(null);  

使用 Object.create(null) 創建的空對象,不會受到原型鏈的干擾。原型鏈終端指向 null,不會有構造函數,也不會有 toStringhasOwnPropertyvalueOf 等屬性,這些屬性來自 Object.prototype。有原型鏈基礎的夥伴們,應該都知道,所有普通對象的原型鏈都會指向 Object.prototype

所以 Object.create(null) 創建的空對象比其它兩種方式,更乾淨,不會有 Object 原型鏈上的屬性。

ES5 版本:

  1. 自行處理參數;
  2. 自實現 Symobo
// ES5 版本    // 模擬Symbol  function getSymbol(obj) {    var uniqAttr = '00' + Math.random();    if (obj.hasOwnProperty(uniqAttr)) {      // 如果已存在,則遞歸自調用函數      arguments.callee(obj);    } else {      return uniqAttr;    }  }    Function.prototype.myCall = function() {    var args = arguments;    if (!args.length) return;      var context = [].shift.apply(args);    context = context || window;      var fn = getSymbol(context);    context[fn] = this;      // 無其它參數傳入    if (!arguments.length) {      return context[fn];    }      var param = args[i];    // 類型判斷,不然 eval 運行會出錯    var paramType = typeof param;    switch(paramType) {      case 'string':        param = '"' + param + '"'      break;      case 'object':        param = JSON.stringify(param);      break;    }      fnStr += i == args.length - 1 ? param : param + ',';      // 藉助 eval 執行    var result = eval(fnStr);    delete context[fn];    return result;  }    // 測試  var mine = {      name: '以樂之名'  }    var person = {    name: '無名氏',    sayHi: function(msg) {      console.log('我的名字:' + this.name + ',', msg);    }  }    person.sayHi.myCall(mine, '很高興認識你!');  // 我的名字:以樂之名,很高興認識!

apply 的源碼實現

call 的源碼實現,那麼 apply 就簡單,兩者只是傳遞參數方式不同而已。

Function.prototype.myApply = function(context, params) {      // apply 與 call 的區別,第二個參數是數組,且不會有第三個參數      if (typeof context === 'object') {          context = context || window;      } else {          context = Object.create(null);      }        let fn = Symbol();      context[fn] = this;      const result context[fn](...params);      delete context[fn];      return result;  }

bind 的源碼實現

  1. bindcall/apply 的區別就是返回的是一個待執行的函數,而不是函數的執行結果;
  2. bind 返回的函數作為構造函數與 new 一起使用,綁定的 this 需要被忽略;

調用綁定函數時作為this參數傳遞給目標函數的值。如果使用new運算符構造綁定函數,則忽略該值。—— MDN

Function.prototype.bind = function(context, ...initArgs) {      // bind 調用的方法一定要是一個函數      if (typeof this !== 'function') {        throw new TypeError('not a function');      }      let self = this;      let F = function() {};      F.prototype = this.prototype;      let bound = function(...finnalyArgs) {        // 將前後參數合併傳入        return self.call(this instanceof F ? this : context || this, ...initArgs, ...finnalyArgs);      }      bound.prototype = new F();      return bound;  }

不少夥伴還會遇到這樣的追問,不使用 call/apply,如何實現 bind

騷年先別慌,不用 call/apply,不就是相當於把 call/apply 換成對應的自我實現方法,算是偷懶取個巧吧。

本篇 call/apply/bind 源碼實現,算是對之前文章系列知識點的一次加深鞏固。

「心中有碼,前路莫慌。」

參考文檔:

  • MDN – Function.prototype.bind()
  • 不用call和apply方法模擬實現ES5的bind方法