1、数据类型
数据类型分为两种, 基本类型 和 引用类型
基本类型: String, Number, Boolean, Null, Undefined,Symbol
引用类型: Object,Array,Date,Function,regexp…
1.1 区别
基本类型是按值访问的,不会影响到其他数据,例如:
var a = '前端'
var b = a
a = '前端工程师'
b // 前端
引用类型的值是按地址访问的,简单的赋值,实际上只是把地址复制了一遍,修改任意一个值会影响到另外一个,例如:
var a = [1,2,3,4]
var b = a
a[1] = '已修改'
b // [1, "已修改", 3, 4]
2、什么是深/浅拷贝?
-
深拷贝和浅拷贝是只针对引用数据类型的。
-
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
-
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
2.1 为什么要使用深拷贝?
我们希望在改变新的数组(对象)的时候,不改变原数组(对象)
3、浅拷贝的实现方式
3.1 Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
注意:当object只有一层的时候,是深拷贝
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
3.2 展开运算符…
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。
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 } }
3.3 Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
3.4 Array.prototype.slice()
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
4、深拷贝的实现方式
4.1 JSON.parse(JSON.stringify())
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
这种实现深拷贝的方法有局限性,它只适用于一般数据的拷贝(对象、数组),有以下情况需要注意:
let obj = {
age: 18,
hh: NaN,
isInfinite: 1.7976931348623157E+10308,
minusInfinity: -1.7976931348623157E+10308,
date: new Date(),
reg: new RegExp('\\w+'),
err: new Error('error message'),
fn: function () {
console.log('fn');
},
hh: undefined
};
let objCopy = JSON.parse(JSON.stringify(obj));
console.log('obj', obj);
console.log('objCopy', objCopy);
总结:
- 不支持函数
- 不支持undefined(支持null)
- 不支持循环引用,比如 a = {name: ‘a’}; a.self = a; a2 = JSON.parse(JSON.stringify(a))
- 不支持Date,会变成 ISO8601 格式的字符串
- 不支持正则表达式
- 不支持Symbol
以上,如果拷贝的对象不涉及上面的情况,可以使用 JSON.parse(JSON.stringify(obj))
实现深拷贝。
参考资料:关于 JSON.parse(JSON.stringify(obj)) 实现深拷贝的一些坑
4.2 手写递归方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
还有一个终极问题,对象有环怎么办?递归不就永远出不来了? 别慌,用缓存解决。我们把原对象和克隆过的对象都放进缓存列表,如果有环,返回对应的新对象即可。
class DeepClone {
constructor() {
this.cacheList = [];
}
clone(source) {
if (source instanceof Object) {
const cache = this.findCache(source); // 如果找到缓存,直接返回
if (cache) return cache;
else {
let target;
if (target instanceof Array) {
target = new Array();
} else if (target instanceof Function) {
target = function () {
return source.apply(this, arguments);
};
} else if (target instanceof Date) {
target = new Date(source);
} else if (target instanceof RegExp) {
target = new RegExp(source.source, source.flags);
} else {
target = new Object(); // 不要忘记普通对象
}
this.cacheList.push([source, target]); // 把原对象和新对象放进缓存列表
for (let key in source) {
if (source.hasOwnProperty(key)) {
// 不拷贝原型上的属性,浪费内存
target[key] = this.clone(source[key]); // 递归
}
}
return target;
}
} else {
return source;
}
}
findCache(source) {
for (let i = 0; i < this.cacheList.length; ++i) {
if (this.cacheList[i][0] === source) {
return this.cacheList[i][1];
}
}
}
}
总结:
对于递归克隆的深拷贝,核心有三点:
- 对象分类
- 递归
- 用缓存对付环
递归克隆看起来很强大,但是完美无缺吗?其实还是有不小的距离:
- 对象类型支持不够多(Buffer,Map,Set等都不支持)
- 存在递归爆栈的风险
如果要解决这些问题,实现一个”完美“的深拷贝,只能求教上百行代码的 Lodash.cloneDeep() 了 。
参考资料