深浅拷贝、对比
# 深浅拷贝、对比
# 赋值和深/浅拷贝的区别
这三者的区别如下,不过比较的前提都是针对引用类型:
- 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
- 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
- 深拷贝:重新在堆中创建内存,将对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
# js 中的浅拷贝
- 🚨 注意:堆内存中开辟一块区域,产生新对象,必然要先开辟一块内存空间
- 基本类型值拷贝,引用数据类型拷贝值引用
- 浅拷贝的限制是只能拷贝一层对象,对嵌套的对象依然拷贝的是引用地址
# 什么是拷贝
let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;
console.log(arr); //[100, 2, 3]
let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;
console.log(arr); //[1, 2, 3]
当修改 newArr 的时候,arr 的值并不改变。什么原因?因为这里 newArr 是 arr 浅拷贝后的结果,newArr 和 arr 现在引用的已经不是同一块空间啦!
这就是浅拷贝!
但是这又会带来一个潜在的问题:
let arr = [1, 2, { val: 4 }];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]
由于对象的浅拷贝保存的是引用地址,所以会出现这种奇怪现象。
# 浅拷贝的几种方法
遍历原数组,手动赋值给新数组
Object.assign()
Object.assign((target = {}), ...sources);
其中
target
是目标对象,sources
是源对象,可以有多个,返回修改后的目标对象target
。concat(),参数为空,返回值就是直接浅拷贝一份
slice(),参数为空,返回值就是直接浅拷贝一份
...展开运算符
let arr = [1, 2, 3]; let newArr = [...arr]; //跟arr.slice()是一样的效果
# 深拷贝
深拷贝可以解决一些问题,比如浅拷贝的对象地址引用。
# 简易版
JSON.parse(JSON.stringify());
// 对象中有时间类型的时候,序列化之后会变成字符串类型。
// 对象中有undefined和Function类型数据的时候,序列化之后会直接丢失。
// 对象中有NaN、Infinity和-Infinity的时候,序列化之后会显示null。
// 对象循环引用的时候,会直接报错。
估计这个 api 能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
- 无法解决
循环引用
的问题。举个例子:
const a = { val: 2 };
a.target = a;
拷贝 a 会出现系统栈溢出,因为出现了无限递归
的情况。
- 无法拷贝一写
特殊的对象
,诸如 RegExp, Date, Set, Map 等。
- 无法拷贝
函数
(划重点)。
- 会抛弃对象的 constructor,所有的构造函数会指向 Object
所以这种解决方式不太完美。
# 完善版
一般开发中会使用 lodash 的 cloneDeep。接下来贴一哈我的手写方式:
function isObject(value) {
const valueType = typeof value;
return value !== null && (valueType === 'object' || valueType === 'function');
}
function deepClone(originValue, map = new WeakMap()) {
// 判断是否是一个Set类型
if (originValue instanceof Set) {
return new Set([...originValue]);
}
// 判断是否是一个Map类型
if (originValue instanceof Map) {
return new Map([...originValue]);
}
// 判断如果是Symbol的value, 那么创建一个新的Symbol
if (typeof originValue === 'symbol') {
return Symbol(originValue.description);
}
// 判断如果是函数类型, 那么直接使用同一个函数
if (typeof originValue === 'function') {
return originValue;
}
// 判断传入的originValue是否是一个对象类型
if (!isObject(originValue)) {
return originValue;
}
if (map.has(originValue)) {
return map.get(originValue);
}
// 判断传入的对象是数组, 还是对象
const newObject = Array.isArray(originValue) ? [] : {};
map.set(originValue, newObject);
for (const key in originValue) {
newObject[key] = deepClone(originValue[key], map);
}
// 对Symbol的key进行特殊的处理
const symbolKeys = Object.getOwnPropertySymbols(originValue);
for (const sKey of symbolKeys) {
// const newSKey = Symbol(sKey.description)
newObject[sKey] = deepClone(originValue[sKey], map);
}
return newObject;
}
// deepClone({name: "why"})
// 测试代码
let s1 = Symbol('aaa');
let s2 = Symbol('bbb');
const obj = {
name: 'why',
age: 18,
friend: {
name: 'james',
address: {
city: '广州',
},
},
// 数组类型
hobbies: ['abc', 'cba', 'nba'],
// 函数类型
foo: function (m, n) {
console.log('foo function');
console.log('100代码逻辑');
return 123;
},
// Symbol作为key和value
[s1]: 'abc',
s2: s2,
// Set/Map
set: new Set(['aaa', 'bbb', 'ccc']),
map: new Map([
['aaa', 'abc'],
['bbb', 'cba'],
]),
};
// 测试一下
obj.info = obj;
const newObj = deepClone(obj);
console.log(newObj === obj);
obj.friend.name = 'kobe';
obj.friend.address.city = '成都';
console.log(newObj);
console.log(newObj.s2 === obj.s2);
console.log(newObj.info.info.info);
# 引用比较
下面三种对比方式用于 Object,实现引用比较功能:
===
==
Object.is()
const hero1 = {
name: 'Batman',
};
const hero2 = {
name: 'Batman',
};
hero1 === hero1; // => true
hero1 === hero2; // => false
hero1 == hero1; // => true
hero1 == hero2; // => false
Object.is(hero1, hero1); // => true
Object.is(hero1, hero2); // => false
# 浅比较
浅对比函数写法有很多,不过其效果都是标准的,就是只比较一层,下面给出了一种写法:
function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}
可以看到,浅对比就是将对象每个属性进行引用对比,算是一种性能上的平衡,尤其在 redux 下有特殊的意义。
下面给出了使用例子:
const hero1 = {
name: 'Batman',
realName: 'Bruce Wayne',
};
const hero2 = {
name: 'Batman',
realName: 'Bruce Wayne',
};
const hero3 = {
name: 'Joker',
};
shallowEqual(hero1, hero2); // => true
shallowEqual(hero1, hero3); // => false
如果对象层级再多一层,浅对比就无效了,此时需要使用深对比。
# 深比较
深对比就是递归对比对象所有简单对象值,遇到复杂对象就逐个 key 进行对比,以此类推。
可以看到,只要遇到 Object 类型的 key,就会递归调用一次 deepEqual
进行比较,否则对于简单类型直接使用 !==
引用对比。
值得注意的是,数组类型也满足 typeof object === "object"
的条件,且 Object.keys
可以作用于数组,且 object[key]
也可作用于数组,因此数组和对象都可以采用相同方式处理。
function deepEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
const val1 = object1[key];
const val2 = object2[key];
const areObjects = isObject(val1) && isObject(val2);
if (
(areObjects && !deepEqual(val1, val2)) ||
(!areObjects && val1 !== val2)
) {
return false;
}
}
return true;
}
function isObject(object) {
return object != null && typeof object === 'object';
}
// 测试
const hero1 = {
name: 'Batman',
address: {
city: 'Gotham',
},
};
const hero2 = {
name: 'Batman',
address: {
city: 'Gotham',
},
};
console.log(deepEqual(hero1, hero2)); // => true
但深对比会造成性能损耗,不要小看递归的作用,在对象树复杂时,深对比甚至会导致严重的性能问题。