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 }

 

posted @ 2020-09-06 14:55  低调奢华有内涵的ID  阅读(566)  评论(0编辑  收藏  举报