本文原载于 SegmentFault 社区
专栏:大前端 
作者:皮小蛋



背景

... 运算符,是 ES6 里一个新引入的运算法,也叫 展开/收集 运算符,我们每天都要和它打交道。

这篇文章,我就带你系统的回顾下这个运算符,介绍一些基础和进阶的用法。

基础篇
先看一下官方描述:
Spread syntax allows an iterable, such as an array expression or string, to be expanded in places where 0 or more arguments or elements are expected or an object expression to be expanded in places where 0 or more key-value pairs (for object literals) are expected.
简而言之就是,... 运算符可以展开一个可迭代对象重的所有项。
可迭代的对象一般是指可以被循环的,包括:string, array, set 等等。

下面我们来看几个基础的例子来加深理解。

基础用法

基础用法 1: 展开

 

const a = [2, 3, 4]
 const b = [1, ...a, 5]

 b; // [1, 2, 3, 4, 5]

基础用法 2: 收集




function foo(a, b, ...c) {
    console.log(a, b, c)     
}

foo(1, 2, 3, 4, 5); // 1, 2, [3, 4, 5]
如果没有命名参数的话,... 就会收集所有的参数:



function foo(...args) {
    console.log(args)     
}

foo(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]

关于这个收集的用法,官方描述:
“A function’s last parameter can be prefixed with ... which will cause all remaining (user supplied) arguments to be placed within a "standard" javascript array. Only the last parameter can be a rest parameter.”
这个运算符一定是在最后一个参数的位置,也很好理解,就是“收集前面剩下的参数”。
Remember that the rest parameter must be the last parameter, or an error will occur.
如果不在最后一位,会报错。

不得不感叹,这个运算符设计的真的是妙,可展开,可收集,收放自如,当真好用。


基础用法 3: 把 类数组 转换为 数组


先回顾下什么是类数组吧.

类数组和数组非常接近,都可以拥有一系列元素,也有length 属性,最大的不同是:

类数组不具备数组的一系列方法。

举个例子:


const nodeList = document.getElementsByClassName("test");
const array = [...nodeList];

console.log(nodeList); //Result: HTMLCollection [ div.test, div.test ]
console.log(array); //Result: Array [ div.test, div.test ]

使用 ... 就可以实现类数组到数组的转换,转换之后,就可以使用数组的各种方法了。

你还记得在这个操作符出来之前是如何转换的吗?

这个问题还是头条的一个前端面试题。

看例子:



// ES5 时代
function bar() {
  var args = Array.prototype.slice.call(arguments);

   // 调用push 加几个元素
  args.push(1, 2, 3);

  // 把args 作为参数传递给foo
  foo.apply(null, args)

}

// ES6 时代

function foo(...args) { // 搜集参数到 args

  args.push(4, 5, 6)

  console.log(...args) // 展开args

}

bar(0); // 0 1 2 3 4 5 6

基础用法 4: 增加元素或属性

1: 为数组新增成员




const pokemon = ['KK', 'Peter'];
const charmander = '郑伊健';

const pokedex = [...pokemon, charmander];

console.log(pokedex); 

//Result: [ 'KK', 'Peter', '郑伊健' ]

2: 为对象新增属性




const basicSquirtle = { name: 'Squirtle', type: 'Water' };
const fullSquirtle = {
  ...basicSquirtle,
  species: 'Tiny Turtle',
  evolution: 'Wartortle'
};

console.log(fullSquirtle); 

//Result: { name: 'Squirtle', type: 'Water', species: 'Tiny Turtle', evolution: 'Wartortle' }
基础用法 5: 合并数组/对象

合并数组:




const pokemon = ['Squirtle', 'Bulbasur', 'Charmander'];
const morePokemon = ['Totodile', 'Chikorita', 'Cyndaquil'];

const pokedex = [...pokemon, ...morePokemon];

console.log(pokedex); 
//Result: [ 'Squirtle', 'Bulbasur', 'Charmander', 'Totodile', 'Chikorita', 'Cyndaquil' ]

// 对象数组也一样:
const pokemon = [
  { name: 'Squirtle', type: 'Water' },
  { name: 'Bulbasur', type: 'Plant' }
];
const morePokemon = [{ name: 'Charmander', type: 'Fire' }];

const pokedex = [...pokemon, ...morePokemon];

console.log(pokedex); 

//Result: [ { name: 'Squirtle', type: 'Water' }, { name: 'Bulbasur', type: 'Plant' }, { name: 'Charmander', type: 'Fire' } ]

合并对象



const baseSquirtle = {
  name: 'Squirtle',
  type: 'Water'
};

const squirtleDetails = {
  species: 'Tiny Turtle Pokemon',
  evolution: 'Wartortle'
};

const squirtle = { ...baseSquirtle, ...squirtleDetails };
console.log(squirtle); 
//Result: { name: 'Squirtle', type: 'Water', species: 'Tiny Turtle Pokemon', evolution: 'Wartortle' }

以上是一些基础费用法

下面介绍一些... 操作符的进阶用法。

进阶篇

1. 复制具有嵌套结构的数据/对象

