JS call、apply、bind手动实现

133 阅读8分钟

JavaScript中call、apply、bind的区别及手动实现

1. 三种方法的共同点

三者之间的共同点是它们都可以用来改变函数或者方法执行时this的指向。

2. 三种方法的区别

下面逐一讲解这三种方法及其手动实现,以帮助理解这三者的区别。

2. 1 call方法

call方法挂载于Function这一构造函数的原型对象上。该方法接受两个参数,一个是想要改变的this指向,另一个是被改变this指向的原函数的参数列表。

call方法具有以下特征 1. 挂载于Function的原型对象上 2. 以参数列表的形式传入原函数的参数 3. 临时改变this指向 4. 调用一次原函数 5. 如果新指向为null或者undefined,则指向全局对象 示例

  // 挂载于全局对象上
  var a = 1,
    b = 2

  let obj = {
    a: 10,
    b: 20
  }

  function f(c, d) {
    console.log(this)
    return this.a + this.b + c + d
  }

  // 直接调用该函数,执行者为全局对象
  console.log(f(1, 2)) // 打印出 全局对象 以及 6

  // 使用call改变this指向
  console.log(f.call(obj, 1, 2)) // 打印obj 以及33

  // call只会临时改变this指向,而非永久改变
  console.log(f(1, 2)) // 打印出 全局对象 以及 6

  // 新指向为null
  console.log(f.call(null, 1, 2)) // globalThis 6

  // 新指向为undefined
  console.log(f.call(undefined, 1, 2)) // globalThis 6

通过这个示例,我们看到

  1. call方法改变了f函数执行时的this指向。
  2. 在改变指向后,call方法直接调用了原函数。
  3. call方法只会临时改变this的指向,而非永久改变。

下面让我们来手动实现call方法

  // call方法具有以下特征
  // 1. 挂载于Function的原型对象上
  // 2. 以参数列表的形式传入原函数的参数
  // 3. 临时改变this指向
  // 4. 调用一次原函数
  // 5. 如果新指向为null或者undefined,则指向全局对象
  Function.prototype.myCall = function (thisArg, ...args) {
    // 1. 判断是否指向null或者undefined
    thisArg = thisArg || globalThis

    // 2. 这里的this就是原函数,因为调用时是这样的 f.myCall()
    // 普通函数中的this是谁调用就指向谁,所以这里的this就是原函数
    const fn = this

    // 3. 我们将原函数挂载到this的新指向中
    // 这里为了防止和新指向重名,所以使用Symbol
    const key = Symbol('fn')

    thisArg[key] = fn

    // 4. 调用一次原函数,只不过是让新指向来调用
    const res = thisArg[key](...args)

    // 5. 复原thisArg,因为call不会永久改变this指向
    delete thisArg[key]

    return res
  }
  // 挂载于全局对象上
  var a = 1,
    b = 2

  let obj = {
    a: 10,
    b: 20
  }

  function f(c, d) {
    console.log(this)
    return this.a + this.b + c + d
  }

  // 直接调用该函数,执行者为全局对象
  console.log(f(1, 2)) // 打印出 window 以及 6

  // 使用myCall改变this指向
  console.log(f.myCall(obj, 1, 2)) // 打印obj 以及33

  // myCall只会临时改变this指向,而非永久改变
  console.log(f(1, 2)) // 打印出 全局对象 以及 6

  // 新指向为null
  console.log(f.myCall(null, 1, 2)) // globalThis 6

  // 新指向为undefined
  console.log(f.myCall(undefined, 1, 2)) // globalThis 6

2.2 apply方法

apply方法也能够改变this的指向,它接受两个参数,一个是新指向,第二个是包含原函数参数的数组。该方法和call方法的使用很类似,它们具备很多相同的特征。

apply方法具有以下特征 1. 挂载于Function的原型对象上 2. 以数组的形式传入原函数的参数 3. 临时改变this指向 4. 调用一次原函数 5. 如果新指向为null或者undefined,则指向全局对象

可以看到和call的唯一不同就是传入参数的形式不同,apply要求将原函数的参数包含在一个数组中进行传递,而call要求以参数列表的形式来传递。

示例

  // 挂载于全局对象上
  var a = 1,
    b = 2

  let obj = {
    a: 10,
    b: 20
  }

  function f(c, d) {
    console.log(this)
    return this.a + this.b + c + d
  }

  // 直接调用该函数,执行者为全局对象
  console.log(f(1, 2)) // 打印出 window 以及 6

  // 使用apply改变this指向
  // 注意传递参数时使用数组
  console.log(f.apply(obj, [1, 2])) // 打印obj 以及33

  // apply只会临时改变this指向,而非永久改变
  console.log(f(1, 2)) // 打印出 全局对象 以及 6

  // 新指向为null
  console.log(f.apply(null, [1, 2])) // globalThis 6

  // 新指向为undefined
  console.log(f.apply(undefined, [1, 2])) // globalThis 6

通过这个示例,我们看到applycall的区别只在于传递参数的形式上。

现在让我们手动实现apply

  // apply方法具有以下特征
  // 1. 挂载于Function的原型对象上
  // 2. 以数组的形式传入原函数的参数
  // 3. 临时改变this指向
  // 4. 调用一次原函数
  // 5. 如果新指向为null或者undefined,则指向全局对象
  Function.prototype.myApply = function (thisArg, arr) {
    // 1. 判断是否指向null或者undefined
    thisArg = thisArg || globalThis

    // 2. 这里的this就是原函数,因为调用时是这样的 f.myApply()
    // 普通函数中的this是谁调用就指向谁,所以这里的this就是原函数
    const fn = this

    // 2. 我们将原函数挂载到this的新指向中
    // 这里为了防止和新指向重名,所以使用Symbol
    const key = Symbol('fn')

    thisArg[key] = fn

    // 4. 调用一次原函数,只不过是让新指向来调用
    const res = thisArg[key](...arr)

    // 5. 复原thisArg,因为call不会永久改变this指向
    delete thisArg[key]

    return res
  }
  // 挂载于全局对象上
  var a = 1,
    b = 2

  let obj = {
    a: 10,
    b: 20
  }

  function f(c, d) {
    console.log(this)
    return this.a + this.b + c + d
  }

  // 直接调用该函数,执行者为全局对象
  console.log(f(1, 2)) // 打印出 window 以及 6

  // 使用apply改变this指向
  // 注意传递参数时使用数组
  console.log(f.myApply(obj, [1, 2])) // 打印obj 以及33

  // apply只会临时改变this指向,而非永久改变
  console.log(f(1, 2)) // 打印出 全局对象 以及 6

  // 新指向为null
  console.log(f.myApply(null, [1, 2])) // globalThis 6

  // 新指向为undefined
  console.log(f.myApply(undefined, [1, 2])) // globalThis 6

2.3 bind方法

bind方法也可以改变this的指向。但是它和apply以及call具有很大的区别。

bind具有以下特征。

  1. 不会执行需要被改变this指向的方法
  2. 返回一个绑定了指定this的新方法,注意是永久绑定
  3. 将调用时传递的参数插入到新方法的参数列表中。

示例

  // 挂载于全局对象上
  var a = 1,
    b = 2

  let obj = {
    a: 10,
    b: 20
  }

  function f(c, d) {
    console.log(this)
    return this.a + this.b + c + d
  }

  console.log(f.bind(obj, 1, 2)) // 不会输出this和加值,而是会输出f函数

  let f1 = f.bind(obj, 1, 2) // bind返回一个新的函数

  console.log(f1()) // obj 33,可以看到绑定时传递的1和2被添加到新函数的参数列表前

  console.log(f1(5, 6)) // obj 33 函数的参数列表被绑定时传递的参数占满了,因此这里读不到新传的5和6

  console.log(f1.call(null)) // 尝试使用call方法改变新方法的this指向
  // 结果输出 obj 和 33 这说明call方法改变新方法的this指向失败

  console.log(f1.apply(null)) // 尝试使用apply方法改变新方法的this指向
  // 结果输出 obj 和 33 这说明apply方法改变新方法的this指向失败

  // 以上两个例子说明了bind绑定后的新方法的this指向被永久改变了
  // 不能通过call和apply进行改变
  // 即使我们通过重新绑定的方式也不能改变f1中this的指向
  let obj1 = {
    a: 10,
    b: 10
  }
  f1 = f1.bind(obj1, 1, 2)
  console.log(f1()) // obj 33

手动实现bind方法

需要利用闭包这一特性才能实现。

Function.prototype.myBind = function (thisArg) {
    // 保存thisArg
    const self = thisArg
    // 保存原函数
    const fn = this
    // 接受传递来的参数
    // 忽略第一个参数
    const contextArgs = Array.prototype.slice.call(arguments, 1)
    // 准备新函数以返回
    const bound = function () {
      const innerArgs = Array.prototype.slice.call(arguments)

      const finalArgs = contextArgs.concat(innerArgs)

      return fn.apply(self, [...finalArgs])
    }
    return bound
}

  function f(c, d) {
    console.log(this)
    return this.a + this.b + c + d
  }
  f.myBind(1, 2, 3)

  // 挂载于全局对象上
  var a = 1,
    b = 2

  let obj = {
    a: 10,
    b: 20
  }

  console.log(f.myBind(obj, 1, 2)) // 不会输出this和加值,而是会输出f函数

  let f1 = f.myBind(obj, 1, 2) // bind返回一个新的函数

  console.log(f1()) // obj 33,可以看到绑定时传递的1和2被添加到新函数的参数列表前

  console.log(f1(5, 6)) // obj 33 函数的参数列表被绑定时传递的参数占满了,因此这里读不到新传的5和6

  console.log(f1.call(null)) // 尝试使用call方法改变新方法的this指向
  // // 结果输出 obj 和 33 这说明call方法改变新方法的this指向失败

  console.log(f1.apply(null)) // 尝试使用apply方法改变新方法的this指向
  // // 结果输出 obj 和 33 这说明apply方法改变新方法的this指向失败

  // // 以上两个例子说明了bind绑定后的新方法的this指向被永久改变了
  // // 不能通过call和apply进行改变
  // // 即使我们通过重新绑定的方式也不能改变f1中this的指向
  let obj1 = {
    a: 10,
    b: 10
  }
  f1 = f1.myBind(obj1, 1, 2)
  console.log(f1()) // obj 33

修复小BUG

这里的myBind方法似乎是正确的,然而如果返回的新函数被当作构造函数来使用,即使用new关键字来构造新实例时就会有问题。因为新函数的this一直指向我们绑定的指向,而作为构造函数时,this应当指向当前正在生成的实例。我们对myBind进行修改。

Function.prototype.myBind = function (thisArg) {
    // 保存原函数
    const fn = this
    // 接受传递来的参数
    // 忽略第一个参数

    // === 构造原型链
    const F = function () {}
    F.prototype = this.prototype

    const contextArgs = Array.prototype.slice.call(arguments, 1)
    // 准备新函数以返回
    const bound = function () {
      const innerArgs = Array.prototype.slice.call(arguments)

      const finalArgs = contextArgs.concat(innerArgs)

      // 这里的this是新函数执行时的执行者,我们判断是不是作为构造函数来使用
      // 如果是就将函数的调用者交给this,否则就交给self
      return fn.apply(this instanceof F ? this : thisArg, finalArgs)
    }
    // 链接原型链
    bound.prototype = new F()
    return bound
}