先记住这个答案
形参是函数声明里写的变量名,属于函数的局部变量;arguments 是每次调用时生成的一个类数组对象,按位置记录这次实际传进来的全部值。所以形参写了两个,调用时传五个,arguments.length 就是 5;传一个,第二个形参就是 undefined。两者一般互不影响,只有在非严格模式、参数列表里没有默认值、解构和 rest 时,改 arguments[0] 才会同步改掉第一个形参,反过来也一样。
- 形参看声明,
arguments看这次调用实际传了什么 fn.length数的是形参,arguments.length数的是实参- 只有非严格且参数列表简单时,两者才会联动
- 新代码用 rest 参数代替
arguments
改 arguments[0] 会不会改到形参
这是面试最爱问的一个细节。同样一句 arguments[0] = 7,放在三种函数里结果不一样。
function loose(a) {
arguments[0] = 7 // 非严格 + 简单参数:会联动 a
return a
}
function strict(a) {
'use strict'
arguments[0] = 7 // 严格模式:各改各的
return a
}
function withDefault(a = 1) {
arguments[0] = 7 // 有默认值:各改各的
return a
}
console.log(loose(2), strict(2), withDefault(2))查看输出与解释
7 2 2只有第一个函数返回了 7。早期 JavaScript 为了实现方便,把形参和 arguments 的下标做成了同一块存储的两个名字,改一个另一个就变。这个行为很反直觉,所以严格模式把它去掉了;后来加的默认值、解构、rest 这些「非简单参数」也一律不联动。
实际项目里这条规则的意义在于:别依赖它。比如一个老函数靠改 arguments[0] 来修正参数,某天有人给形参加了个默认值,联动悄悄失效,结果就变了,而且很难查。
函数 length 和 arguments.length 的区别
两个 length 名字一样,一个看声明,一个看调用,是面试常见陷阱。
length 分别数什么JavaScriptfunction f(a, b = 2, ...rest) {
console.log(arguments.length, a) // 这次调用传了几个值
}
console.log(f.length) // 声明里的形参个数
f()
f(1, 2, 3, 4)查看输出与解释
1
0 undefined
4 1f.length 是 1,因为它只数第一个带默认值的参数之前的形参,rest 参数也不算。这个值在函数定义时就定了,和怎么调用无关。
arguments.length 每次调用都不同。f() 时是 0,这里能看出默认值不会写进 arguments。形参 b 在函数里是 2,但 arguments[1] 仍是 undefined。f(1, 2, 3, 4) 时是 4,多传的值即使没有形参接收,也都在 arguments 里。
- 什么时候会用到
fn.length - 柯里化会用
fn.length判断参数凑够了没有。所以被柯里化的函数如果带默认值,length变小,柯里化就会提前执行,这是一个真实会踩的坑。
现在为什么推荐用 rest 参数
arguments 是类数组:有下标和 length,但不是真数组,不能直接调 map、filter。箭头函数里也没有自己的 arguments,读到的是外层普通函数的。
rest 参数 (...args) 解决了这几个问题。args 是真正的数组,只收集剩下的参数,箭头函数里也能用,TypeScript 还能给它标元组类型。所以新代码里需要不定数量参数时,直接写 rest,arguments 只在读老代码时需要认识。
容易答错的地方
- 把
fn.length当成传了几个参数 fn.length来自函数声明,而且遇到默认值和 rest 会提前停止计数。想知道这次调用传了几个,要在函数里看arguments.length或者args.length。- 以为严格模式下不能用
arguments - 严格模式下照样能读下标和
length,只是不再和形参联动,另外访问arguments.callee会抛TypeError。 - 以为
arguments里会有默认值 - 默认值只作用于形参。没传的位置在
arguments里就是没有,arguments.length也不会把它算进去。
面试官还会怎么问?
怎么把 arguments 转成真正的数组?
Array.from(arguments) 或 [...arguments] 最直接,老代码里常见的是 Array.prototype.slice.call(arguments)。不过更好的办法是一开始就写成 rest 参数。
arguments 能用 for...of 遍历吗?
能。普通函数里的 arguments 实现了 Symbol.iterator,所以可以 for...of,也可以展开。但它的原型还是 Object.prototype,Array.isArray(arguments) 返回 false。
为什么 TypeScript 项目里基本不用 arguments?
arguments 的类型是 IArguments,每一项都是 any,和形参的类型也对不上。rest 参数可以写成 ...args: [string, number] 这样的元组类型,调用方传错了编译期就能发现。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。