Spread 语法
我们刚刚看到了如何从参数列表中获取数组。
有时候我们也需要做与之相反的事。
例如,内建函数 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(arg[0], arg[1], arg[2])
,因为我们不确定这儿有多少个。在代码执行时,参数数组中可能有很多个元素,也可能一个都没有。而且,这样的代码也很不优雅。
Spread 语法 可以解决这个问题!它看起来和 rest 参数很像,也使用 ...
,但是二者的用途完全相反。
当在函数调用中使用 ...arr
时,它会把可迭代对象 arr
“展开”到参数列表中。
以 Math.max
为例:
let arr = [3, 5, 1]; alert( Math.max(...arr) ); // 5(spread 语法把数组转换为参数列表)
我们还可以通过这种方式传入多个可迭代对象:
let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(...arr1, ...arr2) ); // 8
我们甚至还可以将 spread 语法与常规值结合使用:
let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1]; alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25
并且,我们还可以使用 spread 语法来合并数组:
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,然后是 arr,然后是 2,然后是 arr2)
在上面的示例中,我们使用数组展示了 spread 语法,其实我们可以用 spread 语法这样操作任何可迭代对象。
例如,在这儿我们使用 spread 语法将字符串转换为字符数组:
let str = "Hello"; alert( [...str] ); // H,e,l,l,o
Spread 语法内部使用了迭代器来收集元素,与 for..of
的方式相同。
因此,对于一个字符串,for..of
会逐个返回该字符串中的字符,...str
也同理会得到 "H","e","l","l","o"
这样的结果。随后,字符列表被传递给数组初始化器 [...str]
。
对于这个特定任务,我们还可以使用 Array.from
来实现,因为该方法会将一个可迭代对象(如字符串)转换为数组:
let str = "Hello"; // Array.from 将可迭代对象转换为数组 alert( Array.from(str) ); // H,e,l,l,o
运行结果与 [...str]
相同。
不过 Array.from(obj)
和 [...obj]
存在一个细微的差别:
Array.from
适用于类数组对象也适用于可迭代对象。- Spread 语法只适用于可迭代对象。
因此,对于将一些“东西”转换为数组的任务,Array.from
往往更通用。
复制 array/object
还记得我们 之前讲过的Object.assign()
吗?
使用 spread 语法也可以做同样的事情(译注:也就是进行浅拷贝)。
let arr = [1, 2, 3]; let arrCopy = [...arr]; // 将数组 spread 到参数列表中 // 然后将结果放到一个新数组 // 两个数组中的内容相同吗? alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true // 两个数组相等吗? alert(arr === arrCopy); // false(它们的引用是不同的) // 修改我们初始的数组不会修改副本: 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 到参数列表中 // 然后将结果返回到一个新对象 // 两个对象中的内容相同吗? alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true // 两个对象相等吗? alert(obj === objCopy); // false (not same reference) // 修改我们初始的对象不会修改副本: 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 arrCopy = Object.assign([], arr)
复制数组,或使用 let objCopy = Object.assign({}, obj)
复制对象来说更为简便。因此,只要情况允许,我们倾向于使用它。
总结
当我们在代码中看到 "..."
时,它要么是 rest 参数,要么是 spread 语法。
有一个简单的方法可以区分它们:
- 若
...
出现在函数参数列表的最后,那么它就是 rest 参数,它会把参数列表中剩余的参数收集到一个数组中。 - 若
...
出现在函数调用或类似的表达式中,那它就是 spread 语法,它会把一个数组展开为列表。
使用场景:
- Rest 参数用于创建可接受任意数量参数的函数。
- Spread 语法用于将数组传递给通常需要含有许多参数的函数。
我们可以使用这两种语法轻松地互相转换列表与参数数组。
旧式的 arguments
(类数组且可迭代的对象)也依然能够帮助我们获取函数调用中的所有参数。