likes
comments
collection
share

浅解析call、apply 和 bind

作者站长头像
站长
· 阅读数 18

在各种的源码中,都能够看到对于他们的使用的。但是平时做业务的事情却很少接触到。这就导致了很多人对它们有一种神秘感,看了忘,面试前再看,面试完之后接着忘记。

反正我是这样的。所以想要真正的记住它,平时就有机会就使用,没有机会也有创造机会使用,只有能够面对codeReview。

定义

fn.call(obj, param1, param2, ...)
fn.apply(obj, [param1,param2,...])
fn.bind(obj, param1, param2, ...)

通过什么这个例子能够很明显的看出来,使用三者都是通过.来使用的。那么是不是可不可以使用[]来调用呢?

function test() {
    console.log(1)
}

test['call']() // 1

答案是可以肯定的。

所以说,它们就是对象的三个方法而已。和自己创建的对象的方法没有什么不同。只是我们平时没有直接看到该对象上面的方法而已。如下图:

浅解析call、apply 和 bind

  • Function.prototype.call()
  • Function.prototype.bind()
  • Function.prototype.apply()

综上所述:call、apply 和 bind 是挂在 Function 对象上的三个方法,所以调用这三个方法的必须是一个函数。

有了这个认知,我们回到第一个代码片段。

  • fn: 需要调用的函数名,Function的实例。
  • obj: 当前作用域中的对象,一般使用的时候是this所指向的对象。
  • param: 实参,没有可不传。

三者的作用

三者和直接调用函数的区别

这个区别很重要。了解它们之间的区别,其实是为了了解它们存在的意义,为什么委员会会将这三个放到Function上面,它们解决了什么痛点?

call() 允许为不同的对象分配和调用属于一个对象的函数/方法。 call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。 - 来自MDN Function.prototype.call()

简单的来说,提供了可以改变当前函数的this

如果第一个参数为为null的话,那么它们没有任何不同,如下代码片段片段:

function A(name) {
    this.name = name
    console.log('I am A')
}

function B(){
    A.call(null, '砂糖橘加盐')
}

const b = new B()
console.log(b) // B {}

执行的结果是空对象,打印出I am A

而当null -> this,之后:

function B() {
    A.call(this, '砂糖橘加盐')
}
const b = new B()
console.log(b) // B { name: '砂糖橘加盐' }

并且也成功打印出console的结果。还'意外'的让B的实力的对象挂载了A构造函数的this对象。也就是之前说的,它能够改变当前函数的this

改变了之后又有什么作用呢?

我们可设想这么一个场景,当很多对象都存在相同的属性的时候,如果给每个构造函数都要添加一个的属性。如果可以抽离出来的话,我们是不是可以少写很多代码。如下代码片段:

function B() {
  this.name = '狗不理'
  A.call(this, 'wyc')
}

function C() {
  A.call(this, 'wyc')
}
function D() {
  A.call(this, 'wyc')
}
function E() {
  A.call(this, 'wyc')
}

这就是MDN说的继承。

而直接调用函数的话,做不到。这就是call、apply、bind存在的意义。

call、apply、bind三者的区别

这三个方法共有的、比较明显的作用就是,都可以改变函数 func 的 this 指向

call、apply两者和bind的区别:返回的结果不一样,bind返回的是Function类型。如下:

const a = {
  name: '砂糖橘放盐',
  getName: function(msg) {
    return msg + this.name;
  } 
}
const b = {
  name: '橘子哥'
}
console.log(a.getName('你好,'));  // 你好, 砂糖橘放盐

const name = a.getName.bind(b, '你好,');
console.log(name());  // 你好, 橘子哥

call 和 apply 的区别在于:传参的写法不同

call的参数是列表,apply的则是数组形式。

console.log(a.getName.call(b, '你好,'));  // 你好, 橘子哥
console.log(a.getName.apply(b, ['你好,']))  // 你好, 橘子哥

以上就是他们的区别,他们的实际使用场景是什么呢?

使用场景

判断数据类型

toString()最为完善的类型判断,相比instanceofconstructortypeof各有各自的缺陷而言。

除了Object对象,其他的类型由于存在隐式转化的关系,需要通过call/apply来调用,如下代码片段:

Object.prototype.getType = function() {
  let type  = typeof this
  if (type !== "object") {
    return type
  }
  return Object.prototype.toString.call(this).replace(/^$/, '$1')
}

const a = '1'
const b = a.getType()
console.log(b) // [object String]

可以直接放到Object.prototype上面,更加的方便。虽然在工作中不推荐。

类数组借用方法

const arrayLike = { 
  0: '西瓜',
  1: '草莓',
  length: 2
}

Array.prototype.push.call(arrayLike, '橘子', '老虎')

console.log(typeof arrayLike) // 'object'

console.log(arrayLike)

// {0: "西瓜", 1: "草莓", 2: "橘子", 3: "老虎", length: 4}

继承

实现继承的方法有很多,这么只是简单说一下,其他更多的可以查看我之前的文章。

那么下面我们结合着这一讲的内容再来回顾一下组合继承方式,代码如下。

  function Parent () {
    this.name = 'parent'
  }
  Parent.prototype.getName = function () {
    return this.name
  }
  function Child() {
    Parent.call(this)
    this.type = 'child'
  }
  
  Child.prototype = new Parent()
  Child.prototype.constructor = Child
  const s1 = new Child()
  
  console.log(s1.getName());  // 'parent'