【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]
复制代码

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.im/post/5dc9255fe51d45696d32bf0d

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 【Dmitri Pavlutin】…是怎样改变JavaScript的

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址