手写 call, apply 和 bind 源码

445 阅读3分钟

call(), apply() 和 bind() 方法三者功能相似,均可以改变 this 的指向。区别是:

  1. call(), apply() 方法会改变 this 的指向并执行函数;
  2. 而 bind() 方法会改变 this 的指向但并不会执行函数;
  3. call(), apply() 方法区别就是前者接受的是参数列表,而后者接受的是一个参数数组。

下面我们来看看如何实现

function ShowInfo() {
  let res = `My name is ${this.name}, I am ${this.age} years old.`;
  return res;
}

const Dog = {
  name: "Sugary",
  age: 10,
};

console.log(ShowInfo.apply(Dog)); // My name is Sugary, I am 10 years old.
console.log(ShowInfo()); // My name is undefined, I am undefined years old.

由上述代码可知,如果不为 ShowInfo() 方法指定特定的对象,this 的值默认指向全局对象,即 window 对象。因此,apply() 方法实现了两个功能:

  1. 改变了 ShowInfo() 函数的 this 指向;
  2. 执行了 ShowInfo() 函数。

apply() 源码实现

function showInfo() {
  let res = `My name is ${this.name}, I am ${this.age} years old.`;
  return res;
}

const Dog = {
  name: "Sugary",
  age: 10,
};

Function.prototype.createApply = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  context = handleType(context); // 类型转换

  context.fn = this;

  let result;

  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }

  delete context.fn;
  return result;
};

// 数据类型转化
function handleType(context) {
  const type = typeof context;
  if (context === undefined || context === null) {
    context = "window";
  }
  switch (type) {
    case "window":
      context = window;
      break;
    case "number":
      context = Number(context);
      break;
    case "string":
      context = String(context);
      break;
    case "boolean":
      context = Boolean(context);
      break;
    default:
      context = context;
  }
  return context;
}

console.log(showInfo.createApply(Dog)); // My name is Sugary, I am 10 years old.

在ECMAScript 3和非严格模式中,传入call的第一个参数,如果传入的值为null或者undefined都会被全局对象代替,而其他的原始值则会被相应的包装对象所替代。所以我们可以对传入的参数类型做处理,如上述的 handleType() 方法。

call() 方法的实现和 apply() 方法的实现差不多,只是传入的参数格式不一样,我们来看看源码怎么实现

function showInfo() {
  let res = `My name is ${this.name}, I am ${this.age} years old.`;
  return res;
}

const Dog = {
  name: "Sugary",
  age: 10,
};

Function.prototype.createCall = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  context = handleType(context);

  context.fn = this;

  let result;
  let args = [...arguments].slice(1);

  if (args) {
    result = context.fn(...args);
  } else {
    result = context.fn();
  }

  delete context.fn;
  return result;
};

// 数据类型转化
function handleType(context) {
  const type = typeof context;
  if (context === undefined || context === null) {
    context = "window";
  }
  switch (type) {
    case "window":
      context = window;
      break;
    case "number":
      context = Number(context);
      break;
    case "string":
      context = String(context);
      break;
    case "boolean":
      context = Boolean(context);
      break;
    default:
      context = context;
  }
  return context;
}

console.log(showInfo.createCall(Dog)); // My name is Sugary, I am 10 years old.

和 apply(), call() 方法不同的是,bind() 方法并没有在改变 this 指向的时候执行函数,而是返回一个绑定上下文的函数,我们来看一个🌰

function showInfo() {
  let res = `My name is ${this.name}, I am ${this.age} years old.`;
  return res;
}

const Dog = {
  name: "Sugary",
  age: 10,
};

const res = showInfo.bind(Dog);
console.log(res()); // My name is Sugary, I am 10 years old.

由上面的代码可知,bind() 方法实现了以下功能:

  1. 绑定this;
  2. 传入参数;
  3. 返回一个函数;
  4. 函数柯里化。

接下来我们来看看如何实现:

function ShowInfo() {
  let res = `My name is ${this.name}, I am ${this.age} years old.`;
  return res;
}

const Dog = {
  name: "Sugary",
};

Function.prototype.createBind = function (context) {
  if (typeof this !== "function") {
    throw new Error(
      "Function.prototype.bind - what is trying to be bound is not callable"
    );
  }

  let _this = this;
  let args = [...arguments].slice(1);

  return function () {
    return _this.apply(context, args.concat([...arguments]));
  };
};

const res = ShowInfo.createBind(Dog);
console.log(res()); // My name is Sugary, I am undefined years old.

但是 bind() 方法还有一个新特性,我们来看一个例子:

const name = "Kara";
const Dog = {
  name: "Sugary",
};

function Animals(age) {
  this.habit = "shopping";
  console.log(this.name);
  console.log(age);
}

Animals.prototype.friend = "kevin";

let bindFun = Animals.bind(Dog);

let obj = new bindFun(20);

// undefined
// 20

obj.habit; // shopping

obj.friend; // kevin

上面例子中,运行结果 this.name 输出为 undefined ,这不是全局 name 也不是 Dog 对象中的 name ,这说明 bind 的 this 对象失效了,new 的实现中生成一个新的对象,这个时候的 this 指向的是 obj 。但是柯里化依然有效。

可见 new 对 this 的影响比 bind 优先级要高。

我们来看看如何实现:

const name = "Kara";
const Dog = {
  name: "Sugary",
};

function Animals(age) {
  this.habit = "shopping";
  console.log(this.name);
  console.log(age);
}

Animals.prototype.friend = "kevin";
let bindFun = Animals.createBind(Dog);

let obj = new bindFun(20);

console.log(obj);

// undefined
// 20

obj.habit; // shopping

obj.friend; // kevin

Function.prototype.createBind = function (context) {
  // 调用 bind 的不是函数,需要抛出异常
  if (typeof this !== "function") {
    throw new Error(
      "Function.prototype.bind - what is trying to be bound is not callable"
    );
  }

  let _this = this;
  let args = [...arguments].slice(1);

  let fNOP = function () {};
  let fBound = function () {
    let bindArgs = [...arguments];
    return _this.apply(
      fNOP.prototype.isPrototypeOf(this) ? this : context,
      args.concat(bindArgs)
    );
  };

  if (this.prototype) {
    fNOP.prototype = this.prototype;
  }

  fBound.prototype = new fNOP();
  return fBound;
};