转载

【Dmitri Pavlutin】...是怎样改变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]
复制代码

number1number2 数组是通过数组定量值创建的,与此同时使用 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 数组创建于 oddsevens 数组的连接。

克隆数组实例:

const words = ['Hi', 'Hello', 'Good day'];
const otherWords = [...words];
console.log(otherWords);           // => ['Hi', 'Hello', 'Good day']
console.log(otherWords === words); // => false
复制代码

otherWordswords 数组的克隆版本,注意,克隆只发生在数组本身上,而不发生在包含的元素上(即它不是深度克隆)。

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数组 , setsmaps 。对它们可以使用扩展运算符。

例如,让我们看看一个字符串如何遵守迭代协议的:

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.im/post/5dc9255fe51d45696d32bf0d
正文到此结束
Loading...