如何理解 js 的剩余操作符 ...

in 前端 with 0 comment

封面图是一张前阵子在鼓浪屿拍的照片

从一个需求说起

前几天,我以前的一个同事兼朋友说他遇到一个问题,自己写了很长时间越写越糊涂。于是像我求助,需求比较模糊,我具体化举了例子是这样的

题目

例如某一款服装有三种颜色,分别是 黑色红色白色,有三种尺寸分别为 SML,分别从两个仓库发货,分别是:北京广州,源数据长这个样子:

const source = [
  {
    name: '颜色',
    value: ['黑色', '红色', '白色']
  },
  {
    name: '尺寸',
    value: ['S', 'M', 'L']
  },
  {
    name: '仓库',
    value: ['北京', '广东']
  }
]

这种同一个产品的不同型号规则的数据通常被称作 sku(Stock Keeping Unit)库存进出计量的基本单元。用于计算库存时,需要一一对应的列举出所有的型号搭配,并且计算库存。以上面的例子,则应该有 3 x 3 x 2 = 18 种 sku,看起来像这样:

颜色尺寸发货仓库
黑色S北京
黑色S广东
黑色M北京
黑色M广东
黑色L北京
黑色L广东
红色S北京
.........

处理后的结果应该是:
WX20200925234839.png

虽然用中文做 key 看起来非常不合理,我们就假设这是一道题来解

解题思路

显然,我们需要对源数据进行整理,初步思路是:

  1. 声明一个目标数组,用于当作最后输出的数组。初始化为空
  2. 第一层遍历针对源数组,遍历对象为数组的每一个元素,取出每个元素的 name 属性做 key
  3. 在遍历内声明一个临时目标数组,用于存储当前一个型号规格处理完以后的目标数组该有的样子,处理完后赋值给遍历外部的目标数组,初始化为空。当第一层遍历结束后得到的目标数组就是我们想要的结果了
  4. 第二层遍历针对第一层遍历的每一个元素重的 value 属性,也是一个数组遍历:
    在这层遍历中,当外层遍历的第 0 个循环(也就是第一个型号规则)时,目标数组是空的。我们只需要把这一个型号规则的所有型号做成一个对象放进临时目标数组里,并且把临时目标数组赋值给目标数组,例如:
[
  { 颜色:'黑色' },
  { 颜色:'红色' },
  { 颜色:'白色' }
]

当不是第 0 个循环时,例如第 1 个循环,也就是第二个型号规则,在遍历尺寸时,我们再在内部遍历一下目标数组,并且把里面的颜色属性拿出来放进临时目标数组,这样临时目标数组里就应该有 3 x 3 = 9 个元素,像这样:

[
  { 颜色:'黑色', 尺寸: 'S' },
  { 颜色:'黑色', 尺寸: 'M' },
  { 颜色:'黑色', 尺寸: 'L' },
  { 颜色:'红色', 尺寸: 'S' },
  { 颜色:'红色', 尺寸: 'M' },
  { 颜色:'红色', 尺寸: 'L' },
  { 颜色:'白色', 尺寸: 'S' },
  { 颜色:'白色', 尺寸: 'M' },
  { 颜色:'白色', 尺寸: 'L' }
]

当第三个型号规则加入时,我们就得把原有的颜色、尺寸属性都拿出来。当有若干多个型号时,我们不能依次手写,所以在上面的过程中,我们在取目标数组遍历结果的属性时可以直接用剩余操作符 ... 把所有的值解构出来然后填入一个新属性就可以了
最后再把临时目标数组赋值给目标数组,就完成了。这一步有重复操作,可以只写一次

解题

let data = []
for (let i = 0; i < source.length; i ++) {
  const name = source[i].name
  const arr2map = source[i].value
  const newData = []
  for (let j = 0; j < arr2map.length; j ++) {
    if (i === 0) {
      const current = arr2map[j]
      newData.push({
        [name]: current
      })
    } else {
      const current = arr2map[j]
      for (const row of data) {
        const newRow = {
          ...row,
          [name]: current
        }
        newData.push(newRow)
      }
    }
  }
  data = newData
}
console.log('final', data)

当然,用递归也可以

剩余操作符

这个剩余操作符可以说是一个伟大的发明,他可以做很多事情。

剩余元素/属性

它可以解构一个数组成元素,比如拼接两个数组,再在两个数组的中间插入额外的元素:

const $arr1 = [1, 2, 3]
const $arr2 = ['a', 'b', 'c']
const target = [...$arr1, undefined, null, ...$arr2]

最后的输出是:

(8)[1, 2, 3, undefined, null, 'a', 'b', 'c']

对象也适用,解构对象时会使得剩余的部分形成一个新的对象:

const { a, b, ...others } = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  e: 5
}
console.log(a, b, others)

输出是:

1, 2, { c: 3, d: 4, e: 5}

但是对于解构后形成的数组和原数组就没有相等的关系了,因为解构后再生成的数组在内存中是另一个地址

const a = [1, 2, 3]
console.log(a, [...a], a == [...a])

输出是:

(3)[1, 2, 3] (3)[1, 2, 3] false

剩余参数

在方法中也同样适用剩余操作符,他会把剩余的参数放入一个数组里

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

foo(1, 2, 'abc', '123', undefined)

输出是:

1 2 (3)['abc', '123', undefined]

嘱咐两句

剩余操作符有它的方便之处,当然就应该有坑。例如在循环中可能会引起混乱等。不过只要对它的理解到位,就通常就不会出现问题。