专栏名称: 众成翻译
翻译,求知的另一种表达
目录
相关文章推荐
银行家杂志  ·  中央一号文件释放了哪些新信号? ·  昨天  
中国人民银行  ·  中国人民银行征信中心博士后科研工作站2025 ... ·  昨天  
中国人民银行  ·  中共中央 国务院关于进一步深化农村改革 ... ·  2 天前  
51好读  ›  专栏  ›  众成翻译

改变JavaScript的三个点/ spread运算符与rest参数

众成翻译  · 掘金  ·  · 2021-02-01 17:46

正文

阅读 5

改变JavaScript的三个点/ spread运算符与rest参数

译者: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() 做到:用一种不友好且繁琐的方式。让我们看看:

在JS Bin上查看

var fruits = ['banana'];
var moreFruits = ['apple', 'orange'];
Array.prototype.push.apply(fruits, moreFruits);
console.log(fruits); // => ['banana', 'apple', 'orange']
复制代码

幸运的是JavaScript的世界在不断改变。三点运算符 ... 解决了很多这样的问题。这个操作符在ECMAScript 6中被引入,在我看来这是一个显著的改进。

本文将逐一浏览 ... 运算符的使用场景并看看它是如何解决类似问题的。

1. 三个点

rest运算符 用于获取函数调用时传入的参数。

在JS Bin上查看

function countArguments(...args) {
   return args.length;
}
// 获取参数的数量
countArguments('welcome', 'to', 'Earth'); // => 3
复制代码

spread运算符 用于数组的构造,析构,以及在函数调用时使用数组填充参数列表。

在JS Bin上查看

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 对象:

在JS Bin上查看

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

在JS Bin上查看

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参数中。

一起看看下面的例子:

在JS Bin上查看

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 对象。

让我们通过一个例子看看:

在JS Bin上查看

(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() 的使用案例:

在JS Bin上查看

let countries = ['Moldova', 'Ukraine'];
countries.push.apply(countries, ['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']
复制代码

像前面提到的那样,在 .apply() 调用中两次指明上下文 countries 是无关紧要的。属性访问器 countries.push 已经足够判定该方法是在哪个对象上调用的了。

同时,整个调用看起来非常繁琐。

spread运算符 使用来自数组(更确切的说是一个迭代器对象)中的值填充函数调用时的参数。

让我们使用spread运算符优化上边的示例:

在JS Bin上查看

let countries = ['Moldova', 'Ukraine'];
countries.push(...['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']
复制代码

如你所见,spread运算符是一种更简单,直接的解决方案。唯一的额外字符就是三个点( ... )。

Spread运算符可以在构造器调用中使用数组元素作为参数,而这并不能通过 直接使用 .apply() 做到。让我们看一个例子:

在JS Bin上查看

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运算符以及普通参数。下面的例子从数组中移除现有元素并插入其它数组的元素以及一个普通变量:

在JS Bin上查看

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运算符允许把其它数组(或是其它的 迭代器 )的内容动态的插入到一个数组字面量中。

这项改进使得要完成以下描述的常见操作变得更容易。

使用 来自其他数组 的元素 创建 数组

在JS Bin上查看

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个及以上的数组

在JS Bin上查看

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 的结合。

复制 一个数组

在JS Bin上查看

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运算符会提取数组中的部分数据。提取的结果是一个数组。







请到「今天看啥」查看全文