...是怎样改变JavaScript的
翻译:道奇 作者: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
中引入进来的,在我看来它是一个显著的提高。
这篇文章介绍了...
运算符使用场景并且展示了如何解决类似的问题。
1. 三点
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']
以上两种场景相当于相反的过程。
2.优化参数访问
2.1 rest参数
正如在介绍中所提到的,复杂的场景中处理函数体中的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
参数应该是函数参数列表中的最后一个参数。
2.2 可选择的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
对象没有这种可选能力,所以经常会包含所有的值。
2.3 箭头函数的例子
箭头函数在它的函数体内没有定义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
变量,所以它是无意义的。
3.优化函数调用
在本文的简介中,第二个问题需要有更好的方式用数组填充参数。
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]
4.优化数组操作
4.1数组结构
数组定量值[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
数组的克隆版本,注意,克隆只发生在数组本身上,而不发生在包含的元素上(即它不是深度克隆)。
4.2数组解构
解构赋值,在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
中。
5. 扩展运算符和迭代协议
扩展运算符使用迭代协议导航集合上的每个元素。因为对象可以定义运算符怎样提取数据,这使得扩展运算符更加有用。
"当对象符合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()
方法的冗长的语法用起来很不方便。当需要从数组中获取调用参数时,扩展运算符是个不错的替代方案。
扩展运算符优化了数组定量值的使用,可以更简单的用于初始化、连接和克隆数组。
可以使用解构赋值来提取数组的一部分。与迭代协议相结合,扩展运算可以以更多的配置方式使用。
希望从现在起扩展运算符可以更频繁的出现在你的代码中。
转载自:https://juejin.cn/post/6844903993185927181