javascript深拷贝、浅拷贝和循环引用深入理解 一、为什么有深拷贝和浅拷贝? 这个要从js中的数据类型说起,js中数据类型分为基本数据类型和引用数据类型。 基本类型值指的是那些保存在栈内存中的简单数据段,即这种值是完全保存在内存中的一个位置。包含Number,String,Boolean,Null,Undefined ,Symbol。 引用类型值指的是那些保存在堆内存中的对象,所以引用类型的值保存的是一个指针,这个指针指向存储在堆中的一个对象。除了上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等。 正因为引用类型的这种机制, 当我们从一个变量向另一个变量复制引用类型的值时,实际上是将这个引用类型在栈内存中的引用地址复制了一份给新的变量,其实就是一个指针。因此当操作结束后,这两个变量实际上指向的是同一个在堆内存中的对象,改变其中任意一个对象,另一个对象也会跟着改变。 因此深拷贝和浅拷贝只发生在引用类型中。简单来说他们的区别在于: 1. 层次 浅拷贝 只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。 深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。 2. 是否开辟新的栈 浅拷贝 对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」,并没有开辟新的栈,也就是复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变, 深拷贝 而深复制则是开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。 二、浅拷贝 以下是实现浅拷贝的几种实现方式: 1.Array.concat() const arr = [1,2,3,4,[5,6]]; const copy = arr.concat(); \\ 利用concat()创建arr的副本 \\改变基本类型值,不会改变原数组 copy[0] = 2; arr; //[1,2,3,4,[5,6]]; \\改变数组中的引用类型值,原数组也会跟着改变 copy[4][1] = 7; arr; //[1,2,3,4,[5,7]]; 能实现类似效果的还有slice()和Array.from()等,大家可以自己尝试一下~ 2.Object.assign() const obj1 = {x: 1, y: 2}; const obj2 = Object.assign({}, obj1); obj2.x = 2; \\修改obj2.x,改变对象中的基本类型值 console.log(obj1) //{x: 1, y: 2} //原对象未改变 console.log(obj2) //{x: 2, y: 2} const obj1 = { x: 1, y: { m: 1 } }; const obj2 = Object.assign({}, obj1); obj2.y.m = 2; \\修改obj2.y.m,改变对象中的引用类型值 console.log(obj1) //{x: 1, y: {m: 2}} 原对象也被改变 console.log(obj2) //{x: 2, y: {m: 2}} 三、深拷贝 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格式能表示的所有数据类型,但是有以下几个缺点: undefined、任意的函数、正则表达式类型以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时); 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object; 如果对象中存在循环引用的情况无法正确处理。 2.递归 function deepCopy1(obj) { // 创建一个新对象 let result = {} let keys = Object.keys(obj), key = null, temp = null; for (let i = 0; i < keys.length; i++) { key = keys[i]; temp = obj[key]; // 如果字段的值也是一个对象则递归操作 if (temp && typeof temp === 'object') { result[key] = deepCopy(temp); } else { // 否则直接赋值给新对象 result[key] = temp; } } return result; } const obj1 = { x: { m: 1 }, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; const obj2 = deepCopy1(obj1); obj2.x.m = 2; console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)} console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)} 四、循环引用 看似递归已经完全解决我们的问题了,然而还有一种情况我们没考虑到,那就是循环引用 1.父级引用 这里的父级引用指的是,当对象的某个属性,正是这个对象本身,此时我们如果进行深拷贝,可能会在子元素->父对象->子元素...这个循环中一直进行,导致栈溢出。比如下面这个例子: const obj1 = { x: 1, y: 2 }; obj1.z = obj1; const obj2 = deepCopy1(obj1); \\栈溢出 解决办法是:只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,可以修改上面的deepCopy函数: function deepCopy2(obj, parent=null) { //创建一个新对象 let result = {}; let keys = Object.keys(obj), key = null, temp = null, _parent = parent; //该字段有父级则需要追溯该字段的父级 while(_parent) { //如果该字段引用了它的父级,则为循环引用 if(_parent.originParent === obj) { //循环引用返回同级的新对象 return _parent.currentParent; } _parent = _parent.parent } for(let i=0,len=keys.length;i