Skip to content
大纲

手写 call, applybind

实现函数原型方法 call, applybind

call 的实现

  • 第一个参数为null或者undefined时,this 指向全局对象window,值为原始值的指向该原始值的自动包装对象,如 StringNumberBoolean
  • 为了避免函数名与上下文(context)的属性发生冲突,使用Symbol类型作为唯一值
  • 将函数作为传入的上下文(context)属性执行
  • 函数执行完成后删除该属性,不然会对传入对象造成污染
  • 返回执行结果
点我查看详细

方法 1

js
// 函数原型方法 `call` 的实现
// 将要改变 this 指向的方法挂到目标上执行并返回
// 注意:手写模拟实现,是否可以不使用高级语法 如 ... 运算符,改造如下
//     [...arguments].slice(1) 改为 [].slice.call(arguments, 1),这里有误,不应该内部再使用 call
//     Symbol() 可以用随机数 Math.random().toString() 同时避免已存在
//     在调用时,去除 ... 难以处理参数格式 (想到的办法,就是拼接 new Function 字符串了)
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }

  // context = context || window 改为如下 (这里使用 globalThis 处理 nodejs 执行无 window 的情况)
  if ([undefined, null].includes(context)) {
    context = globalThis || window
  }

  // context.fn = this 改为如下
  // 将当前被调用的方法定义在 context.tempFn 上。(为了能以对象调用形式绑定this)
  // FIXED: 避免函数名与上下文属性冲突,改为使用 Symbol
  let tempFn = Symbol()
  // 思考下为什么要这样做?arguments[0].fn = this
  context[tempFn] = this

  // 以对象调用形式调用tempFn, 此时this指向 context 也就是传入的需要绑定的this指向
  // FIXED: 既然是实现 call, 不应该内部再消费 call
  // const arg = [].slice.call(arguments, 1)
  const args = [...arguments].slice(1)

  // 执行保存的函数, 这个时候作用域就是在执行方的对象的作用域下执行,这会改变的this的指向
  // 如果 args 为空数组时,这里就是无参数,无需判断 args 长度
  const result = context[tempFn](...args)

  // FIXED: 删除该方法,不然会对传入对象造成污染(被添加该方法)
  delete context[tempFn]

  return result
}

方法 2

js
Function.prototype.myCall = function (context, ...args) {
  const fn = Symbol()
  try {
    context[fn] = this
    return context[fn](...args)
  } catch (e) {
    // Turn primitive types into complex ones 1 -> Number, thanks to Mark Meyer for this.
    context = new context.constructor(context)
    context[fn] = this
  }
  return context[fn](...args)
}

Function.prototype.myApply = function (context, args) {
  return this.call(context, ...args)
}

Function.prototype.myBind = function (context, ...args) {
  return (...args2) => this.call(context, ...args, ...args2)
}

apply 的实现

  • 前部分与 call 的实现一样
  • 第二个参数可以不传,但类型必须为数组或者类数组
点我查看详细
js
// 函数原型方法 `apply` 的实现
// 既然是模拟实现,不应该用更高级的方法 如 ... 运算符
Function.prototype.myApply = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('not funciton')
  }
  const arr = arguments[1]
  if (typeof arr !== 'undefined' && !Array.isArray(arr)) {
    throw new TypeError('not array')
  }

  if ([undefined, null].includes(context)) {
    context = globalThis || window
  }

  let tempFn = Symbol()
  context[tempFn] = this

  // arr 可能不传值
  const result = arr ? context[tempFn](...arr) : context[tempFn]()

  delete context[tempFn]

  return result
}

bind 的实现

需要考虑:

  • bind 除了 this 外,还可传入多个参数;
  • bind 创建的新函数可能传入多个参数;
  • 新函数可能被当做构造函数调用;
  • 函数可能有返回值;

实现方法:

  • bind 方法不会立即执行,需要返回一个待执行的函数;(闭包)
  • 实现作用域绑定(apply
  • 参数传递(apply 的数组传参)
  • 当作为构造函数的时候,进行原型继承
点我查看详细
js
// https://www.smashingmagazine.com/2014/01/understanding-javascript-function-prototype-bind/
// 非常简单的示例
// 方案一
Function.prototype.simpleBind = function simpleBind(scope) {
  var fn = this
  return function simpleBinded() {
    return fn.apply(scope)
  }
}

// 方案二
Function.prototype.fakeBind = function (obj, ...args) {
  return (...rest) => this.call(obj, ...args, ...rest)
}

// 函数原型方法 `bind` 的实现
Function.prototype.myBind = function myBind(context, ...args) {
  if (typeof this !== 'function') {
    // if (!(this instanceof Function)) {
    // 当前调用 bind 方法的不是函数
    throw new TypeError('this is not a function type.')
  }

  // 表示当前函数 this
  const _this = this

  // 判断有没有传参进来,若为空则赋值[] 或直接使用 args
  const arg = [...arguments].slice(1)

  return function newFn(...newFnArgs) {
    // 处理函数使用 new 的情况
    if (this instanceof newFn) {
      return new _this(...arg, ...newFnArgs)
    } else {
      return _this.apply(context, [...arg, ...newFnArgs])
    }
  }
}

function es5Bind() {
  //arguments are just Array-like but not actual Array. Check MDN.
  let bindFn = this,
    bindObj = arguments[0],
    bindParams = [].slice.call(arguments, 1) //----> [arg1,arg2..] Array.isArray --> true
  return function () {
    bindFn.apply(bindObj, bindParams.concat([].slice.call(arguments)))
  }
}

function es6Bind(...bindArgs) {
  let context = this
  return function (...funcArgs) {
    context.call(bindArgs[0], ...[...bindArgs.slice(1), ...funcArgs])
    // we can use above line using call (OR) below line using apply
    //context.apply(bindArgs[0], [...(bindArgs.slice(1)), ...funcArgs]);
  }
}

// Function.prototype.es5Bind = es5Bind;
// Function.prototype.es6Bind = es6Bind;

扩展阅读

While each browser has its own source code for implementing Javascript, you can find how many of the native Javascript functions are implemented with the ECMA specifications found here:

http://www.ecma-international.org/ecma-262/10.0/index.html#sec-properties-of-the-function-prototype-object

  • For specs of apply, see: 19.2.3.1
  • For specs of bind, see: 19.2.3.2
  • For specs of call, see: 19.2.3.3

If you're interested for example, how Node implemented apply, you can dig into their source code on Github here: https://github.com/nodejs/node

参考