# 对象的深浅拷贝
基本数据类型数据存储在栈中;引用数据类型存储的是该对象在栈中的引用,真实的数据存放在堆内存里(引用类型(
object
)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。)。
基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值(
null
、undefined
,布尔值,数字和字符串)也是不会改变的。
基本类型比较的是值,引用类型比较的是栈内存的地址是否指向同一个堆内存对象。
var str = "abc";
console.log(str[1]="f"); // f
console.log(str); // abc
2
3
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的是基本类型的值,如果属性是引用类型则拷贝的是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象(逐个成员拷贝)。浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存.
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响到原来的对象。
# 赋值和深浅拷贝的区别
- 赋值:当新建一个变量然后赋值一个对象给它时,赋的其实是对象在栈中的地址,而不是堆中的数据,这样两个对象共用一份地址,所以两个对象就联动了。切忌误以为这是浅拷贝。啥数据类型的属性都受影响
var obj1 = {
name: 'linjiaheng',
language: ['vue', 'ts', 'js']
}
var obj2 = obj1
console.log(obj2 === obj1)
obj2.name = 'xielin';
obj2.language[2] = 'React';
console.log(obj2)
console.log(obj1)
2
3
4
5
6
7
8
9
10
- 浅拷贝:拷贝后对象的基本数据类型不受影响,引用类型因为共用一块内存所以互相影响。基本数据类型不受影响(重新在堆中创建内存)
给最外层新增属性时,拷贝对象不会跟着新增属性,这点和赋值不同。
// 浅拷贝
var obj1 = {
name: 'linjiaheng',
language: ['vue', 'ts', 'js']
}
var obj3 = shallowCopy(obj1);
obj3.name = "lisi";
obj3.language[3] = 'nuxt';
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
function deepClone(obj) {
var newObj = obj instanceof Array ? []:{}; // 考虑数组
if(typeof obj !== 'object') {
return obj;
} else {
for(var i in obj) {
newObj[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
}
}
return newObj;
}
2
3
4
5
6
7
8
9
10
11
# 浅拷贝实现方式
Object.assign()
很多人认为这个函数是用来深拷贝的。其实并不是,
Object.assign
只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。针对深拷贝,需要使用其他方法,因为Object.assign()
拷贝的是可枚举属性值。假如源值是一个对象的引用,它仅仅会复制其引用值。
例子:
const log = console.log;
function test() {
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1);
log(JSON.stringify(obj2));
// { a: 0, b: { c: 0}}
obj1.a = 1;
log(JSON.stringify(obj1));
// { a: 1, b: { c: 0}}
log(JSON.stringify(obj2));
// { a: 0, b: { c: 0}}
obj2.a = 2;
log(JSON.stringify(obj1));
// { a: 1, b: { c: 0}}
log(JSON.stringify(obj2));
// { a: 2, b: { c: 0}}
obj2.b.c = 3;
log(JSON.stringify(obj1));
// { a: 1, b: { c: 3}}
log(JSON.stringify(obj2));
// { a: 2, b: { c: 3}}
// Deep Clone
obj1 = { a: 0 , b: { c: 0}};
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 4;
obj1.b.c = 4;
log(JSON.stringify(obj3));
// { a: 0, b: { c: 0}}
}
test()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var obj = {a: 1, b: 2}
var newObj = Object.assign({}, obj)
console.log(JSON.stringify(newObj))
obj.a = 3
console.log(JSON.stringify(newObj)) // 不变,对象属性只有一层为浅拷贝
console.log(obj === newObj) // false
2
3
4
5
6
Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2=arr.concat();
arr2[2].username = 'wade';
console.log(arr);
2
3
4
5
6
Array.prototype.slice()
- 展开运算符
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
2
3
4
5
# 深拷贝实现方式
JSON.parse(JSON.stringify())
正反序列化
这也是利用
JSON.stringify
将对象序列化成JSON字符串,再用JSON.parse
反序列化把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于
JSON.stringify
和JSON.parse
处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(被忽略了)了。JSON.stringify()
可以用来判断两个对象值是否相等。为什么会忽略呢,是因为JSON.stringfy
对undefinde,Symbol,函数
会转成undefined
,在转成对象时既然是undefined
就被忽略了咯。
这个方法缺点:
undefined
、任意的函数以及symbol
值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。- 会抛弃对象的
constructor
,所有的构造函数会指向Object
Date
日期调用了toJSON()
将其转换为了string
字符串(Date.toISOString())
,因此会被当做字符串处理。NaN
和Infinity
格式的数值及null
都会被当做null
。- 其他类型的对象,包括
RegExp/Map/Set/WeakMap/WeakSet
等引用类型对象,会变成空对象。
// 构造函数 function person(pname) { this.name = pname; } const Messi = new person('Messi'); // 函数 function say() { console.log('hi'); }; const oldObj = { a: say, // 被忽略了 b: new Array(1), // [null] c: new RegExp('ab+c', 'i'), // 变为{} d: Messi, e: undefined, // 被忽略了 f: null, // null g: Symbol(222), // 被忽略了 h: new Date(), // 转成字符串 p: NaN, // null q: [say,Symbol(111),undefined], // [null,null,null] z: new Set(1,2), // {} o: new Map() // {} }; const newObj = JSON.parse(JSON.stringify(oldObj)); // 无法复制函数 console.log(newObj.a, oldObj.a); // undefined [Function: say] // 稀疏数组复制错误 console.log(newObj.b[0], oldObj.b[0]); // null undefined // 无法复制正则对象 console.log(newObj.c, oldObj.c); // {} /ab+c/i // 构造函数指向错误 console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32- 对象有循环引用会报错:
const oldObj = {}; oldObj.a = oldObj; const newObj = JSON.parse(JSON.stringify(oldObj)); console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON
1
2
3
4解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
TIP
这个存储空间需要可以存储
key-value
形式的数据,且key
可以是一个引用类型,可以选择Map
这种数据结构- 检查
map
中有无克隆过的对象 - 有则直接返回
- 没有将当前对象作为
key
,克隆对象作为value
进行存储 - 继续克隆
function clone(target, map = new Map()) { if (typeof target === 'object') { let cloneTarget = Array.isArray(target) ? [] : {}; if (map.get(target)) { return map.get(target); } map.set(target, cloneTarget); for (const key in target) { cloneTarget[key] = clone(target[key], map); } return cloneTarget; } else { return target; } };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 手写递归
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
递归容易爆栈,会发生栈溢出问题
// 生成指定深度和每层广度的代码
function createData(deep, breadth) {
var data = {};
var temp = data;
for (var i = 0; i < deep; i++) {
temp = temp['data'] = {};
for (var j = 0; j < breadth; j++) {
temp[j] = j;
}
}
return data;
}
createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当clone
层很深的话栈就会溢出,但数据的广度不会造成溢出
clone(createData(1000)); // ok
clone(createData(10000)); // Maximum call stack size exceeded
clone(createData(10, 100000)); // ok 广度不会溢出
2
3
破解递归
function cloneLoop(x) {
const root = {};
// 栈,借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点。
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
MessageChannel
用于深拷贝
所需拷贝的对象含有内置类型并且不包含函数,可以使用
MessageChannel
function structuralClone(obj) {
return new Promise(resolve => {
const { port1, port2 } = new MessageChannel()
port2.onmessage = ev => resolve(ev.data)
port1.postMessage(obj)
})
}
var obj = {
a: 1,
b: {
c: 2
}
}
obj.b.d = obj.b
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
const clone = await structuralClone(obj)
console.log(clone)
}
test()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 赋值,深浅拷贝总结
- 赋值是完完全全把旧对象的地址赋值给了新对象,这个时候对旧对象进行增删改属性,都会对新对象造成联动影响。
- 浅拷贝只拷贝了最外一层,里面多层共用一份内存。所以最外一层如果删除或者增加属性对新对象不影响。
function shallowClone(o) {
const obj = {};
for ( let i in o) {
obj[i] = o[i];
}
return obj;
}
// 被克隆对象
const oldObj = {
a: 1,
b: [ 'e', 'f', 'g' ],
c: { h: { i: 2 } }
};
const newObj = shallowClone(oldObj);
oldObj.c = {}
console.log(newObj)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 无论是哪种拷贝,拷贝后的对象和旧对象一定不相等。
# 工具库lodash的拷贝函数源码学习
深拷贝_.cloneDeep(obj)