深入理解解构、展开运算符和剩余参数

ES2015 之后,JavaScript 多了几组和数组、对象相关的新语法:解构赋值展开(spread)剩余参数(rest)
它们本质上都只是在帮你更方便地拆/装对象和数组,但第一次看确实有点抽象。

这篇笔记主要做三件事:

  • 把对象解构、数组解构、展开、剩余参数放到一个上下文里理解。

  • 每种语法给几个最常写到的例子,便于记忆。

  • 提示一些容易踩坑的点(比如浅拷贝、命名冲突等)。


一、解构赋值(Destructuring)

解构赋值就是:从对象或数组里“拆出”字段/元素,直接一次性赋值到变量里

1.1 对象解构

有一个最常见的例子:从对象里拿出几个字段:

const note = {
  id: 1,
  title: "My first note",
  date: "01/01/1970",
}

传统写法要一行一行赋值:

const id = note.id
const title = note.title
const date = note.date

用对象解构可以写成一行:

const { id, title, date } = note

console.log(id)    // 1
console.log(title) // "My first note"
console.log(date)  // "01/01/1970"

**注意:**解构不会改动原对象,note 还是原来的样子。

给解构出来的变量改个名字

有时候你不想用原来的属性名当变量名,可以用冒号起别名:

const { id: noteId, title, date } = note

console.log(noteId) // 1

这里 noteId 是新变量名,仍然来自 note.id

解构嵌套对象

再稍微复杂一点,假设 note 里有个 author 对象:

const note = {
  id: 1,
  title: "My first note",
  date: "01/01/1970",
  author: {
    firstName: "Sherlock",
    lastName: "Holmes",
  },
}

你可以一层写掉:

const {
  id,
  title,
  date,
  author: { firstName, lastName },
} = note

console.log(`${firstName} ${lastName}`) // "Sherlock Holmes"

如果既想要 author 整个对象,又想直接拿到内部字段,可以分开声明:

const {
  author,
  author: { firstName, lastName },
} = note

console.log(author) // { firstName: "Sherlock", lastName: "Holmes" }

原始值也可以被解构(借助包装对象)

比如字符串有 length 属性,可以直接这么写:

const { length } = "A string"

console.log(length) // 8

引擎会临时把 "A string" 转成一个 String 对象来拿属性。


1.2 数组解构

数组解构类似,只是用下标位置来匹配:

const date = ["1970", "12", "01"]

const [year, month, day] = date

console.log(year)  // "1970"
console.log(month) // "12"
console.log(day)   // "01"

跳过某些元素

中间不想要的可以直接用逗号占位:

const [year, , day] = date

console.log(year) // "1970"
console.log(day)  // "01"

解构嵌套数组

const nestedArray = [1, 2,, 5][1][2]

const [one, two, [three, four], five] = nestedArray

console.log(one, two, three, four, five) // 1 2 3 4 5

函数参数里直接解构

经常会看到这种写法,把参数在函数定义处就解构掉:

const note = {
  id: 1,
  title: "My first note",
  date: "01/01/1970",
}

Object.entries(note).forEach(([key, value]) => {
  console.log(`${key}: ${value}`)
})

// 或者 for...of
for (const [key, value] of Object.entries(note)) {
  console.log(`${key}: ${value}`)
}

输出类似:

id: 1
title: My first note
date: 01/01/1970

解构时用默认值 + 组合对象/数组

默认参数 + 对象解构 + 数组解构可以混合用:

const note = {
  title: "My first note",
  author: {
    firstName: "Sherlock",
    lastName: "Holmes",
  },
  tags: ["personal", "writing", "investigations"],
}

const {
  title,
  date = new Date(),
  author: { firstName },
  tags: [personalTag, writingTag],
} = note

console.log(date) // 如果 note 里没有 date,这里就是当前时间

