JS 深拷贝与浅拷贝
写在前面:
在了解深浅拷贝之前,我们先来了解一下堆栈。
堆栈是一种数据结构,在JS中
- 栈:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
读写快,但是存储的内容较少
- 堆:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收(垃圾回收机制)
读写慢,但是可以存储较多的内容
(!!注意:若堆中已动态分配的内存,在使用完之后由于某种原因没有被释放或者无法释放,就会造成系统内存的浪费,导致程序运行速度降低甚至崩溃,这种情况称为内存泄漏!!)
- 基本数据类型(存储在栈中):string,number,boolean,undefined,null,symbol,以及复杂类型的指针
- 复杂数据类型(存储在堆中):object,array,function 等
栈内存和堆内存
当我们创建变量并赋值的时候,如果是基本数据类型会直接存储在栈中,
而复杂数据类型会存储在堆中, 当我们将复杂数据类型赋值给某个变量时,只是将该数据在堆中的地址,赋值给了这个变量(指针)。这个变量(指针)存储了一个指向堆中数据的地址,一般称之为指针
浅拷贝
堆栈中数据的拷贝,如果是基本数据类型,那么拷贝的就是数据;
1 var a = 10;
2 var b = a;
3 console.log('改变前 打印变量 a b');
4 console.log(a); // 10
5 console.log(b); // 10
6
7 var c = 10;
8 var d = c; // 拷贝的是数据
9 d = 20; // 修改拷贝之后的数据
10 console.log("打印变量 c d");
11 console.log(c); // 10
12 console.log(d); // 20
如果拷贝的书复杂数据类型,以对象为例,当我们想通过简单的赋值方式拷贝一个对对象时,
例如:
1 //复杂数据类型的拷贝
2 var obj1 = { a: 10 };
3 var obj2 = obj;
4 console.log(obj1); // { a: 10 }
5 console.log(obj2); // { a: 10 }
6 //到这里 我们可以看到obj1和obj2 的打印结果完全相同 也许我们完成了数据的拷贝,
7 //但是当我们修改拷贝过来的对象的数据时就会出现一个问题
8 obj2.a = 20; // 修改拷贝之后的数据
9 console.log(obj1); // { a: 20 }
10 console.log(obj2); // { a: 20 }
11 //此处我们将obj2的a 修改为了20 但是当我们打印这个对象时 , 发现 obj1 中的 a 也被改变了
12 // 思考: 为什么会发生这种情况??
分析: 文章开头关于堆栈的描述中,有提到过当我们新建对象并赋值给一个变量(var obj1 = { a: 10 })的时候,该变量存储的不是对象的数据,而是该对象在堆中的地址。因此当我们通过这种简单的方式(obj2 = obj;)拷贝复杂数据类型时,只是拷贝了指针中的地址而已,当你通过原引用修改了对象中的数据,另一个也会感知到这个对象的变化。这种行为被称为浅拷贝
复杂数据类型通过普通方式(obj1=obj2)拷贝的是指针,两个指针引用地址相同,读取操作的都是同一个数据。
一般情况下,等号赋值,函数传参,都是浅拷贝,也就是只拷贝了数据的地址。
1 let foo = {title: "hello obj"}
2
3 // 等号赋值
4 let now = foo;
5 now.title = "this is new title";
6
7 console.log(foo.title); // this is new title
8 console.log(now.title); // this is new title
9
10 // 函数传参
11 function change(o) {
12 o.title = "this is function change title";
13 }
14 change(foo);
15 console.log(foo.title); // this is function change title
如何实现深拷贝?
所谓对象的拷贝,其实就是基于复杂数据在拷贝时的异常处理,我们将复杂数据的默认拷贝定义为浅拷贝;就是只拷贝复杂数据的地址,而不拷贝值。那么与之对应的就是不拷贝地址,只拷贝值,也被称为深拷贝。
1.函数递归方式
1 //代码分析: 形参obj 代表被拷贝目标, 调用函数 传入拷贝目标,
2 // 通过Array.isArray(obj)判断obj的类型是否为数组
3 // 通过 for in 遍历拷贝目标,
4 // 使用 typeof 判断其每一个元素或者属性, 是否为obj类型(typeof Array/Object 返回值皆为object)
5 // 若该属性/元素, 部位null 并且 typeof返回值为object, 则代表其为复杂数据类型, 递归调用 deepCopy(obj[key]),继续拷贝其内部
6 // 否则: 代表该元素非 数组 非对象, 为基本数据类型/函数 等 , 直接赋值拷贝即可
7 // 最后返回拷贝完成的result ,函数执行完毕
8 function deepCopy(obj) {
9 var result = Array.isArray(obj) ? [] : {};
10 for (var key in obj) {
11 if (typeof obj[key] === 'object' && obj[key] !== null) {
12 result[key] = deepCopy(obj[key]); //递归复制
13 } else {
14 result[key] = obj[key];
15 }
16 }
17 return result;
18 }
2.利用JS中对JSON的解析方法
什么是JSON?
JSON( JavaScript Object Notation) 是一种轻量级的存储和传输数据的格式。经常在数据从服务器发送到网页时使用。
JavaScript JSON方法
JSON.stringify(value) 方法用于将 JavaScript 值转换为 JSON 字符串,并返回该字符串。
JSON.parse(value) 用于将一个 JSON 字符串转换为对象 并返回该对象。
1 let obj = {
2 title: {
3 sTitle: 0,
4 list: [1, 2, { a: 3, b: 4 }]
5 }
6 }
7 let obj2 = JSON.parse(JSON.stringify(obj));
8 //通过JSON的方式对数据进行处理转换时, 不是改变原数据, 而是在内存中开辟一个新空间来存储转换的数据,
9 //这样两次转换后, 返回的数据 ,与原数据内容相同但是存储地址不同, 不存在引用关系
10 console.log(obj,obj2);
11 // 深拷贝成功
12 console.log(foo === now); // false
缺陷 : 受json数据的限制,无法拷贝函数,undefined,NaN属性
1 let obj={
2 a:10,
3 b:[1,2,3,{c:10}],
4 d:undefined,
5 e(){
6 console.log(this.a);
7 },
8 f:NaN
9 }
10 let obj2 = JSON.parse(JSON.stringify(obj));
11 console.log(obj,obj2);
3.利用ES6 提供的 Object.assign()
只能可以拷贝一层数据,无法拷贝多层数据,内层依然为浅拷贝
1 let foo = {
2 title:{
3 show:function(){},
4 num:NaN,
5 empty:undefined
6 }
7 }
8
9 let now = {};
10
11 Object.assign(now, foo);
12
13 console.log(foo); // {title: {{num: NaN, empty: undefined, show: ƒ}}}
14 console.log(now); // {title: {{num: NaN, empty: undefined, show: ƒ}}}
15
16 // 外层对象深拷贝成功
17 console.log(foo === now); // false
18 // 内层对象依然是浅拷贝
19 console.log(foo.title === now.title); // true
4. 利用ES6 提供的展开运算符:...
1 let foo = { 2 title:{ 3 show:function(){}, 4 num:NaN, 5 empty:undefined 6 } 7 } 8 9 let now = {...foo}; 10 11 console.log(foo); // {title: {{num: NaN, empty: undefined, show: ƒ}}} 12 console.log(now); // {title: {{num: NaN, empty: undefined, show: ƒ}}} 13 14 // 外层对象深拷贝成功 15 console.log(foo === now); // false 16 // 内层对象依然是浅拷贝 17 console.log(foo.title === now.title); // true
5.使用函数库lodash中的cloneDeep()方法
使用方法:
1.下载模块
1 cnpm i lodash --save 2 yarn add lodash
2.引入模块
1 import _ from 'lodash'
3.使用
1 let obj1 = loodash.cloneDeep(obj)
6. 使用immutable-js
其他(参考用, 仍可完善)
1 function cloneObj(source, target) { 2 // 如果目标对象不存在,根据源对象的类型创建一个目标对象 3 if (!target) target = new source.constructor(); 4 // 获取源对象的所有属性名,包括可枚举和不可枚举 5 var names = Object.getOwnPropertyNames(source); 6 // 遍历所有属性名 7 for (var i = 0; i < names.length; i++) { 8 // 根据属性名获取对象该属性的描述对象,描述对象中有configurable,enumerable,writable,value 9 var desc = Object.getOwnPropertyDescriptor(source, names[i]); 10 // 表述对象的value就是这个属性的值 11 // 判断属性值是否不是对象类型或者是null类型 12 if (typeof desc.value !== "object" || desc.value === null) { 13 // 定义目标对象的属性名是names[i],值是上面获取该属性名的描述对象 14 // 这样可以将原属性的特征也复制了,比如原属性是不可枚举,不可修改,这里都会定义一样 15 Object.defineProperty(target, names[i], desc); 16 } else { 17 // 新建一个t对象 18 var t = {}; 19 // desc.value 就是源对象该属性的值 20 // 判断这个值是什么类型,根据类型创建新对象 21 switch (desc.value.constructor) { 22 // 如果这个类型是数组,创建一个空数组 23 case Array: 24 t = []; 25 break; 26 // 如果这个类型是正则表达式,则将原值中正则表达式的source和flags设置进来 27 // 这两个属性分别对应正则desc.value.source 正则内容,desc.value.flags对应修饰符 28 case RegExp: 29 t = new RegExp(desc.value.source, desc.value.flags); 30 break; 31 // 如果是日期类型,创建日期类型,并且把日期值设置相同 32 case Date: 33 t = new Date(desc.value); 34 break; 35 default: 36 // 如果这个值是属于HTML标签,根据这个值的nodeName创建该元素 37 if (desc.value instanceof HTMLElement) 38 t = document.createElement(desc.value.nodeName); 39 break; 40 } 41 // 将目标元素,设置属性名是names[i],设置value是当前创建的这个对象 42 Object.defineProperty(target, names[i], { 43 enumerable: desc.enumerable, 44 writable: desc.writable, 45 configurable: desc.configurable, 46 value: t 47 }); 48 // 递归调用该方法将当前对象的值作为源对象,将刚才创建的t作为目标对象 49 cloneObj(desc.value, t); 50 } 51 } 52 return target; 53 }