你知道什么是call、apply、bind?

3,183 阅读5分钟

前言

在平时面试中,call、apply、bind这三个得用法还是会经常问的,比如三者得区别,作用,或者手写call、apply、bind,因此整理了一下

call、apply、bind作用

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

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

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

这三个解释来自MDN,我们可以发现这三个函数都和this有关系,也就是改变函数运行时this得指向。

举个栗子

我们先来看一下this指向的代码

function Person() {}
Person.prototype = {
  name: 'lee',
  showName: function () {
    console.log(this.name)
  },
}
Person.prototype.showName() 

上面得代码中调用showName()方法会打印出 lee ,因为this得指向在这里指向得是 Person这个对象,这个例子很简单吧,再往下看

function Person() {}
Person.prototype = {
  name: 'lee',
  showName: function () {
    console.log(this.name)
  },
}

Person.prototype.showName()

let obj = {
  name: 'herry',
}

我们新增了一个对象字面量,也想打印出name这个变量,怎么办呢?难道我们在直接写一个consloe.log()吗?这多麻烦,那我们可不可以通过Person的showName方法来调用呢?直接复用showName()方法不就好了吗?答案是当然可以。

function Person() {}
Person.prototype = {
  name: 'lee',
  showName: function () {
    console.log(this.name)
  },
}

Person.prototype.showName() //lee

let obj = {
  name: 'herry',
}

Person.prototype.showName.call(obj) //herry
Person.prototype.showName.apply(obj) //herry
Person.prototype.showName.bind(obj)() //herry

上述例子我们就已经看出来call、apply、bind的用法了,他们的作用就是动态改变了上下文,也就是改变了this的指向。

call、apply、bind区别

上面的例子中我们分别通过call、apply、bind来实现想要的效果,结果也都是一模一样的,难道就没有一点区别吗?肯定有区别的,要不然设计ES方案的也都是吃饱了没事干的主,那有什么区别呢?从MDN给出的解释中我们也可以找到去别的。

三者区别

  • call和apply 改变了函数的this上下文之后便立即执行函数,bind则是返回改变了上下文后的一个函数。

    也就是call 和apply 立即执行,bind不立即执行

  • call和apply基本类似,但是他们立即传入的参数不一样,call方法接收的时若干个参数列表,apply接收的时一个包含多个的参数的数组

举个栗子

求数组中最大值和最小值

var arr = [34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687]
//apply接收数组
Math.max.apply(Math, arr)
//call接收若干个参数
Math.max.call(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)
//bind不会立即执行  要加上()去执行函数
Math.max.bind(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)()

这个栗子很好的看出来了三者的区别。

手写call、apply、bind

我们已经了解了call、apply、bind的作用,也知道了三者的区别,那我们再进一步去手写三者函数,话不多说,搞起来。

手写call

我们在来看一下call的定义

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

也就是说call接收多个参数,最重要的是第一个参数是this,其他的都是函数的参数了。

我们根据这个理解和上面的例子先来写一下

Function.prototype.myCall = function (context) {
  //这里得this指向得showName函数
  console.log(this)
  //context指得就是传进来得obj对象,也就是我们指定得this值
  console.log(context)
}
Person.prototype.showName.myCall(obj)

这里我们先想一下,为什么this的指向指得是showName函数对象呢?

首先我们要明白this的指向,this永远指向最后调用它的那个对象,Person.prototype.showName.myCall(obj)

myCall最后就是被showName方法来调用的,明白了吗?下面我们继续

Function.prototype.myCall = function (context) {
  //这里得this指向得showName函数
  console.log(this)
  //context指得就是传进来得obj对象,也就是我们指定得this值
  console.log(context)
  //传输obj的对象上添加调用的方法
  context.fn = this
  //执行fn
  context.fn()
}

Person.prototype.showName.myCall(obj)  //herry

awesome!这么简单的吗? 其实并不是,主要是我们上文这个例子太理想化了,考虑得并不全面,就拿我们上述求数组得最大值和最小值用这个方法都通不过。

  1. 要考虑call传递参数个数得问题,可能是多个,也可能不传递参数
  2. 如果是多参数要把参数传给扩展方法
Function.prototype.myCall = function (context) {
  //如果没有参数context指向得是window
  context = context || window
  //传输obj的对象上添加调用的方法,这里this得指向是max
  context.fn = this
  //处理参数 去除第一个参数this 其它传入fn函数
  let arg = [...arguments].slice(1)
  //执行fn
  let result = context.fn(...arg)
  //删除fn
  delete context.fn
  //返回执行结果
  return result
}
console.log(Math.max.myCall(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)) //687

手写apply

上文中我们已经说了 apply和call作用其实是一样的,就是传递参数不一样,apply传递得是参数,所以稍微修改一下即可

Function.prototype.myApply = function (context) {
  //如果没有参数context指向得是window
  context = context || window
  //传输obj的对象上添加调用的方法,这里this得指向是max
  context.fn = this
  let result
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

console.log(Math.max.myApply(Math, [34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687])) //687

手写bind

bind返回得是函数,不立即执行

Function.prototype.myBind = function (context) {
  //返回一个绑定得this,保存this
  let _this = this
  let arg = [...arguments].slice(1)
  //返回一个函数
  return function F() {
    // 处理函数使用new的情况
    if (this instanceof F) {
      return new _this(...arg, ...arguments)
    } else {
      // 返回函数绑定this,传入两次保存的参数
      //考虑返回函数有返回值做了return
      return _this.apply(context, arg.concat(...arguments))
    }
  }
}
console.log(Math.max.myBind(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)())