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

前端 0 1296
小小草
小小草 2020年8月15日 08:26 发表
摘要:不管在写插件,还是写框架,又或者其它业务开发中。我们都会遇到在执行函数的时候,如果需要保证函数内部this不被污染或者说需要使函数内部this指向到指定对象的时候,都会按情况分别使用到call、apply、bind方法来实现需求。
前言

不管在写插件,还是写框架,又或者其它业务开发中。我们都会遇到在执行函数的时候,如果需要保证函数内部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) // windowfn.apply(null) // windowfn.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 if (typeof context === 'string') {    context = new String(context)  } else if (typeof context === 'number') {    context = new Number(context)  } else if (typeof context === 'boolean') {    context = new Boolean(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 if (typeof context === 'string') {    context = new String(context)  } else if (typeof context === 'number') {    context = new Number(context)  } else if (typeof context === 'boolean') {    context = new Boolean(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后的函数特性。


摘自:https://my.oschina.net/u/4153418/blog/4494166


点赞 0 收藏(0)    分享
相关标签: javascript
问题没解决?让chatGPT帮你作答 智能助手
0 个评论
  • 消灭零评论