深浅拷贝

Posted chaimi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深浅拷贝相关的知识,希望对你有一定的参考价值。

了解深浅拷贝之前,先了解下基本数据类型和引用数据类型

栈(stack)和堆(heap)

栈为自动分配的内存空间,有系统自由释放;堆为动态分配的内存,大小也不一定会自动释放

数据类型

基本数据类型

Number String bolean Null Undefined Symbol(Es6)

是指放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接按值访问

==简单来说==,值类型就是将一个变量赋值给另一个变量后,两个变量完全独立,改变其中的一个并不会影响另一个

var a = 1;
var b = a; // b = 1
a = 2; // a = 2   b = 1

像上面的例子中,虽然后声明的变量b赋予了a的值,但是改变a的值,b却没有改变

引用数据类型

Object Array Function
(除了基本数据类型以外的都是对象,正则表达式是对象)

存放在堆内存中的对象,变量其实是保存的在栈内存中的一个指针(保存的是堆内存中的引用地址),这个指针指向堆内存。

引用类型数据在栈内存中保存的实际上是==对象在堆内存中的引用地址==。通过这个引用地址可以快速查找到保存中堆内存中的对象
所以一旦内存上的值改变,所有指向这块内存的变量的值都会被改变

var c = [1,2,3];
var d = c; // d = [1,2,3]
c[0] = 0; // c = [0,2,3]   d = [0,2,3]

注意下面这种情况 d 并没有被改变

var c = [1,2,3];
var d = c; // d = [1,2,3]
c = [4,5,6]; // c = [4,5,6]   d = [1,2,3]

其实并没有不对,上面例子中的 c[0] 改变的是原内存地址中存储的值,因为c、d指向相同,所有都被改变;
而后一个例子中==为c重新赋值==,相当于是在内存中重新开辟了一块区域存储新值,改变了 c 原来的指向,但 d 的指向却没有改变,所以我们看到的值也就没变

深浅拷贝

let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。

通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题

浅拷贝

先可以通过 Object.assign 来解决这个问题。

let a = {
    age: 1
}
let b = Object.assign({}, a)//关于Object.assign的用法在文章最后有补充
a.age = 2
console.log(b.age) // 1

当然我们也可以通过展开运算符(…)来解决

