前言 什么是拷贝,为什么我们需要拷贝 拷贝分为哪几种 分别适用于什么情况
在 JavaScript 的世界里,拷贝数据就像复制一把钥匙——看起来一模一样,但用起来可能打开同一扇门(引用传递),也可能打开不同的门(值传递)。你是不是也遇到过这样的困扰:明明拷贝了一个对象,修改原对象时新对象也跟着变了?这就是浅拷贝和深拷贝在"暗中较劲"。今天,我们就来彻底搞懂它们!从 V8 引擎的存储秘密,到 6 种浅拷贝妙招,再到深拷贝的终极方案,一篇带你通关数据拷贝的核心知识!
一、V8 数据存储机制:内存里的"双城记"
在 JavaScript 的内存世界里,存在着两座截然不同的城市,它们遵循着完全不同的生存法则,这就是 V8 引擎精心设计的栈内存(Stack) 和堆内存(Heap) 。
1. 什么是栈内存"
举个生动的例子:
当你声明一个变量 let a = 42;
时,V8 引擎就像在流水线上快速组装好一辆数字汽车,并给它贴上标签"a"。这个数字 42 就被直接存放在栈内存中。
2.什么是堆内存"
举个生动的例子:
当你创建一个对象 const obj = { name: 'Alice' };
时,V8 引擎会在堆内存中开辟一块空间,像建造一座秘密基地一样存储这个对象,然后在栈内存中存放这个基地的"地址门票"(内存地址)。
二、数据类型的"内存身份证":基本类型 vs 引用类型
JavaScript 的数据类型就像拥有不同"内存身份证"的公民,决定着它们在内存世界中的生存方式。
1. 基本类型(Primitive Types):栈内存的"独行侠"
成员清单:
- Number
- String
- Boolean
- null
- undefined
- Symbol
- BigInt
2. 引用类型(Reference Types):堆内存的"社交达人"
成员清单:
- Object
- Array
- Function
- Date
- RegExp
- Map/Set
拷贝
-
复刻一个对象,和原对象长的一样
-
浅拷贝:只拷贝对象的最外层,原对象的属性值修改会影响新对象
浅拷贝的6种方法
-
Object.create()
-
[].concat(arr)
-
数组解构[...arr]
-
let arr2=arr.slice(0,arr.length)
-
Object.assign({},obj)
-
arr.toReversed().reverse()
第一种方法 Object.create()
原理:
Object.create()
会创建一个新对象,并将新对象的原型(__proto__
)指向传入的原型对象。这就像给新对象找了个"干爹",继承原型对象的属性。
开始之前,有个问题:所有的对象都有原型对象吗?
不是的哦,js中有其实一个方法是可以创建对象但没有原型对象
这个方法就是
Objecr.create(null) 能够得到一个没有原型的对象
let obj={
a:1
}
let obj2=Object.create(obj)
console.log(obj2.a);
Object.create()可以让创建的新对象的对象原型(隐式原型)指向我们传进来的对象,于是我们打印obj.a的时候在obj的上找不到a,就会去obj的对象原型(隐式原型)上找a,obj的隐式原型是指向obj对象
第二种方法 [].concat(arr)
该方法可以创建一个新数组,其内容是两个数组拼接的,且对原数组没有影响
那么 [].concat(arr)就可以得到一个与原来数组一样的数组
第三种方法 数组解构[...arr]
let arr = [1, 2, 3];
const [x, y, z] = arr;
//解构对象
console.log(x, y, z);
console.log(...arr);
let arr2=[...arr]
console.log(arr2);
第四种方法 let arr2=arr.slice(0,arr.length)
数组的两种方法(splice)(slice),傻傻分不清
splice第一种用法
splice(0,1)第一个参数表示删除从第几个下标开始,第二个参数表示删除几个数组元素
splice第二种用法
作为增加数组元素的方法,前两个同理,第二个参上为0时,新加第三个参数,表示插入的元素
通过slice(0,arr.length)来拷贝数组,参数时左闭右开的
通过数组的解构来实现拷贝,先解构数组,再将解构的数组装到新数组中
第五种方法 Object.assign({},obj)
对象的拼接
可以拼接对象,但是会对原来的对象产生影响。
6. arr.toReversed().reverse()
使用arr.reverse()可以将数组反转,但是原数组会受到影响,数组新加了一个方法toReversed()可以返回一个反转一个数组且原数组不受影响
以上就是浅拷贝的6种方法
浅拷贝的与深拷贝的区别:
原对象内容发生变化,不是深拷贝
原因是因为引用数据是在堆内存中存储的,obj中存储的并不是like对象,而是like对象的地址值, Object.assign是将对象内容复制到新对象,复制对象里的对象时,复制的是地址。所以对对象的引用数据更改时,新拷贝的对象的引用数据内容也会跟着改变。
经典面试题:
- 手写浅拷贝:
Object.prototype.toDo = "sleep";
let obj = {
name: "cc",
age: 18,
a: "我爱金铲铲",
};
function shallowCopy(obj) {
let newObj = {};
for (let key in obj) {
//for in 循环遍历对象属性
newObj[key] = obj[key];
}
return newObj;
}
console.log(shallowCopy(obj));
关键机制解析
-
原型链污染(Object.prototype.toDo = "sleep")
- 通过修改
Object.prototype
,相当于在 JavaScript 世界的"基因库"中插入了一个新特性 - 所有对象都会通过原型链继承这个属性,就像所有人类都继承了直立行走的能力
- 这种全局修改会导致意外的属性泄露,是典型的"副作用污染"
- 通过修改
-
for...in 的特性
- 该循环会遍历对象自身的可枚举属性 + 继承的可枚举属性
- 就像检查一个人的随身物品时,不仅检查口袋,还检查了家族传承的传家宝
- 未使用
hasOwnProperty()
检查,导致继承属性被误认为自有属性
-
浅拷贝的副作用
- 浅拷贝像"皮肤移植",只复制表面属性
- 继承属性会被当作自有属性"移植"到新对象
- 导致新对象携带了本不该存在的"基因缺陷"
注意在遍历对象属性时不能写成newObj.key而要用[]的形式,不然就是往newObj里面存了一个属性名为key的属性。
深拷贝的两种方法
- 深拷贝:层层拷贝,新对象不受原对象的影响
- JSON.parse(JSON.stringify(obj))
-
无法识别bigint类型,无法处理函数,无法处理Symbol类型
-
无法处理循环引用
- structuredClone()
1. JSON.parse(JSON.stringify(obj))
JSON.parse(JSON.stringify(obj))
自带方法将对象转换为字符串,再将字符串转化为对象,就达到了深拷贝
这样深拷贝的缺陷:
-
无法识别bigint类型,无法处理函数,无法处理Symbol类型
-
无法处理循环引用
obj.a=obj.e
obj.e.n=obj.a
2. structuredClone()
缺陷:
没办法复制函数
没办法复制Symbol
没办法复制BigInt类型
面试题:
手写深拷贝
function deepClone(obj) {
let newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
//先判断obj[key]类型,如果是原始类型就直接赋值,如果是引用类型
if (typeof obj[key] === "object" && obj[key] !== null) {
newObj[key] = deepClone(obj[key]);
} else {
newObj[key] = obj[key];
}
}
}
return newObj;
}
let newObjj = deepClone(obj);
obj.like.a = "你爱我我爱你,蜜雪甜蜜蜜";
console.log(newObjj);
if (typeof obj[key] === "object" && obj[key] !== null) null是特殊的原始类型,typeof的结果是object所以要排除null
深拷贝这里使用了递归,因为逻辑都是一样的,对象里面套对象
结语
走完这趟拷贝探索之旅,相信你已经明白:
-
数据存储决定拷贝方式:基本类型直接复制,对象类型复制的是"地址钥匙"
-
浅拷贝适合简单场景:
Object.assign()
、解构等 6 招轻松应对单层数据 -
深拷贝要层层深入:
JSON 大法
简单但会丢失函数和 SymbolstructuredClone()
能处理循环引用却搞不定函数- 递归深拷贝才是最可靠的终极武器
我的实战建议:日常开发优先用
structuredClone()
,但是它是最新推出的函数,部分浏览器无法使用,遇到复杂嵌套对象时,手写递归深拷贝才是王道。记住,当对象里藏着函数、Symbol 或者循环引用时,你的深拷贝函数还需要升级装备——这将是我们的下一个冒险故事!
拷贝的奥秘已在你手中,下次遇到对象套对象的复杂数据,可别再被浅拷贝"坑"啦!
欢迎友友们点赞评论,如有错误欢迎指正