先看一个例子:



const pokemon = {
  name: 'Squirtle',
  type: 'Water',
  abilities: ['Torrent', 'Rain Dish']
};

const squirtleClone = { ...pokemon };

pokemon.name = 'Charmander';
pokemon.abilities.push('Surf');

console.log(squirtleClone); 

//Result: { name: 'Squirtle', type: 'Water', abilities: [ 'Torrent', 'Rain Dish', 'Surf' ] }
当我们修改原对象的 name 属性时,我们的克隆对象的 name 属性没有受影响,这是符合我们预期的。

但是当修改原对象的 abilities 属性时,我们的克隆对象也被修改了。

原因也很简单,因为复制过来的 abilities 是一个引用类型,原数据改了,用到他的地方也会跟着改。

知道原因,再解决就很简单了,两种方式:

1、复制引用类型的数据




const pokemon = {
  name: 'Squirtle',
  type: 'Water',
  abilities: ['Torrent', 'Rain Dish']
};

const squirtleClone = { ...pokemon, abilities: [...pokemon.abilities] };

pokemon.name = 'Charmander';
pokemon.abilities.push('Surf');

console.log(squirtleClone);

//Result: { name: 'Squirtle', type: 'Water', abilities: [ 'Torrent', 'Rain Dish' ] }

这样就 OK 了

2、深克隆


在这里就不多解释了。

2. 增加条件属性

顾名思义,就是需要根据条件添加的属性。

看个例子:



const pokemon = {
  name: 'Squirtle',
  type: 'Water'
};

const abilities = ['Torrent', 'Rain dish'];
const fullPokemon = abilities ? { ...pokemon, abilities } : pokemon;

console.log(fullPokemon);

3短路




const pokemon = {
  name: 'Squirtle',
  type: 'Water'
};

const abilities = ['Torrent', 'Rain dish'];
const fullPokemon = {
  ...pokemon,
  ...(abilities && { abilities })
};

console.log(fullPokemon);

如果 abilities 为 true,就相当于是



const fullPokemon = {
  ...pokemon,
  ...{ abilities }
}

这也是一个很有用的技巧。

4. 默认结构和添加默认属性

默认结构:

我们知道,当结构一个对象的时候,如果这个对象里没有某个属性,解出来是undefined , 我们可以添加默认值来解决:



const pokemon = {
  id: 1,
  name: 'Squirtle'
};

const { type, name } = pokemon;
console.log(name); //Result: Squirtle
console.log(type); //Result: undefined

//Assigning default value to the type variable
const { type = 'Water', name } = pokemon;
console.log(type); //Result: Water

添加默认属性

有时候从我们会遇到这样的情况,一个对象,大部分属性是相似的,只有小部分是不不同的,这时候我们就可以设置一个基础对象,具备基础属性,其他的对象可以通过扩展这个对象来得到。
看例子:



const pokemon = {
  name: 'Squirtle',
  type: 'Water'
};

//  给abilities默认赋值
const { abilities = [], ...rest } = pokemon;

const fullSquirtle = { ...rest, abilities };

console.log(rest); //Result: { name: 'Squirtle', type: 'Water' }
console.log({ fullSquirtle }); //Result: { name: 'Squirtle', type: 'Water', abilities: [] }

这里就是通过展开 rest , 合并 abilities 得到完全体的数据。
如果有批量的数据需要处理,这种方法也非常方便:



const pokemon = [
  {
    name: 'Charmander',
    type: 'Fire'
  },
  { name: 'Squirtle', type: 'Water', abilities: ['Torrent', 'Rain Dish'] },
  {
    name: 'Bulbasur',
    type: 'Plant'
  }
];

function setDefaultAbilities(object) {
  const { abilities = [], ...rest } = object;
  return { ...rest, abilities };
}

// Applying the setDefaultAbilities function to all the pokemon in the array:
const normalizedPokemon = pokemon.map(pokemon => setDefaultAbilities(pokemon));

console.log(normalizedPokemon);

//Result: [ { name: 'Charmander', type: 'Fire', abilities: [] },   { name: 'Squirtle', type: 'Water', abilities: [ 'Torrent', 'Rain Dish' ] }, { name: 'Bulbasur', type: 'Plant', abilities: [] } ]

这样迭代一遍,所有的对象就都具备 abilities 属性了。

总结
... 运算符非常灵活,收放自如,非常强大,希望我们都能很好的掌握这个工具。

内容就这么多,希望对大家有所帮助,如有纰漏,欢迎指正。

原创系列推荐



4. 
5. 
6. 
7. 

回复“加群”与大佬们一起交流学习~

点这,与大家一起分享本文吧~