二、展开运算符(Spread:...

展开运算符的核心:把数组/可迭代对象/对象“摊开”成单个值
常见用途是组合数组、复制数组/对象、向函数传参等。

2.1 数组上的 spread

合并两个数组:

const tools = ["hammer", "screwdriver"]
const otherTools = ["wrench", "saw"]

const allTools = [...tools, ...otherTools]

console.log(allTools)
// ["hammer", "screwdriver", "wrench", "saw"]

不修改原数组、增加一个元素:

const users = [
  { id: 1, name: "Ben" },
  { id: 2, name: "Leslie" },
]

const newUser = { id: 3, name: "Ron" }

const updatedUsers = [...users, newUser]

console.log(users)        // 原数组不变
console.log(updatedUsers) // 多了一个用户

对比一下“引用复制”的坑:

const originalArray = ["one", "two", "three"]
const secondArray = originalArray

secondArray.pop()

console.log(originalArray) // ["one", "two"]

而用 spread 做浅拷贝则不会影响原数组:

const originalArray = ["one", "two", "three"]
const secondArray = [...originalArray]

secondArray.pop()

console.log(originalArray) // ["one", "two", "three"]

也可以用来把 Set、字符串变成数组:

const set = new Set()
set.add("octopus")
set.add("starfish")
set.add("whale")

const seaCreatures = [...set]
console.log(seaCreatures) // ["octopus", "starfish", "whale"]

const string = "hello"
const stringArray = [...string]
console.log(stringArray) // ["h", "e", "l", "l", "o"]

2.2 对象上的 spread

对象的浅拷贝:

const originalObject = { enabled: true, darkMode: false }
const secondObject = { ...originalObject }

console.log(secondObject) // { enabled: true, darkMode: false }

在不改原对象的前提下,添加/覆盖属性:

const user = {
  id: 3,
  name: "Ron",
}

const updatedUser = { ...user, isLoggedIn: true }

console.log(updatedUser)
// { id: 3, name: "Ron", isLoggedIn: true }

嵌套对象时要记得“内层也要 spread”,否则会覆盖整个子对象:

const user = {
  id: 3,
  name: "Ron",
  organization: {
    name: "Parks & Recreation",
    city: "Pawnee",
  },
}

// 错误:这一写法会把 organization 其他字段丢掉
const brokenUser = { ...user, organization: { position: "Director" } }

// 正确:先展开原来的 organization
const updatedUser = {
  ...user,
  organization: {
    ...user.organization,
    position: "Director",
  },
}

2.3 函数调用里的 spread

把数组拆成单独参数传入函数:

function multiply(a, b, c) {
  return a * b * c
}

const numbers =[3][4][1]

console.log(multiply(...numbers)) // 6

在没有 spread 的时代只能用 apply

multiply.apply(null, ) // 6[4][1][3]

三、剩余参数(Rest:...

rest 参数语法看起来和 spread 一样,作用刚好相反
spread 是“把一堆东西拆开”,rest 是“把一堆东西收集起来”。

3.1 函数参数里的 rest

让一个函数接收任意数量的参数:

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

restTest(1, 2, 3, 4, 5, 6)
//[2][5][6][1][3][4]

也可以先拿前几个参数,剩下的用数组装起来:

function restTest(one, two, ...args) {
  console.log(one) // 1
  console.log(two) // 2
  console.log(args) //[5][6][1][2]
}

restTest(1, 2, 3, 4, 5, 6)

和历史上的 arguments 对比:

function testArguments() {
  console.log(arguments)
}

testArguments("how", "many", "arguments")
// Arguments(3) ["how", "many", "arguments"]

arguments 的问题:

  • 在箭头函数里不可用。
  • 不是“真数组”,很多数组方法直接用不了。
  • 总是收集所有参数,不能像 rest 那样只收“剩余部分”。

rest 则没有这些限制。

3.2 解构里的 rest

在数组解构里:

const [firstTool, ...rest] = ["hammer", "screwdriver", "wrench"]

console.log(firstTool) // "hammer"
console.log(rest)      // ["screwdriver", "wrench"]

在对象解构里:

const { isLoggedIn, ...rest } = { id: 1, name: "Ben", isLoggedIn: true }

console.log(isLoggedIn) // true
console.log(rest)       // { id: 1, name: "Ben" }

这在场景里很常用:你只关心一个字段,剩下全部打包再往下传。


四、小结 & 自己写代码时的使用习惯

为了以后少踩坑,我自己给这几个语法总结了几个习惯用法:

  • 对象/数组解构

    • 函数组件 props / API 返回结果 / 配置对象,尽量在入口处就解构好。
    • 复杂解构(多层嵌套)适量使用,超过两层就考虑拆变量。
  • spread

    • 改数组/对象时,优先考虑“新建一个再返回”,不要直接改原引用。
    • 处理嵌套对象时,记得内外都要 spread 一层。
  • rest

    • 写通用工具函数或日志函数时,用 rest 参数收集“剩余参数”。
    • 在解构时用 rest 保留“剩余 props”,比如 React 里的 const { className, ...rest } = props