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

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