手写bind_可能是最详细的call、apply、bind实现教学

cc654f85e1cb702947ae47a622b9c51f.png

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

最近在看一些手写实现的文章。
只要是涉及到手写实现的,一定会写到call、apply、bind。
call和apply原理比较简单,所以没什么问题。但是bind我看很多人都只是贴了代码,但并没有解释清楚如何写,为什么这么写以及思路。
于是今天来写一篇和this有关的这三个api的文章。

ps: 该文章内容主要以API面试为主,大牛可以略过。

call和apply

Function.prototype.call

语法

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

参数

thisArg

在fun运行时指定的this值。

arg1, arg2, ...

给到fun的参数列表。

返回值

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

Function.prototype.apply

语法

fun.apply(thisArg, [argsArray])

参数

thisArg

在fun运行时指定的this值。

[argsArray]

给到fun的参数数组。

返回值

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

apply、call的区别

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

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

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

手写实现apply

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

  1. 检查调用apply的对象是否为函数
  2. 将函数作为传入的context对象的一个属性,调用该函数
  3. 不要忘了调用之后删除该属性

代码如下:

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的指向确认。这一知识点可以在我的另一篇文章如何判断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。

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

手写实现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。

MDN的描述有些拗口,我来解释下:

  1. 创建一个空的简单对象{};
  2. 将这个空对象的构造函数指定为new操作符操作的函数(即定义中说的另一个对象)。其实就是原型链绑定,相关知识可以看我的另一篇文章原型与继承;
  3. 将操作符操作的函数的this指向步骤1创建的空对象;
  4. 运行该函数,如果该函数没有返回对象,则返回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
}

bind

好了,让我们来看看bind

Function.prototype.bind

语法

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

参数

thisArg

在fun运行时指定的this值。

arg1, arg2, ...

给到fun的参数列表。

返回值

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

手写实现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指向操作符自己创建的那个空对象。但是实际上指向了thovinonew操作符的第三步动作并没有成功!

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

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
}

这里我写的非常原始,是因为这样保留了实现步骤,让大家更好的看懂bind的实现原理。

至此,整个bind实现完成,剩下的就是如何精简代码,将代码写的更优雅一些。


如果有任何疑问或错误,欢迎留言进行提问或给予修正意见。

如果喜欢或对你有所帮助,欢迎Star我的博客,对作者是一种鼓励和推进。

也欢迎关注我的掘金,浏览更多优质文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值