当访问函数调用参数值的时候,我总是对 arguments
对象感觉不舒服。它硬编码的名字使得在一个内部函数中访问外部函数的 arguments
变得困难。
更糟糕的是 JS 中它作为类数组对象,但是不能使用数组的方法像 .map()
或 .forEach()
。
在封闭函数中访问 arguments
,你不得不把它存储在一个临时变量中。遍历类数组对象,你不得不用鸭子类型间接调用。来看下面的例子:1
2
3
4
5
6
7
8
9
10function outerFunction() {
// store arguments into a separated variable
var argsOuter = arguments;
function innerFunction() {
// args is an array-like object
var even = Array.prototype.map.call(argsOuter, function(item) {
// do something with argsOuter
});
}
}
另外一种情形是函数调用接收动态数量的参数。用一个数组作为参数是非常令人讨厌的。
以 .push(item1, ..., itemN)
将元素一个个的插入到一个数组为例:
你必须枚举每个作为参数的元素。这样做有时候是不方便:经常整个数组的元素需要被添加。
在 ES5 中的解决方法是 .apply()
:一个不友好而且繁琐的方法。让我们看看:1
2
3
4var fruits = ['banana'];
var moreFruits = ['apple', 'orange'];
Array.prototype.push.apply(fruits, moreFruits);
console.log(fruits); // ['banana', 'apple', 'orange']
幸运的是 JS 世界正在变化。三点操作符 ...
能解决很多这种情形的问题。ES6 引入了这个操作符,对 JS 是一个显著的改善。
这篇文章将通过 ...
操作符的使用场景展示怎么解决类似问题。
Three dots
可变操作符被用来获得通过函数调用的变量列表。1
2
3
4
5function countArguments(...args) {
return args.length;
}
// get the number of arguments
countArguments('welcome', 'to', 'Earth'); // => 3
展开操作符被用于数组的解构,以及用数组填充函数调用参数。1
2
3
4
5
6
7
8
9
10
11let cold = ['autumn', 'winter'];
let warm = ['spring', 'summer'];
// construct an array
[...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
// destruct an array
let otherSeasons, autumn;
[autumn, ...otherSeasons] = cold;
otherSeasons // => ['winter']
// function arguments from an array
cold.push(...warm);
cold // => ['autumn', 'winter', 'spring', 'summer']
改良的参数存取
可变参数
在简介中提到的,在复杂场景中函数体内处理 arguments
对象变得麻烦。
例如 JS 内部函数 filterNumbers()
想访问外部函数 sumOnlyNumbers()
的 arguments
:1
2
3
4
5
6
7
8
9
10
11function 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
对象。
这种方法可行,但是太繁琐了。
可变操作符优雅的解决了这个问题。它允许在函数声明中定义一个可变参数 ...args
:1
2
3
4
5
6
7
8function 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
接收调用参数放在一个数组中。名字冲突解决了, args
就能够被内部的 filterNumbers()
使用了。
并且忘记类数组对象:args
是一个数组,非常棒的结果。 filterNumbers()
也能摆脱 Array.prototype.filter.call()
了,筛选方法直接调用 args.filter()
。
注意可变参数应该是函数的最后一个参数。
有选择的可变参数
如果不是所有的参数都包含在可变参数内,你可以一开始用逗号分隔的参数定义他们。明确的定义参数不包含在可变参数内。
让我们看个例子:1
2
3
4
5function 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
对象,但是能访问封闭作用域的 arguments
对象。如果你想要获取所有的参数,可以用可变参数。让我们在这个例子中尝试:1
2
3
4
5
6
7
8(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
可变参数在一个数组中包含了函数调用的所有参数。并且 arguments
对象是从封闭作用域中取得与 outerArguments
变量相等,所以它没有实际意义。
改良的函数调用
在文章简介中,第二个问题提出了有没有更好的方式用一个数组填充调用参数。
ES5 在函数对象上提供了 .apply()
方法解决这个问题。不幸的是,这个方法有三个问题:
- 必须手动指定函数调用的上下文
- 不能用做构造函数调用
- 最好有一个更短的解决方案
让我们来看 .apply()
的用例:1
2
3let countries = ['Moldova', 'Ukraine'];
countries.push.apply(countries, ['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']
上面例子中, .apply()
中表示的第二次上下文 countries
看起来是不相干的。在对象上 countries.push
决定方法调用已经足够了。
整个调用看起来很繁琐。
展开运算符用数组中的值填充函数调用的参数。让我们用展开运算符改良上面的例子:1
2
3let countries = ['Moldova', 'Ukraine'];
countries.push(...['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']
正如所见,展开运算符是更清晰简单的解决方案。只附加了三点(...
)字符。
展开运算符能让一个数组填充构造函数的调用参数,用 .apply()
的时候这事不可能的。让我们看个例子:1
2
3
4
5
6
7
8
9
10
11
12class 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'
此外你也可以在同一个调用中联合多个展开操作符和普通参数。下面的例子移除数组中已经存在的元素,然后添加另一个数组和一个元素:1
2
3
4
5var numbers = [1, 2];
var evenNumbers = [4, 8];
const zero = 0;
numbers.splice(0, 2, ...evenNumbers, zero);
console.log(numbers); // => [4, 8, 0]
改良的数组操作
数组结构
数组字面量 [item1, item2, ...]
除了枚举不能提供的功能。
展开运算符通过允许插入其他数组(或任何可迭代的类型)到初始化实例中改良数组字面量。
这个改良使下面描述的任务更容易实现。
创建一个初始化元素来自其他数组的数组:1
2
3
4
5var 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
中的项被用来初始化。
连结两个或更多的数组:1
2
3
4var 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
数组创建。
克隆数组实例:1
2
3
4var words = ['Hi', 'Hello', 'Good day'];
var otherWords = [...words];
console.log(otherWords); // => ['Hi', 'Hello', 'Good day']
console.log(otherWords === words); // => false
otherWords
是 words
数组的一个克隆版本。注意克隆只发生在数组本身,不是内部元素(即不是深度克隆)。
数组解构
在 ES6 中可以使用的解构赋值是很强大的从数组对象中提取数据的表达式。
作为解构的一部分,展开操作符提取数组的一部分。提取结果总是一个数组。
按照语法,展开操作符应该是解构赋值的最后一个:[extractItem1, ...extractedArray] = destructedArray
。
让我们看一些应用:1
2
3
4
5var seasons = ['winter', 'spring', 'summer', 'autumn'];
var coldSeason, otherSeasons;
[coldSeason, ...otherSeasons] = seasons;
console.log(coldSeason); // => 'winter'
console.log(otherSeasons); // => ['spring', 'summer', 'autumn']
[coldSeason, ...otherSeasons]
提取了第一项 winter
到 coldSeason
变量,余下的元素放在 otherSeason
数组。
展开操作符和迭代协议
展开操作符用迭代协议在元素上操作并收集结果。这使得展开操作符非常有用,因为任意的对象能定义操作符怎么提取数据。
一个遵守迭代协议的对象是可迭代的。
迭代协议要求对象包含一个特殊属性。这个属性名叫 Symbol.iterator
,它的值是一个返回迭代对象的函数。这个迭代对象应该遵守迭代协议。它需要提供一个 next
属性,属性的值是一个函数,函数的返回值是包含 done
(表示迭代结束的布尔值)和 value
(迭代结果)属性的对象。
看起来通过文字描述理解了迭代协议,然后后面的代码相当简单。
操作符从对象或原生类型取出数据必须被有序迭代。
很多原生类型和对象能被迭代:字符串、数组、类型数组、sets 和 maps。所以他们默认能使用展开操作符。
我们来看遵守迭代协议的字符串怎么被展开的:1
2
3
4
5
6
7var str = 'hi';
var 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']
我喜欢用对象自定义迭代执行展开操作符。你能控制怎样展开你的对象——一个实用的代码技巧。
下面的例子制造一个符合迭代协议的类数组对象,然后用展开操作符转换成数组。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function iterator() {
var index = 0;
return {
next: () => ({ // Conform to Iterator protocol
done : index >= this.length,
value: this[index++]
})
};
}
var arrayLike = {
0: 'Cat',
1: 'Bird',
length: 2
};
arrayLike[Symbol.iterator] = iterator; //Conform to Iterable Protocol
var array = [...arrayLike];
console.log(array); // => ['Cat', 'Bird']
arrayLike[Symbol.iterator]
在对象上创建一个包含迭代函数 iterator()
的属性使对象符合迭代协议。 iterator()
返回一个包含返回一个控制对象 {done: <boolean>, value: <item>}
的 next
函数的对象(符合迭代协议)。
由于现在 arrayLike
是可迭代的,展开操作符就能取出它的元素放入一个数组: [...arrayLike]
。
最后
三点操作符为 JS 添加了一堆非常好的特性。
可变操书似的收集参数更容易了。它理应替代硬编码的类数组对象 arguments
。如果情形允许在可变参数和 arguments
之间选择,那就选择第一个。
.apply()
的冗长语法是不方便的。当调用参数取自一个数组的时候,展开操作符是一个很好的选择。
展开操作符改良了数组字面量。你能更简单的初始化、连结和克隆数组。
你能用解构赋值取出数组的一部分。结合迭代协议在更多可配置的方式中使用展开操作符变得可能。
我希望从现在开始展开操作符经常出现在你的代码中!