手写call、apply、bind实现及详解

911 阅读4分钟

2021_08_26_08_27_IMG_0006.JPG

无垢清净光,慧日破诸暗,能伏灾风火,普明照世间。 --法华经

介绍

在function的原型上有三个方法 call apply bind,所有函数都是Function的实例,所以所有的函数都可以调用这三个方法,而这三个方法都是用来改变this指向的

三者特征

call、apply、bind三者都是用来重定向this指向问题的,但是又有一定的不同。

call

  • 功能: 调用函数 + 改变this指向
  • 参数:
    第一个参数 :用来 设置this的指向,如果指定了 null 或者 undefined 则内部 this 指向 window
    剩余参数 : 对应函数的参数
  • call的返回值就是函数的返回值

apply

  • 功能: 调用函数 + 改变this指向
  • 第一个参数 : 设置函数内部this的指向

第二个参数 : 是数组

  • call的返回值就是函数的返回值

bind

  • bind函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
  • 当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
  • 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

调用举例:

toString.call([],1,2,3);
toString.apply([],[1,2,3]);

哪个性能会好一点

参数在三个以内,call 和 apply 的性能差不多,

参数超过三个,call 的性能要比 apply 的好一点

call

我们先来梳理call做了什么

var foo = {
    value: 1
};

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

bar.call(foo); // 1

输出结果是:

我们发现,这里call方法做了两件事

  1. this指向了对象foo
  2. 执行力bar函数

这里是call方法最核心的两个功能

可以理解给,把bar方法绑定到了foo上,由foo调用了一次,然后再删除这个对象,以免对foo产生干扰。根据这个思路,我们来写一下实现

V1-基本实现

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

const foo = {
  value: 1
};

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

bar.call1(foo); // 1

V2-多种情况考虑

发现我们确实实现了这个功能,打印的结果和call一样,那么我们给call方法不仅传递了要绑定的对象,还传递了参数。这里汇总一下可能的情况

  1. 函数有参数或者参数为空
  2. 函数有返回值

最终版本如下

Function.prototype.call2 = function(context) {
  // 这里是为了考虑到context为空的情况,为空则设置为Window对象
  context = context || window;
  // 这一步是取出arguments类数组对象除要绑定的对象外的参数,在本🌰中为  "努力", 18
  let arg = [...arguments].slice(1);
  // 这里的this是调用call2的对象,也就是bar
  context.fn = this;
  // 执行bar函数
  let res = context.fn(...arg);
  // 删除bar,避免对foo产生影响
  delete context.fn;
  // 返回函数的返回值
  return res;
};

const foo = {
  value: 1
};

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

bar.call2(foo, "努力", 18); 

apply

apply的实现方式和call几乎一模一样,唯一不同的是前者的参数是数组

由于思路一样,就不再赘述,实现方式如下

Function.prototype.apply1 = function (context, arg) {
  context = Object(context) || window;

  context.fn = this;

  let result;
  if (arg) {
    result = context.fn(...arg);
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
};

bind

bind的实现是三者里面最难的,因为他不仅要绑定this,他的返回值还能作为构造函数,来使用new关键字创建实例

所以,我们来梳理一下bind

  1. 绑定this
  2. 和call、apply相比,不执行函数
  3. 返回值可以用做构造函数

根据这些要求,我们来做一下简单的实现

Function.prototype.bind2 = function(context) {

  const self = this; //this 代表的是调用bind的对象

 return function() {

    return self.apply(self);
  };
};

测试一下:

const value = 2;

const foo = {
  value: 1
};

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

bar.prototype.friend = "kevin";

const bindFoo = bar.bind2(foo, "daisy");
// bindFoo('18');
const obj = new bindFoo("18");

结果OK

加点难度

再把构造函数等需求加进来

Function.prototype.bind2 = function(context) {

  const self = this; //this代表的是调用bind的对象
  const args = Array.prototype.slice.call(arguments, 1);

  const fNOP = function() {
  };

  const fBound = function() {

    const bindArgs = Array.prototype.slice.call(arguments);
    // 这里使用this instanceof fNOP会为true是因为,fBound.prototype = new fNOP();这句代码,相当于fNOP位于fBound的原型链上,
    // 原直接将 fBound. prototype = this.prototype,我们直接修改 fBound.prototype 的时候,
    // 也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转
    return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
  };

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

引用

JavaScript深入之call和apply的模拟实现 · Issue #11 · mqyqingfeng/Blog

JavaScript深入之bind的模拟实现 · Issue #12 · mqyqingfeng/Blog

this、apply、call、bind - 掘金