使用JS简单实现一下apply、call和bind方法

436 阅读5分钟

1.方法介绍

apply、call和bind都是系统提供给我们的内置方法,每个函数都可以使用这三种方法,是因为apply、call和bind都实现在了Function的原型上(Function.prototype),而他们的作用都是给我们函数调用时显式绑定上this。下面先介绍一下它们的基本用法:

  • apply方法:调用一个具有给定this值的函数,以及以一个数组(或类数组对象) 的形式提供的参数。

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

      • thisArg:在func函数调用时绑定的this值;
      • [argsArray]:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func函数;
    • 使用效果:

      function foo(x, y ,z) {
        console.log(this, x, y, z)
      }
      
      const obj = { name: 'curry', age: 30 }
      /**
       * 1.将obj对象绑定给foo函数的this
       * 2.数组中的1 2 3分别传递给foo函数对应的三个参数
       */
      foo.apply(obj, [1, 2, 3])
      

  • call方法:使用一个指定的 this值和单独给出的一个或多个参数来调用一个函数。

    • 使用语法:func.call(thisArg, arg1, arg2, ...)

      • thisArg:在func函数调用时绑定的this值;
      • arg1, arg2, ...:指定的参数列表,将作为参数传递给func函数;
    • 使用效果:

      function foo(x, y ,z) {
        console.log(this, x, y, z)
      }
      
      const obj = { name: 'curry', age: 30 }
      /**
       * 1.将obj对象绑定给foo函数的this
       * 2.call剩余参数中的a b c分别传递给foo函数对应的三个参数
       */
      foo.call(obj, 'a', 'b', 'c')
      

  • bind方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

    • 使用语法:func.bind(thisArg[, arg1[, arg2[, ...]]])

      • thisArg:调用func函数时作为this参数传递给目标函数的值;
      • arg1, arg2, ...:当目标函数被调用时,被预置入func函数的参数列表中的参数;
    • 使用效果: function foo(...args) { console.log(this, ...args) }

      const obj = { name: 'curry', age: 30 }
      /**
       * 1.将obj对象绑定给foo函数的this
       * 2.bind剩余参数中的1 2 3分别传递给foo函数中参数
       * 3.也可在newFoo调用时传入参数,这时bind传递的参数会与newFoo调用时传递的参数进行合并
       */
      const newFoo = foo.bind(obj, 1, 2, 3)
      newFoo()
      newFoo('a', 'b', 'c')
      

总结:

  • apply和call主要用于在函数调用时给函数的this绑定对应的值,两者作用类似,主要区别就是除了第一个参数,apply方法接受的是一个参数数组,而call方法接受的是参数列表。
  • bind也是给函数指定this所绑定的值,不同于apply和call的是,它会返回一个新的函数,新函数中的this指向就是我们所指定的值,且分别传入的参数会进行合并。

2.apply、call和bind方法的实现

为了所有定义的函数能够使用我们自定义的apply、call和bind方法,所以需要将自己实现的方法挂在Function的原型上,这样所有的函数就可以通过原型链找到自定义的这三个方法了。

2.1.apply的实现

    Function.prototype.myCall = function(thisArg, ...args) {
      // 定义fn变量储存this,可写可不写,因为我们调用这个函数是点调用的,this会隐式绑定到调用者
      const fn = this;

      // 判断thisArg是否为null或者,如果是,则将thisArg指向window,如果不是,则将thisArg指向Object(thisArg)
      thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg);

      // 接下来是绑定this的过程
      // 在thisArg中添加一个fn属性,并将this指向fn,这里通过symbol来定义一个独一无二的属性
      const fnSymbol = Symbol();

      Object.defineProperty(thisArg,fnSymbol,{
        enumerable: false,
        configurable: false,
        writable: false,
        // value: fn
        // 或者
        value : this
      })

      // 也可以简单写成
      // thisArg[fnSymbol] = fn

      // 对args进行判断。
      args = args || []

      // 当函数作为对象的方法被调用时,也就是通过类似 obj.someFunction() 这种形式调用函数,函数内部的 this 会隐式地绑定到前面的对象 obj
      const result = thisArg[fnSymbol](...args)

      // 调用完删除添加的属性,操作仅仅聚焦在删除名为 fnSymbol 的属性上,
      //不会对其他属性(包括名为 this 的属性,如果有的话)产生影响,它们在对象中各自独立存在并且遵循各自的属性生命周期和操作规则。
      delete thisArg[fnSymbol] 


      // 返回结果
      return result

    }

测试:虽然打印出来的对象中还存在Symbol属性,实际上已经通过delete删除了,这里是对象引用的问题。

function foo(x, y, z) {
  console.log(this, x, y, z)
}

foo.myApply({name: 'curry'}, [1, 2, 3])

2.2.call的实现

call方法的实现和apply方法的实现差不多,主要在于后面参数的处理。

Function.prototype.myApply = function (thisArg, args) {
        const fn = this;
        thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg);
        const fnSymbol = Symbol();
        Object.defineProperty(thisArg, fnSymbol, {
          enumerable: false,
          configurable: false,
          writable: false,
          value: fn
        });
        const result = thisArg[fnSymbol](...args);
        delete thisArg[fnSymbol];
        return result;
      };

测试:

function foo(x, y, z) {
  console.log(this, x, y, z)
}

foo.myCall({name: 'curry'}, 1, 2, 3)

2.3.bind的实现

bind方法的实现稍微复杂一点,需要考虑到参数合并的问题,以及返回一个新对象

Function.prototype.myBind = function(thisArg, ...argsArray) {
  // 1.获取当前的目标函数,也就是当前使用myBind方法的函数
  const fn = this

  // 2.对传入的thisArg进行边界判断
  thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)

  // 3.将获取的fn添加到thisArg对象上
     const fnSymbol = Symbol();
      Object.defineProperty(thisArg,fnSymbol,{
        enumerable: false,
        configurable: false,
        writable: false,
        value:this
      })

  // 4.定义一个新的函数
  //这里用到了闭包,保证参数不会消失
  function newFn(...args) {
    // 4.1.合并myBind和newFn传入的参数
    const allArgs = [...argsArray, ...args]
    // 4.2.调用真正需要被调用的函数,并将合并后的参数传递过去
    const result = thisArg[fnSymbol](...allArgs)
    // 4.3.调用完后删除添加的属性
    delete thisArg[fnSymbol]

    // 4.4.将结果返回
    return result
  }

  // 6.将新函数返回
  return newFn
}

测试:

function foo(x, y, z) {
  console.log(this, x, y, z)
}

const newFoo = foo.myBind({ name: 'curry' }, 1, 2)
newFoo(3)