浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
我们用一张图理解一下
总而言之,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
在比较的都是在引用类型的前提下
赋值:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
//对象赋值
let obj1 = {
person: {
name: "冰冰",
age: 20
},
sports:'run'
};
let obj2=obj1;//赋值
obj2.person.name='bb';
obj2.sports='baseball';
console.log(obj2);//{person:{name:'bb',age:20},sport:'baseball'}
console.log(obj1);//{person:{name:'bb',age:20},sport:'baseball'}
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
//浅拷贝
let obj1 = {
person: {
name: "冰冰",
age: 20
},
sports:'run'
};
let obj3=lightClone(obj1);
obj3.person.name='bb';
obj3.sports='baseball';
console.log(obj3);//{person:{name:'bb',age:20},sport:'baseball'}
console.log(obj1);//{person:{name:'bb',age:20},sport:'run'}
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
//深拷贝
let obj1 = {
person: {
name: "冰冰",
age: 20
},
sports:'run'
};
let obj4=deepClone(obj1);
obj4.person.name='bb';
obj4.sports='baseball';
console.log(obj4);//{person:{name:'bb',age:20},sport:'baseball'}
console.log(obj1);//{person:{name:'冰冰',age:20},sport:'run'}
obj1是原始对象,obj2是赋值操作得到的对象,obj3浅拷贝得到的对象,obj4深拷贝得到的对象
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
注意
如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
如果该参数不是对象,则会先转成对象,然后返回。但是由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。而且Object.assign()拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。属性名为 Symbol 值的属性,也会被Object.assign()拷贝。
更详细的可以看对象的新增方法 - ECMAScript 6入门
let obj1 = {
person: {
name: "冰冰",
age: 20
},
sports:'run'
};
let obj2 = Object.assign({}, obj1);
console.log(obj1);
obj2.person.name = "chen";
obj2.sports = 'football'
console.log(obj1);
// { person: { name: "chen" , age: 20 } , sports:'run'};
用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
函数库lodash也有提供_.clone用来做 Shallow Copy。
第一步:
npm i lodash
第二步:
var _=require('lodash');
let obj1 = {
person: {
name: "冰冰",
age: 20
},
sports:'run'
};
let obj2=_.clone(obj1);
console.log(obj2.person==obj1.person);
//true
展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。
let obj1 = {
person: {
name: "冰冰",
age: 20
},
sports:'run'
};
let obj2={...obj1};
console.log(obj2.person==obj1.person);
//true
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[0]=6;
arr2[2].username = 'wade';
console.log(arr);
//[ 1, 3, { username: 'wade' } ]
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.slice();
arr2[0]=6;
arr2[2].username = 'wade';
console.log(arr);
//[ 1, 3, { username: 'wade' } ]
利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
let obj = {
person:{
name:'bb',
age:20
},
sports:'run'
};
let obj2 = JSON.parse(JSON.stringify(obj));
obj2.person.name = '冰冰';
console.log(obj,obj2)
但是这个方法有坑!
当对象中有时间类型的元素时候,时间类型会被变成字符串类型数据
const obj = {
date:new Date()
}
console.log(typeof obj.date)//object
const objCopy = JSON.parse(JSON.stringify(obj));
console.log(typeof objCopy.date); //string
到最后,getTime()调不了了,getYearFull()也调不了了,所有时间类型的内置方法都调不动了。但,string类型的内置方法全能调了。
当对象中有undefined类型或function类型的数据时, undefined和function会直接丢失
const obj = {
undef: undefined,
fun: () => { console.log('复制我就消失!') }
}
console.log(obj,"obj");
const objCopy = JSON.parse(JSON.stringify(obj));
console.log(objCopy,"objCopy")
当对象中有NaN、Infinity和-Infinity这三种值的时候 ,会变成null
const obj = {
nan:NaN,
infinityMax:1.7976931348623157E+10308,//浮点数最大值
infinityMin:-1.7976931348623157E+10308,//浮点数最小值
}
console.log(obj, "obj");
const objCopy = JSON.parse(JSON.stringify(obj));
console.log(objCopy,"objCopy")
当对象循环引用的时候会报错
const obj = {
objChild:null
}
obj.objChild = obj;
const objCopy = JSON.parse(JSON.stringify(obj));
console.log(objCopy,"objCopy")
symbol 值在序列化过程中会被忽略
其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
// 对象序列化,undefined和函数丢失问题
const JSONStringify = (option) => {
return JSON.stringify(option,
(key, val) => {
// 处理函数丢失问题
if (typeof val === 'function') {
return `${val}`;// 转化为字符串
}
// 处理undefined丢失问题
if (typeof val === 'undefined') {
return 'undefined';
}
return val;
},
)
}
// 对象序列化解析
const JSONParse = (objStr) => {
return JSON.parse(objStr, (k, v) => {
if (typeof v === 'string' && v.indexOf && v.indexOf('function') > -1) {
// eval 可能在eslint中报错,需要加入下行注释
// eslint-disable-next-line
return eval(`(function(){return ${v}})()`);
// 立即执行函数 返回原有的 eval('字符串') 字符串是要执行的语句
}
return v;
});
}
不过如果只是为了一个深复制功能引入整个第三方库就显得很不值得了,可以考虑一下按需加载,用哪个模块加载哪个模块。
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
jquery 有提供一個$.extend可以用来做 Deep Copy
$.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
不过jQuery的感觉现在不常用了,这个方法仅适用于JQuery构建的项目。
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
具体思路:
创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。
如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,如果有更深层次的对象可以继续递归直到属性为原始类型。
如果是原始类型,无需继续拷贝,直接返回
如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。
//深拷贝函数
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
//测试
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
let objClone=clone(target);
console.log(objClone)
有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,就会发生栈溢出。
如果你在上面代码中加入target.target=target;,也就是
为了解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储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;
}
};
你会发现,target变成一个循环应用的类型。整个对象也可以被完美的深拷贝下来。
其实还有一个优化点,上面提到需要可以存储key-value形式的数据,且key可以是一个引用类型,所以我们用来Map。
但是,设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。
Set 和 Map 数据结构 - ECMAScript 6入门
我们知道,WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
弱引用:在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
举个例子
使用Map,对象间是存在强引用关系,虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放。
let obj = { name : 'bb'}
const target = new Map();
target.set(obj,'冰冰');
obj = null;
如果是WeakMap的话,target和obj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
let obj = { name : 'bb'}
const target = new Map();
target.set(obj,'冰冰');
obj = null;
我们都知道,循环的方式有很多种,for in 、forEach、for、while等等,但是他们的执行效率并不同。采用相同代码块进行测试发现,while的效率是最好的,所以,我们可以想办法把for in遍历改变为while遍历。
我们先使用while来实现一个通用的forEach遍历,iteratee是遍历的回调函数,他可以接收每次遍历的value和index两个参数:
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
现在我们把forEach用到clone函数中,当遍历数组时,直接使用forEach进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把forEach会调函数的value当作key使用:
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone2(target[key], map);
});
return cloneTarget;
} else {
return target;
}
}
我们可以用console.time();和console.timeEnd();配合算出clone函数的执行时间,最后会发现每次都是优化后的比较快。
在上面的代码中,我们其实只考虑了普通的object和array两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型。
首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
if (!isObject(target)) {
return target;
}
//...
获取数据类型,我们可以使用toString来获取准确的引用类型,前提是此方法在自定义对象中未被覆盖,toString才会达到预想的效果。事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。
function getType(target) {
return Object.prototype.toString.call(target);
}
在上面的结果中,我们可以分为
可以继续遍历的类型
不可以继续遍历的类型
我们先考虑object、array、Map,Set
有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的[]和{},我们可以通过拿到constructor的方式来通用的获取。
例如:const target = {}就是const target = new Object()的语法糖。另外这种方法还有一个好处:因为我们还使用了原对象的构造方法,所以它可以保留对象原型上的数据,如果直接使用普通的{},那么原型必然是丢失了的。
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
我们改写clone函数,对可继续遍历的数据类型进行处理:
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
}
// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value,map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value,map));
});
return cloneTarget;
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次进行处理:
Bool、Number、String、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
default:
return null;
}
}
//克隆Symbol
function cloneSymbol(target) {
return Object(Symbol.prototype.valueOf.call(target));
}
//克隆正则:
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(target));
result.lastIndex = target.lastIndex;
return result;
}
//可继续遍历的数据类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag=['object Arguments'];
//不可继续遍历的数据类型
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const funcTag='[object Function]';
const deepTag=[mapTag,setTag,arrayTag,objectTag,argsTag];
// 通用while循环
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
// 判断是否为引用类型
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
// 获取实际类型
function getType(target) {
return Object.prototype.toString.call(target);
}
// 初始化被克隆的对象
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
// 克隆Symbol
function cloneSymbol(target) {
return Object(Symbol.prototype.valueOf.call(target));
}
// 克隆正则
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new target.constructor(target.source, reFlags.exec(target));
result.lastIndex = target.lastIndex;
return result;
}
// 克隆函数
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
// 克隆不可遍历类型
function cloneOtherType(target, type) {
const Ctor = target.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return cloneReg(target);
case symbolTag:
return cloneSymbol(target);
case funcTag:
return cloneFunction(target);
default:
return null;
}
}
function clone(target,map=new WeakMap()){
// 原始类型直接返回
if(!isObject(target)){
return target;
}
// 根据不同类型进行操作
const type=getType(target);
let cloneTarget;
if(deepTag.includes(type)){
cloneTarget=getInit(target,type);
}else{
return cloneOtherType(target,type);
}
// 处理循环引用
if(map.get(target)){
return target;
}
map.get(target,cloneTarget);
// 处理set
if(type===setTag){
target.forEach(value=>{
cloneTarget.add(clone(value));
});
return cloneTarget;
}
// 处理map
if(type==mapTag){
target.forEach((value,key)=>{
cloneTarget.set(key,clone(value))
});
return cloneTarget;
}
// 处理对象和数组
const keys=type===arrayTag?undefined:Object.keys(target);
forEach(keys||target,(value,key)=>{
if(keys){
key=value;
}
cloneTarget[key]=clone(target[key],map);
})
return cloneTarget;
}
const map = new Map();
map.set('key', 'value');
map.set('bb', '冰冰真棒');
const set = new Set();
set.add('冰冰很爱吃');
set.add('冰冰啥都吃');
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
empty: null,
map,
set,
bool: new Boolean(true),
num: new Number(2),
str: new String(2),
symbol: Object(Symbol(1)),
date: new Date(),
reg: /\d+/,
error: new Error(),
func1: () => {
console.log('冰冰很自恋');
},
func2: function (a, b) {
return a + b;
}
};
let obj2=clone(target);
console.log(obj2)
如何写出一个惊艳面试官的深拷贝?
深拷贝的终极探索(99%的人都不知道)
对象的新增方法 - ECMAScript 6入门
JSON.stringify()还能这么玩