译者:loveky
当在函数调用中通过
arguments
对象访问参数时,我总是感觉很不爽。它那硬编码的名字使得要想在内层函数(它拥有自己的
arguments
)中访问外层函数的
arguments
变得很困难。
更糟糕的是它是一个类数组对象。这意味着你不能直接在它身上调用类似
.map()
或是
.forEach()
这样的方法。
要想在内层函数里访问外层的
arguments
,你需要采用把它保存在一个单独变量里这样的变通方法。并且当需要遍历这个类数组对象时,你不得不使用鸭子类型并进行间接调用。看看下面的例子:
function outerFunction() {
// 把arguments保存在单独变量中
var argsOuter = arguments;
function innerFunction() {
// args is an array-like object
var even = Array.prototype.map.call(argsOuter, function(item) {
// 访问argsOuter
});
}
}
复制代码
另一种情况是当函数调用接收可变数量的参数时。把数组元素填充到参数列表是很讨厌的。
例如,
.push(item1, ..., itemN)
会把元素一个接一个的插入数组:你不得不循环每个元素将其作为参数。但这并不总是很方便:有时需要把一整个数组的元素push到目标数组。
在ES5中这可以通过
.apply()
做到:用一种不友好且繁琐的方式。让我们看看:
var fruits = ['banana'];
var moreFruits = ['apple', 'orange'];
Array.prototype.push.apply(fruits, moreFruits);
console.log(fruits); // => ['banana', 'apple', 'orange']
复制代码
幸运的是JavaScript的世界在不断改变。三点运算符
...
解决了很多这样的问题。这个操作符在ECMAScript 6中被引入,在我看来这是一个显著的改进。
本文将逐一浏览
...
运算符的使用场景并看看它是如何解决类似问题的。
1. 三个点
rest运算符 用于获取函数调用时传入的参数。
function countArguments(...args) {
return args.length;
}
// 获取参数的数量
countArguments('welcome', 'to', 'Earth'); // => 3
复制代码
spread运算符 用于数组的构造,析构,以及在函数调用时使用数组填充参数列表。
let cold = ['autumn', 'winter'];
let warm = ['spring', 'summer'];
// 构造数组
[...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
// 析构数组
let otherSeasons, autumn;
[autumn, ...otherSeasons] = cold;
otherSeasons // => ['winter']
// 将数组元素用于函数参数
cold.push(...warm);
cold // => ['autumn', 'winter', 'spring', 'summer']
复制代码
2. 改进的参数访问
2.1 Rest参数
像在文章开始描述的那样,在复杂情景中的函数体内操作
arguments
对象是很麻烦的。
举例来说,一个内层函数
filterNumbers()
想要访问外层函数
sumOnlyNumbers()
的
arguments
对象:
function sumOnlyNumbers() {
var args = arguments;
var 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
。
这种方式可以工作,但是太繁琐了。
rest运算符可以优雅的解决这个问题。它允许你在函数声明时定义一个
rest参数
...args
:
function sumOnlyNumbers(...args) {
var 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
以数组的形式接受调用参数。由于不存在命名冲突,现在可以在
filterNumbers()
中访问
args
了。
同样的,忘掉类数组对象吧:
args
就是一个数组
。因此
filterNumbers()
可以摒弃
Array.prototype.filter.call()
而像
args.filter()
这样直接调用
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
而是会访问外层作用域中的
arguments
对象。
让我们通过一个例子看看:
(function() {
let outerArguments = arguments;
const concat = (...items) => {
console.log(arguments === outerArguments); // => true
return items.reduce((result, item) => result + item, '');
};
concat(1, 5, 'nine'); // => '15nine'
})();
复制代码
rest参数
items
包含了所有函数调用的参数。同时,
arguments
对象来自外层作用域并且和
outerArguments
变量指向同一个对象,因此并没有什么实际意义。
3. 改进的函数调用
在本文的引言中提出的第二个问题是如何更好的把数组中的元素作为参数填充到函数调用中。
ES5在函数对象上提供了
.apply()
来解决这个问题。不幸的是这项技术有3个问题:
-
它需要手工的指定函数调用的上下文
-
它不能使用在构造器函数调用中
-
人们倾向于一个更短的解决方案
让我们看一个
.apply()
的使用案例:
let countries = ['Moldova', 'Ukraine'];
countries.push.apply(countries, ['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']
复制代码
像前面提到的那样,在
.apply()
调用中两次指明上下文
countries
是无关紧要的。属性访问器
countries.push
已经足够判定该方法是在哪个对象上调用的了。
同时,整个调用看起来非常繁琐。
spread运算符 使用来自数组(更确切的说是一个迭代器对象)中的值填充函数调用时的参数。
让我们使用spread运算符优化上边的示例:
let countries = ['Moldova', 'Ukraine'];
countries.push(...['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']
复制代码
如你所见,spread运算符是一种更简单,直接的解决方案。唯一的额外字符就是三个点(
...
)。
Spread运算符可以在构造器调用中使用数组元素作为参数,而这并不能通过
直接使用
.apply()
做到。让我们看一个例子:
class King {
constructor(name, country) {
this.name = name;
this.country = country;
}
getDescription() {
return `${this.name} leads ${this.country}`;
}
}
var details = ['Alexander the Great', 'Greece'];
var Alexander = new King(...details);
Alexander.getDescription(); // => 'Alexander the Great leads Greece'
复制代码
此外,你还可以在一次调用中组合使用多个spread运算符以及普通参数。下面的例子从数组中移除现有元素并插入其它数组的元素以及一个普通变量:
var numbers = [1, 2];
var evenNumbers = [4, 8];
const zero = 0;
numbers.splice(0, 2, ...evenNumbers, zero);
console.log(numbers); // => [4, 8, 0]
复制代码
4. 改进的数组操作
4.1 构造数组
数组字面量
[item1, item2, ...]
除了枚举数组元素以外并没有提供其它功能。
Spread运算符允许把其它数组(或是其它的 迭代器 )的内容动态的插入到一个数组字面量中。
这项改进使得要完成以下描述的常见操作变得更容易。
使用 来自其他数组 的元素 创建 数组
var initial = [0, 1];
var numbers1 = [...initial, 5, 7];
console.log(numbers1); // [0, 1, 5, 7]
let numbers2 = [4, 8, ...initial];
console.log(numbers2); // => [4, 8, 0, 1]
复制代码
创建
number1
和
number2
两个数组时使用了数组字面量同时使用来自
initial
中的元素进行了初始化。
串联 2个及以上的数组
var odds = [1, 5, 7];
var evens = [4, 6, 8];
var all = [...odds, ...evens];
console.log(all); // => [1, 5, 7, 4, 6, 8]
复制代码
创建数组
all
时使用了数组
odds
和
evens
的结合。
复制 一个数组
var words = ['Hi', 'Hello', 'Good day'];
var otherWords = [...words];
console.log(otherWords); // => ['Hi', 'Hello', 'Good day']
console.log(otherWords === words); // => false
复制代码
otherWords
是数组
words
的一份拷贝。要注意的是复制只发生在数组自身上,而不会复制数组内的元素(换句话说,这不是深拷贝)。
4.2 析构数组
在ECMAScript 6中引入的 析构赋值 表达式可以很容易的从数组,对象中提取数据。
作为析构的一部分,spread运算符会提取数组中的部分数据。提取的结果是一个数组。