JavaScript高级深入浅出:手写 call、apply、bind 函数与了解 arguments

374 阅读3分钟

介绍

本文是 JavaScript 高级深入浅出系列的第七篇,将手写 apply-bind-call 与认识 arguments

正文

注意:这里的手写仅仅是为了练习函数、this 和调用关系,不会考虑一些特殊情况

1. call 函数

Function.prototype._call = function(thisArg, ...args) {
  // 1. 获取到需要被执行的函数,也就是调用 _call 的实例函数
  var fn = this
  // 2. 对 thisArg 转为对象类型(防止类型出错)Object(xxx) 可以直接给出其包装类
  // 如果是 undefined 或者 null,那么 this => window
  thisArg =
    typeof thisArg === 'undefined' || thisArg === null
      ? window
      : Object(thisArg)
  // 3. 将传入的 thisArg 更改 fn 的 this
  thisArg.fn = fn
  // 4. 执行函数(使用展开运算符),如果有返回值就直接将执行的结果返回
  const res = thisArg.fn(...args)
  // 5. 删除 thisArg 的多余函数
  delete thisArg.fn
  return res
}

测试:

function fn(username, password) {
  console.log('fn 函数被执行')
  console.log('this.name', this.name)  // foo
  console.log('username', username)    // username
  console.log('password', password)    // password
}

const foo = {
  name: 'foo',
}

fn._call(foo, 'username', 'password')

2. apply 函数

Function.prototype._apply = function(thisArg, argArr) {
  var fn = this
  thisArg =
    typeof thisArg === 'undefined' || thisArg === null
      ? window
      : Object(thisArg)
  thisArg.fn = fn
  // 只有参数是不同的,apply的第二个参数是一个数组,这里做一下空值检查
  const res = thisArg.fn(...(argArr || []))
  delete thisArg.fn
  return res
}

3. bind 函数

Function.prototype._bind = function(thisArg, ...args) {
  const fn = this
  thisArg =
    typeof thisArg === 'undefined' || thisArg === null
      ? window
      : Object(thisArg)
  // 返回一个函数(闭包环境,所以能访问到 thisArg)
  return function(...funcArgs) {
    thisArg.fn = fn
    // 这一步,因为可能参数传入的数量不同,所以需要将两次传入的参数混合一下,同时 funcArgs 需要替换掉同样位置的 args 的值
    const finalArgs =
      funcArgs && args && funcArgs.length >= args.length
        ? funcArgs.map((item, index) => (item = funcArgs[index]))
        : args.map((item, index) => (item = funcArgs[index] || args[index]))
    // 如果 funcArgs、args 任何一个为空,那么 findArgs === undefined,所以这里做一下空值检测
    const result = thisArg.fn(...(finalArgs || []))
    delete thisArg.fn
    return result
  }
}

测试一下特殊情况

function foo(username, password) {
}

// 1. bind 传入 2 个参数,单独调用传入 0 个参数   success
const barFn = foo._bind(bar, 'alex', 'zhang')
barFn()
// 2. bind 传入 2 个参数,单独调用传入 1 个参数   success
const barFn = foo._bind(bar, 'alex', 'zhang')
barFn('alexzzz')
// 3. bind 传入 2 个参数,单独调用传入 2 个参数   success
const barFn = foo._bind(bar, 'alex222', 'zhang222')
barFn('alex', 'zhang')
// 4. bind 传入 1 个参数,单独调用传入 2 个参数   success
const barFn = foo._bind(bar, 'alex')
barFn('alexzzz', 'zhangzzz')
// 5. bind 传入 1 个参数,单独调用传入 1 个参数   success
const barFn = foo._bind(bar, 'alex')
barFn('alexzzz')
// 6. bind 传入 1 个参数,单独调用传入 0 个参数   success
const barFn = foo._bind(bar, 'alex')
barFn()
// 7. bind 传入 0 个参数,单独调用传入 2 个参数   success
const barFn = foo._bind(bar)
barFn('alexzzz', 'zhangzzz')
// 8. bind 传入 0 个参数,单独调用传入 1 个参数   success
const barFn = foo._bind(bar)
barFn('alexzzz')
// 9. bind 传入 0 个参数,单独调用传入 0 个参数   success
const barFn = foo._bind(bar)
barFn()

经测试,各种参数数量输入情况均可以

4. arguments

arguments是一个对应于传递给函数的参数的类数组(array-like)对象

  • 类数组:类似数组结构,但实际上是一个对象,拥有一些数组的特性,例如 length、通过索引值来访问值等,但是并不能调用数组内置方法,例如 forEach、map 等
function foo() {
  console.log('arguments is', arguments)
}

foo(1, 2)  // [1, 2]
foo(1, 2, 3, 4, 5, 6)  // [1, 2, 3, 4, 5, 6]
foo(1, 2, 3, 4, 5, 6, 7, 8, 9)  // [1, 2, 3, 4, 5, 6, 7, 8, 9]

常见的对于arguments的操作:

  • 获取参数的长度arguments.length
  • 根据索引值获取某一个参数arguments[2]获取第 3 个参数
  • arguments.callee:获取当前的函数

将类数组转换为数组的方法:

// ES6 之前
var res = Array.prototype.slice.call(arguments)  // res 是一个真正的数组了
//        等价于 [].slice.call(arguments)
// ES6 之后
res = Array.from(arguments)

4.1 注意

  • ES6 之后不再推荐使用 arguments,推荐使用剩余参数
  • 箭头函数中没有 arguments

总结

本文中,你学习了 2 个知识点

  • 手写 call、apply、bind 函数,同时复习了 this 指向等知识
  • 了解了 arguments 与如何将类数组转换为数组