8、深拷贝与浅拷贝
引用大佬ConardLi的图来理解深浅拷贝
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值,
如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
8.1 浅拷贝的实现
1. Object.assign()
首先可以通过
Object.assign
来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign
只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
2. 函数库lodash的_.clone方法
该函数库也有提供
_.clone
用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
3. 展开运算符…
通过展开运算符
...
来实现浅拷贝
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与Object.assign ()
的功能相同。
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
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 } }
4. Array.prototype.concat()
5. Array.prototype.slice()
利用数组API
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
//let arr3 = arr.slice();
arr2[2].username = 'wade';
//arr3[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
8.2 深拷贝的实现
1.JSON.parse(JSON.stringify())
通常可以通过
JSON.parse(JSON.stringify(object))
来解决
这是利用JSON.stringify
将对象转成JSON
字符串,再用JSON.parse
把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
该方法的局限性在于:
- 会忽略
undefined
- 会忽略
symbol
- 不能序列化函数
- 不能解决循环引用的对象
JSON.stringify()
方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个replacer
函数,则可以选择性地替换值,或者指定的replacer
是数组,则可选择性地仅包含数组指定的属性。
console.log(JSON.stringify({ x: 5, y: 6 }));
// expected output: "{"x":5,"y":6}"
console.log(JSON.stringify([new Number(3), new String('false'), new Boolean(false)]));
// expected output: "[3,"false",false]"
console.log(JSON.stringify({ x: [10, undefined, function(){}, Symbol('')] }));
// expected output: "{"x":[10,null,null,null]}"
console.log(JSON.stringify(new Date(2006, 0, 2, 15, 4, 5)));
// expected output: ""2006-01-02T15:04:05.000Z""
JSON.parse()
方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver
函数用以在返回之前对所得到的对象执行变换(操作)。
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json);
console.log(obj.count);
// expected output: 42
console.log(obj.result);
// expected output: true
2. 函数库lodash的_.cloneDeep
方法
该函数库也有提供
_.cloneDeep
用来做 Deep Copy
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
8.3 提升
根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝
实现浅拷贝
//浅拷贝
function shallowClone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
}
实现深拷贝
深拷贝的问题其实可以分解成两个问题,浅拷贝+递归
//深拷贝
function deepClone(target) {
if (typeof target === 'object') {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
} else {
return target;
}
}
提升
解决上述深拷贝的问题
- 没有对参数做检验,判断是否对象的逻辑不够严谨
首先我们需要抽象一个判断对象的方法,其实比较常用的判断对象的方法如下
//抽象一个判断对象的方法即可解决
function isObject(x) {
return Object.prototype.toString.call(x) === '[object Object]'
}
function deepClone(target) {
// 使用isObject对参数做检验,判断是否对象
if (isObject(target)) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
} else {
return target;
}
}
- 没有考虑数组
function isObject(x) {
return Object.prototype.toString.call(x) === '[object Object]'
}
function deepClone(target) {
// 使用isObject对参数做检验,判断是否对象
if (isObject(target)) {
// 考虑数组
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
} else {
return target;
}
}
//测试
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
var cloneTarget = deepClone(target)
- 无法解决循环引用
- 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
- 当需要拷贝当前对象时,先去存储空间
map
中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
-
这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:
-
检查map中有无克隆过的对象
有 - 直接返回
没有 - 将当前对象作为key,克隆对象作为value进行存储 -
继续克隆
//代码
function isObject(x) {
return Object.prototype.toString.call(x) === '[object Object]'
}
function deepClone(target, map = new Map()) {
// 使用isObject对参数做检验,判断是否对象
if (isObject(target)) {
// 考虑数组
let cloneTarget = Array.isArray(target) ? [] : {};
//使用map解决循环引用问题
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = deepClone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
}
//测试用例
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
target.target = target
var cloneTarget = deepClone(target)
console.log(cloneTarget)
输出:target属性,变为了一个Circular类型,即循环引用的意思。
- 递归爆栈