翻译:道奇
作者:Dmitri Pavlutin
原文: How Three Dots Changed JavaScript
当访问调用函数的参数时我不喜欢使用 arguments
关键字,它的硬编码形式使得在函数内部访问外部函数(有自己的 arguments
)的 arguments
变得很困难。
更糟糕的是 arguments
是个类数组对象,你不能像方法一样直接在它上面使用 .map()
或 forEach()
。
如果要在嵌套函数中访问外部函数的 arguments
,就需要将它存储在独立的变量上,要遍历这个类似数组的对象,必须使用 duck typing
(动态类型风格之一)并进行间接调用。看下面的例子:
function outerFunction() { // 将arguments存储到独立的变量上 const argsOuter = arguments; function innerFunction() { // args是个类数组对象 const even = Array.prototype.map.call(argsOuter, function(item) { // 用argsOuter做一些处理 }); } } 复制代码
另外一种情况是函数调用接受动态数量的参数,往数组里塞参数可不是让人愉快的事。
例如 .push(item1, ..., itemN)
一个接一个向数组插入元素:这就需要我们自己枚举参数的每个元素,这经常会很不方便:经常会碰到需要在不创建新实例的情况下,将整个数组的元素推入另一个数组。
在 ES5
中,通过 .apply()
解决:不友好且冗长的方法。可以看一下:
const fruits = ['banana']; const moreFruits = ['apple', 'orange']; Array.prototype.push.apply(fruits, moreFruits); console.log(fruits); // => ['banana', 'apple', 'orange'] 复制代码
幸运的是, JavaScript
的世界一直在变,三点运算符 ...
解决了很多类似的问题,这个运算符是在 ECMAScript 6
中引入进来的,在我看来它是一个显著的提高。
这篇文章介绍了 ...
运算符使用场景并且展示了如何解决类似的问题。
rest运算符用于在函数调用和数组解构时获取参数列表,一种场景就是当 运算符在操作之后收集剩下的rest 。
function countArguments(...args) { return args.length; } // 获取参数的数量 countArguments('welcome', 'to', 'Earth'); // => 3 // 解构数组 let otherSeasons, autumn; [autumn, ...otherSeasons] = ['autumn', 'winter']; otherSeasons // => ['winter'] 复制代码
扩展运算符用于数组的构造和解构,在调用时从数组中填充函数参数,一种场景就是当 运算符扩展数组元素 。
let cold = ['autumn', 'winter']; let warm = ['spring', 'summer']; // 构造数组 [...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer'] // 来源于数组的函数参数 cold.push(...warm); cold // => ['autumn', 'winter', 'spring', 'summer'] 复制代码
以上两种场景相当于相反的过程。
正如在介绍中所提到的,复杂的场景中处理函数体中的 arguments
对象非常麻烦。
例如, JavaScript
中的内部函数 filterNumbers()
要访问它的外部函数 sumOnlyNumbers()
的 arguments
:
function sumOnlyNumbers() { const args = arguments; const numbers = filterNumbers(); return numbers.reduce((sum, element) => sum + element); function filterNumbers() { return Array.prototype.filter.call(args, element => typeof element === 'number' ); } } sumOnlyNumbers(1, 'Hello', 5, false); // => 6 复制代码
为了访问 filterNumbers()
内部函数 sumOnlyNumbers()
的 arguments
,你必须创建一个临时变量 args
,这样做是因为 filterNumbers()
定义了它自己的 arguments
对象,而它会覆盖外部的 arguments
。
这种方法有用,但是太啰嗦了 ,const args = arguments
可以省略, Array.prototype.filter.call(args)
也可以通过使用 rest
参数改成 args.filter()
。让我们在这节中对它进行优化。
rest
运算符很优雅的解决了这个问题,它允许在函数声明中定义 rest
参数 ...args
:
function sumOnlyNumbers(...args) { const numbers = filterNumbers(); return numbers.reduce((sum, element) => sum + element); function filterNumbers() { return args.filter(element => typeof element === 'number'); } } sumOnlyNumbers(1, 'Hello', 5, false); // => 6 复制代码
函数声明 function sumOnlyNumbers(...args)
, args
表示接收的调用参数是数组形式的。因为名称冲突的问题解决了, args
就可以在 filterNumbers()
内部使用。
也不用管类数组对象: args
是个数组
,这是个非常好的好处。因此, filterNumbers()
可以去掉 Array.prototype.filter.call()
,直接调用 filter
方法 args.filter()
。
注意, rest
参数应该是函数参数列表中的最后一个参数。
如果不需要把所有的值包含到 rest
参数中,你可以在开头以逗号分隔的形式定义这些参数, rest
参数中不包含显式定义的参数。
让我们看个例子:
function filter(type, ...items) { return items.filter(item => typeof item === type); } filter('boolean', true, 0, false); // => [true, false] filter('number', false, 4, 'Welcome', 7); // => [4, 7] 复制代码
arguments
对象没有这种可选能力,所以经常会包含所有的值。
箭头函数在它的函数体内没有定义 arguments
但是可以访问到一个这样的参数,如果你需要获取所有参数,可以使用 rest
参数。在下面的例子中试一下:
(function() { let outerArguments = arguments; const concat = (...items) => { console.log(arguments === outerArguments); // => true return items.reduce((result, item) => result + item, ''); }; concat(1, 5, 'nine'); // => '15nine' })(); 复制代码
items
这个 rest
参数包含数组内所有函数调用参数,封闭域内也可以拿到 arguments
对象,它等于 outerArguments
变量,所以它是无意义的。
在本文的简介中,第二个问题需要有更好的方式用数组填充参数。
ES5
在函数对象上提供了 .apply()
函数来解决这个问题,不幸的是,这种方法有 3
个问题:
我们看一个.apply()使用的例子:
const countries = ['Moldova', 'Ukraine']; const otherCountries = ['USA', 'Japan']; countries.push.apply(countries, otherCountries); console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan'] 复制代码
就像前面提到的,在 apply()
中第二次引用上下文 countries
看起来是不相关的,属性访问器 countries.push
足以确定对象上的方法调用。上面整个调用看起来就有点冗长。
扩展运算符使用数组中的值填充函数调用的参数(或者更严格地从可迭代对象开始,可以看第5节)。 下面用扩展运算符优化一下上面的例子:
const countries = ['Moldova', 'Ukraine']; const otherCountries = ['USA', 'Japan']; countries.push(...otherCountries); console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan'] 复制代码
就像上面看到的,扩展运算符是一种更干净更直接的解决方法,唯一的额外字符是 3
个点 (...)
。
扩展运算符从数组中配置构造函数调用参数,当在使用 .apply()
时就 不可能很直接
。可以看个例子:
class King { constructor(name, country) { this.name = name; this.country = country; } getDescription() { return `${this.name} leads ${this.country}`; } } const details = ['Alexander the Great', 'Greece']; const Alexander = new King(...details); Alexander.getDescription(); // => 'Alexander the Great leads Greece' 复制代码
更重要的是你可以在同一个调用中合并多个扩展运算符和常规参数,下面的例子将数组的现有元素移除,再添加另外的数组和元素:
const numbers = [1, 2]; const evenNumbers = [4, 8]; const zero = 0; numbers.splice(0, 2, ...evenNumbers, zero); console.log(numbers); // => [4, 8, 0] 复制代码
数组定量值 [item1, item2, .., itemN]
除了提供枚举数组初始化元素的功能外,不提供其他功能。
扩展运算符允许快速将其他数组(或者其他定量值)插入初始化实例中,这优化了数组定量值的操作,这种改进使得完成下面这种常见任务变得更加容易。
利用 另外的数组 的初始化元素 创建 一个数组:
const initial = [0, 1]; const numbers1 = [...initial, 5, 7]; console.log(numbers1); // => [0, 1, 5, 7] const numbers2 = [4, 8, ...initial]; console.log(numbers2); // => [4, 8, 0, 1] 复制代码
number1
和 number2
数组是通过数组定量值创建的,与此同时使用 initial
中的项进行初始化。
连接两个或多个数组:
const odds = [1, 5, 7]; const evens = [4, 6, 8]; const all = [...odds, ...evens]; console.log(all); // => [1, 5, 7, 4, 6, 8] 复制代码
all
数组创建于 odds
和 evens
数组的连接。
克隆数组实例:
const words = ['Hi', 'Hello', 'Good day']; const otherWords = [...words]; console.log(otherWords); // => ['Hi', 'Hello', 'Good day'] console.log(otherWords === words); // => false 复制代码
otherWords
是 words
数组的克隆版本,注意,克隆只发生在数组本身上,而不发生在包含的元素上(即它不是深度克隆)。
解构 赋值
,在 ECMAScript 6
可以用,是从数组和对象中提取数据的强大表达式。
作为解构的一部分, rest
运算符提取数组中的一部分,提取的结果也经常是数组。
在语法方面, rest
运算符应该在解构赋值语的最后一项: [extractedItem1, ...restArray] = destructuredArray
。
让我们看一下应用:
const seasons = ['winter', 'spring', 'summer', 'autumn']; const head, restArray; [head, ...restArray] = seasons; console.log(head); // => 'winter' console.log(restArray); // => ['spring', 'summer', 'autumn'] 复制代码
[head, ...restArray]
将第一个项 'winter'
提取到变量 head
中,剩下的元素提取到 restArray
中。
扩展运算符使用 迭代协议 导航集合上的每个元素。因为对象可以定义运算符怎样提取数据,这使得扩展运算符更加有用。
"当对象符合 Iterable 协议时,它就是可迭代的"
迭代协议需要对象包含特殊的属性,属性的名称必须是 Symbol.iterator
并且它的值是一个返回迭代对象的函数。
interface Iterable { [Symbol.iterator]() { //... return Iterator; } } 复制代码
"可迭代对象必须符合 迭代协议 "
需要提供一个属性 next
,该属性值是一个函数,它返回带 done
(指示迭代结束的布尔值)和 value
(迭代结果)属性的对象。
interface Iterator { next() { //... return { value: <value>, done: <boolean> }; }; } 复制代码
从口头描述上看起来很难理解迭代协议,但在协议后的代码是非常简单的。
对象或原始值 必须 是可以迭代的,扩展运算符才可以从中提到数据。
很多原先原始类型和对象是可迭代的:字符串, 数组, typed数组
, sets
和 maps
。对它们可以使用扩展运算符。
例如,让我们看看一个字符串如何遵守迭代协议的:
const str = 'hi'; const iterator = str[Symbol.iterator](); iterator.toString(); // => '[object String Iterator]' iterator.next(); // => { value: 'h', done: false } iterator.next(); // => { value: 'i', done: false } iterator.next(); // => { value: undefined, done: true } [...str]; // => ['h', 'i'] 复制代码
我喜欢扩展运算符使用对象常规的迭代实现,你可以控制扩展运算符如何使用对象-这是一种有效的 coding
技术。
下面的例子让一个类数组对象遵守迭代协议,然后使用扩展运算符将它转换成数组:
function iterator() { let index = 0; return { next: () => ({ // Conform to Iterator protocol done : index >= this.length, value: this[index++] }) }; } const arrayLike = { 0: 'Cat', 1: 'Bird', length: 2 }; // Conform to Iterable Protocol arrayLike[Symbol.iterator] = iterator; const array = [...arrayLike]; console.log(array); // => ['Cat', 'Bird'] 复制代码
arrayLike[Symbol.iterator]
在包含迭代函数 iterator()
的对象上新建了一个属性,使得这个对象遵守迭代协议。
terator()
返回一个带 next
属性的对象,这个 next
属性用于返回控制对象: {done: <boolean>, value:<item>}
。
因为 arrayLike
现在是可迭代的,扩展运算符用来将它的元素提取进数组: [...arrayLike]
。
三个点运算符给 JavaScript
带来了一大波很棒的功能。
rest
参数使得收集参数变得很简单,它是硬编码类数组对象 arguments
的合理替代方案,如果情况允许选择 rest
参数和 arguments
,建议选择前者。
.apply()
方法的冗长的语法用起来很不方便。当需要从数组中获取调用参数时,扩展运算符是个不错的替代方案。
扩展运算符优化了数组定量值的使用,可以更简单的用于初始化、连接和克隆数组。
可以使用解构赋值来提取数组的一部分。与迭代协议相结合,扩展运算可以以更多的配置方式使用。
希望从现在起扩展运算符可以更频繁的出现在你的代码中。