JS浅拷贝和深拷贝

353 阅读6分钟

概念

深浅拷贝的概念主要是针对引用类型值。

在内存中,引用数据的名字存储在栈内存(值为引用类型值的地址),引用类型的真正值存储在堆内存中。所以栈内存中会提供一个引用类型的地址指向堆内存中的值。

还有一种理解方式:浅拷贝是拷贝一层,深层次的对象级别的就拷贝引用(for-in循环对象属性还是对象);深拷贝是拷贝多层,每一级别的数据都会拷贝出来。

浅拷贝

浅拷贝:b复制了a的引用地址,而不是复制了a的值,所以是a和b共同指向了a堆内存的值。

深拷贝:b不仅复制得到了a的内容,而且b在堆内存中有专门的内存为其存放内容。

实现浅拷贝:

1. for-in循环一层,对象属性还是对象

function simplyCopy(obj1){

    var obj2 = Array.isArray(obj1) ? []:{};

    for(let i in obj1){

        obj2[i] = obj1[i]

    }

    return obj2;

}

var obj1 = {

    a : 1,

    b : 2,

    c : {

        d : 3

    }

}

var obj2 = simplyCopy(obj1);

obj2.a = 2;

obj2.c.d = 4;

console.log(obj1.a);

console.log(obj2.a);

console.log(obj1.c.d);

console.log(obj2.c.d);

输出:

其实现过程可以视为:

所以更改了obj2.c中的d,obj1.c.d也会改变;但是更改了属性a却没有改变。

而如果对象中全是基本类型值,或者嵌套多次,循环多层则其实可以算是深拷贝。

function simplyCopy(obj1){

    var obj2 = Array.isArray(obj1) ? []:{};

    for(let i in obj1){

        obj2[i] = obj1[i]

    }

    return obj2;

}

var obj1 = {

    a : 1,

    b : 2

}

var obj2 = simplyCopy(obj1);

console.log(obj2 == obj1);

输出为:false

所以这个结果和我们说的另外一种方式更加贴合。

这里增加以下for-in和for-of的区别

1. for-in主要用于对象属性的遍历,在对象中只能用for-in而不能用for-of,会报错提示对象是not iterable的;

2. 而在数组中,用for-in循环可以读取键名即key值,而for-of获取键值即value。

3. 在set和map结构中,一般采用of来遍历循环得到值;

4. 在其它类似数组的对象中,如字符串,dom NodeList对象和arguments对象中使用for-of

for-in的缺点:

1.index索引为字符串型数字(注意,非数字),不能直接进行几何运算。

2.遍历顺序有可能不是按照实际数组的内部顺序(可能按照随机顺序)。

3.使用for-in会遍历数组所有的可枚举属性,包括原型。

为什么for-of不能遍历对象?

因为for of遍历依靠的是迭代器Iterator。迭代器用于提供一种不依赖于索引取值的方式。而for-of不能遍历对象是因为对象中没有Symbol.iterator方法。而数组Arrays 2)字符串Strings 3)Map4)Set5)arguments6)Typed Arrays7)Generators,早就内置好了Iterator(迭代器),所以它们可以被for of遍历。

2. Object.assign方法

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget =Object.assign(target, source);
console.log(target);
// expected output: Object { a: 1, b: 4, c:
5 }

console.log(returnedTarget);

// expected output: Object { a: 1, b: 4, c: 5 }

Object.assign()拷贝的是(可枚举)属性值。假如源值是一个对象的引用,它仅仅会复制其引用值。

var obj = {

a: 1,

b: 2

}

var obj1 = Object.assign(obj);

obj1.a = 3;

console.log(obj.a) // 3

其复制过程如图:

但是如果给予其一个空对象用来存储obj,则结果不同。

var obj1 = Object.assign({},obj);

obj1.a = 3;

console.log(obj.a)

则输出:1

和上面不同的在于是否为其在运行之前分配了具有内存的空对象。

而如果obj中还存在引用类型值,如下面代码所示:

var obj = {

a: 1,

b: 2,

c : {

d:3

}

}

var obj1 = Object.assign({},obj);

obj1.a = 3;

obj1.c.d = 4;

console.log(obj,obj1)

输出结果为:

这样就与for-in类似了,即obj的基本类型值会有自己独立的内存和值,但是引用类型值都会指向堆内存。

3. 直接赋值

let a=[0,1,2,3,4],

b=a;

console.log(a===b);

a[0]=1;

console.log(a,b);

输出:true,[1,1,2,3,4], [1,1,2,3,4]

