技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

完全理解 JS:arguments

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/93-完全理解JS--arguments
本文目录9 个章节
  1. 什么是 arguments?
  2. 创建一个灵活的格式化函数
  3. 参数对象与形参的映射
  4. 把 arguments 转换成真正的数组
  5. 通过参数对象封装函数
  6. 创建引用自身的函数
  7. arguments、rest 与函数长度的区别
  8. 小结
  9. 参考资料

完全理解 JS:arguments

Category(分类): JavaScript Status: 已整理

什么是 arguments

调用非箭头函数时,函数内部通常可以访问一个本地的 arguments 对象。它记录本次调用实际传入的参数数量和参数值:

  • arguments.length 是实际传入的实参数量,不是函数声明的形参数量;
  • arguments[0]arguments[1] 等按索引读取实参;
  • 它是 array-like(类数组对象),不是 Array 实例;
  • 箭头函数没有自己的 arguments,箭头中的 arguments 会词法捕获外层非箭头函数的绑定;
  • 类方法和构造器有自己的 arguments,但类体是严格模式,参数对象不会与形参映射。

调用处提供的是实参(actual arguments),函数声明中的变量是形参(formal parameters)。JavaScript 按值传递;当值是对象时,传递的是对象引用这一种值,不是按引用传递。

function inspect(first, second) {
  console.log(arguments.length)
  console.log(arguments[0], arguments[1], arguments[2])
  console.log(first, second)
}

inspect('a', 'b', 'extra')
inspect('only one')

arguments 没有 Array.prototype 上的 push 等方法:

function checkArguments() {
  console.log([] instanceof Array) // true
  console.log(arguments instanceof Array) // false
  console.log(Array.isArray(arguments)) // false
  console.log(arguments.push) // undefined

  // 可以借用数组方法,但现代代码通常更推荐 rest 参数。
  Array.prototype.push.call(arguments, 'added')
  console.log(arguments[arguments.length - 1]) // added
}

checkArguments(1, 2)

“使用 arguments.push 会报错”并不准确:上面的属性通常只是 undefined;只有直接调用 arguments.push('x') 才会因为不是函数而抛 TypeError

创建一个灵活的格式化函数

原文使用 %1%2 这样的占位符。动态构造字符类的写法无法正确处理 %10,还会把模板字符串本身误算进 arguments.length。可以使用 rest 参数和多位数字匹配:

function format(template, ...values) {
  return String(template).replace(/%(\d+)/g, (match, digits) => {
    const index = Number(digits) - 1
    return index >= 0 && index < values.length
      ? String(values[index])
      : match
  })
}

console.log(
  format(
    'And the %1 want to know whose %2 you %3',
    'papers',
    'shirt',
    'wear'
  )
)
// And the papers want to know whose shirt you wear

console.log(format('value=%10', ...Array.from({ length: 10 }, (_, i) => i)))

这里的 format 是位置格式化函数,不是 ECMAScript 的模板字面量。模板字面量是另一种语法,例如 `hello ${name}`

如果必须兼容旧代码,也可以直接使用 arguments

function formatWithArguments(template) {
  const values = arguments
  return String(template).replace(/%(\d+)/g, (match, digits) => {
    const index = Number(digits)
    return index >= 1 && index < values.length
      ? String(values[index])
      : match
  })
}

console.log(formatWithArguments('I like %1, not %2.', 'JavaScript', 'Java'))

现代可变参数函数优先使用 ...rest,因为 rest 参数直接是真正的数组;需要了解旧 API 或参数映射时再使用 arguments

参数对象与形参的映射

arguments 与形参是否联动取决于函数形式。只有在**非严格模式 + simple parameter list(没有默认值、rest 或解构)**时,普通函数通常具有 mapped arguments

// 需要在浏览器 classic script 的非严格环境中观察 mapped 行为。
function mapped(a) {
  a = 2
  return arguments[0]
}

function mappedBack(a) {
  arguments[0] = 3
  return a
}

console.log(mapped(1)) // 2
console.log(mappedBack(1)) // 3

严格函数、模块、类方法以及使用默认参数/rest/解构参数的函数使用 unmapped arguments

function strictArguments(a) {
  'use strict'
  a = 2
  return arguments[0]
}

function withDefault(a = 1) {
  a = 2
  return arguments[0]
}

function withRest(first, ...rest) {
  return [arguments[0], first, rest]
}

console.log(strictArguments(1)) // 1
console.log(withDefault(1)) // 1
console.log(withRest(1, 2, 3)) // [1, 1, [2, 3]]