【JS】388- 深入了解强大的 ES6 「 ... 」 运算符的更多相关文章

  1. js基础 js自执行函数、调用递归函数、圆括号运算符、函数声明的提升 js 布尔值 ASP.NET MVC中设置跨域

    js基础 目录 javascript基础 ESMAScript数据类型 DOM JS常用方法 回到顶部 javascript基础 常说的js包括三个部分:dom(文档document).bom(浏览器 ...

  2. 巧用 .NET 中的「合并运算符」获得 URL 中的参数

    获取 URL 中的 GET 参数,无论用什么语言开发网站,几乎是必须会用到的代码.但获取 URL 参数经常需要注意一点就是要先判断是否有这个参数存在,如果存在则取出,如果不存在则用另一个值.这个运算称 ...

  3. ES6,扩展运算符的用途

    ES6的扩展运算符可以说是非常使用的,在给多参数函数传参,替代Apply,合并数组,和解构配合进行赋值方面提供了很好的便利性. 扩展运算符就是三个点“...”,就是将实现了Iterator 接口的对象 ...

  4. [转帖]详解shell脚本括号区别--$()、$「 」、$「 」 、$(()) 、「 」 、「[ 」]

    详解shell脚本括号区别--$().$「 」.$「 」 .$(()) .「 」 .「[ 」] 原创 波波说运维 2019-07-31 00:01:00 https://www.toutiao.com ...

  5. js 模板引擎 - 超级强大

    本来没想写这篇文章,但是网上误导大众的文章太多了,所以今天就抽出半小时时间谈一下我对前端模板引擎的感受吧. 前端模板引擎相信大家都再熟悉不过了,市面上非常多的号称最好.最快.最牛逼的,随便就能找到一大 ...

  6. 《React Native 精解与实战》书籍连载「Node.js 简介与 React Native 开发环境配置」

    此文是我的出版书籍<React Native 精解与实战>连载分享,此书由机械工业出版社出版,书中详解了 React Native 框架底层原理.React Native 组件布局.组件与 ...

  7. js模块化AMD、CMD、ES6

    AMD CMD ES6模块化 各个模块化规范对比理解 一.AMD 在上一篇js模块化入门与commonjs解析与应用中详细的解析了关于commonjs模块化规范,commonjs采用的用同步加载方式, ...

  8. js 模块化的一些理解和es6模块化学习

    模块化 1 IIFE 2 commonjs 3 浏览器中js的模块化 4 简单理解模块加载器的原理  5 es6 之前在参加百度前端技术学院做的小题目的时候,自己写模块的时候 都是写成立即调用表达式( ...

  9. JS创建对象、继承原型、ES6中class继承

    面向对象编程:java中对象的两个基本概念:1.类:类是对象的模板,比如说Leader 这个是泛称领导,并不特指谁.2:实例:实例是根据类创建的对象,根据类Leader可以创建出很多实例:liyi,y ...

随机推荐

  1. Spring简单的示例

    参考资料:https://how2j.cn/k/spring/spring-ioc-di/87.html.https://www.w3cschool.cn/wkspring/dgte1ica.html ...

  2. Spring Boot2 系列教程(二十六)Spring Boot 整合 Redis

    在 Redis 出现之前,我们的缓存框架各种各样,有了 Redis ,缓存方案基本上都统一了,关于 Redis,松哥之前有一个系列教程,尚不了解 Redis 的小伙伴可以参考这个教程: Redis 教 ...

  3. JAVA语 言 的 特 点

    Java到 底 是 一 种 什 么 样 的 语 言 呢? Java是 一 种 简 单 的 面 象 对 象 的 分 布 式 的 解 释 的 健 壮 的 安 全 的 结 构 中 立 的 可 移 植 的 性 ...

  4. 快速遍历OpenCV Mat图像数据的多种方法和性能分析 | opencv mat for loop

    本文首发于个人博客https://kezunlin.me/post/61d55ab4/,欢迎阅读! opencv mat for loop Series Part 1: compile opencv ...

  5. tcpip协议

    几个概念 1.分层(我们使用四层模型更为贴合我们的实际网络) 分层是为什么,其实和公司中职位是一样的,不同职位的人做不同的事情,然后不同职位的人合起来,一起完成了数据传输的事情. 链路层  在这个层面 ...

  6. websrom编译器

    webstorm less环境配置 备注: 安装node后,在命令行输入npm install -g less 即可安装less,打开webstorm setting-Tools-FileWatche ...

  7. Mybatis日志体系

    承接上一篇关于spring 5.x的日志体系,本篇看看Mybatis的日志体系及实现,Mybatis版本基于3.x. 关于mybatis的官方文档比较友好,分门别类,各有论述,如mybatis官方文档 ...

  8. [UWP]UIElement.Clip虽然残废,但它还可以这样玩

    1. 复习一下WPF的UIElement.Clip 用了很久很久的WPF,但几乎没有主动用过它的Clip属性,我只记得它很灵活,可以裁剪出多种形状.在官方文档复习了一下,大致用法和效果如下: < ...

  9. Kotlin Coroutines在Android中的实践

    Coroutines在Android中的实践 前面两篇文章讲了协程的基础知识和协程的通信. 见: Kotlin Coroutines不复杂, 我来帮你理一理 Kotlin协程通信机制: Channel ...

  10. MySQL查询字段类型为json的数据

    测试表如下: /* Navicat Premium Data Transfer Source Server : Source Server Type : MySQL Source Server Ver ...