js中的深拷贝和浅拷贝总结

目录

一、概念

1、浅拷贝

2、深拷贝

二、实现方式

1、浅拷贝的实现方式

1.1 Object.assign(target,source)方法

1.2 Array.prototype.concat()方法

1.3 Array.prototype.slice(start,end)方法

2、深拷贝的实现方式

2.1 JSON.parse(JSON.stringify(obj))

2.2 lodash函数库

3.3 手写递归方法


一、概念

JavaScript中的数据分为基本类型和引用类型,一般基本类型的数据占用内存大小确定,保存于栈中;引用类型的数据由于在创建之初数据占用内存的大小不确定,但是保存数据的地址占用内存大小确定,因此引用类型的数据实体保存于堆中,按需分配,引用类型在堆中保存数据实体的起始地址保存于栈中。当解释器寻找引用类型的值时,会首先检索其在栈中的地址,取得地址后再从堆中获得数据实体。

一般来说,浅拷贝和深拷贝只是相对于引用类型的对象而言。

1、浅拷贝

只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

2、深拷贝

会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

要点:

  • 新旧对象共享内存,修改其中一个则会影响另外一个,则为浅拷贝;
  • 新旧对象不共享内存,修改其中一个不会影响另一个,则为深拷贝。

二、实现方式

在对引用类型的数据进行操作的时候,到底怎样才能进行浅拷贝?怎样才能进行深拷贝呢?下面就分别介绍浅拷贝和深拷贝的实现方式。

1、浅拷贝的实现方式

目前,一个对象进行浅拷贝的方式有三种,分别为Object.assign(target,source)、Array.prototype.concat(source)和Array.prototype.slice(start,end)。

1.1 Object.assign(target,source)方法

Object.assign(target,source) 方法可以把一个或者多个源对象(第一个参数后边的所有参数对象均为源对象)自身的可枚举属性拷贝给目标对象(第一个参数为目标对象),然后返回目标对象。但是 Object.assign(target,source)进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。【对于可枚举属性有不清楚的同学可以看看 遍历一个对象包含的可枚举属性的方法

var obj = {
    "name":"Zs",
    "age":18,
    "grade":3,
    "language":["Chinese",{"name":"English"},"French"],
    "say":function(){console.log("hi~");}
};
Object.defineProperty(obj, 'grade', {  //设置obj对象的grade不可枚举
    enumerable: false
});
var objSon = Object.assign({},obj);
objSon .name = "Ls";
objSon .language[1].name = "American";
console.log(obj,objSon);

结果如下图所示:

特点:

  • 该方法仅对所有源对象的自身可枚举属性进行浅拷贝,对于不可枚举属性不进行操作(如obj的grade属性);
  • 对于源对象自身可枚举属性为基本类型属性时,Object.assign()相当于对该属性进行深拷贝(如obj的name属性);
  • 对于源对象自身可枚举属性为引用类型属性时,Object.assign()相当于对该属性进行浅拷贝(如obj的language属性).

1.2 Array.prototype.concat()方法

concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。concat方法创建一个新的数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(如果参数是数组)或参数本身(如果参数不是数组)。它不会递归到嵌套数组参数中。

案例一

var array1 = ['a', 'b', 'c'];
var array2 = ['d', {"name":"obj"}, 'f',Symbol("id"),function three(){console.log("three~");}];
var array3 = array1.concat(array2);
array1[1] = 'm';
array2[1].name = 'array2NewName';
console.log(array1,array2,array3);

结果如下图所示:

案例二

var obj1 = {
    "name":"Zs",
    "age":18,
    "grade":3,
    "language":["Chinese",{"name":"English"},"French"],
    "say":function(){console.log("hi~");}
};
Object.defineProperty(obj1, 'grade', {  //设置obj对象的grade不可枚举
    enumerable: false
});
var obj2 = {
    "name":"Ls",
    "age":18,
    "grade":3,
    "language":["Chinese",{"name":"English"},"French"],
    "say":function(){console.log("hi~");}
};
var objSon = Array.prototype.concat.call(obj1,obj2);
objSon.name = "Wmz";
obj1.language[1].name = "American";
console.log(obj1,obj2,objSon);

结果如下图所示:

特点

  • concat()方法对均为数组类型的待浅拷贝对象,返回结果的数组元素是按照数组参数顺序将每个数组的元素按其在原数组中打印顺序进行拷贝的(不区分数组元素的类型为Symbol或者function);
  • concat()方法对需要拷贝的对象的每个元素进行拷贝时,若该元素为基础类型,则对该元素进行的是深拷贝(如array1[1]属性);若该元素是引用类型,则对该元素进行的是浅拷贝(如array2[1].name属性);
  • concat()方法对于需要浅拷贝的对象均不是数组的引用类型对象,需要调用Array.prototype.concat.call()方法;
  • concat()方法对于浅拷贝的对象均不是数组类型的引用对象而言,其返回结果是按照参数顺序将每个对象整体均作为一个数组元素,最后将所有数组元素拼接组成新数组,数组长度等于参数个数(不区分每个参数对象中的属性是否为可枚举类型)。

1.3 Array.prototype.slice(start,end)方法

slice(start,end) 方法返回一个新的数组对象,这一对象是一个由 start 和 end 决定的原数组的浅拷贝(包括 start 但不包括 end)。不会改变原数组。

案例一

var array1 = ['d', {"name":"obj"}, 'f',Symbol("id"),function three(){console.log("three~");},2,3];
var array2 = array1.slice(1,array1.length-2);
array1[array1.length] = 'm';
array1[1].name = 'array1NewName';
console.log(array1,array2);

结果如下图所示:

特点

  • slice()不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
  • 如果该元素是个对象引用 (不是实际的对象),slice会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 StringNumber 或者 Boolean 对象),slice会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
  • 如果向两个数组任一中添加了新元素,则另一个不会受到影响。

2、深拷贝的实现方式

目前,一个对象进行深拷贝的方式有三种,分别为JSON.parse(JSON.stringify(obj))、lodash函数库和手写递归方法。

2.1 JSON.parse(JSON.stringify(obj))

JSON.stringify() 方法将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串。

JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。返回值为Object 类型, 对应给定 JSON 文本的对象/值。

该方法的原理是先将引用类型的对象转换为基础类型的字符串,然后将其从字符串再重新转换为对应的引用类型对象,在进行转换为引用对象的时候,会为对象的每个属性重新分配堆内存,即进行了深拷贝。(自己的理解)

var obj = {
    "name":"Zs",
    "age":18,
    [Symbol("id")]:"ddd",
    "grade":3,
    "language":["Chinese",{"name":"English"},"French"],
    "say":function(){console.log("hi~");}
};
Object.defineProperty(obj, 'grade', {  //设置obj对象的grade不可枚举
    enumerable: false
});
var objSon = JSON.parse(JSON.stringify(obj));
obj.name = "Ls";
obj.language[1].name = "American";
console.log(obj,objSon);

结果如下图所示:

特点

  • JSON.stringify(obj)在进行对象属性串行化时,当属性为function、Symbol类型、undefined时会被忽略或被转为null;
  • JSON.stringify(obj)对所有以 symbol 为属性键的属性都会被完全忽略;
  • JSON.stringify(obj)对NaN 和 Infinity 格式的数值及 null 都会被当做 null;
  • JSON.stringify(obj)仅会序列化可枚举的属性。

2.2 lodash函数库

Lodash是一个著名的javascript原生库,不需要引入其他第三方依赖。Lodash使用了一个简单的 _ 符号,就像Jquery的 $ 一样,十分简洁。lodash函数库提供 _.cloneDeep用来做深拷贝。

var lodashUrl = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js";
loadJS(lodashUrl,lodashLoadSuccess);
//加载完js文件的回调函数:用lodash库函数中的cloneDeep方法实现深拷贝对象
function lodashLoadSuccess(){
    var obj = {
        "name":"Zs",
        "age":18,
        [Symbol("id")]:"ddd",
        "grade":3,
        "language":["Chinese",{"name":"English"},"French"],
        "say":function(){console.log("hi~");}
    };
    Object.defineProperty(obj, 'grade', {  //设置obj对象的grade不可枚举
        enumerable: false
    });
    var objSon = _.cloneDeep(obj);
    obj.name = "Ls";
    obj.language[1].name = "American";
    obj..language[2] = "Japanese";
    console.log(obj,objSon );
}
//用js加载js文件的方法,其中url为待加载的js文件的地址,callBack为js文件加载完成时执行的回调函数
function loadJS( url, callback ){
    var script = document.createElement('script'),fn = callback || function(){};
    script.type = 'text/javascript';
    if(script.readyState){//IE
        script.onreadystatechange = function(){
            if( script.readyState == 'loaded' || script.readyState == 'complete' ){
                script.onreadystatechange = null;
                fn();
            }
        };
    }else{ //其他浏览器
        script.onload = function(){
            fn();
        };
    }
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}

显示结果如下图所示:

特点

  • lodash中的cloneDeep()方法可以对所有类型的对象做深拷贝,包括函数对象。
  • 用lodash的cloneDeep()进行深拷贝后的对象拥有独立的内存空间,修改其中一个不会影响到另一个。
  • 用lodash中cloneDeep()进行深拷贝时,仅对可枚举属性进行拷贝。

3.3 手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

案例一

//深拷贝递归函数(只对自身的可枚举类型的属性进行拷贝)
function deepClone(source){
    const targetObj = source.constructor===Array?[]:{};//判断复制的目标是数组还是对象                      
    for(let keys in source){ //遍历目标自身的和继承的可枚举属性
        if(source.hasOwnProperty(keys)){//仅对自身可枚举属性进行拷贝
            if(source[keys] && typeof source[keys]==='object'){//如果值是对象,就递归一下
                targetObj[keys] = source[keys].constructor === Array ? [] : {};
                targetObj[keys] = deepClone(source[keys]);//递归调用
            }else{ //如果不是,就直接赋值
                targetObj[keys] = source[keys];
            }
        }
    }
    return targetObj;
}

var obj = {
    "name":"Zs",
    "age":18,
    [Symbol("id")]:"ddd",//设置Symbol类型键的属性
    "grade":3,
    "language":["Chinese",{"name":"English"},"French"],//数组类型的属性
    "say":function(){console.log("hi~");}//函数类型的属性
};
Object.defineProperty(obj, 'grade', {  //设置obj对象的grade不可枚举
    enumerable: false
});
var objSon= deepClone(obj);
obj.name = "Ls";
obj.language[1].name = "American";
obj.language[2] = "Japanese";
console.log(obj,objSon);

显示结果如下图所示:

由上图可知:该方法可以对数组或者函数类型的属性进行深拷贝,但是会忽略Symbol类型和不可枚举类型的属性。

案例二

//深拷贝递归函数(可对自身的除Symbol类型的所有属性进行拷贝)
function deepClone(source){
    const targetObj = source.constructor===Array?[]:{};//判断复制的目标是数组还是对象
    Reflect.ownKeys(source).forEach(function(keys){//遍历目标自身所有的属性                      
        if(typeof keys != "symbol"){//源对象中非Symbol类型的属性的拷贝
            if(source[keys] && typeof source[keys]==='object'){//如果属性值非空且值是对象,就需要进一步递归处理
                targetObj[keys] = source[keys].constructor === Array ? [] : {};
                targetObj[keys] = deepClone(source[keys]);//递归调用
            }else{//对象该属性值为空或者属性值为基础类型,就直接赋值
                targetObj[keys] = source[keys];
                //将对应的属性的描述对象进行同步
                //避免出现源对象的属性为不可枚举,深拷贝后的属性默认为可枚举属性
                keysDesc = Object.getOwnPropertyDescriptor(source,keys);
                console.log(keys,keysDesc);
                for(key in keysDesc){
                    Object.defineProperty(targetObj, keys, {//设置obj对象的grade不可枚举
                        [key]: keysDesc[key]
                    });
                }
                console.log(Object.getOwnPropertyDescriptor(targetObj,keys));
            }
        }else{//源对象中Symbol类型的属性处理
            console.log("我是Symbol属性!");
            console.log(keys);
        }
    });
    return targetObj;
}

var obj = {
    "name":"Zs",
    "age":18,
    [Symbol("id")]:"ddd",//设置Symbol类型键的属性
    "grade":3,
    "language":["Chinese",{"name":"English"},"French"],//数组类型的属性
    "say":function(){console.log("hi~");}//函数类型的属性
};
Object.defineProperty(obj, 'grade', {  //设置obj对象的grade不可枚举
    enumerable: false
});
var objSon= deepClone(obj);
obj.name = "Ls";
obj.language[1].name = "American";
obj.language[2] = "Japanese";
console.log(obj,objSon);

结果如下图所示:

由上图可知:对于不可枚举类型的属性也可以进行拷贝,但是需要将属性的所有描述对象属性也拷贝给新对象,避免出现虽然新对象与原对象相对应的属性都相同,但是属性描述不同的现象。

参考文献:https://blog.csdn.net/chaopingyao/article/details/105026649

发布了35 篇原创文章 · 获赞 40 · 访问量 911

猜你喜欢

转载自blog.csdn.net/chaopingyao/article/details/105432129