JavaScript 之 call、apply、bind的原理实现

202 阅读3分钟

call、apply 以及 bind 的区别和用法

语法

function.call(thisArg, arg1, arg2, ...)
function.apply(thisArg, argsArray)
function.bind(thisArg,arg1, arg2, ...)

call()apply()bind()方法都是用来改变 this 指向.

  1. call()apply()方法类似,区别在于call()方法接受一个参数列表,而apply()接受的是一个数组
  2. bind()call()apply()两个之间的区别在于, bind()返回新创建的绑定函数,不会立即执行。而call()apply()调用时就会执行。

模拟实现

模拟实现 apply

当以 thisArg 和 argArray 为参数在一个对象 func 上调用 apply 方法时,将进行以下步骤:

  1. 如果 func 不可被调用,则抛出一个 TypeError 异常
  2. 如果 argArray 是 null 或 undefined, 则返回提供 thisArg 作为 this 值并以空参数列表调用 的 [[Call]] 内部方法的结果
  3. 如果 argArray 的类型不是 Object ,则抛出一个 TypeError 异常
  4. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果
var foo = {
  value: 1,
  bar: function (...args) {
    console.log(this.name)
    console.log(args);
  },
}

var person = {
  name: 'jack',
}
// 浏览器环境 非严格模式
function getGlobalObject() {
  return this
}

Function.prototype.applyFn = function (thisArg, argArray) {
  // 1
  if (typeof this !== 'function') {
    throw new TypeError(`this is not function`)
  }

  // 2
  if (typeof argArray === 'undefined' || argArray === null) {
    argArray = []
  }

  // 3
  if (argArray !== new Object(argArray)) {
    throw new TypeError(`CreateListFromArrayLike called on non-object`)
  }

  if (typeof thisArg === 'undefined' || thisArg === null) {
    // 在外面传入的 thisArg 值会修改并成为 this 值。
    // thisArg 是 undefined 或 null 时它会被替换成全局对象
    thisArg = getGlobalObject()
  }

  thisArg = new Object(thisArg)
  var __fn = Symbol()
  thisArg[__fn] = this

  // 4
  var result = thisArg[__fn](...argArray)
  delete thisArg[__fn]
  return result
}

foo.bar.applyFn(person, [1, 2])  // jack [1,2]

模拟实现 call

当以 thisArg 和可选的 arg1, arg2 等等作为参数在一个 func 对象上调用 call 方法,采用如下步骤:

  1. 如果 func 不可被调用,则抛出一个 TypeError 异常
  2. 设置 argList 为一个空列表
  3. 如果传递了多个参数,则按从左到右的顺序开始,将每个参数作为argList的最后一个元素追加
  4. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果
Function.prototype.callFn = function (thisArg) {
  // 1
  if (typeof this !== 'function') {
    throw new TypeError(`this is not function`)
  }
  // 2
  var argList = []
  // 3
  var argsLen = arguments.length
  for (var i = 0; i < argsLen - 1; i++) {
    argList[i] = arguments[i + 1]
  }

  thisArg = new Object(thisArg)

  var __fn = Symbol()
  thisArg[__fn] = this

  // 4
  var result = thisArg[__fn](...argList)
  delete thisArg[__fn]
  return result
}

foo.bar.callFn(person, 1, 2)  // jack [1,2]

模拟实现 bind

bind 方法需要一个或更多参数,thisArg 和(可选的)arg1, arg2, 等等,执行如下步骤返回一个新函数对象:

  1. 创建变量 target 并将赋值 this
  2. 如果 target 不可被调用,则抛出一个 TypeError 异常
  3. 创建变量 args 为(可能为空的)新内部列表,它包含按顺序的 thisArg 后面的所有参数(arg1, arg2 等等)。
  4. 创建绑定函数
    4.1. new 调用判断,通过 instanceof 判断函数是否是 new 调用的
    4.2. 绑定 this ,传入合并参数执行
    4.3. 返回绑定函数的执行结果
  5. 设置绑定函数的原型
  6. 返回绑定函数
Function.prototype.bindFn = function (thisArg) {
  // 1 创建变量 target 并将赋值 this
  var target = this
  // 2 如果 target 不可被调用,则抛出一个 TypeError 异常
  if (typeof this !== 'function') {
    throw new TypeError(this + 'is not a function')
  }

  // 3 创建变量 args 为(可能为空的)新内部列表,它包含按顺序的 thisArg 后面的所有参数(arg1, arg2 等等)
  var args = [].slice.call(arguments, 1)
  // 4
  var bound = function () {
    // bind 函数的参数
    var boundArgs = [].slice.call(arguments)
    var argsList = args.concat(boundArgs)
    // new 调用
    if (this instanceof bound) {
      // apply 修改 this 指向 为 bound
      var result = target.apply(this, argsList)
      // 如果函数返回对象类型是 Object,那么直接返回指向结果
      var isObject = typeof result === 'object' && result !== null
      var isFunction = typeof result === 'function'
      if (isObject || isFunction) {
        return result
      }
      // 如果函数没有返回对象类型 Object,那么 new 操作中的函数将返回 bound
      return this
    } else {
      // apply 修改 this 指向,把两个函数的参数集合传给 target 函数并执行,返回执行结果
      return target.apply(thisArg, argsList)
    }
  }
  // 5
  // target 不是箭头函数,才进行指向prototype
  if (target.prototype) {
    // 创建一个新对象,将  bound 的原型指定为 targe.prototype
    function Empty() {}
    Empty.prototype = target.prototype
    bound.prototype = new Empty()
  }
  // 6
  return bound
}

var fn = foo.bar.bindFn(person,1)
fn()   // jack [1]

参考

Annotated ES5