许多 JavaScript 内置函数支持任意数量的参数。
例如
Math.max(arg1, arg2, ..., argN)
– 返回参数中的最大值。Object.assign(dest, src1, ..., srcN)
– 将src1..N
中的属性复制到dest
中。- ……等等。
在本章中,我们将学习如何执行相同操作。此外,还将学习如何将数组作为参数传递给此类函数。
剩余参数 ...
无论如何定义函数,都可以使用任意数量的参数调用该函数。
如下所示
function
sum
(
a,
b
)
{
return
a +
b;
}
alert
(
sum
(
1
,
2
,
3
,
4
,
5
)
)
;
由于“过多的”参数,不会出现错误。但当然,结果中只会计算前两个参数,因此上面代码中的结果是 3
。
可以使用三个点 ...
后跟将包含它们的数组的名称,将剩余的参数包含在函数定义中。这些点实际上表示“将剩余的参数收集到一个数组中”。
例如,将所有参数收集到数组 args
中
function
sumAll
(
...
args
)
{
// args is the name for the array
let
sum =
0
;
for
(
let
arg of
args)
sum +=
arg;
return
sum;
}
alert
(
sumAll
(
1
)
)
;
// 1
alert
(
sumAll
(
1
,
2
)
)
;
// 3
alert
(
sumAll
(
1
,
2
,
3
)
)
;
// 6
我们可以选择将第一个参数作为变量获取,而只收集其余参数。
这里前两个参数进入变量,其余参数进入 titles
数组
function
showName
(
firstName,
lastName,
...
titles
)
{
alert
(
firstName +
' '
+
lastName )
;
// Julius Caesar
// the rest go into titles array
// i.e. titles = ["Consul", "Imperator"]
alert
(
titles[
0
]
)
;
// Consul
alert
(
titles[
1
]
)
;
// Imperator
alert
(
titles.
length )
;
// 2
}
showName
(
"Julius"
,
"Caesar"
,
"Consul"
,
"Imperator"
)
;
剩余参数收集所有剩余参数,因此以下内容没有意义并会导致错误
function
f
(
arg1,
...
rest,
arg2
)
{
// arg2 after ...rest ?!
// error
}
...rest
必须始终是最后一个。
“arguments” 变量
还有一个名为 arguments
的特殊类数组对象,它按其索引包含所有参数。
例如
function
showName
(
)
{
alert
(
arguments.
length )
;
alert
(
arguments[
0
]
)
;
alert
(
arguments[
1
]
)
;
// it's iterable
// for(let arg of arguments) alert(arg);
}
// shows: 2, Julius, Caesar
showName
(
"Julius"
,
"Caesar"
)
;
// shows: 1, Ilya, undefined (no second argument)
showName
(
"Ilya"
)
;
在过去,该语言中不存在剩余参数,而使用 arguments
是获取函数所有参数的唯一方法。它仍然有效,我们可以在旧代码中找到它。
但缺点是,尽管 arguments
既类似数组又可迭代,但它不是数组。它不支持数组方法,因此我们无法调用 arguments.map(...)
。
此外,它始终包含所有参数。我们无法部分捕获它们,就像我们使用剩余参数所做的那样。
因此,当我们需要这些特性时,则优先使用剩余参数。
"arguments"
如果我们从箭头函数访问 arguments
对象,它将从外部“普通”函数中获取它们。
这是一个例子
function
f
(
)
{
let
showArg
=
(
)
=>
alert
(
arguments[
0
]
)
;
showArg
(
)
;
}
f
(
1
)
;
// 1
正如我们所记得的,箭头函数没有自己的 this
。现在我们知道它们也没有特殊的 arguments
对象。
展开语法
我们刚刚看到如何从参数列表中获取数组。
但有时我们需要做完全相反的事情。
例如,有一个内置函数 Math.max,它返回列表中最大的数字
alert
(
Math.
max
(
3
,
5
,
1
)
)
;
// 5
现在假设我们有一个数组 [3, 5, 1]
。我们如何使用它调用 Math.max
?
“按原样”传递它不起作用,因为 Math.max
期望一个数字参数列表,而不是一个数组
let
arr =
[
3
,
5
,
1
]
;
alert
(
Math.
max
(
arr)
)
;
// NaN
而且我们肯定不能在代码 Math.max(arr[0], arr[1], arr[2])
中手动列出项,因为我们可能不确定有多少项。当我们的脚本执行时,可能有很多,也可能没有。那会变得很丑陋。
展开语法 来救援!它看起来类似于剩余参数,也使用 ...
,但恰恰相反。
当在函数调用中使用 ...arr
时,它将可迭代对象 arr
“展开”为参数列表。
对于 Math.max
let
arr =
[
3
,
5
,
1
]
;
alert
(
Math.
max
(
...
arr)
)
;
// 5 (spread turns array into a list of arguments)
我们还可以通过这种方式传递多个可迭代对象
let
arr1 =
[
1
,
-
2
,
3
,
4
]
;
let
arr2 =
[
8
,
3
,
-
8
,
1
]
;
alert
(
Math.
max
(
...
arr1,
...
arr2)
)
;
// 8
我们甚至可以将展开语法与正常值结合起来
let
arr1 =
[
1
,
-
2
,
3
,
4
]
;
let
arr2 =
[
8
,
3
,
-
8
,
1
]
;
alert
(
Math.
max
(
1
,
...
arr1,
2
,
...
arr2,
25
)
)
;
// 25
此外,展开语法还可以用于合并数组
let
arr =
[
3
,
5
,
1
]
;
let
arr2 =
[
8
,
9
,
15
]
;
let
merged =
[
0
,
...
arr,
2
,
...
arr2]
;
alert
(
merged)
;
// 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2)
在上面的示例中,我们使用了一个数组来演示展开语法,但任何可迭代对象都可以使用。
例如,这里我们使用展开语法将字符串转换为字符数组
let
str =
"Hello"
;
alert
(
[
...
str]
)
;
// H,e,l,l,o
展开语法在内部使用迭代器来收集元素,与 for..of
的方式相同。
因此,对于字符串,for..of
返回字符,...str
变成 "H","e","l","l","o"
。字符列表传递给数组初始化器 [...str]
。
对于此特定任务,我们还可以使用 Array.from
,因为它将可迭代对象(如字符串)转换为数组
let
str =
"Hello"
;
// Array.from converts an iterable into an array
alert
(
Array.
from
(
str)
)
;
// H,e,l,l,o
结果与 [...str]
相同。
但是 Array.from(obj)
和 [...obj]
之间存在细微差别
Array.from
同时适用于类数组和可迭代对象。- 展开语法仅适用于可迭代对象。
因此,对于将某些内容转换为数组的任务,Array.from
往往更通用。
复制数组/对象
还记得我们过去讨论过的 Object.assign()
吗?
使用展开语法可以执行相同操作。
let
arr =
[
1
,
2
,
3
]
;
let
arrCopy =
[
...
arr]
;
// spread the array into a list of parameters
// then put the result into a new array
// do the arrays have the same contents?
alert
(
JSON
.
stringify
(
arr)
===
JSON
.
stringify
(
arrCopy)
)
;
// true
// are the arrays equal?
alert
(
arr ===
arrCopy)
;
// false (not same reference)
// modifying our initial array does not modify the copy:
arr.
push
(
4
)
;
alert
(
arr)
;
// 1, 2, 3, 4
alert
(
arrCopy)
;
// 1, 2, 3
请注意,可以执行相同操作来复制对象
let
obj =
{
a:
1
,
b:
2
,
c:
3
}
;
let
objCopy =
{
...
obj }
;
// spread the object into a list of parameters
// then return the result in a new object
// do the objects have the same contents?
alert
(
JSON
.
stringify
(
obj)
===
JSON
.
stringify
(
objCopy)
)
;
// true
// are the objects equal?
alert
(
obj ===
objCopy)
;
// false (not same reference)
// modifying our initial object does not modify the copy:
obj.
d =
4
;
alert
(
JSON
.
stringify
(
obj)
)
;
// {"a":1,"b":2,"c":3,"d":4}
alert
(
JSON
.
stringify
(
objCopy)
)
;
// {"a":1,"b":2,"c":3}
这种复制对象的方式比 let objCopy = Object.assign({}, obj)
或数组 let arrCopy = Object.assign([], arr)
短得多,因此我们希望尽可能使用它。
总结
当我们在代码中看到 "..."
时,它要么是剩余参数,要么是展开语法。
有一种简单的方法可以区分它们
- 当
...
位于函数参数的末尾时,它就是“剩余参数”,并将参数列表的其余部分收集到数组中。 - 当
...
出现在函数调用或类似位置时,它被称为“展开语法”,并将数组展开为列表。
使用模式
- 剩余参数用于创建接受任意数量参数的函数。
- 展开语法用于将数组传递给通常需要多个参数列表的函数。
它们共同帮助轻松地在列表和参数数组之间进行转换。
函数调用的所有参数也存在于“旧式”arguments
:类数组可迭代对象中。