JavaScript call() 與 apply() 方法的實現與思路解析

用一個例子開始

greet = "Hi, Im global";

let foo = {
  greet: "Hello"
};

let bar = function(name, character) {
  console.log(this.greet);

  return {
    greet: this.greet,
    person: `${name} is ${character}`
  };
};

call() 方法

let result = bar.call(foo, "Lucy", "sexy");
console.log(result);

輸出

Hello
{ greet: ‘Hello’, person: ‘Lucy is sexy’ }

let result = bar.call(null, "Lucy", "sexy");
console.log(result);

Hi, Im global
{ greet: ‘Hi, Im global’, person: ‘Lucy is sexy’ }

思路

對於 bar.call(foo) 的情況,就好像 foo 對象調用了 bar 方法。

用代碼描述

let newFoo = {
  greet: "Hello",
  bar: function(name, sex) {
    console.log(this.greet);
    return {
      greet: this.greet,
      person: `${name} is ${sex}`
    };
  }
};

把 bar 方法搬進了 foo 對象,成了後者的一個屬性。

如何實現這種效果呢,先上結果,解析如下。

Function.prototype.fakeCall = function(context) {
  context = context || global;
  context.__fn__ = this;
  let args = [];
  for (let i = 1; i < arguments.length; i++) {
    args.push(`arguments[${i}]`);
  }
  let result = eval(`context.__fn__(${args})`);
  delete context.__fn__;
  return result;
};

解析

如何模擬這個 newFoo 對象

bar.fakeCall(foo)

context 就是上面那行代碼中傳入的 foo 對象;
this 就是 fakeCall 函數的調用者,即 bar 函數。

給 foo 對象添加一個臨時的方法, 通過 this ,給它賦值爲 bar 函數 ,由此模擬了上面那個 newFoo 對象。

context.__fn__()
//相當於
newFoo.bar()

如何爲 bar 函數傳參

bar.fakeCall(foo, "Lucy", "sexy")

ECMAScript 中的參數在內部是用一個類數組來表示的。在函數體內,可以通過 arguments 對象來訪問這個類數組,從而獲得傳遞給函數的每一個參數。

call() 方法的第一個參數用來指定 this 綁定的對象,我們需要將從第二個開始的參數傳遞給 bar 函數。

for 循環獲取從 arguments[1]arguments[arguments.length - 1] 的參數,用一個 args 數組來保存這些參數。

eval() 方法的作用

在熟悉 ES6 的情況下,既然已經獲得了參數數組 args ,就很容易想到在 context.__fn__() 中展開數組。

但事實上 ...args 這些都是語法糖,它的實現也用到了 apply() 方法。要模擬 call() ,用這些現成工具難免有取巧之嫌。

而用eval(),可以利用 JavaScript 隱式類型轉換,先將數組 toString() 成字符串以後,填入下面這句的參數位置。再進行解析。

context.__fn__(arguments[1], arguments[2])

所以,上面獲取 args 數組的時候,不是直接獲取值,而是獲取個數。

args.push(`arguments[${i}]`); //正
args.push(arguments[i]); //誤

否則解析就會出問題,因爲將字符串參數 “qq” 作爲變量名來解析了。

bar.ffCall(foo, "qq", "ali")

context.fn(qq,ali)
^
ReferenceError: qq is not defined
at eval (eval at Function.ffCall (/Users/beijiyang/coding/goodJS/test.js:26:12), :1:16)
at Function.ffCall (/Users/beijiyang/coding/goodJS/test.js:26:12)

一些細節

全局對象

非嚴格模式下,如果第一個參數是 nullundefined ,則 this 指向全局對象。

console.log(bar.ffCall(null, "qq", "ali"));

Hi, Im global
{ greet: ‘Hi, Im global’, person: ‘qq is ali’ }

代碼中

context = context || global;
  • 在 node 環境下,是 global
  • 瀏覽器中是 window

嚴格模式

context = Object(context) || global

context 爲 nullundefinedObject(context) 返回空對象,不會被賦值爲global。

console.log(bar.ffCall(undefined, "qq", "ali"));

undefined
{ greet: undefined, person: ‘qq is ali’ }

防止原有屬性被覆蓋

如果傳入的對象原本就有 __fn__ 屬性,那麼該屬性將會被覆蓋並刪除。

所以,可以增添一個取名函數,其中用 hasOwnProperty() 方法檢測目前屬性是否已經存在。

function getPropName(obj) {
  let propName = Math.random();
  if (obj.hasOwnProperty(propName)) {
    getPropName(obj)
  } else {
    return propName
  }
}

完整代碼如下

call() 方法的兩種實現


Function.prototype.fakeCall = function(context) {
  context = context || global;
  let __fn__ = getPropName(context);
  context.__fn__ = this;
  let args = [];
  for (let i = 1; i < arguments.length; i++) {
    args.push(`arguments[${i}]`);
  }
  let result = eval(`context.__fn__(${args})`);
  delete context.__fn__;
  return result;
};

二 ES6版

Function.prototype.fakeCallEs6 = function(context, ...args) {
  context = context || global;
  let __fn__ = getPropName(context);
  context.__fn__ = this;
  let result = context.__fn__(...args);
  delete context.__fn__;
  return result;
};

apply() 方法的實現

apply()call() 的區別在於傳入參數的形式不同。

apply() 接受兩個參數,第一個參數指定了函數體內 this 對象的指向,第二個參數爲一個數組或類數組,apply() 方法把這個集合中的元素作爲參數傳遞給被調用的函數。

二者的實現很相似。

Function.prototype.fakeApply = function(context) {
  context = context || global;
  let __fn__ = getPropName(context);
  context.__fn__ = this;
  let result = eval(`context.__fn__(${arguments[1]})`);
  delete context.__fn__;
  return result;
};

二 ES6版

Function.prototype.fakeApplyEs6 = function(context, args) {
  context = context || global;
  let __fn__ = getPropName(context);
  context.__fn__ = this;
  let result = context.__fn__(...args);
  delete context.__fn__;
  return result;
};

參考:https://github.com/mqyqingfeng/Blog/issues/11

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章