最详细的apply、call、bind手写实现教学

1、call

Function.prototype.call

语法

fun.call(thisArg[,arg1[,arg2[, ...]]])

参数

thisArg

在fun运行时指定的this值。

arg1, arg2, ...

给到fun的参数列表(每个参数都写出来)。

返回值
使用调用者提供的this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined

2、apply

Function.prototype.apply

语法

fun.apply(thisArg, [argsArray])

参数

thisArg

在fun运行时指定的this值。

[argsArray]

给到fun的参数数组(将参数装成一个数组)。

返回值
使用调用者提供的this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined

3、apply、call的区别

apply和call所做的事情都相同,那就是改变函数内部的this指向并调用它

唯一的区别在于调用时所传递给被调用函数的参数的书写形式

call传递的参数以逗号分隔;apply传递的参数为数组形式

4、手写实现apply

apply手写实现很简单,思路如下:

  • 检查调用apply的对象是否为函数
  • 将函数作为传入的context对象的一个属性,调用该函数
    不要忘了调用之后删除该属性
    代码如下:
Function.prototype.apply = function (context, args) {
  // 检查调用```apply```的对象是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('not a function')
  }

  // 将函数作为传入的```context```对象的一个属性,调用该函数
  const fn = Symbol()
  context[fn] = this
  context[fn](...args)

  // 不要忘了调用之后删除该属性
  delete context[fn]
}

详解:

手写实现该API的核心知识点是关于this的指向确认
apply的语法为fun.apply(thisArg, [argsArray]),我们知道如果有诸如obj.func形式的函数调用,那么这里func内部的this就是指向obj的。所以我们这里书写的this,其实就是将来以func.bind()形式使用bind时那个被调用的函数func。所以第一步如何写就解决了。

第二步,利用第一步的那个关于this的知识点,我们将被调用函数this作为传入对象的属性进行调用,就能让被调用函数内部的this指向该对象。如此一来我们就完成了该API最大的功能**,改变被调用函数内的this指向**。

我在这里使用到了新的Symbol数据类型,主要是避免在把函数赋值给context对象的时候,因为属性名冲突而覆盖掉原有属性。至于为什么使用Symbol作为属性名不会发生冲突,可以看看阮一峰大大的解释ECMAscript-Symbol。

第三步,删掉该属性,避免对传入对象造成污染。

5、手写实现call

call和apply唯一区别就是传给被调用函数的参数写法不同,这里只贴个代码,不多写废话了。

代码如下:

Function.prototype.call = function (context, ...args) {
  // 检查调用```apply```的对象是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('not a function')
  }

  // 将函数作为传入的```context```对象的一个属性,调用该函数
  const fn = Symbol()
  context[fn] = this
  context[fn](...args)

  // 不要忘了调用之后删除该属性
  delete context[fn]
}

没错,比apply多了三个点。

new操作符
在手写实现bind之前,我们必须先掌握另一个关键字的——new操作符的手写实现。

当然,手写实现new操作符也是常见面试题之一。

我们可以看看MDN-new上对new操作符的介绍。

new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

创建一个空的简单对象{};
将这个空对象的构造函数指定为new操作符操作的函数(即定义中说的另一个对象)。其实就是原型链绑定;
将操作符操作的函数的this指向步骤1创建的空对象;
运行该函数,如果该函数没有返回对象,则返回this。
看下代码:

function myNew (fn, ...args) {
  // 第一步,创建一个空的简单JavaScript对象(即{});
  let obj = {}

  // 第二步,原型链绑定
  fn.prototype !== null && (obj.__proto__ = fn.prototype)

  // 第三步,改变this指向并运行该函数
  let ret = fn.call(obj, ...args)

  // 第四步,如果该函数没有返回对象,则返回this
  // 别忘了 typeof null 也返回 'object' 的bug
  if ((typeof ret === 'object' || typeof ret === 'function') && ret !== null) {
    return ret 
  }
  return obj
}