所以我们可以直接得到一个结论,基本类型值拷贝后是可以直接拷贝且有自己的内存的,但是引用类型值拷贝后第一层是拷贝了其引用值,还需要再深层次的拷贝,这也就是浅拷贝的由来。

### 原生方法中的浅拷贝

##### 4. 数组的concat,slice

concat用于连接两个数组,不会修改已有数组,返回新数组。但实际当数组存在嵌套情况时,可以发现从第二层开始还是引用关系,concat和slice都是浅拷贝。
 
##### 5.  **...** 展开运算符

展开运算符是对对象第一层进行了深拷贝,后续都是拷贝引用值;

深拷贝

实现深拷贝:

1. 采用递归去拷贝所有层级属性。

对每一层数据都实现创建对象和对象赋值过程:

  • 判断类型,简单类型直接返回
  • 对于循环引用,拷贝时判断存储空间内是否已经存在该对象,有就直接返回
  • 对引用类型采用递归拷贝直到为原始类型为止

判断其子元素是不是对象,如果是,则继续递归,如果不是则简单拷贝其值

function deepClone(obj){

    let objClone = Array.isArray(obj)?[]:{};

    if(obj && typeof obj==="object"){

        for(key in obj){

            if(obj.hasOwnProperty(key)){

                //判断ojb子元素是否为对象,如果是,递归复制

                if(obj[key]&&typeof obj[key] ==="object"){

                    objClone[key] = deepClone(obj[key]);

                }else{

                    //如果不是,简单复制

                    objClone[key] = obj[key];

                }

            }

        }

    }

    return objClone;

}    

let a=[1,2,3,4],

    b=deepClone(a);

a[0]=2;

console.log(a,b);

2. 通过json对象(undefined,function,symbol会在转换过程中被忽略)

首先通过JSON.stringify将对象或值转换成JSON字符串。再用JSON.parse将JSON字符串转成值或者对象。

console.log(JSON.stringify({ a: 1, b: 2,c:{ d:3} }));

const json = '{"a":1,"b":2,"c":{"d":3}}';

const obj = JSON.parse(json);

console.log(obj);

//{a: 1, b: 2, c: {d: 3} }

缺点:无法实现对象中undefined,function,symbol的深拷贝,会显示为undefined

3. 通过JQuery的extend

$.extend()方法:将一个或多个对象的内容合并到目标对象。

$.extend( [deep ], target, object1 [, objectN ] )

deep:可选,表示是否深度合并对象。

target:Object类型目标对象。

object:合并的对象。

var newArray = $.extend(true,[],array);

4. 通过Object.create()

function deepClone(initalObj, finalObj) {    

  var obj = finalObj || {};    

  for (var i in initalObj) {        

      var prop = initalObj[i];        

     // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况

     if(prop === obj) {            

       continue;

    }        

    if (typeof prop === 'object') {

      obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);

    } else {

      obj[i] = prop;

    }

  }    

  return obj;

}

检查每一个initalObj的属性,即prop,如果其不是引用类型则直接将值赋值给obj,如果是,先检查其构造函数是否为Array,如果是则将传入空数组,如果不是则调用Object.create方法。

5. 手动复制

自己将另外一个对象中的值赋值给对应的新对象列中。

var obj1 = { a: 10, b: 20, c: 30 };

var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };

自己手写一个深拷贝(用递归的方式)

还应该考虑undefined,function,symbol这三种情况。

//递归实现深拷贝

function deepClone(obj){

    if (obj instanceof Object != true || obj == null){

        var newObj = obj;

    }else{

        var newObj = Array.isArray(obj)?[]:{}

        for(key in obj){

            if(obj.hasOwnProperty(key)){

                     //这里一定要把function类型,Date类型和正则提出来

                if( obj[key] instanceof Function || obj[key] instanceof Date|| obj[key] instanceof RegExp){

                    newObj[key] = obj[key];

                }else if(typeof obj[key] == 'object'){

                    newObj[key] = deepClone(obj[key]);

                }else{

                    newObj[key] = obj[key];

                }

            }

        }

    }

    return newObj;

}

let a = 'abc';

let b = [1,2,3,4];

let c = {

    a : 1,

    b : 2,

    c : {

        d:3

    },

    e:function(){

 

    },

    f:undefined,

    g:Symbol

}

 

console.log(deepClone(a),deepClone(b),deepClone(c));

var test1 = deepClone(a);

var test2 = deepClone(b);

var test3 = deepClone(c);

c.a = 2;

c.c.d = 4;

console.log(a,b,c);

console.log(test1,test2,test3)

输出: