1. 赋值 & 浅拷贝 & 深拷贝
一句话总结:
- 浅拷贝:拷贝的是引用的内存地址,所以把A赋值给B,改B,也会把A改了
- 深拷贝:完全复制出一个新的,完全一样但毫不相关。
(1)赋值
- 基本类型赋值:为新的变量在栈内存中分配一个新值。
- 引用类型赋值:新的变量在栈内存中分配一个值,赋的其实是该对象的在栈中的地址,而不是堆中的数据,这个值仅仅是指向同一个对象的引用,和原对象指向的都是堆内存中的同一个对象。
const obj1 = { name:'estellanini', age:18, tel:'15000000000', arraylist:[1,[3,4],[5,6]], objectlist:{ id:20, address:'beijing' } };
//直接赋值,obj2与obj1完全相同
let obj2 = obj1;
console.log('obj1',obj1);
console.log('obj2',obj2);
// 此时直接更改obj2中的属性值,观察obj1中的值是否变化
// 结果:更改obj2中的属性值,obj1中的值也跟随发生变化
// 原理:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。
// 两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容
let obj2 = obj1;
obj2.name = "jenny";
obj2.arraylist[2]=[8,9,10];
obj2.objectlist.address="tianjin";
console.log('obj1',obj1); //更改后的
console.log('obj2',obj2); //更改后的
(2)浅拷贝
- 一句话概括:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 理解:如果是对象类型,则只拷贝一层,如果对象的属性又是一个对象,那么此时拷贝的就是此属性的引用
- 为什么要存在浅拷贝,不直接全部都是深拷贝呢?
- 性能与内存开销
- 什么情况下会使用浅拷贝呢?
(3)深拷贝
- 一句话概括:深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
- 理解:浅拷贝是只拷贝一层,深拷贝会拷贝所有的属性。深拷贝前后两个对象互不影响
- 常见的深拷贝库方法:lodash-cloneDeep
2. 手写浅拷贝
(1)手写循环
- 在该对象中循环判断hasOwnProperty是否有某属性,若有,则赋值给新对象。
- 如果 object 具有带指定名称的属性,则 hasOwnProperty 方法返回 true,否则返回 false。此方法不会检查对象原型链中的属性;该属性必须是对象本身的一个成员。
- 效果:更改基本类型,新对象与原对象无关,互不影响。更改引用类型,更改的是引用,所以会互相影响,更改一个另一个也会变。
- 注意:这里仅复制第一层属性即可。
const obj1 = {
name:'estellanini',
age:18,
tel:'15000000000',
arraylist:[1,[3,4],[5,6]],
objectlist:{
id:20,
address:'beijing'
}
};
const shallowCopy = (obj) => {
const newObj={};
for(let prop in obj){
//console.log(prop); // 此处输出的是name age tel arraylist objectlist
if(obj.hasOwnProperty(prop)){
newObj[prop]=obj[prop];
}
}
return newObj;
}
const obj3 = shallowCopy(obj1);
obj3.name = "jenny"; // 修改后,obj1的此属性值不变
obj3.arraylist[2]=[8,9,10]; // 修改后,obj1的此属性值改变
obj3.objectlist.address="shanghai"; // 修改后,obj1的此属性值改变
console.log(obj1)
console.log(obj3)
(2)使用Object.assign进行浅拷贝
- Object.assign(target,sources):target代表目标对象,sources代表原对象,返回值是目标对象。
// 使用Object.assign进行浅拷贝,直接更改obj3中的属性值,观察obj1中的变化
// 结果:更改obj3中的属性值,在obj1中,基本类型值没有变化,但引用类型值发生了变化
// 原理:如果是对象类型,则只拷贝一层,如果对象的属性又是一个对象,那么此时拷贝的就是此属性的引用
let obj3 = Object.assign({}, obj1);
obj3.name = "jenny"; //修改后,obj1的此属性值不变
obj3.arraylist[2]=[8,9,10]; //修改后,obj1的此属性值改变
obj3.objectlist.address="tianjin"; //修改后,obj1的此属性值改变
console.log('obj1',obj1);
console.log('obj3',obj3);
(3)数组浅拷贝Array.prototype.concat()
- concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
const arr1 = [1,2,{
'name':'estellanini',
'age':18,
}];
//数组浅拷贝 Array.prototype.concat(),直接更改arr2数组中的某个值,观察arr1中的该值是否变化
//结果:更改arr2中的属性值,在arr1中,基本类型值没有变化,但引用类型值发生了变化
let arr2 = arr1.concat();
arr2[0]=99; //修改后,arr1的此属性值不变
arr2[2].name='lala'; //修改后,arr1的此属性值改变
console.log(arr1);
console.log(arr2);
(4)数组浅拷贝 Array.prototype.slice()
- slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。
// 数组浅拷贝 Array.prototype.slice(),直接更改arr2数组中的某个值,观察arr1中的该值是否变化
// 结果:更改arr3中的属性值,在arr1中,基本类型值没有变化,但引用类型值发生了变化
let arr3 = arr1.slice();
arr3[0]=99; //修改后,arr1的此属性值不变
arr3[2].name='lala'; //修改后,arr1的此属性值改变
console.log(arr1);
console.log(arr3);
(5)使用扩展运算符进行浅拷贝
// 结果:更改arr4中的属性值,在arr1中,基本类型值没有变化,但引用类型值发生了变化
let arr4=[...arr1];
arr4[0]=99; //修改后,arr1的此属性值不变
arr4[2].name='lala'; //修改后,arr1的此属性值改变
console.log(arr1);
console.log(arr4);
(6)函数库lodash
_.clone(value)
3. 手写深拷贝
- 简单版深拷贝实现:
// obj为要拷贝的对象
const deepClone = (obj={}) => {
if(typeof obj !== 'object' || obj == null){
// 排除obj是null、以及非对象数组
return obj;
}
// 初始化返回结果
let result;
if(obj instanceof Array){
result = [];
} else {
result = {};
}
for (let key in obj) {
// 保证key不是原型链的属性
if(obj.hasOwnProperty(key)){
result[key] = deepClone(obj[key]);
}
}
return result;
}
- 下面来具体分析深拷贝有哪些实现方式:这里给一个较为全面的测试数据
// 测试数据:obj1对象包括基本类型以及引用类型
const obj1 = {
'name':'estellanini',
'age':18,
'tel':'15000000000',
'arraylist':[1,[3,4],[5,6]],
'objectlist':{
'id':20,
'address':'beijing'
},
'date': new Date(),
'reg': new RegExp('\w+'),
'err': new Error('error message'),
'map': new Map([
['name', '张三'],
['title', 'Author']
]),
};
(1)JSON.parse(JSON.stringify()) 序列化反序列化
- 利用 JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象;序列化的作用是存储和传输。
- 只适用于一般数据的拷贝(对象、数组)。
- 存在的问题:
-
如果JSON里面有时间对象,则序列化结果:时间对象=>字符串的形式,而不是时间对象
-
如果JSON里有RegExp、Error、Set、Map对象,则序列化的结果将只得到空对象
-
如果JSON里有 function、undefined,则序列化的结果会把 function、undefined 丢失
-
如果JSON里有NaN、Infinity和-Infinity,则序列化的结果会变成 null
-
如果JSON里有对象是由构造函数生成的,则序列化的结果会丢弃对象的 constructor
-
如果对象中存在循环引用的情况也无法实现深拷贝,直接报错 TypeError: Converting circular structure to JSON
// 验证循环检测 var a={}; a.a=a; console.log(DeepCopy(a)) // Uncaught TypeError: Converting circular structure to JSON -
递归爆栈问题
-
引用丢失问题:假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象
// 深拷贝方法之JSON.parse(JSON.stringify(obj)) // 结果:RegExp、Error序列化的结果将只得到空对象 // 时间对象,则序列化结果:时间对象=>字符串的形式,而不是时间对象 // JSON.parse(JSON.stringify(obj))会存在多种问题 // 只适用于一般数据的拷贝(对象、数组) function DeepCopyJSON(obj) { return JSON.parse(JSON.stringify(obj)) }
-
(2)递归实现
-
【初版】
-
存在的问题:
- 判断是否对象的逻辑不够严谨
- 没有对参数做检验
- 递归爆栈问题
- 引用丢失问题
- 循环引用问题
- 没有考虑数组的兼容
function DeepCopy1(obj){ const newObj={}; for(let prop in obj){ if(obj.hasOwnProperty(prop)){ if(typeof obj[prop]==='object'){ newObj[prop]=DeepCopy1(obj[prop]); }else{ newObj[prop]=obj[prop]; } } } return newObj; } -
初版存在问题1“判断是否对象的逻辑不够严谨”的解决方法:Object.prototype.toString.call(x)
- 在JavaScript里使用typeof判断数据类型,只能区分基本类型,即:number、string、undefined、boolean。
- 对于null、array、function、object来说,使用typeof都会统一返回object字符串。
- 要想区分对象、数组、函数、单纯使用typeof是不行的。在JS中,可以通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。
- 分为null、string、boolean、number、undefined、array、function、object、date、math。
function isObject(x) { //强烈注意,后面的Object是大写 return Object.prototype.toString.call(x)==='[object,Object]'; } -
初版存在问题2“没有对参数做检验”的解决方法:
if (!isObject(obj)) { return obj; } -
初版存在问题3“递归爆栈问题”的解决方法:
// 递归方法最大的问题在于爆栈,当数据的层次很深时就会栈溢出 // 下面代码可以生成指定深度和每层广度的代码 function createData(deep, breadth) { var data = {}; var temp = data; for (var i = 0; i < deep; i++) { temp = temp['data'] = {}; for (var j = 0; j < breadth; j++) { temp[j] = j; } } return data; } const test=createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}} createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}} console.log(test)
-
-
【改版1】
- 解决:“判断是否对象的逻辑不够严谨”这个问题
- 但仍存在以上其他所有问题
function isObject(x){
return Object.prototype.toString.call(x)==='[object,Object]'; //强烈注意,后面的Object是大写
}
function DeepCopy2(obj){
const newObj={};
for(let prop in obj){
if(obj.hasOwnProperty(prop)){
if(isObject(obj[prop])){
newObj[prop]=DeepCopy2(obj[prop]);
}else{
newObj[prop]=obj[prop];
}
}
}
return newObj;
}
(3)循环实现
-
【改版2】
-
存在问题:RegExp、Error序列化的结果将只得到空对象
-
时间对象,序列化结果:时间对象=>字符串的形式,而不是时间对象
-
同时,也没有解决循环引用的问题
function DeepCopyLoop(x) { const root = {}; // 栈 const loopList = [{ parent: root, key: undefined, data: x, }]; while(loopList.length) { // 深度优先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素 let res = parent; if (typeof key !== 'undefined') { res = parent[key] = {}; } for(let k in data) { if (data.hasOwnProperty(k)) { if (typeof data[k] === 'object') { // 下一次循环 loopList.push({ parent: res, key: k, data: data[k], }); } else { res[k] = data[k]; } } } } return root; }- 没有解决循环引用的问题:
//验证循环检测 var a={}; a.a=a; console.log(DeepCopy(a)) //Uncaught TypeError: Converting circular structure to JSON
-
-
【改版3】
- 解决以下问题:属性是基本类型、属性是对象、属性是数组、循环引用的情况,比如 obj.prop1 = obj
- 仍存在的问题:一些特殊类型的对象,比如 Date, 正则,Set,Map等没有处理、使用typeof 来判断是否是对象是有问题的,typeof null 的结果也是 'object'
function DeepCopy3(originObj, map = new WeakMap()) { // 判断是否为基本数据类型 if(typeof originObj === 'object') { // 判断是都否为数组 const cloneObj = Array.isArray(originObj) ? [] : {}; // 判断是否为循环引用 if(map.get(originObj)) { return map.get(originObj); } map.set(originObj, cloneObj); for(const prop in originObj) { cloneObj[prop] = DeepCopy3(originObj[prop], map); } return cloneObj; } else { return originObj; } } obj1.obj2 = obj1; const aa = DeepCopy3(obj1); console.log(aa);
-
【改版4】
- 解决版本3的问题:
function DeepCopy4(originObj, map = new WeakMap()) { // 判断是否为基本数据类型 if(isObject_v2(originObj)) { // 判断是否为循环引用 if(map.get(originObj)) { return map.get(originObj); } // 判断是否为几种特殊需要处理的类型 let type = [Date, RegExp, Set, Map, WeakMap, WeakSet]; if(type.includes(originObj.constructor)) { return new originObj.constructor(originObj); } // 其他类型 let allDesc = Object.getOwnPropertyDescriptors(originObj); let cloneObj = Object.create(Object.getPrototypeOf(originObj), allDesc); // Reflect.ownKeys 可以获取到 for(const prop of Reflect.ownKeys(originObj)) { cloneObj[prop] = isObject_v2(originObj[prop]) && typeof originObj[prop] !== 'function' ? DeepCopy4(originObj[prop], map) : originObj[prop]; } return cloneObj; } else { return originObj; } } // 是否为引用类型 function isObject_v2(obj) { return typeof obj === 'object' || typeof obj === 'function' && obj !== null; }
- 解决版本3的问题: