探究 JavaScript 中的深浅拷贝
堆和栈
其实深拷贝和浅拷贝的主要区别就是其在内存中的存储类型不同。
堆和栈都是内存中划分出来用来存储的区域。
栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。
ECMAScript 的数据类型
基本数据类型存放在栈中
存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。
基本数据类型值不可变
javascript中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。
基本类型的比较是值的比较
基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的。
比较的时候最好使用严格等,因为 ==
是会进行类型转换的,比如:
var a = 1;
var b = true;
console.log(a == b);//true复制代码
引用类型
引用类型存放在堆中
引用类型(object
)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配,例如。
var person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};
引用类型值可变
引用类型是可以直接改变其值的,例如:
var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5
引用类型的比较是引用的比较
所以每次我们对 js 中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。例如:
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象,所以他们是不相等的。
传值与传址
了解了基本数据类型与引用类型的区别之后,我们就应该能明白传值与传址的区别了。
在我们进行赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把再将值赋值到新的栈中。例如:
var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10复制代码
所以说,基本类型的赋值的两个变量是两个独立相互不影响的变量。
但是引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。例如:
var a = {}; // a保存了一个空对象的实例
var b = a; // a和b都指向了这个空对象
a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'
b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22
console.log(a == b);// true复制代码
浅拷贝
赋值(=)和浅拷贝的区别
那么赋值和浅拷贝有什么区别呢,我们看下面这个例子:
var obj1 = { //原始数据
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1; //赋值操作得到
var obj3 = shallowCopy(obj1); //浅拷贝得到
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,["二","三"],["四","五"]],
//};复制代码
这是因为浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。所以就会出现改变浅拷贝得到的 obj3
中的引用类型时,会使原始数据得到改变。
深拷贝:将 B 对象拷贝到 A 对象中,包括 B 里面的子对象,
浅拷贝:将 B 对象拷贝到 A 对象中,但不包括 B 里面的子对象
-- | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |
深拷贝
深拷贝是对对象以及对象的所有子对象进行拷贝。
Js自带的深拷贝方法
1、Array
- slice()、concat、Array.from()、... 操作符:只能实现一维数组的深拷贝
var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice();
console.log(arr1); //[1, 2, [3, 4]]
console.log(arr2); //[1, 2, [3, 4]]
arr2[0] = 2
arr2[2][1] = 5;
console.log(arr1); //[1, 2, [3, 5]]
console.log(arr2); //[2, 2, [3, 5]]
2、Object
- Object.assign():只能实现一维对象的深拷贝
var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 1, y: {m: 2}}
- JSON.parse(JSON.stringify(obj)):可实现多维对象的深拷贝,但会忽略
undefined、任意的函数、symbol 值
,循环引用会报错,相同的引用会被重复复制
var obj1 = {
x: 1,
y: {
m: 1
},
a:undefined,
b:function(a,b){
return a+b
},
c:Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ƒ, c: Symbol(foo)}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ƒ, c: Symbol(foo)}
console.log(obj2) //{x: 2, y: {m: 2}}
**注:**进行JSON.stringify()
序列化的过程中,undefined、任意的函数以及 symbol 值
,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
由上面可知,JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题,因此我们应该自己实现。
深拷贝函数简单写法(递归实现)
function deepClone(obj){
let result = Array.isArray(obj)?[]:{};
if(obj && typeof obj === "object"){
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(obj[key] && typeof obj[key] === "object"){
result[key] = deepClone(obj[key]);
}else{
result[key] = obj[key];
}
}
}
}
return result;
}
// 测试用
var obj1 = {
x: {
m: 1
},
y: undefined,
z: function add(z1, z2) {
return z1 + z2
},
a: Symbol("foo"),
b: [1,2,3,4,5],
c: null
};
var obj2 = deepClone(obj1);
obj2.x.m = 2;
obj2.b[0] = 2;
console.log(obj1);
console.log(obj2);
但上面的深拷贝方法遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈。如:
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepClone(obj1);
因此需要改进。
深拷贝函数改进(防止循环递归)
解决因循环递归而暴栈的问题,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可。
function deepClone(obj, parent = null){ // 改进(1)
let result = Array.isArray(obj)?[]:{};
let _parent = parent; // 改进(2)
while(_parent){ // 改进(3)
if(_parent.originalParent === obj){
return _parent.currentParent;
}
_parent = _parent.parent;
}
if(obj && typeof obj === "object"){
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(obj[key] && typeof obj[key] === "object"){
result[key] = deepClone(obj[key],{ // 改进(4)
originalParent: obj,
currentParent: result,
parent: parent
});
}else{
result[key] = obj[key];
}
}
}
}
return result;
}
// 调试用
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepClone(obj1);
console.log(obj1); //太长了去浏览器试一下吧~
console.log(obj2); //太长了去浏览器试一下吧~
深拷贝函数最终版(支持基本数据类型、原型链、RegExp、Date类型)
function deepClone(obj, parent = null){
let result; // 最后的返回结果
let _parent = parent; // 防止循环引用
while(_parent){
if(_parent.originalParent === obj){
return _parent.currentParent;
}
_parent = _parent.parent;
}
if(obj && typeof obj === "object"){ // 返回引用数据类型(null已被判断条件排除))
if(obj instanceof RegExp){ // RegExp类型
result = new RegExp(obj.source, obj.flags)
}else if(obj instanceof Date){ // Date类型
result = new Date(obj.getTime());
}else{
if(obj instanceof Array){ // Array类型
result = []
}else{ // Object类型,继承原型链
let proto = Object.getPrototypeOf(obj);
result = Object.create(proto);
}
for(let key in obj){ // Array类型 与 Object类型 的深拷贝
if(obj.hasOwnProperty(key)){
if(obj[key] && typeof obj[key] === "object"){
result[key] = deepClone(obj[key],{
originalParent: obj,
currentParent: result,
parent: parent
});
}else{
result[key] = obj[key];
}
}
}
}
}else{ // 返回基本数据类型与Function类型,因为Function不需要深拷贝
return obj
}
return result;
}
// 调试用
function construct(){
this.a = 1,
this.b = {
x:2,
y:3,
z:[4,5,[6]]
},
this.c = [7,8,[9,10]],
this.d = new Date(),
this.e = /abc/ig,
this.f = function(a,b){
return a+b
},
this.g = null,
this.h = undefined,
this.i = "hello",
this.j = Symbol("foo")
}
construct.prototype.str = "I'm prototype"
var obj1 = new construct()
obj1.k = obj1
obj2 = deepClone(obj1)
obj2.b.x = 999
obj2.c[0] = 666
console.log(obj1)
console.log(obj2)
console.log(obj1.str)
console.log(obj2.str)
注:Function类型的深拷贝:
- bind():使用
fn.bind()
可将函数进行深拷贝,但因为this指针指向问题而不能使用; - eval(fn.toString()):只支持箭头函数,普通函数
function fn(){}
则不适用; - new Function(arg1,arg2,...,function_body):需将参数与函数体提取出来;
PS:一般也不需要深拷贝Function。
深度优先、广度优先实现深拷贝
<!--工具函数-->
const _toString = Object.prototype.toString
function getType(obj) {
return _toString.call(obj).slice(8, -1)
}
<!--深度优先-->
function DFSDeepClone(obj, vistied = new Set(), level = 0) {
let res = {}
if (getType(obj) === 'Object' || getType(obj) === 'Array') {
if (vistied.has(obj)) {
// 处理环状结构
res = obj
} else {
vistied[level] = obj
vistied.add(obj)
res = getType(obj) === 'Object' ? {} : []
Object.keys(obj).forEach(k => {
res[k] = DFSDeepClone(obj[k], vistied, level + 1)
})
}
} else if (typeof obj === 'function') {
res = eval(`(${obj.toString()})`)
} else {
res = obj
}
return res
}
<!--广度优先-->
function BFSDeepClone(obj) {
if (getType(obj) !== 'Object' && getType(obj) !== 'Array') {
if (typeof obj === 'function') {
obj = eval(`(${obj.toString()})`)
}
return obj
}
let res = {}
const origin = [obj]
const copy = [res]
const vistied = new Set([obj])
while (origin.length) {
const _obj = origin.shift()
const copyObj = copy.shift()
Object.keys(_obj).forEach(k => {
const item = _obj[k]
if (getType(item) === 'Object' || getType(item) === 'Array') {
if (vistied.has(item)) {
copyObj[k] = item
} else {
vistied.add(item)
copyObj[k] = getType(item) === 'Object' ? {} : []
origin.push(item)
copy.push(copyObj[k])
}
} else if (typeof item === 'function') {
copyObj[k] = eval(`(${item.toString()})`)
} else {
copyObj[k] = item
}
})
}
return res
}
没有实现对Symbol属性的处理,如果想实现的话可以用 Object.getOwnPropertySymbols()得到所有的Symbol属性,然后去遍历。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论