原生JS实现call、apply、bind(超详细!)

598 阅读4分钟

想要实现实现一个方法,必然要先了解它。其实call、apply、bind都是我们日常开发中比较常用的一些方法。他们都有一个共同的作用:改变this指向

call和apply本质上来说并没有什么区别,他们的作用是一样,区别仅在于param的传参方式。call接受连续传参,apply接受数组传参。

bind和call\apply的区别想对来说就大一些,call\apply都是立即执行的,它们的返回结果就是fn的执行结果;而bind并不立即执行,它的返回结果是fn的拷贝,改变this指向后不会立即执行,需要自行调用这个新函数。

/**
 @params: targetThis  (可选) fn的this的目标指向,默认指向window
 @params: param (可选) 传入fn的参数
*/
fn.call(targetThis, param1, param2, param3 ...)
fn.apply(targetThis, [param1, param2, param3 ...])
fn.bind(targetThis, param1, param2, param3..)
(PS: 为了统一,下面文章都用fn、targetThis、params来叙述
  • call的实现

    call的作用只说一句改变this指向就太笼统了,它的执行可以大致拆分为如下几个步骤:

    1. 将传入的fn作为targetThis的私有方法,使fn可以在this指向targetThis的前提下执行
    2. 通过arguments截取连续传入的不定量的参数
    3. 执行fn、记录执行结果
    4. 删除临时挂在targetThis的fn(不改变原来的targetThis)
    5. 返回执行结果
Function.prototype._call = function (context = window) {
  // 首先了解各个参数的值
  console.log('this', this) // foo
  console.log('context', context) // target {value: 1}
  console.log('arguments', arguments) // { {value: 1}, 'param1', 'param2'}
  let _context = context // 第一个参数不传默认为window
  _context.fn = this // 将foo作为target的私有方法, this改变
  const args = [...arguments].slice(1) // 截取下标从1开始的参数 {'param1', 'param2'}
  const result = _context.fn(...args) // 立即执行
  delete _context.fn // 删除fn 不改变target
  return result
}
// eg
let target = {
  value: 1
}
function foo(param1, param2) {
  console.log(param1)
  console.log(param2)
  console.log(this.value)
}
foo._call(target, 'param1', 'param2') // param1 param2 1
  • apply的实现 apply的实现与call基本上是一致的,区别仅在于params的形式上,apply的参数为数组形式。
Function.prototype._apply = function (context = window, arr) {
  // 首先了解各个参数的值
  console.log('this', this) // fn
  console.log('context', context) // target {value: 1}
  let _context = context// 第一个参数不传默认为window
  _context.fn = this // 将fn作为target的私有方法, this改变
  const result = arr.length ? _context.fn(...arr) : _context.fn() // 判断是否传入arr
  delete _context.fn // 删除fn 不改变target
  return result
}
// eg
let target = {
  value: 1
}
function fn(param1, param2) {
  console.log(param1)
  console.log(param2)
  console.log(this.value)
}
fn._apply(target, ['param1', 'param2']) // param1 param2 1
  • bind的实现

由于ES5内置的Function.prototype.bind(..)实现过于复杂,这里我们借助call/apply来实现bind,在实现bind之前我们明确几个重点:

  1. bind并不是立即执行的,它会返回一个函数,调用返回的函数改变this指向
  2. 在this的绑定规则中, new绑定的优先级高于硬绑定(划重点,待会要考)
  3. MDN中的原话:

绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。

(划重点加感叹号)

我们来解读一下, “绑定函数也可以使用new运算构造 ” 就是说会在new中使用硬绑定,而new绑定的优先级又高于硬绑定,我们需要对有new的情况做特殊处理;“前置参数仍会提供给模拟函数”就是说,在new中使用硬绑定函数可以预先设置一些参数,在使用new进行初始化时可以传入其他参数,是柯里化的一种。

Function.prototype._bind = function (context = window) {
    // 首先搞清楚各个参数的含义
    console.log('this', this) // foo
    console.log('context', context) // target {value: 1}
    console.log('argument', arguments) // { {value1: 1}, 'param1', 'param2'}
    const _this = this // 保存this
    //  这里arguments为foo的arguments
    const args = [...arguments].slice(1) // 截取下边为1开始的参数 ['param1', 'param2']
    var fn =  function () {
        const bindArgs = [...arguments] // 这里的arguments为fn的
        // 作为构造函数时,this指向实例fn this instanceof fn 返回true
        // 不作为构造函数时, this指向window, this instanceof fn 返回false 需要将this指向改变为context
        // [...args, ...bindArgs] 合并预先传入的参数和new 实例化时传入的参数
        return _this.apply( this instanceof fn ? this : context , [...args, ...bindArgs ])
    }
    // 修改绑定函数的prototype为foo的prototype,继承foo的属性
    // 创建一个空对象,让空对象__proto__指向_this.prototype, 做到修改fn的prototye不会影响到foo
    fn.prototype = Object.create(_this.prototype)
    return fn
}
// eg
let target = {
    value: 1
}
function foo(param1, param2, param3) {
    console.log(param1)
    console.log(param2)
    console.log(param3)
    console.log(this.value)
}
const bindFn = foo._bind(target, 'param1', 'param2') 
bindFn() // param1 param2 undefined 1

const newBindFn = new bindFn('param3') // param1 param2 param3 undefined