加载中...

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

吴佳
2020-08-13 17:47:46
分类:JavaScript
232
0
0

前言

不管在写插件,还是写框架,又或者其它业务开发中。我们都会遇到在执行函数的时候,如果需要保证函数内部this不被污染或者说需要使函数内部this指向到指定对象的时候,都会按情况分别使用到call、apply、bind方法来实现需求。
那么,你知道它们三者之间的区别吗?又分别如何实现的呢?接下来,请准我一一道来并分别实现它们吧~

正文

call、apply、bind的区别

bind

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

用一个例子来理解一下吧

let obj = {
  name: 'wujia',
  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: 'wujia',
  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: 'wujia',
  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后的函数特性。

结语

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

扫码关注后,回复“资源”免费领取全套视频教程

前端技术专栏

0

发表评论(共0条评论)

请输入评论内容
啊哦,暂无评论数据~