apply,call,bind 的实现

我们知道 applycallbind是用来改变函数内部 this 指向的,在我们讨论如何使用 JS 代码实现它们之前,先来了解一下这三个方法。

apply

用法:func.apply(thisArg, [argsArray])

thisArgfunc 函数执行时内部所指向的 this 的值,在非严格模式下默认为 nullundefined 或全局对象 window

argsArray 是一个数组或类数组对象,作为传入 func 函数的参数列表。

用法参考

const obj = {
  arr: [1, 2, 3, 4, 5, 6],
  result: 0,
}
add.apply(obj, obj.arr)
function add(...argsArray) {
  for (let i of argsArray) {
    this.result += i
  }
}
console.log(obj.result) // 21

代码实现

首先我们要在 Function.prototype 上创建一个 customApply 属性,接受上下文 thisArg 和参数列表 argsArray

// thisArg 默认指向 window,args 为空数组
Function.prototype.customApply = function (thisArg = window, argsArray = []) {}

对于 customApply,我们需要做的事情就是,将被执行函数 fnthis 指向目标 thisArg,也就是说要以 thisArg.fn(...argsArray) 的形式调用目标函数,然后再将函数执行所得结果返回。

Function.prototype.customApply = function (thisArg = window, argsArray = []) {
  // this 指向的是调用 customApply 的函数
  thisArg.fn = this
  const result = thisArg.fn(...argsArray)
  // 记得删除临时属性 fn
  delete thisArg[fn]
  return result
}

这就实现了一个简单的 customApply,但注意,这个方法有漏洞!我们在目标 thisArg 上添加了一个临时元素 fn,如果 thisArg 本身就拥有用户自定义的 fn 怎么办?因此我们要创建一个独一无二的元素避免属性覆盖的情况,我们知道 Symbol 可以生成唯一的值,同样可以用作对象的键名:

const fn = Symbol('fn')
thisArg[fn] = this

这样我们就可以避免因为属性覆盖的问题了:

Function.prototype.customApply = function (thisArg = window, argsArray = []) {
  // this 指向的是调用 customApply 的函数
  const fn = Symbol('fn')
  thisArg[fn] = this
  const result = thisArg[fn](...argsArray)
  // 记得删除临时属性 fn
  delete thisArg[fn]
  return result
}

// test1,我们为数组的原型链上加上一个方法max计算数组最大元素
Array.prototype.max = function () {
  return Math.max.customApply(null, this)
}
console.log([2, 3, 6, 3, 1, 5, 8, 1].max()) // 8

// test2,使用 slice 将伪数组转化成真数组
const fakeArr = {
  0: 123,
  length: 5,
}
console.log(Array.prototype.slice.customApply(fakeArr)) // [123, empty × 4]

Notice

Array.prototype.slice 不止能对数组进行切片,还可以把类数组转化成新数组,这需要将该方法绑定到类数组上执行,就像我们上面的 test2 使用 customApply 一样。

call

用法:function.call(thisArg, arg1, arg2, ...)

apply 一样,第一个参数为函数内 this 指向目标,后面接着的是参数列表。

用法参考

call 实现继承父构造函数:

function Animal(species, bark) {
  Object.assign(this, { species, bark })
}
function Dog(species, bark, favor) {
  Animal.call(this, species, bark)
  this.favor = favor
}
console.log(new Dog('哈巴狗', '汪汪汪', '吃骨头'))
// {bark: "汪汪汪",favor: "吃骨头",species: "哈巴狗"}

代码实现

callapply 除了参数形式之外没有其他区别,因此实现方法是类似的:

Function.prototype.customCall = function (thisArg, ...argsArray) {
  thisArg = thisArg || window
  const fn = Symbol('fn')
  thisArg[fn] = this
  const result = thisArg[fn](...argsArray)
  delete thisArg[fn]
  return result
}

// test,就拿用法参考里面的例子
function Animal(species, bark) {
  Object.assign(this, { species, bark })
}
function Dog(species, bark, favor) {
  Animal.customCall(this, species, bark)
  this.favor = favor
}
console.log(new Dog('哈巴狗', '汪汪汪', '吃骨头'))
// {bark: "汪汪汪",favor: "吃骨头",species: "哈巴狗"}

bind

用法:function.bind(thisArg[, arg1[, arg2[, ...]]])

thisArg 是需要绑定的目标,如果使用 new 运算符构造绑定函数,则忽略该参数。如果没有传递该参数则使用原来的执行作用域。

arg1, arg2, ... 是函数调用时传入的参数。

调用 bind 函数并不会执行被绑定的函数,只会返回该函数的拷贝,该拷贝函数拥有指定的 this 值。

用法参考

const obj = {
  a: 2,
  get2Power(power) {
    return this.a ** power
  },
}
// get2PowerFunc 作为 window 属性而不是作为 obj 属性被调用
const get2PowerFunc = obj.get2Power
get2PowerFunc.bind(obj)(10) // 1024

代码实现

首先我们的 customBind 函数接收一个 thisArg 作为 this 指向的目标,同时返回一个接受若干参数的函数。

Function.prototype.customBind = function (thisArg) {
  return (...argsArray) => {
    // 这里的 this 指向的就是调用 customBind 的函数
    return this.apply(thisArg, argsArray)
  }
}

这还不够,后续的参数列表 argsArray 可以分开传,比如调用 bind 的时候传几个参数,执行绑定后函数的时候再传几个参数:

func.bind(this, arg1)(arg2, arg3)

其实也很简单,只要把两次传的参数一起传给 apply 就好了:

Function.prototype.customBind = function (thisArg, ...bindArgs) {
  return (...argsArray) => {
    return this.apply(thisArg, [...bindArgs, ...argsArray])
  }
}

我们初步完成了 customBind 的功能,但我们前面说过:如果使用 new 运算符构造绑定函数,则忽略该参数

比如说:

const obj = {
  a: 2,
  get2Power(power) {
    return this.a ** power
  },
}
const get2PowerFunc = obj.get2Power
// 使用 new 调用了 bind 返回的函数
new get2PowerFunc.bind(obj)

因此,我们要判断被绑定的函数是否是被 new 调用的:

Function.prototype.customBind = function (thisArg, ...bindArgs) {
  const self = this
  /**
   * 因为下面我们需要将this.prototype绑定到bindFunc.prototype上
   * 但如果之后修改了bindFunc.prototype的话也会造成绑定函数的prototype的改变
   * 因此需要一个临时函数tempFunc进行中转,代替bindFunc.prototype
   */
  const tempFunc = function () {}
  tempFunc.prototype = self.prototype
  bindFunc.prototype = new tempFunc()
  // 由于需要支持new操作符,因此返回的函数bindFunc就不能是箭头函数了
  function bindFunc(...argsArray) {
    // 如果this指向的是bindFunc,说明使用了new进行实例化,忽略thisArg
    // new.target代替this instanceof bindFunc判断函数是否被new调用
    return self.apply(new.target ? this : thisArg, [...bindArgs, ...argsArray])
  }
  return bindFunc
}

参考文章