一、复习导论(数据类型相关)
想掌握JS的深浅拷贝,首先来回顾一下JS的数据类型,JS中数据类型分为基本数据类型和引用数据类型。
基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问。包含Number、String、Boolean、null、undefined 、Symbol、bigInt。
引用类型是存放在堆内存中的对象,变量其实是保存的在栈内存中的一个指针,这个指针指向堆内存中的引用地址。除了上面的 7 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等
由于基本数据类型和引用数据类型存储方式的差异,所以我们在进行复制变量时,基本数据类型复制后会产生两个独立不会互相影响的变量,而引用数据类型复制时,实际上是将这个引用类型在栈内存中的引用地址复制了一份给新的变量,其实就是一个指针。因此当操作结束后,这两个变量实际上指向的是同一个在堆内存中的对象,改变其中任意一个对象,另一个对象也会跟着改变。于是在引用数据类型的复制过程中便出现了深浅拷贝的概念。
二、深浅拷贝的区别
浅拷贝,对于目标对象第一层为基本数据类型的数据,就是直接赋值,即传值;而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即传地址,并没有开辟新的栈,也就是复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变。
深拷贝,则是开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
三、浅拷贝的实现方式
1.对象的浅拷贝
(1)Object.assign()
ES6中新增的方法,用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),详细用法传送门。代码示例:
let obj = { a:1, b:{ m:'2', n:'3' }, c:[1,2,3,4,5,6] } let copyObj = Object.assign({},obj) obj.a = 5 obj.b.m = '222' console.log(copyObj) //{ a: 1, b: { m: '222', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }
上面的代码修改了obj内部a的值和b.m的值,但是在复制出来的对象中,a的值并未改变,m的值改变了。所以Object.assign()复制时遇到基本数据类型时直接复制值,但是遇到引用数据类型仍然复制的是地址,严格来讲属于浅拷贝。
(2)循环遍历
let obj = { a:1, b:{ m:'2', n:'3' }, c:[1,2,3,4,5,6] } let copyObj = {} for(var k in obj){ copyObj[k] = obj[k] } copyObj.a = 5 copyObj.b.m = '333' console.log(obj) //{ a: 1, b: { m: '333', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] } console.log(copyObj) //{ a: 5, b: { m: '333', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }
2.数组的浅拷贝
Array.concat()、Array.slice(0)、 Array.from()、拓展运算符(...)
let arr = [1,2,3,4,[5,6]] let copyArr = arr.concat() //arr.slice(0) 、Array.from()、[...arr] copyArr[0] = '555' copyArr[4][1] = 7 console.log(arr) //[ 1, 2, 3, 4, [ 5, 7 ] ] console.log(copyArr) //[ '555', 2, 3, 4, [ 5, 7 ] ]
四、深拷贝的实现方式
1.JSON.parse()和JSON.stringify()
const obj1 = { x: 1, y: { m: 1 } }; const obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 1}} 原对象未改变 console.log(obj2) //{x: 2, y: {m: 2}}
这种方法使用较为简单,可以满足基本日常的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是有以下几个缺点:
(1)undefined、任意的函数、正则表达式类型以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时);
(2) 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;
(3) 对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)
(4) 如果对象中存在循环引用的情况无法正确处理。
//忽略undefined、symbol 和函数 let obj = { name: 'muyiy', a: undefined, b: Symbol('muyiy'), c: function() {} } console.log(obj); // { // name: "muyiy", // a: undefined, // b: Symbol(muyiy), // c: ƒ () // } let b = JSON.parse(JSON.stringify(obj)); console.log(b);// {name: "muyiy"} //循环引用情况下,会报错 let obj = { a: 1, b: { c: 2, d: 3 } } obj.a = obj.b; obj.b.c = obj.a; let b = JSON.parse(JSON.stringify(obj));// Uncaught TypeError: Converting circular structure to JSON //正则情况下 let obj = { name: "muyiy", a: /'123'/ } console.log(obj); // {name: "muyiy", a: /'123'/} let b = JSON.parse(JSON.stringify(obj)); console.log(b);
2.jQuery.extend()
附上源码解析:
jQuery.extend = jQuery.fn.extend = function() { //给jQuery对象和jQuery原型对象都添加了extend扩展方法 var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; //以上其中的变量:options是一个缓存变量,用来缓存arguments[i],name是用来接收将要被扩展对象的key,src改变之前target对象上每个key对应的value。 //copy传入对象上每个key对应的value,copyIsArray判定copy是否为一个数组,clone深拷贝中用来临时存对象或数组的src。 // 处理深拷贝的情况 if (typeof target === "boolean") { deep = target; target = arguments[1] || {}; //跳过布尔值和目标 i++; } // 控制当target不是object或者function的情况 if (typeof target !== "object" && !jQuery.isFunction(target)) { target = {}; } // 当参数列表长度等于i的时候,扩展jQuery对象自身。 if (length === i) { target = this; --i; } for (; i < length; i++) { if ((options = arguments[i]) != null) { // 扩展基础对象 for (name in options) { src = target[name]; copy = options[name]; // 防止永无止境的循环,这里举个例子, // 如 var a = {name : b}; // var b = {name : a} // var c = $.extend(a, b); // console.log(c); // 如果没有这个判断变成可以无限展开的对象 // 加上这句判断结果是 {name: undefined} if (target === copy) { continue; } if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) { if (copyIsArray) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是数组的话就让clone副本等于src否则等于空数组。 } else { clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是对象的话就让clone副本等于src否则等于空数组。 } // 递归拷贝 target[name] = jQuery.extend(deep, clone, copy); } else if (copy !== undefined) { target[name] = copy; // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性。 } } } } // 返回修改的对象 return target; };
jQuery的extend方法使用基本的递归思路实现了浅拷贝和深拷贝,但是这个方法也无法处理源对象内部循环引用。
3.lodash.cloneDeep()
已经有大佬专门写了lodash的源码解析(传送门)
4.自己实现一个深拷贝
function deepClone(source) { if (!source && typeof source !== 'object') { throw new Error('error arguments', 'deepClone') } const targetObj = source.constructor === Array ? [] : {} Object.keys(source).forEach(keys => { if (source[keys] && typeof source[keys] === 'object') { targetObj[keys] = deepClone(source[keys]) } else { targetObj[keys] = source[keys] } }) return targetObj }
参考文档:https://segmentfault.com/a/1190000015042902
https://github.com/yygmind/blog/issues/29