js - 手写call、apply、bind

111 阅读3分钟

1. call / apply / bind 对比

call()  方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。

bind()  方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

  • 相同点:
    • 都用于绑定函数的this的指向。
    • 三个函数都在 Function.prototype上
  • 区别:
    • call/apply调用后会执行该函数,bind调用后会返回一个新函数
    • call的函数参数接收的是参数列表,bind接受的是一个含有多个参数的数组
    • bind的this参数后的其他参数,会在所返回函数进行调用时作为参数。
var name = "globalName";

const obj = {
    name: "objName"
}

function logName(name1) {
  console.log(this.name, name1);
}

logName("test"); // globalName test

logName.call(obj, "test"); // objName test
logName.apply(obj, ["test"]); // objName test

temp = logName.bind(obj);
temp('test'); // objName test

temp = logName.bind(obj, "test");
temp(); // objName test

2. 实现

2.1 call

先看下面这段代码:

const obj = {
   logThis(){
       console.log(this);
   }
}

obj.logThis() // {logThis: ƒ}

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。这里的上下文对象就是obj

Function.prototype.myCall = function(context){
  context.fn = this;
  context.fn();
}

myCallFunction.prototype上的一个函数,由前面隐式绑定可知,当以targetFn.myCall()的形式调用时,myCall中的this指向targetFn

context为用户指定的this指向的对象,可以同样利用隐式绑定,在该对象上添加键fn,值为this,即targetFn,当以context.fn()的形式调用时,该函数的this指向context

验证一下:

Function.prototype.myCall = function (context) {
  context.fn = this;
  context.fn();
};

name = 'global';
obj = {
  name: "obj",
};

function logName() {
  console.log(this.name);
}

logName(); // global
logName.myCall(obj); // obj

但是还有其他问题需要解决:

  1. 往Function的原型对象上了不需要的属性

    • 在调用后 delete 该属性
  2. Function的原型对象可能本来就存在fn属性,原值会被覆盖

    • 使用Symbol生成唯一的键
  3. 需要限制只有函数对象才可以调用

    • 通过this来判断调用该方法的上下文对象
  4. this 指定为 null 或 undefined 时会自动替换为指向window

    • 参数处理
  5. 多的参数需要作为targetFn调用时的参数

    • 利用 argments对象 或 ...args
  6. 需要返回调用结果

完善一下代码:

Function.prototype.myCall = function (context, ...args) {
  if (typeof this !== "function") throw new Error("type error");
  context = context || window;
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  const res = context[fnSymbol](...args);
  delete context[fnSymbol];
  return res;
};

2.2 apply

由于apply只是在参数的形式上与call存在区别,所以我们可以在myCall的基础上对参数进行处理即可实现。

// 仅修改args
Function.prototype.myApply = function (context, args = []) {
  if (typeof this !== "function") throw new Error("type error");
  context = context || window;
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  const res = context[fnSymbol](...args);
  delete context[fnSymbol];
  return res;
};

2.3 bind

首先考虑bind返回的是一个函数,其次该函数执行的效果与原来的函数调用call/apply的效果是一致的。因此个人认为可以将其视为一种call/apply的延迟执行。

按照这个思路,可以写出以下代码:

Function.prototype.myBind = function (context, ...args) {
  const _this = this;
  return function () {
    _this.call(context, ...args);
  };
};

前面已经提过 this指向targetFn.myBind()中的targetFn,此处用_this将其保存下来。

除了在myCall处调到的问题,myBind需要解决的特别的问题:

  1. 返回的函数在调用时可以继续传参
    • 接受参数,并与原来的参数合并
  2. 返回的函数被new时,会忽略原先绑定的this值
    • new调用函数时,函数中的this为所调用函数的一个实例
Function.prototype.myBind = function (context, ...args) {
  if (typeof this !== "function") throw new Error("type error");
  context = context || window;
  const _this = this;
  return function Fn(...otherArgs) {
    return _this.call(
      this instanceof Fn ? this : context,
      ...args,
      ...otherArgs
    );
  };
};

首先将原来返回一个匿名函数改为返回函数Fn,此处通过this instanceof Fn来判断调用场景,若用new调用,则此条件符合,call指定this为当前this。