如果参数列表含默认值、rest 或解构,函数体内不能再通过单独的函数级 'use strict' 指令切换严格模式;严格模式应由外层 classic script、模块或类提供。

arguments 转换成真正的数组

历史写法是借用 slice

function toArrayLegacy() {
  return Array.prototype.slice.call(arguments)
}

console.log(toArrayLegacy(1, 2, 3)) // [1, 2, 3]

现代写法通常更清晰:

function toArrayModern() {
  return Array.from(arguments)
}

function toArrayWithRest(...items) {
  return items
}

function toArrayWithSpread() {
  return [...arguments]
}

console.log(toArrayModern('a', 'b'))
console.log(toArrayWithRest('a', 'b'))
console.log(toArrayWithSpread('a', 'b'))

Array.from 和展开语法会创建新数组;它们不会让调用者传入的对象自动变成数组。类数组对象至少需要合理的 length 和索引才能得到预期结果。

跨 realm 时,另一个 iframe 中创建的真数组可能使 value instanceof Arrayfalse;检测真数组应使用 Array.isArray(value)arguments 本身仍然不是数组。

通过参数对象封装函数

原文的 makeFunc 展示了“预置参数 + 后续参数”的闭包。apply 的第一个参数是 thisArg,不是函数的 lexical scope;arguments 也不是 apply 的参数数组。

function makeFunc(func, ...preset) {
  if (typeof func !== 'function') {
    throw new TypeError('func must be callable')
  }

  return function wrapped(...later) {
    return Reflect.apply(func, this, [...preset, ...later])
  }
}

const formatMessage = (template, first, second) =>
  format(template, first, second)

const message = makeFunc(formatMessage, 'I like %1 not %2.')
console.log(message('JavaScript', 'Java'))
console.log(message('TypeScript', 'JavaScript'))

这个 helper 会保留调用 wrapped 时的 this,但它仍不能构造 class;如果目标是构造调用,应使用 newReflect.construct,不能使用 apply

创建引用自身的函数

原文使用 arguments.callee 让匿名函数引用自身。arguments.callee 是遗留 API:严格模式、模块、类方法和非简单参数中访问它会抛错,也会妨碍引擎优化。现代代码使用命名函数表达式或命名函数:

function repeat(fn, times, delay) {
  if (typeof fn !== 'function') {
    throw new TypeError('fn must be callable')
  }
  if (!Number.isInteger(times) || times < 0) {
    throw new RangeError('times must be a non-negative integer')
  }

  return function run(...args) {
    if (times-- <= 0) return

    Reflect.apply(fn, this, args)
    if (times > 0) {
      const receiver = this
      setTimeout(() => Reflect.apply(run, receiver, args), delay)
    }
  }
}

const say = repeat((value) => console.log(value), 3, 2_000)
say('Can you hear me, major tom?')

这个示例第一次调用立即执行,后续调用由宿主定时器调度;2_000 是最小延迟提示,不保证每隔精确两秒执行。生产实现还应考虑取消、页面销毁、异常传播和 delay 的有效范围。

历史代码中可能见到:

// 仅作历史识别,不要在严格模式或模块中使用。
function legacyRecursive() {
  return arguments.callee()
}

不要把 arguments.callee 当作“惊喜功能”。命名函数表达式既能自引用,也不会依赖被弃用的调用参数对象属性。

arguments、rest 与函数长度的区别

function demo(a, b = 1, ...rest) {
  return {
    actualCount: arguments.length,
    declaredCount: demo.length,
    first: a,
    rest
  }
}

console.log(demo(10, 20, 30, 40))
// { actualCount: 4, declaredCount: 1, first: 10, rest: [30, 40] }
  • arguments.length:这一次调用实际传入的参数数量;
  • demo.length:函数定义中第一个默认参数之前的形参数量;
  • rest:从指定位置开始收集的真正数组;
  • arguments:非箭头函数的类数组参数对象,主要用于兼容旧 API 或解释参数映射。

小结

  1. arguments 通常只属于非箭头函数的单次调用,不是数组,也不是每个函数对象都拥有的全局属性。
  2. JavaScript 的实参值被赋给形参;对象传递的是引用值的副本,不是按引用传递。
  3. 非严格 simple 参数函数可能存在 mapped arguments;严格、模块、类和非简单参数使用 unmapped 语义。
  4. 现代可变参数函数优先使用 rest 参数,转换类数组可使用 Array.from 或展开语法。
  5. arguments.callee 已是遗留 API,使用命名函数表达式替代。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS