JS 深拷贝与浅拷贝


写在前面: 

  在了解深浅拷贝之前,我们先来了解一下堆栈

  堆栈是一种数据结构,在JS中

  • 栈:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。

      读写快,但是存储的内容较少

  • 堆:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收(垃圾回收机制)

        读写慢,但是可以存储较多的内容

     (!!注意:若堆中已动态分配的内存,在使用完之后由于某种原因没有被释放或者无法释放,就会造成系统内存的浪费,导致程序运行速度降低甚至崩溃,这种情况称为内存泄漏!!)

  JS数据按照在内存中的存储形式可以分为两种: 
  • 基本数据类型(存储在中):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 }

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM