手写篇之call apply bind

158 阅读6分钟

我们先弄明白这三个问题 是什么?怎么做?为什么?

1、是什么?

call, apply, bind 这三个api是什么?它们有什么区别?

简单来说它们是用来强制改变this 指向问题的api。

那么什么是this呢?

  • this是执行上下文中的一个属性
  • this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。
  • 这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

它们有什么区别?

call

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

apply

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数

bind

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

其实 apply 和 call 基本类似,他们的区别只是传入的参数不同。

call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。

// call
function.call(thisArg, arg1, arg2, ...)

// apply
func.apply(thisArg, [argsArray])

bind 是创建一个新的函数,我们必须要手动去调用

接受数组或者类数组

function.bind(thisArg[, arg1[, arg2[, ...]]])

2、怎么做?

怎么去实现这三个 api?

让我们来试着一一去实现

MyCall

先看看call 做了什么

// call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

var foo = {
    value: 1
};

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

bar() // undefined

bar.call(foo); // 1

我们直接调用bar时打印undefined 而当我们使用call 打印出了1,

直接调用 this 执行global,在global 中找不到value值,

而当我们使用call 时,this 指向了foo 对象

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

// var foo = {
//     value: 1
// };

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

// bar() // undefined

// bar.call(foo); // 1

// 那么call的功能有哪些呢?
// 1、改变this指向
// 2、执行函数

// 那么我们试着来实现这两个功能
Function.prototype.myCall = function (context) {
  // call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
  // 根据定义我们先进行判断,如果不是函数的话我们直接抛出错误
  if (typeof this !== "function") {
    throw new TypeError("error this is not function!");
  }

  // 首先 context 为可选参数,如果不传的话默认上下文为 window
  context = context || window;

  // 接下来给contexnt创建一个fn 属性,并将值设置为需要调用的函数 (也就是this)
  context.fn = this;
  // 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
  const args = [...arguments].slice(1);
  // 然后调用函数,并将这个创建的fn属性删除
  const result = context.fn(...args);

  delete context.fn;

  return result;
};

let obj = {
  value: "call",
};

function getValue(name, age) {
  console.log(name, age); // '清风' 18
  console.log(this.value); // call
}

getValue.myCall(obj, "清风", 18);

MyApply

apply 的实现跟 call 类似,只在处理参数时不同

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

Function.prototype.myApply = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("error this is not function");
  }

  // context 不存在时指向window
  context = context || window;

  // 在context上创建属性fn 指向函数this
  context.fn = this;

  // 获取参数值
  let result;

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

  // 删除fn属性
  delete context.fn;

  return result;
};

let obj = {
  value: "apply",
};

function getValue(name, age) {
  console.log(name, age); // '清风' 18
  console.log(this.value); //apply
}

getValue.myApply(obj, ["清风", 18]);

MyBind

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

bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题

1、bind返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new的方式,我们先来说直接调用的方式

2、对于直接调用来说,这里选择了apply的方式实现,但是对于参数需要注意以下情况:因为bind可以实现类似这样的代码f.bind(obj, 1)(2),

所以我们需要将两边的参数拼接起来,于是就有了这样的实现args.concat(...arguments)

3、最后来说通过new的方式,在之前的章节中我们学习过如何判断this,对于new的情况来说,不会被任何方式改变this,

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

// bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题

// 1、bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
// 2、对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),
// 所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)
// 3、最后来说通过 new 的方式,在之前的章节中我们学习过如何判断 this,对于 new 的情况来说,不会被任何方式改变 this,
// 所以对于这种情况我们需要忽略传入的 this

Function.prototype.myBind = function (context) {
  if (typeof this !== "function") {
    throw new TypeError("error this is not function");
  }
  //   返回一个新的函数,这里我们就不用想call和apply一样创建fn属性了
  const _this = this;

  //   获取参数,可以一次行传,也可以在调用时候传参数
  const args = [...arguments].slice(1);

  //   bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
  return function F() {

    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments);
    }
    return _this.apply(context, args.concat(...arguments));
  };

};

let obj = {
  value: "bind",
};

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

const bind = getValue.myBind(obj, "清风")(18);
// bind('18')
// 清风 18 bind

3、为什么?

为什么一下出现三种类似的api?它们分别解决了什么问题?

this 指向问题在开发中容易造成一下问题,而使用call, apply, bind 进行显示绑定的话,能够方便理解以及解决this 指向不确定问题,而之所以会出现三种不同的api 当然也是为了应对不同的场景;也就是对应着三者的区别