日積硅步,apply/call/bind 自我實現

call/apply/bind 日常編碼中被開發者用來實現 “對象冒充”,也即 “顯示綁定 this“。

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

相關知識點:

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

call/apply/bind 的區別

  1. 三者都可用於顯示綁定 this;
  2. call/apply 的區別方式在於參數傳遞方式的不同;

    • fn.call(obj, arg1, arg2, ...), 傳參數列表,以逗號隔開;
    • fn.call(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 源碼實現,算是對之前文章系列知識點的一次加深鞏固。

“心中有碼,前路莫慌。”


參考文檔:

更多前端基石搭建,盡在 Github,期待 Star!
https://github.com/ZengLingYong/blog

作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章