模拟实现call/apply/bind方法

252 阅读2分钟

call

call方法是使用指定一个this和若干个参数,调用某一个函数或方法 先举个例子

// e.g
const foo = {
  value: 'foo',
}

function bar() {
  console.log(this.value);
}

bar.call(foo); // foo

分析一下call都干了什么:

  1. call改变了this的指向,将其指向了foo
  2. bar函数执行 根据这个思路,那么我们试着模拟实现上面两点
Function.prototype._call = function (target) {
  // 判断target是否为null
  // 如果是null,将执行上下文指向window
  const context = target || window;
  // 由于arguments是类数组,将形参加入到数组中
	// 注意是从第二个参数开始循环,因为第一个参数是target
  const args = [];
  for (let i = 1; i < arguments.length; i++) {
    args.push(`arguments[${i}]`);
  }
  // 注意this,他其实就是当前调用call的函数
  // 将方法添加到上下文中,以便执行
  context.fn = this;
  // 用eval拼成字符串执行
  const result = eval(`context.fn(${args});`);
  // 将方法从上下文中移除
  delete context.fn;
  return result;
}

注意: 传参个数不确定如何解决,我们可以这样

// 由于arguments是类数组,可以用for循环
for (let i = 1; i < arguments.length; i++) {
  args.push(`arguments[${i}]`);
}
// 执行后,args为['arguments[1]', 'arguments[2]', ... , 'arguments[i]']

不定长参数问题解决了,我们把参数数组放到要执行的函数的参数里去。 由于call是ES3的方法,为了模拟实现一个ES3的方法,可以用eval方法拼成一个函数去执行,类似于:

eval(`context.fn(${args});`)

这里args会自动调用Array.toString()方法。 函数是有返回值的,所以要返回eval方法执行后的结果。

const result = eval(`context.fn(${args});`);
return result;

apply

和call一样,唯一的不同点在于apply接受一个数组作为参数,这里直接给出代码

Function.prototype._apply = function (target, params) {
  const context = target || window;
  context.fn = this;
  if(!params) {
    return context.fn();
  }
  const args = [];
  for (let i = 0; i < params.length; i++) {
    args.push(`params[${i}]`);
  }
  const result = eval(`context.fn(${args})`);
  delete context.fn;
  return result;
}

bind

MDN解释

bind()会创建一个新方法,当这个新函数被调用时,bind()的第一个参数将作为它运行时的this,之后的一序列参数将会在传递的实参前传入作为它的参数。 按照MDN的解释,举个例子

const foo = {
  value: 'foo'
};

function bar(name) {
  console.log(this.value); // foo
  console.log(name); // elvis
}

const fbind = bar.bind(foo, 'elvis');
fbind();

总结出bind的特点:

  1. 返回一个新方法
  2. 可以传入参数 根据特点来模拟实现bind方法
Function.prototype._bind = function (target) {
  const fn = this;
  const context = target || window;
  const args = Array.prototype.slice._call(arguments, 1);
  return function () {
    const fnArgs = Array.prototype.slice._call(arguments);
    return fn._apply(context, args.concat(fnArgs));
  }
}

注意:代码中用到了上文实现的_call,_apply方法,完成以上功能还不算完。

重点来了!!

绑定创造的函数还可以被构造调用,这时bind中指定的this绑定会失效,但参数依然有效

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数 举个例子,调用原生bind之后可以看到this.value的结果是undefined,证明指定的this被忽略了。

const foo = {
  value: 'foo',
}

function bar(name, age) {
  this.job = "developer";
  console.log(this.value); // undefined
  console.log(name); // elvis
  console.log(age); // 32
}

const fBind = bar.bind(foo, 'elvis');
const obj = new fBind('32');
console.log(obj.job); // developer

根据上面的结果,我们可以通过修改返回函数的原型来实现

Function.prototype._bind = function (target) {
  const fn = this;
  const context = target || window;
  const args = Array.prototype.slice._call(arguments, 1);
  const fBind = function () {
    const fnArgs = Array.prototype.slice._call(arguments);
    // 当作为构造函数时,this指向实例
    // 当作为普通函数调用时,this指向bind指定的target
    fn._apply(this instanceof fBind ? this : context, args.concat(fnArgs));
  }
  // 修改返回函数的prototype,指向绑定函数的prototype,这样就可以访问绑定函数原型中的值了
  fBind.prototype = this.prototype;
  return fBind;
}

构造调用的优化

在这个写法中,fBind.prototype = this.prototype也会直接修改绑定函数的原型,我们可以通过一个空函数进行中转

Function.prototype._bind = function (target) {
  const fn = this;
  const context = target || window;
  const args = Array.prototype.slice._call(arguments, 1);
  const f = function () { };
  const fBind = function () {
    const fnArgs = Array.prototype.slice._call(arguments);
    fn._apply(this instanceof f ? this : context, args.concat(fnArgs));
  }
  f.prototype = this.prototype;
  fBind.prototype = new f();

  return fBind;
}

到这里为止,大的问题都解决了👍🏻