6、bind

Function.prototype.bind

语法

function.bind(thisArg[,arg1[,arg2[, ...]]])

参数

thisArg

在fun运行时指定的this值。

arg1, arg2, ...

给到fun的参数列表。

返回值
返回一个原函数的拷贝并拥有指定的this值和初始参数

7、手写实现bind

bind和call、apply能力一样,都是改变某个函数内部的this指向

不同的是bind并不是立即调用该函数,而是返回一个原函数的拷贝

下面我们来一步一步实现一个bind

第一步
首先,看看bind做了些什么:bind返回一个改变了this指向的函数,该函数是原函数的拷贝,并且可以带入部分初始参数

Function.prototype.bind = function (context, ...outerArgs) {
  return (...innerArgs) => {
    this.call(context, ...outerArgs, ...innerArgs)
  }
}

实现很简单,我们返回一个函数,里面使用call更改this指向就好了

第二步
其实事情并没有这么简单,由于bind会返回一个函数,理所当然的可以对其使用new操作符

如果你对bind返回的函数使用new操作符,会发现有些问题

首先你会遇到报错

TypeError: thovinoEat is not a constructor

这是因为上面我用了箭头函数,new操作符无法改变this指向了

修改一下:

Function.prototype.bind = function (context, ...outerArgs) {
  let that = this;
  return function (...innerArgs) {
    that.call(context, ...outerArgs, ...innerArgs)
  }
}

接着我们来测试一下

// 声明一个上下文
let thovino = {
  name: 'thovino'
}

// 声明一个构造函数
let eat = function (food) {
  this.food = food
  console.log(`${this.name} eat ${this.food}`)
}
eat.prototype.sayFuncName = function () {
  console.log('func name : eat')
}

// bind一下
let thovinoEat = eat.bind(thovino)

let instance = new thovinoEat('orange') // thovino eat orange

console.log('instance:', instance) // {}

运行一下,你会发现好像有些问题。生成的实例居然是个空对象!

不要着急,一步一步分析一下为什么

还记得new干了哪些事情吗?

在new操作符执行时,我们的thovinoEat函数可以看作是这样:

function thovinoEat (...innerArgs) {
  eat.call(thovino, ...outerArgs, ...innerArgs)
}

在new操作符进行到第三步的操作thovinoEat.call(obj, …args)时,这里的obj是new操作符自己创建的那个简单空对象{},但它其实并没有替换掉thovinoEat函数内部的那个上下文对象thovino。这已经超出了call的能力范围,因为这个时候要替换的已经不是thovinoEat函数内部的this指向,而应该是thovino对象。

换句话说,我们希望的是new操作符将eat内的this指向操作符自己创建的那个空对象。但是实际上指向了thovino,new操作符的第三步动作并没有成功!

清楚这一点之后,我们就知道应该如何进行修改了:

Function.prototype.bind = function (context, ...outerArgs) {
  let that = this;
  function ret (...innerArgs) {
    if (this instanceof ret) {
      // new操作符执行时
      // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
      that.call(this, ...outerArgs, ...innerArgs)
    } else {
      // 普通bind
      that.call(context, ...outerArgs, ...innerArgs)
    }
  }

  return ret
}

第三步
用回第二步的测试代码,发现还有最后一个小问题没有解决,那就是eat.prototype.sayFuncName函数没有继承到。

要解决这个问题非常简单,只需要将返回的函数链接上被调用函数的原型就可以实现方法继承了:

Function.prototype.bind = function (context, ...outerArgs) {
  let that = this;

  function ret (...innerArgs) {
    if (this instanceof ret) {
      // new操作符执行时
      // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
      that.call(this, ...outerArgs, ...innerArgs)
    } else {
      // 普通bind
      that.call(context, ...outerArgs, ...innerArgs)
    }
  }

  ret.prototype = this.prototype

  return ret
}

版权声明:本文为weixin_45819276原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。