当面试官微笑着说"手写一个深浅拷贝"时,别慌——这不过是代码世界的「克隆人战争」。且看我拆解这场拷贝宇宙的生存法则。
一、前置知识: 🧠 数据存储:V8的「记忆宫殿」
1. JS 宇宙中两大物质阵营(数据类型)
在 JS 中数据类型分为基本数据类型和引用数据类型
基本数据类型:
let s = 'hello' //字符串
let n = 123 //数字
let f = true //布尔值
let u = undefined //未定义
let nu = null //空值
let sym = Symbol() //符号
let b = 123321321321n //大整形
引用数据类型:
let arr = [] //数组
let obj = {} //对象
let fun = function () {} //函数
let date = new Date() //日期
let set = new Set() //集合
let map = new Map() //映射
2. V8的存储密室解剖图
存储法则:
- 🧱 栈内存:存放基本类型值 + 引用类型门牌号
- 🏭 堆内存:存放引用类型本体(对象/数组等)
执行时刻:
-
V8引擎启动时创建调用栈(执行上下文舞台)
-
遇到引用类型时:
- 在堆内存开辟空间存放本体
- 在栈内存存入指向该空间的地址指针
二、 🔁 拷贝:代码世界的「克隆术
拷贝: 复刻一个对象,和原对象长得一模一样
在JS宇宙中,拷贝分为两大流派——浅拷贝(表面功夫)和深拷贝(灵魂复刻)。掌握它们,你就能操控对象的生死轮回!
1. 🎭 浅拷贝:只扒第一层皮的"易容术"
核心特性:
- 创建新对象外壳
- 嵌套对象仍共享内存(修改原对象内嵌属性会影响副本)
六大神器(附代码示例):
第一大神器: Object.create(obj)
Object.create(obj) 这个方法可以帮我们创建一个新对象,并且让新对象的隐式原型指向 obj
let obj = {
a: 1,
}
let obj2 = Object.create(obj)
console.log(obj2.a)
第二大神器: [].concat(arr)
(arr).concat(arr2)这个方法能将 arr与 arr2 进行合并,并返回一个新数组
let arr = [1,2,3]
let arr2 = [4,5]
console.log(arr.concat(arr2) ) //将arr2拼接到arr
[1,2,3,4,5]
所以使用 [].concat(arr)这个方法,让 arr 与一个空数组进行拼接,并生成一个新数组,能实现拷贝的目的
let arr3 = [].concat(arr)
console.log(arr3)
第三大神器: 数组解构 [...arr]
es6 中的解构语法:定义一个与 arr 结构体一模一样的结构,并承接 arr 中的值
//定义一个与 arr 结构体一模一样的结构
let arr = [1,2,3]
const [x,y,z] = arr
console.log(x,y,z)
1,2,3
'...'是一个常见的解构语法:允许将一个可迭代对象(如数组、字符串、Set 等)展开为多个独立的元素。
let arr = [1,2,3]
console.log(...arr)
1,2,3
所以使用[...arr]这个方法,先定义一个空数组 arr2 让他承接 arr 的解构,这样可以达到拷贝目的
let arr = [1,2,3]
let arr2 = [...arr]
console.log(arr2)
[1,2,3]
第四大神器: arr.slice(0,arr.length)
数组中的 arr.splice(start[, deleteCount[, item1, item2, ...]]);方法, start:开始修改的索引位置(从 0 开始)。若为负数,则从数组末尾倒数(如 -1 表示最后一个元素)。 deleteCount(可选):要删除的元素个数。若省略或大于剩余元素数,则删除从 start 到末尾的所有元素。
item1, item2, ...(可选):要插入到 start 位置的新元素。
let arr = ['a','b','c','d','e']
arr.splice(0,1,'0') //从0下标切1个加一个'o' 会影响原数组
console.log(arr);
[ '0', 'b', 'c', 'd', 'e' ]
可见 arr.splice(start[, deleteCount[, item1, item2, ...]]);方法会直接改变原数组的值。而数组中的 arr.slice(start,end)方法不会改变原数组的值, start(可选):开始截取的索引位置(包含)。若省略则从 0 开始;若为负数,则从末尾倒数。 end(可选):结束截取的索引位置(不包含)。若省略则截取到末尾;若为负数,则从末尾倒数。
let arr = ['a','b','c','d','e']
let arr2 = arr.slice(0,2) //从0下标切到2但不包括2 不会影响原数组
console.log(arr2);
console.log(arr);
[ 'a', 'b' ]
[ 'a', 'b', 'c', 'd', 'e' ]
所以使用arr.slice(0,arr.length)将 arr 全部切给 arr2 达到拷贝目的
let arr = ['a','b','c','d','e']
let arr2 = arr.slice(0,arr.length) //全切就是拷贝
console.log(arr2)
console.log(arr);
[ 'a', 'b', 'c', 'd', 'e' ]
[ 'a', 'b', 'c', 'd', 'e' ]
第五大神器: Object.assign({},obj)
Object.assign(obj,girl) 方法可以将后面对象拼接到前面对象且原对象受影响
let obj = {
name: '令狐冲',
age: 18
}
let girl = {
nickname: '东方不败'
}
let newObj = Object.assign(obj,girl)
console.log(newObj)
{ name: '令狐冲', age: 18, nickname: '东方不败' }
所以使用 Object.assign({},obj) 方法,将一个空数组拼接 obj 后用 newObj 承载,达到拷贝目的
let obj = {
name: '令狐冲',
age: 18
}
let newObj = Object.assign({}, obj) //将后面对象拼接到前面对象 原对象不受影响
console.log(newObj)
{ name: '令狐冲', age: 18 }
第六大神器: arr.toReversed().reverse()
调用 arr.reverse()会将 arr 内容反转,且原数组会改变
let arr = [1,2,3]
let newarr = arr.reverse()
console.log(newarr)
console.log(arr)
[ 3, 2, 1 ]
[ 3, 2, 1 ]
调用arr.toReversed() 能将数组反转,且不会影响原数组
let arr = [1,2,3]
let newarr = arr.toReversed()
console.log(newarr)
console.log(arr)
[ 3, 2, 1 ]
[ 1, 2, 3 ]
所以我们先调用arr.toReversed() 将数组反转,且不改变原数组,再调用arr.reverse()将反转的数组再反转,用 newarr 承接,达到拷贝目的
let arr = [1,2,3]
let newArr = arr.toReversed().reverse() //将数组反转 不会影响原数组
console.log(newArr,arr)
[ 1, 2, 3 ] [ 1, 2, 3 ]
2. 🎭 浅拷贝的致命陷阱:你以为的"复制"只是假象!
六大神器看似强大,实则都是只披着克隆外衣的共享傀儡。看这段代码如何撕开浅拷贝的伪装:
let obj={
name:'坤坤',
age:18,
like:{ // 💣 嵌套对象 - 浅拷贝的爆破点
a:'唱',
b:'跳',
c:'rap'
}
}
// 使用神器5号:Object.assign()
let newObj = Object.assign({}, obj);
/*----- 篡改时刻 -----*/
obj.age = 19; // 修改第一层属性
obj.like.c = '🏀篮球'; // 修改嵌套属性
console.log(newObj);
{
name: "坤坤",
age: 18, // ✅ 基本类型安全
like: { // ❌ 嵌套对象沦陷!
a: "唱",
b: "跳",
c: "🏀篮球" // 被污染了!
}
}
🚨 六大神器的共同软肋
- 只克隆「外壳」不克隆「灵魂」
就像复印身份证——卡套是新的,但里面的芯片仍是原件 - 嵌套对象=共享炸弹
修改任何层级的嵌套属性都会引爆所有副本 - 最危险的是你以为已安全
当基本属性保持独立时,开发者容易误以为拷贝完全成功
三、🧬 深拷贝:突破嵌套的终极克隆术
面对浅拷贝的共享诅咒,深拷贝祭出两大杀招——但各有致命缺陷!看这段代码如何揭露真相:
let obj = {
name: '坤坤',
age: 18,
like: { // 嵌套对象-深拷贝的试金石
a: '唱',
b: '跳',
c: 'rap'
},
a: undefined, // 幽灵属性
b: null, // 空值属性
c: function() { console.log('我是函数') }, // 会消失的魔法
d: Symbol(1), // 唯一密钥
e: new Date(), // 时间胶囊
f: new Set([1,2,3]), // 集合
g: new Map([['key', 'value']]) // 映射
};
// 自引用陷阱(循环引用)
obj.self = obj;
⚔️ 深拷贝方案一:JSON核弹法
let newObj = JSON.parse(JSON.stringify(obj))
// 篡改原对象
obj.like.a = '🏀篮球';
console.log(newObj)
{
name: "坤坤",
age: 18,
like: { a: "唱", b: "跳", c: "rap" }, // ✅ 未受篡改影响
b: null, // null幸存
e: "2023-05-15T12:00:00.000Z", // ❌ 日期变字符串
f: {}, // ❌ Set被掏空
g: {}, // ❌ Map被掏空
self: {} // ❌ 循环引用被腰斩
}
// 消失的属性:a(undefined), c(函数), d(Symbol)
☠️ 核弹辐射范围:
| 数据类型 | 下场 |
|---|---|
undefined | 人间蒸发 |
Function | 灰飞烟灭 |
Symbol | 量子分解 |
BigInt | 引发核爆(报错) |
Date | 降维成字符串 |
Set/Map | 退化成空对象 |
| 循环引用 | 引发黑洞(报错) |
🛡️ 深拷贝方案二:structuredClone()
let newObj = structuredClone(obj)
console.log(newObj)
obj.like.b = '💃街舞';
console.log(newObj.like.b); // "跳" ✅ 未受影响
console.log(newObj.e instanceof Date); // true ✅
console.log(newObj.f instanceof Set); // true ✅
console.log(newObj.self === newObj); // true ✅ 循环引用完美闭环!
🚀 优势突破:
- 支持
Date、Set、Map等复杂类型 - 突破循环引用结界
- 保留
undefined和null
💔 仍存缺陷:
console.log(现代副本.c); // undefined ❌ 函数消失
console.log(现代副本.d); // undefined ❌ Symbol蒸发
🔍 深拷贝能力对照表
| 数据类型 | JSON法 | structuredClone | 完美深拷贝 |
|---|---|---|---|
| 基本类型 | ✅ | ✅ | ✅ |
| 嵌套对象 | ✅ | ✅ | ✅ |
undefined | ❌ | ✅ | ✅ |
Function | ❌ | ❌ | ✅ |
Symbol | ❌ | ❌ | ✅ |
BigInt | ❌ | ❌ | ✅ |
Date | ⚠️字符串 | ✅ | ✅ |
Set/Map | ❌空对象 | ✅ | ✅ |
| 循环引用 | ❌报错 | ✅ | ✅ |
| DOM元素 | ❌ | ❌ | ✅ |
💡 决策指南:
- 纯数据对象无循环引用 →
JSON法(简单够用)- 现代浏览器环境 →
structuredClone(首选方案)- 含函数/Symbol/循环引用 → 手写深拷贝(终极方案)
四、 ✨ 手写深拷贝
⚙️ 核心原理四步走
-
创建新容器
let newObj = {}创建空对象作为克隆基础 -
属性遍历
for...in循环获取对象所有可枚举属性 -
过滤原型属性
hasOwnProperty确保只复制对象自身属性(不复制原型链上的属性) -
类型分级处理:
typeof obj[key] === 'object'→ 引用类型 → 递归深挖- 其他 → 原始类型 → 直接赋值
let obj = {
name: '坤坤',
age: 18,
like: {
a: '唱',
b: '跳',
c: 'rap',
}}
function deepCopy(obj) {
let newObj = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 先判断 obj[key] 值的类型,如果是原始类型,直接赋值,如果是引用类型,xxxxx
if (typeof obj[key] === 'object' && obj[key] !== null) {
newObj[key] = deepCopy(obj[key])
} else {
newObj[key] = obj[key]
}
}
}
return newObj
}
let obj2 = deepCopy(obj)
obj.like.a = '篮球'
console.log(obj2);
{ name: '坤坤', age: 18, like: { a: '唱', b: '跳', c: 'rap' } }
拷贝江湖生存指南总结🎯
JS 拷贝就像场克隆大战!浅拷贝是 "表面兄弟",用Object.assign等六大神器只能复制第一层,嵌套对象会共享内存,改原对象副本也遭殃。深拷贝才是 "灵魂复刻":JSON 法像核弹,能炸平简单对象但会让函数、Symbol 消失,Date 变字符串;structuredClone是现代武器,支持复杂类型和循环引用,却搞不定函数和 Symbol。终极杀招是手写深拷贝,通过递归层层克隆,不过得小心循环引用。记住:简单数据用 JSON,现代环境选structuredClone,复杂场景就手写,做 JS 世界的拷贝大师!