toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

Arguments 对象与简易柯里化 #269

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

配图源自 Freepik

简述

arguments 是函数实参对象,常被称为“类数组”(array-like)。形如:

arguments = {
  0: xx,
  1: xx,
  ...,
  n - 1: xx,
  length: n // n 取决于实参的数量
}

arguments 的一些特性:

function foo() {
  // 1. arguments 不是数组,自然也不具备数组 forEach 等方法
  Object.prototype.toString.call(arguments) // "[object Arguments]"
  arguments instanceof Array // false

  // 2. arguments 对象具有数字索引属性
  arguments[0] // "a"
  arguments[1] // "b"

  // 3. arguments 对象有 length 属性,反映实参个数。
  //    但与函数的 length 属性不同,foo.length 反映形参个数。可在 ES6 之后由于参数默认值、REST 参数等新特性,使 foo.length 变得不可靠
  arguments.length // 2
  foo.length // 0
}

foo('a', 'b')

类数组对象的特征:含有 length 属性、索引元素属性,但不包含数组任何方法。

常见的类数组,除了 arguments 之外,还有 HTMLCollection(通过 getElementsByName() 等返回的 DOM 列表)、NodeList(通过 querySelectorAll() 返回的节点列表)。

arguments 使用

arguments 通常用于不能确定实参个数的应用场景,例如函数柯里化等。

它还经常被转换为数组使用。

// ES5
function foo() {
  // 方法一:会阻止某些 JS 引擎的优化,如 V8
  // 请看:https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments
  var args = Array.prototype.slice.call(arguments) 
  // var args = [].slice.call(arguments)

  // 方法二(推荐,尽管丑了点)
  var args = arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments)
}

// ES6
function foo() {
  // 利用 arguments 的 Iterator 接口,可以快速转化为数组
  const args = [...arguments]
  // const args = Array.from(arguments)
}
function foo(...args) {
  // 直接使用 REST 参数,args 天生就是数组,可以直接使用 Array 的方法
}

Function.prototype.apply()Array.prototype.slice() 等方法也可接受类数组,无需转换为数组再进行操作。举个例子:

// 求最大数
function getMax() {
  return Math.max.apply(null, arguments)
}

getMax(1, 2, 3) // 3

都 2021 年了,更被推荐按 ES6 的写法。

注意点

关于 arguments 只能在所有(非箭头)函数内部可用的局部变量。

  • 存在于所有(非箭头)函数内部。函数上下文的 AO 对象就包括 arguments 属性。

  • 箭头函数不存在 arguments 对象。有时候箭头函数内可以使用 arguments 是“视觉”认知错误。

  • ES6 的箭头函数中,要获取实参对象,可通过 REST 参数获得。

  • 非严格模式下,arguments 允许重新赋值。而且形参的更新,也会伴随着 arguments 对象相应属性值的更新。

  • 严格模式下,则不允许对 arguments 对象赋值,且不会最终参数的变化。也不允许使用 arguments.callee 方法(关于严格模式对 arguments 对象的限制,可看这篇文章)。

在函数外使用会抛出 ReferenceError。此时它就是一个标识符而已。

// 1. 相当于一个变量 arguments,因此会抛出引用错误
console.log(arguments) // ReferenceError: arguments is not defined

// 2. 非严格模式下,可将其作为变量
let arguments = 1 // or arguments = 'any'
console.log(arguments) // 1

// 3. 严格模式下,还是将其声明变量或对其进行赋值操作
'use strict'
arguments = 1 // Wrong, SyntaxError: 'arguments' can't be defined or assigned to in strict mode code

在箭头函数内,没有 arguments 变量。例如以下这样使用同样会报错。

const foo = () => {
  console.log(arguments) // ReferenceError: arguments is not defined
}

foo()

但这样用不会报错,这就是前面提到的“视觉认知”错误。

function foo() {
  const bar = () => {
    console.log(arguments) 
    // 由于箭头函数 bar 没有 arguments,
    // 这里引用的 arguments 对象其实是函数 foo 的实参对象。
  }
  bar('b')
}

foo('a') // { 0: 'a', length: 1 }

非严格模式与严格模式,对 arguments 对象的操作。

function foo(x) {
  x = 10
  console.log(x) // 10
  console.log(arguments[0]) // 10
}

function bar(x) {
  'use strict'
  x = 10
  console.log(x) // 10
  console.log(arguments[0]) // 1

  // 以下这样将会直接抛出语法错误:SyntaxError: Unexpected eval or arguments in strict mode
  // arguments = {}
}

foo(1)
bar(1)

求和函数(柯里化)

假设我们有一个求和函数 sum(),要实现下面的需求:

sum(1, 2, 3) // 6
sum(1, 2)(3)(4) // 10
sum(1)(2, 3)(4, 5, 6) // 21
// ...

其实上面的需求是有问题的,它没有出口,导致不知道什么时候求和。我们可以稍微改下需求:

sum(1, 2, 3).value() // 6
sum(1, 2)(3)(4).value() // 10
sum(1)(2, 3)(4, 5, 6).value() // 21
// ...

就是说 sum() 函数的结束时机(出口)是调用 value() 方法的时候。

function sum(...args) {
  const arr = [...args]
  function repeat(...nextArgs) {
    ;[].push.apply(arr, nextArgs)
    return repeat
  }
  repeat.value = () => {
    if (!arr.length) return 0
    return arr.reduce((a, b) => a + b)
  }
  return repeat
}

也可以改成调用 sum(1, 2)(3)(4)() 时进行求和,我们修改一下:

function sum(...args) {
  if (!args.length) return 0
  const arr = [...args]
  function repeat(...nextArgs) {
    if (!nextArgs.length) {
      return arr.reduce((a, b) => a + b)
    }
    ;[].push.apply(arr, nextArgs)
    return repeat
  }
  return repeat
}

sum() // 0
sum(1, 2, 3)() // 6
sum(1, 2)(3)(4)() // 10
sum(1)(2, 3)(4, 5, 6)() // 21

The end.