引言
作为前端工程师,在项目中“拷贝对象/数组”的场景随处可见。本文从 Object.assign() 开场,层层深入到 JSON 序列化、structuredClone、再到手写深拷贝,用贴近业务的案例讲清楚每个方法的边界与取舍。每个知识点都配有代码与简短说明,面试也能现场“表演”。
一、赋值 vs 引用:先把底层说清楚
const a = { n: 1 }
const b = a // b 与 a 指向同一块内存
b.n = 2
console.log(a.n) // 2(对象是引用类型)
const x = 1
const y = x // 基本类型是“值拷贝”
- 对象/数组/函数等是“引用类型”;基本类型(number/string/boolean/undefined/null/symbol/bigint)是“值类型”。
二、Object.assign():最简单的浅拷贝与合并
- 语法:Object.assign(target, ...sources) 返回的是 target(“修改后的目标对象”),不是新对象。
- 只拷贝“可枚举的自有属性”(不拷贝原型链上的)。
- 浅拷贝:嵌套对象的引用会被共享。
const state = { user: { name: 'Tom' }, age: 18 }
// 1) 浅拷贝
const copy = Object.assign({}, state)
copy.age = 20
copy.user.name = 'Jerry'
console.log(state.age) // 18(顶层值已拷贝)
console.log(state.user.name) // 'Jerry'(嵌套对象是同一个引用)
// 2) 返回值是 target 本身
const target = { a: 1 }
const result = Object.assign(target, { b: 2 })
console.log(result === target) // true
// 3) 多源合并 & 覆盖
const merged = Object.assign({}, { a: 1 }, { a: 2, b: 3 })
console.log(merged) // { a: 2, b: 3 }
// 4) getter 在拷贝时会执行(拷贝的是 getter 的返回值)
const src = { get val() { console.log('run'); return 1 } }
const o = Object.assign({}, src) // 控制台打印 run
console.log(o.val) // 1
适用场景
- 合并配置、顶层属性替换、扁平对象浅拷贝
- 不适合“深层结构安全隔离”(会共享嵌套引用)
三、数组浅拷贝:slice / concat / 展开符
const arr = [1, { n: 1 }]
// 1) slice
const a1 = arr.slice()
// 2) concat
const a2 = [].concat(arr)
// 3) 展开符
const a3 = [...arr]
a1[1].n = 2
console.log(arr[1].n) // 2(同样是浅拷贝,嵌套对象引用共享)
四、JSON.parse(JSON.stringify()):简单粗暴的“伪深拷贝”
优点:一行代码,简单。 缺点:有丢失、报错、类型退化等问题。
const obj = {
n: 1,
d: new Date(),
r: /hi/gi,
u: undefined,
f: () => {},
s: Symbol('s'),
m: new Map([['k', 'v']]),
loop: null,
}
obj.loop = obj // 循环引用
// 1) 基本深拷贝
const deep = JSON.parse(JSON.stringify({ a: 1, b: { c: 2 } }))
deep.b.c = 99 // 原对象不变
// 2) 丢失与报错
// JSON.stringify(obj) 会抛出 TypeError:循环引用
// 即使去掉循环引用:
// - 函数/undefined/Symbol 会被丢弃
// - Date 变成字符串
// - RegExp/Map/Set/TypedArray 等无法保真
适用场景
- 后端 JSON 场景/纯数据对象/无循环引用/不包含复杂类型
- 配置快照、简单表单状态复制
五、现代首选:structuredClone(原生深拷贝)
浏览器原生(现代环境),Node 17+ 可用,能拷贝大多数内置类型,支持循环引用。
// 现代环境(浏览器/Node 17+)
const obj = {
n: 1,
d: new Date(),
r: /hi/gi,
m: new Map([['k', { x: 1 }]]),
s: new Set([1,2,3]),
a: new Uint8Array([1,2,3]),
}
const cloned = structuredClone(obj)
console.log(cloned.m.get('k') !== obj.m.get('k')) // true(已深拷贝)
// 注意:函数/DOM 节点不可被 clone
// structuredClone(() => {}) // 会抛错
适用场景
- 需要可靠深拷贝(包含 Map/Set/Date/RegExp/TypedArray/循环引用)的现代项目
- 如果要兼容旧环境,可考虑 polyfill 或降级方案
六、手写深拷贝:能“表演”的版本(含循环引用/常见内置类型)
最小可用版本(对象/数组 + 循环引用):
function deepCloneBasic(value, visited = new WeakMap()) {
if (value === null || typeof value !== 'object') return value
if (visited.has(value)) return visited.get(value)
const result = Array.isArray(value) ? [] : {}
visited.set(value, result)
for (const key of Object.keys(value)) {
result[key] = deepCloneBasic(value[key], visited)
}
return result
}
可用于“表演”的增强版(支持 Date/RegExp/Map/Set/TypedArray/循环引用):
function isObject(val) {
return val !== null && typeof val === 'object'
}
function cloneRegExp(re) {
const flags = re.flags !== undefined ? re.flags :
(re.global ? 'g' : '') + (re.ignoreCase ? 'i' : '') + (re.multiline ? 'm' : '')
return new RegExp(re.source, flags)
}
function deepClone(value, visited = new WeakMap()) {
// 基本类型
if (!isObject(value)) return value
// 已克隆过(循环引用)
if (visited.has(value)) return visited.get(value)
// 特例类型
const type = Object.prototype.toString.call(value)
if (type === '[object Date]') return new Date(value.getTime())
if (type === '[object RegExp]') return cloneRegExp(value)
if (type === '[object Map]') {
const result = new Map()
visited.set(value, result)
value.forEach((v, k) => {
result.set(deepClone(k, visited), deepClone(v, visited))
})
return result
}
if (type === '[object Set]') {
const result = new Set()
visited.set(value, result)
value.forEach(v => result.add(deepClone(v, visited)))
return result
}
if (ArrayBuffer.isView(value)) {
return new value.constructor(value) // TypedArray/DataView
}
if (type === '[object ArrayBuffer]') {
return value.slice(0)
}
// 普通对象/数组
const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value))
visited.set(value, result)
// 仅处理可枚举属性,必要时可改为 Reflect.ownKeys
for (const key of Object.keys(value)) {
result[key] = deepClone(value[key], visited)
}
return result
}
// 使用
const source = {
d: new Date(),
r: /hi/gi,
m: new Map([['k', { x: 1 }]]),
s: new Set([1, 2, 3]),
a: new Uint16Array([1, 2, 3]),
}
source.self = source
const c = deepClone(source)
console.log(c !== source) // true
console.log(c.m.get('k') !== source.m.get('k')) // true
console.log(c.self === c) // true(循环引用保留)
说明
- 用 WeakMap 记录“已克隆对象”解决循环引用
- 常见内置类型逐个处理
- 保留原型链(Object.create(Object.getPrototypeOf(value)))
- 如需拷贝 symbol-key/不可枚举属性/属性描述符,可改用 Reflect.ownKeys + getOwnPropertyDescriptors
七、如何选择:按业务下菜
- 简单顶层变更/合并配置:Object.assign() / 展开符
- 数组简单复制:slice/concat/展开符
- JSON 数据快照、表单回显:JSON.parse(JSON.stringify())
- 现代项目、类型保真、支持循环引用:structuredClone
- 框架/库内需要可控行为:手写深拷贝(或 lodash.cloneDeep)
- 性能敏感:尽量“局部更新”,不要随意大对象深拷贝
八、面试场景“表演要点”
- 从 Object.assign() 开场,快速示范“浅拷贝 + 可枚举自有属性 + 返回 target + getter 执行”
- 讲 JSON 序列化的边界(函数/undefined/symbol/循环引用/内置类型)
- 强推 structuredClone 的现代优势
- 手写深拷贝时:先基础版再进阶版(WeakMap/内置类型/原型),边写边解释设计取舍
- 落地到业务:让拷贝“服务于场景”,不是“追求银弹” 、