let a = {
    age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

深拷贝

let a = {
    age: 1,
    jobs: {
        first: ‘FE‘
    }
}
let b = {...a}
a.jobs.first = ‘native‘
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

深拷贝是真正意义上实现了==数组和对象的拷贝==,它创建了另外一个一模一样的对象,和原对象不是一个内存地址,修改一个值不会影响另一个的值。

首先深复制和浅复制只针对像 Object, Array 这样的复杂对象的。简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

stringify() 方法可以将一个JS对象序列化一个JSON字符串,parse()方法可以将JSON字符串反序列化为一个JS对象。

var a = {age : 1, jobs:{first: ‘fe‘}}; 
let b = JSON.stringify(a);
let c = JSON.parse(b); 
console.log(a, b, c);
//b 为 {"age":1,"jobs":{"first":"fe"}}"
//c 的值也就是a对象

这个过程相当于重新给a 赋值,即在内存重新开辟一个地址,里面存的值是一样的,但地址不一样,所以互不影响

例如这种情况

var c = [1,2,3];
var d = c; // d = [1,2,3]
c[0] = 0; // c = [0,2,3]   d = [0,2,3]

可能有的小伙伴会说,不对呀,下面这种情况 d 并没有被改变

var c = [1,2,3];
var d = c; // d = [1,2,3]
c = [4,5,6]; // c = [4,5,6]   d = [1,2,3]

其实并没有不对,上面例子中的 c[0] 改变的是原内存地址中存储的值,因为c、d指向相同,所有都被改变;而后一个例子中,为 c 重新赋值,相当于是在内存中重新开辟了一块区域存储新值,改变了 c 原来的指向,但 d 的指向却没有改变,所以我们看到的值也就没变

let a = {
    age: 1,
    jobs: {
        first: ‘FE‘
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = ‘native‘
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

会忽略 undefined
会忽略 symbol
不能序列化函数
不能解决循环引用的对象

let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol(‘male‘),
    jobs: function() {},
    name: ‘yck‘
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你会发现在上述情况中,该方法会忽略掉函数,==原因是在序列化JS对象的过程中,所有的函数和原型成员会被有意的忽略==。和 undefined 。

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,==并且该函数是内置函数中处理深拷贝性能最快的==。当然如果你的数据中含有以上三种情况下,可以使用 ==lodash 的深拷贝函数==(这个没有了解)

深浅拷贝封装函数

递归思想

 //封装函数 实现深浅拷贝  deep为true深拷贝 false浅拷贝
   function copy(oldObj,deep){
        let newObj = {};
        if (oldObj instanceof Array){
            newObj = [];
        }
        for (let key in oldObj){
            let value = oldObj[key];
            if (deep && typeof value === "object" && value !== null){ //如果原对象的某个属性是引用类型数据,递归调用copy
                newObj[key] = copy(value,deep);
            } else{            //如果原对象的某个属性是基本类型数据,直接将此属性赋值给新对象的相应属性
                newObj[key] = value;
            }
        }
        return newObj;
    }
let obj1 = {a:1,
            b:{c:2}
};
    
let obj2 = copy(obj1,true);  //深拷贝
let obj3 = copy(obj1,false); //浅拷贝
obj1.a = 2;
obj1.b.c = 3;
console.log(obj2);  //{a:1,b:{c:2}}
console.log(obj3);  //{a:1,b:{c:3}}

补充,对于数组的深拷贝有slice concat for..in..

var arr = ["One","Two",[2,3]];
var arrtoo = arr.slice(0);
或
var arrtoo = arr.concat();
arrtoo[1] = "set Map";
arrtoo[2] = 3;
document.writeln("数组的原始值:" + arr + "<br />");//Export:数组的原始值:One,Two,2,3
document.writeln("数组的新值:" + arrtoo + "<br />");//Export:数组的新值:One,set Map,3

有些知识不太清楚了,补充下:

constructor 属性返回对创建此对象的数组函数的引用

<script type="text/javascript">

var test=new Array();

if (test.constructor==Array)  //返回true]
{
document.write(test.constructor);//返回function Array() { [native code] }
}

hasOwnProperty

判断一个属性是定义在对象本身而不是继承自原型链,我们需要使用从 Object.prototype 继承而来的 hasOwnProperty 方法。
hasOwnProperty 方法是 Javascript 中唯一一个处理对象属性而不会往上遍历原型链的。

// Poisoning Object.prototype
Object.prototype.bar = 1; //原型基础
var foo = {goo: undefined};

foo.bar; // 1
‘bar‘ in foo; // true

foo.hasOwnProperty(‘bar‘); // false
foo.hasOwnProperty(‘goo‘); // true

在这里,只有 hasOwnProperty 能给出正确答案,这在遍历一个对象的属性时是非常必要的。Javascript 中没有其他方法能判断一个属性是定义在对象本身还是继承自原型链。
总结 当判断对象属性存在时,hasOwnProperty 是唯一可以依赖的方法。这里还要提醒下,当我们使用 ==for in loop 来遍历对象时,使用 hasOwnProperty== 将会很好地避免来自原型对象扩展所带来的困扰。

Obejct.assign(target,source1,source2).

方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

注意:

1,该方法只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)

2,该方法是浅拷贝,意思是,如果合并一个引用类型的对象,如果源对象改变,则目标对象属性值也得到相应的改变。

3,同名属性的替换,这点要特别的小心。

4,有个好玩的数组特性可以使用,将源对象一个数组,塞入到另外一个数组的前面并删除同样长度的target数组。
技术分享图片

用途:

1,给一个对象的原型上面添加属性,target:someClass.prototype,source:一个对象,

2,给对象添加属性,这样子new出来的实例对象上面都有x,y的属性了。(constructor指向的就是该对象本身)
技术分享图片




以上是关于深浅拷贝的主要内容,如果未能解决你的问题,请参考以下文章

《关于JavaScript的深浅拷贝》

python--is/id==,集合,深浅拷贝

python深浅拷贝

我要学python之深浅拷贝原理

Python高级语法-深浅拷贝-总结(4.2.1)

Python 的深浅拷贝 终于明白了