手写 实现apply, call, bind

118 阅读4分钟

apply, call, bind 都是改变函数 this 指向的函数,那他们有什么区别?

看一个例子

const test1 = {
  0: 0,
  1: 1,
  length: 2
}

// Array.prototype.push.call(test1, 2, 3) // { '0': 0, '1': 1, '2': 2, '3': 3, length: 4 }
// Array.prototype.push.apply(test1, [2, 3]) // { '0': 0, '1': 1, '2': 2, '3': 3, length: 4 }
const bindTest = Array.prototype.push.bind(test1, 2, 3);
bindTest() // { '0': 0, '1': 1, '2': 2, '3': 3, length: 4 }
console.log(test1)

可以看到上述使用 apply call bind 都达到了借用 Array 原型的 push 方法达到添加属性的效果,但三种的使用方式略有不同

  • apply 的第二个参数,一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数,而其他两个的第二个以及第二个之后的所有参数都会传入原函数

  • call apply 都会立即调用函数, 而 bind 会返回一个原函数this指向改变后的函数,并且拥有初始参数,bind 返回的函数在调用的时候传入的参数会依次传入到借用的函数参数列表中

举个例子

const bindTest = Array.prototype.push.bind(test1, 2, 3);
bindTest(4, 5, 6) // { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, length: 7 }
console.log(test1)

上述代码在原来的基础上给 bindTest 添加了三个参数 4, 5, 6 ,可以看到 依次作为 push 的参数被传入进去

想了解这三个函数,会手写是必不可少的,接下来开始实现

在手写之前先分析一下它做了什么工作

  • 将原函数的this指向为传递过来的对象
  • 调用原函数,并且把参数传递给原函数
  • 返回结果 (bind)返回的是this改变后的函数,不会去调用

demo

const test1 = {
  name: 'jack',
  getName(str = 'hei!') {
    return str + ' ' + this.name
  }
}

const test2 = {
  name: 'lusi'
}

console.log(test1.getName.apply(test2, ['hello!']))  // hello! lusi
console.log(test1.getName.call(test2, 'hello!'))  // hello! lusi
console.log(test1.getName.bind(test2, 'hello!')())  // hello! lusi

开始手撸 myApplymyCall, myBind 实现相同的效果

apply

Function.prototype.myApply = function (arg, argsArr = []) {
  const thisArg = arg || globalThis; // 兜底为globalThis 这里用globalThis 是为了兼容多端
  if (!Array.isArray(argsArr)) {
    throw new Error('The second argument is an array')
  }

  const fn = Symbol(arg);
  thisArg[fn] = this;

  const result = thisArg[fn](...argsArr);
  delete thisArg.func
  return result
}

console.log(test1.getName.myApply(test2, ['hello!'])) // hello! lusi

分析

Function.prototype.myApply = function (arg, argsArr = []) {
  const thisArg = arg || globalThis; // 兜底为globalThis 这里用globalThis 是为了兼容多端
  if (!Array.isArray(argsArr)) {
    // 如果不是数组抛出错误
    throw new Error('The second argument is an array')
  }

  const fn = Symbol(arg); // 这里用symbool的意思是保证属性不会重复, 为了不覆盖原对象的属性
  thisArg[fn] = this; // 当前的this为调用的那个函数,将目标对象身上添加func为调用的this
  // this:  [Function: getName]
  // thisArg: { name: 'lusi', [Symbol([object Object])]: [Function: getName] }

  // 此时thisArg对象里已经有目标函数了 已经达到借用函数的效果了 下来我们开始调用
  const result = thisArg[fn](...argsArr); // 调用这个方法 在这个方法里面的this为目标this
  delete thisArg[fn] // 调用完成删除这个func (悄悄的来,悄悄的走)

  return result 
}

可以看到 原理其实就是借用了原函数为目标对象身上的函数,利用这个想法可以很快写出来 call

call

Function.prototype.myCall = function (arg, ...restArgs) {
  const thisArg = arg || globalThis; // 兜底为globalThis 这里用globalThis 是为了兼容多端

  const fn = Symbol(arg); // 这里用symbool的意思是保证属性不会重复, 为了不覆盖原对象的属性
  thisArg[fn] = this; // 当前的this为调用的那个函数,将目标对象身上添加func为调用的this
  // this:  [Function: getName]
  // thisArg: { name: 'lusi', [Symbol([object Object])]: [Function: getName] }

  // 此时thisArg对象里已经有目标函数了 已经达到借用函数的效果了 下来我们开始调用
  const result = thisArg[fn](...restArgs); // 调用这个方法 在这个方法里面的this为目标this
  delete thisArg[fn] // 调用完成删除这个func (悄悄的来,悄悄的走)

  return result
}

console.log(test1.getName.myCall(test2, 'hello!')) // hello! lusi

apply 实现了 其实和 call的区别就是参数的区别 这里改一下就好了

bind

const test1 = {
  name: 'jack',
  getName(str = 'hei!', a, b, c) {
    console.log(a, b, c)
    return str + ' ' + this.name
  }
}

const test2 = {
  name: 'lusi'
}
Function.prototype.myBind = function(arg, ...rest) {
  const thisArg = arg || globalThis;

  const fn = Symbol(arg);
  thisArg[fn] = this;

  return function(...returnRest) {
    const result = thisArg[fn](...rest, ...returnRest);
    delete thisArg[fn]
    return result
  }
}
console.log(test1.getName.myBind(test2, 'hello!','a1')('b2', 'c3')) // a1 b2 c3  hello! lusi

可以看到 区别就是 bind 返回的是一个函数,这个函数接收的参数会扔给原函数,换汤不换药,改变 this 的道理都是一样的

到这里,简版的就算是实现了