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。