Skip to content
大纲

手写 call, applybind

实现函数原型方法 call, applybind

call 的实现

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

方法 1

js
// 函数原型方法 `call` 的实现
// 将要改变 this 指向的方法挂到目标上执行并返回
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
  }

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

  const args = [...arguments].slice(1)
  const result = context[tempFn](...args)

  delete context[tempFn]

  return result
}

// prettier-ignore
// 其他一些思考或说明
// 1. [...arguments].slice(1) 等同 [].slice.call(arguments, 1),
//    但这里不推荐使用,手写 call 不应该内部再使用 call 方法
// 2. Symbol() 也可以用随机数 Math.random().toString() 方法同时辅助缓存,避免已存在
//    为什么要使用 Symbol()?避免函数名与上下文属性冲突
// 3. 在调用时,能不使用 ... 语法吗?这会增加难度,难以处理参数格式
//    想到的办法,就是拼接 eval 或 new Function 了,实现如下
// `
// let args = [];
// for (let i = 1, len = arguments.length; i < len; i++) {
//   args.push('arguments[' + i + ']');
// }
// let result = eval('context.fn(' + args + ')'); // 相当于传参 ...args
// `
// 4. context[tempFn] = this 为什么要这样做?
//    将当前被调用的方法定义在 context.tempFn 上。(为了能以对象调用形式绑定 this)
//    以对象调用形式调用 tempFn, 此时 this 指向 context 也就是传入的需要绑定的 this 指向
// 5. context[tempFn](...args)
//    执行保存的函数,这个时候作用域就是在执行方的对象的作用域下执行,这会改变的 this 的指向
//    如果 args 为空数组时,这里写法兼容无参数,无需判断 args 长度
// 6. delete context[tempFn] 为什么还要执行这个?
//    执行这个,是为了删除该方法,不然会对传入对象造成污染(被添加该方法)

方法 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]()
  const result = context[tempFn](...arr)

  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

参考