一、不同类型数据的比较、赋值和拷贝
1. 比较
- 基本类型的比较是值大小的比较。
- 引用类型的比较是比较保存在栈内存的指针是否指向同一个对象。
2. 赋值
- 传值:基本类型的赋值是在内存中新开辟一段栈内存,然后再把值赋值到新的栈内存中,新旧变量之间没有影响。
- 传址:引用类型的赋值只是改变了指针的指向,使两个变量指向同一个对象,新旧变量之间互相有影响。
3. 深拷贝、浅拷贝和赋值区别
浅拷贝:重新创建了一个新对象,如果原对象的属性是基本类型、那就会拷贝基本类型的值,如果属性是引用类型、拷贝的就是内存地址,互相有影响。
深拷贝:将对象和对象内部所有的属性都拷贝了,不论基本类型还是引用类型。
二、源码实现
1. 浅拷贝
法1:手写浅拷贝
for...in
用来遍历一个对象的属性,但它不仅遍历对象自身的属性,还会遍历继承的属性。因此需要结合hasOwnProperty()
判断一下某属性是否为对象自身的属性。
const shallowCopy = function(obj) {
if (obj === null || typeof obj != 'object') return obj; // 基本类型则直接返回
const res = Array.isArray(obj) ? [] : {};
for (let key in obj) { // 属性遍历 用 in
if (obj.hasOwnProperty(key)) { // 如果是对象自身的属性
res[key] = obj[key];
}
}
return res;
}
法2:Object.assign():引用类型
ES6 新增了Object.assign()
方法,通常用于对象的合并。它会将源对象 obj 的所有可枚举属性复制到目标对象 target 中,浅拷贝时只有引用类型是有效的。
- 第一个参数:目标对象
- 其他参数:源对象
// Object.assign(target, source1, source2);
const target = Object.assign({}, obj);
法3:展开运算符: 引用类型
const target = {...obj}; // 注意,是花括号
法4:Array.prototype.concat():只适用数组
const obj = {a: 1};
const arr = [obj];
const newArr = arr.concat();
法5:Array.prototype.slice():只适用数组
2. 深拷贝
法1:JSON.parse(JSON.stringify())
JSON.stringify()
可以将 js对象 转为 json字符串。
JSON.parse()
又将 json字符串 转成 js对象。
let target = JSON.parse(JSON.stringify(obj));
不足:
- obj 中不能有
函数、undefined
,否则会丢失。 - 不能存放
NaN、Infinity、-Infinity
,否则会变成null
。 - 不能存放正则、时间对象等。
法2:递归法 + 循环引用
let obj1 = {};
let obj2 = { b: obj1 };
obj1.a = obj2;
如果存在循环引用的情况,也就是对象直接或间接地引用了自身,那么递归后会导致栈内存溢出。怎么解决呢?
可以额外开辟一个存储空间Map
,存放已经被拷贝的对象。当需要拷贝当前对象时,先向Map
查询该对象,如果存在直接取出返回即可!(实际上是改变了对象引用)
注意:不能使用WeakMap
,因为WeakMap
只接受对象作为键名,不接受其他类型的值作为键名。
const deepCopy = function(obj, map = new Map()) {
if (obj === null || typeof obj != 'object') return obj; // 基本类型则直接返回
if (map.has(obj)) return map.get(obj); // 如果 map有存储,那么直接返回
const res = Array.isArray(obj) ? [] : {};
map.set(obj, res);
for (let key in obj) { // 属性遍历 用 in
if (obj.hasOwnProperty(key)) {
res[key] = deepCopy(obj[key], map);
}
}
return res;
}