解析call、apply、bind三者区别及实现原理


前言


不管在写插件,还是写框架,又或者其它业务开发中。我们都会遇到在执行函数的时候,如果需要保证函数内部this不被污染或者说需要使函数内部this指向到指定对象的时候,都会按情况分别使用到call、apply、bind方法来实现需求。

那么,你知道它们三者之间的区别吗?又分别如何实现的呢?接下来,请准我一一道来并分别实现它们吧~


正文


call、apply、bind的区别

bind

bind与call或apply最大的区别就是bind不会被立即调用,而是返回一个函数,函数内部的this指向与bind执行时的第一个参数,而传入bind的第二个及以后的参数作为原函数的参数来调用原函数。

用一个例子来理解一下吧

let obj = {

  name: 'gujianwen',

  fn: function (a, b, c) {

    console.log(this.name, a, b, c)

  }

}

window.name = '谷建文'

let nFn = obj.fn.bind(window, '第一个参数')

nFn('第二个参数', '第三个参数') 

// 最后输出:谷建文,第一个参数,第二个参数,第三个参数

根据以上例子,不难看出,我们把obj.fn函数内部this改变成window了,所以this.name的输出实际就是获取window上面的name属性。但这里要注意的是参数方面,我这么写是为了让大家更容易看清楚,我们在bind的时候只传入了一个参数,然后在执行这个bind之后的新函数(这里后面就称之为绑定函数)又传入了两个参数,其实这中间有一个过程就是参数合并,合并后的顺序就是相当于把bind执行的第二参数及之后参数与新绑定函数参数做了一个合并,新绑定函数参数会基于bind方法函数第二参数及之后参数结束位置开始进行合并。当然,如果知道柯里化的同学,就会发现好像有点柯里化的感觉,对吧。

还需要注意的一个地方,就是通过new关键字去实例这个绑定函数时,也就是通过new的方式创建一个对象,bind()函数在this层面上是没有效的,但是在参数层面上是有效的。

同样,用一个例子理解一下吧

let obj = {

  name: 'gujianwen',

  fn: function (a, b, c) {

    this.age = 20

    console.log(this.name, a, b, c)

  }

}

window.name = '谷建文'

let nFn = obj.fn.bind(window, '第一个参数')

new nFn('第二个参数','第三个参数')

// 最后输出结果:Undefined,第一个参数,第二个参数,第三个参数

根据上面例子的输出可以看到,我们通过bind为fn函数重新指定了this,this指向了window却并没有生效,但是参数生效了,都打印出来了。fn函数内部打印的this.name为Undefined的原因是因为this通过new关键字去实例化绑定函数的时候,因为bind方法内部做了特殊处理,这个处理可以看作成过滤了当前bind的本次this指向操作,让this指向就指向与现在自己。所以我们通过new去实例化对象的时候,实际上就是去new obj.fn() 而fn内部this指向的就是当前实例化对象,所以再从实例化对象上面去找name属性是肯定找不到的,但是一定会有一个age属性在里面。

call & apply

call、apply其实都是为了改变某个函数运行时的上下文而存在的,简单点说就是为了改变某个运行时函数内部this指向。

call、apply的调用会直接返回函数的执行结果。

使用call或者apply方法,它们第一个参数,都是设置函数内部this需要指向的目标。而区别就在于后续参数传递的不同,apply第二参数需要是一个参数数组,call的第二参数及其之后的参数需要是数组里面的元素。

其实可以看做成,apply第二参数需要一个聚合的参数数组列表,而call的第二参数及其之后的参数都需要展开数组挨个传递。

用个例子理解一下

let obj = {

  name: 'gujianwen',

  fn: function (a, b, c) {

    this.age = 20

    console.log(a, b, c)

    return this.name

  }

}

window.name = '谷建文'

const name1 = obj.fn.call(window, '第一个参数', '第二个参数', '第三个参数')

const name2 = obj.fn.apply(window, ['第一个参数', '第二个参数', '第三个参数'])

// 两个方法的打印输出:第一个参数, 第二个参数, 第三个参数 

// name1 & name2 值都为谷建文

需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向window。如果指定为数字或字符串或者布尔值的this值,则会指向该值的包装对象。

请看以下例子

function fn () {

    console.log(this)

}

// call方法的输出与apply一致

fn.apply(undefined) // window

fn.apply(null) // window

fn.apply('') // String {""}

fn.apply(1) // Number {1}

fn.apply(true) // Boolean {true}

call、apply、bind的实现

call

Function.prototype.call = function (context) {

  // 基础类型转包装对象

  if (context === undefined || context === null) {

    context = window

  } else {

    context = new Object(context)

  }

  // 保存原函数至指定对象的fn属性上

  context.fn = this

  // 获取除第一个参数之后的所有参数

  const args = Array.from(arguments).slice(1)

  // 通过指定对象的fn属性执行原函数并出入参数

  const fnValue = context.fn(...args)

  delete context.fn // 从context中删除fn原函数

  return fnValue

}

apply

Function.prototype.apply = function (context, arr) {

  // 基础类型转包装对象

  if (context === undefined || context === null) {

    context = window

  } else {

    context = new Object(context)

  }

  // 非对象,非undefined,非null的值才会抛错

  if (typeof arr !== 'object' && typeof arr !== 'undefined' && typeof arr !== 'null') throw new TypeError('CreateListFromArrayLike called on non-object')

  arr = Array.isArray(arr) && arr || [] // 非数组就赋值空数组

  // 保存原函数至指定对象的fn属性上

  context.fn = this

  // 通过指定对象的fn属性执行原函数并出入参数

  const fnValue = context.fn(...arr)

  delete context.fn // 从context中删除fn原函数

  return fnValue

}

bind

Function.prototype.bind = function (context) {

  // 保存原函数

  const ofn = this

  // 获取除第一个参数之后的所有参数

  const args = Array.from(arguments).slice(1)

  function O() {}

  function fn() {

    // 第一个参数的判断是为了忽略使用new实例化函数时让this指向它自己,否则就指向这个context指定对象

    // 第二个参数的处理做了参数合并, 就是 bind & fn 两个函数的参数合并

    ofn.apply(this instanceof O ? this : context, args.concat(Array.from(arguments)))

  }

  O.prototype = this.prototype

  fn.prototype = new O()

  return fn

}

如果new这个bind之后return的fn函数,this就会指向一个空对象,这个空对象的原型就会指向构造器的prototype。那么此时this instanceof O 就为true,所以返回的this就是当前被实例化的对象;这样就会忽略掉bind方法的this指向,实现上述new一个bind后的函数特性。


结语


以上就是这次总结的全部内容,如果当中总结的有问题;欢迎各位指教,一起讨论~

点击查看所有示例集合

转载请务必注明出处,欢迎分享

前端资料库 2019-01-15 14:12:28 通过 网页 浏览(951)

共有0条评论!

发表评论