JavaScript 中对象的深拷贝_javascript技巧_脚本之家

在JavaScript中,对对象进行拷贝的场景比较常见。但是简单的复制语句只能对对象进行浅拷贝,即复制的是一份引用,而不是它所引用的对象。而更多的时候,我们希望对对象进行深拷贝,避免原始对象被无意修改。

对象的深拷贝与浅拷贝的区别如下:

0.1. 浅拷贝

拷贝就是把父对象的属性,全部拷贝给子对象

下面这个函数,就是在做拷贝:

var Chinese = {    nation: '中国'}var Doctor = {    career: '医生'}function extendCopy {    var c = {};    for (var i in p) {        c[i] = p[i];    }    c.uber = p;    return c;}

使用的时候,这样写:

var Doctor = extendCopy;Doctor.career = '医生';console.log(Doctor.nation);

但是,这样的拷贝有一个问题.那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能.

现在给Chinese添加一个”出生地”属性,它的值是一个数组

Chinese.birthPlaces = ['北京','上海','香港'];

通过extendCopy()函数,Doctor继承了Chinese.

var Doctor = extendCopy;

然后,我们为Doctor的”出生地”添加一个城市:

Doctor.birthPlace.push;

看一下打印结果:

console.log(Chinese.birthPlaces);  //  ["北京", "上海", "香港", "厦门"]console.log(Doctor.birthPlaces);  //  ["北京", "上海", "香港", "厦门"]

结果是两个的出生地都被改了

所以,extendCopy()只是拷贝了基本类型的数据,我们把这种拷贝叫做’浅拷贝’

对象的深拷贝与浅拷贝的区别如下:

浅拷贝:仅仅复制对象的引用,而不是对象本身;深拷贝:把复制的对象所引用的全部对象都复制一遍。

0.2. 深拷贝

因为浅拷贝有如此弊端,所以我们接下来看一下深拷贝.要实现深拷贝有很多方法,有最简单的JSON.parse()方法,也有常用的递归拷贝方法,和ESS中的Object.create()方法

  • 浅拷贝:仅仅复制对象的引用,而不是对象本身;
  • 深拷贝:把复制的对象所引用的全部对象都复制一遍。

一. 浅拷贝的实现

0.2.1. 使用JSON.parse()方法

要实现深拷贝有很多方法,比如最简单的办法是使用JSON.parse();

深拷贝:

function deepClone(initalObj) {    return JSON.parse(JSON.stringify(initalObj));}

客户端调用:

var obj = {    a: {        a: 'world',        b: 21    }}var cloneObj = deepClone;cloneObj.a = 'changed';console.log(JSON.stringify; // {"a":{"a":"world","b":21}}console.log(JSON.stringify);  // {"a":"changed"}

这种方法简单易用.

但是这种方法也有不少坏处,比如它会抛弃对象的constructor.也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object

这种方法能正确处理的对象只有Number,String,Boolean,Array,扁平对象,即那些能够被json直接表示的数据结构.RegExp对象是无法通过这种方式深拷贝

一. 浅拷贝的实现

浅拷贝的实现方法比较简单,只要使用是简单的复制语句即可。

浅拷贝的实现方法比较简单,只要使用是简单的复制语句即可。

0.2.2. 递归拷贝

所谓’深拷贝’,就是能够实现真正意义上的数组和对象的拷贝.它的实现并不难,只要递归调用’浅拷贝’就行了

function deepCopy {    var c = c || {};    for (var i in p) {        if (typeof p[i] === 'object') {            c[i] = (p[i].constructor === Array) ? [] : {};            arguments.callee(p[i], c[i]);        }else {            c[i] = p[i];        }    }    return c;}

使用方法:

var Doctor = deepCopy;

现在,给父对象加一个属性,值为数组.然后,在子对象上修改这个属性:

var Doctor = deepCopy;Doctor.career = '医生';Doctor.birthPlaces.push('厦门');console.log(Chinese.birthPlaces);  //  ["北京", "上海", "香港"]console.log(Doctor.birthPlaces);  //  ["北京", "上海", "香港", "厦门"]

上述代码确实可以实现深拷贝.但是当遇到两个互相引用的对象,会出现死循环的情况.

为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否相互引用对象,如果是则退出循环

改进版代码如下:

function deepCopy {    var c = c || {};    for (var i in p) {        var prop = p[i];        // 避免相互引用对象导致死循环,如p[i].a = p[i];的情况        if(typeof prop === 'object') {            continue;        }        if (typeof p[i] === 'object') {            c[i] = (p[i].constructor === Array) ? [] : {};            arguments.callee(p[i], c[i]);        }else {            c[i] = p[i];        }    }    return c;}

1.1 方法一:简单的复制语句

/* ================ 浅拷贝 ================ */
function simpleClone(initalObj) {
    var obj = {};
    for ( var i in initalObj) {
        obj[i] = initalObj[i];
    }
    return obj;
}

/* ================ 客户端调用 ================ */
var obj = {
    a: "hello",
    b: {
        a: "world",
        b: 21
    },
    c: ["Bob", "Tom", "Jenny"],
    d: function() {
        alert("hello world");
    }
}
var cloneObj = simpleClone(obj); // 对象拷贝

console.log(cloneObj.b); // {a: "world", b: 21}
console.log(cloneObj.c); // ["Bob", "Tom", "Jenny"]
console.log(cloneObj.d); // function() { alert("hello world"); }

// 修改拷贝后的对象
cloneObj.b.a = "changed";
cloneObj.c = [1, 2, 3];
cloneObj.d = function() { alert("changed"); };

console.log(obj.b); // {a: "changed", b: 21} // // 原对象所引用的对象被修改了

console.log(obj.c); // ["Bob", "Tom", "Jenny"] // 原对象所引用的对象未被修改
console.log(obj.d); // function() { alert("hello world"); } // 原对象所引用的函数未被修改

1.1 方法一:简单的复制语句

0.2.3. 使用Object.create()方法

直接使用var newObj = Object.create;可以达到深拷贝的效果

function deepClone(initalObj, finalObj) {    var obj = finalObj | {};    for (var i in initalObj) {        var prop = initalObj[i];        if (prop === obj) {            continue;        }        if (typeof prop === 'object') {            obj[i] = (prop.constructor === Array) ? [] : Object.create;        } else {            obj[i] = prop;        }    }    return obj;}

1.2 方法二:Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

var obj = { a: {a: "hello", b: 21} };

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

initalObj.a.a = "changed";

console.log(obj.a.a); // "changed"
/* ================ 浅拷贝 ================ */function simpleClone {var obj = {};for  {obj[i] = initalObj[i];}return obj;}

/* ================ 客户端调用 ================ */var obj = {a: "hello",b: {a: "world",b: 21},c: ["Bob", "Tom", "Jenny"],d: function() {alert;}}var cloneObj = simpleClone; // 对象拷贝console.log; // {a: "world", b: 21}console.log; // ["Bob", "Tom", "Jenny"]console.log; // function() { alert; }// 修改拷贝后的对象cloneObj.b.a = "changed";cloneObj.c = [1, 2, 3];cloneObj.d = function; };console.log; // {a: "changed", b: 21} // // 原对象所引用的对象被修改了console.log; // ["Bob", "Tom", "Jenny"] // 原对象所引用的对象未被修改console.log { alert; } // 原对象所引用的函数未被修改

0.2.4. $.extend()

jquery中$.extend()如同

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

参数如下:

  1. deep

  2. 类型:Boolean

  3. 如果为true,合并成为递归,默认值为false

  4. target

  5. 类型:Object

  6. 对象扩展:这将接收新的属性

  7. object1

  8. 类型:Object

  9. 包含额外的属性合并到第一个参数

  10. objectN

  11. 类型:Object

  12. 包含额外的属性合并到第一个参数

当我们提供两个或多个对象给$.extend(),对象的所有属性都添加到目标对象

如果只有一个参数提供给$.extend(),这意味着目标参数被忽略.在这种情况下,jquery对象本身被默认为目标对象.这样,我们可以在jquery的命名空间下添加新的功能.这对于插件开发者希望jquery中添加新函数时是很有用的

请记住,目标对象将被修改,并且将通过$.extend()返回.然而,如果我们想保留原对象,我们可以通过传递一个空对象作为目标对象

var object = $.extend({},object1,object2);

在默认情况下,通过$.extend()合并操作不是递归的;如果第一个对象的属性本身是一个对象或数组,那么它将完全用第二个对象相同的key重写一个属性.这些值不会被合并.可以通过检查下面例子中banana的值,就可以了解这一点.然而,如果将true作为该函数的第一个参数,那么会在对象上进行递归的合并.

警告:不支持第一个参数传递false

实例

  1. 合并两个对象,并修改第一个对象

     // 合并两个对象,并修改第一个对象 var object1 = {     apple: 0,     banana: {         price: 200     },     cherry: 97 }; var object2 = {     banana: {         weight: 52,         price: 100     },     duration: 100 }; // merge object2 into object1,recursively $.extend(object1, object2); // assuming JSON.stringify - not avalable in IE<8 console.log(JSON.stringify; // {"apple":0,"banana":{"weight":52,"price":100},"cherry":97,"duration":100}
    
  2. 采用递归方式合并两个对象,并修改第一个对象

     // 合并两个对象,并修改第一个对象 var object1 = {     apple: 0,     banana: {         weight: 52,         price: 100     },     cherry: 97 }; var object2 = {     banana: {         price: 200     },     duration: 100 }; // merge object2 into object1,recursively $.extend(true, object1, object2); // assuming JSON.stringify - not avalable in IE<8 console.log(JSON.stringify; // {"apple":0,"banana":{"weight":52,"price":200},"cherry":97,"duration":100}
    
  3. 合并defaults 和
    options对象,并且不修改defaults对象.这是常用的插件开发模式

     var defaults = {     validate: false,     limit: 5,     name: 'foo' }; var options = {     validate: true,     name: 'bar0' }; // merge default and options,without modifying defaults var settings = $.extend({}, defaults, options); console.log(JSON.stringify); // {"validate":false,"limit":5,"name":"foo"} console.log(JSON.stringify; // {"validate":true,"name":"bar0"} console.log(JSON.stringify); // {"validate":true,"limit":5,"name":"bar0"}
    

二. 深拷贝的实现

要实现深拷贝有很多办法,有最简单的 JSON.parse() 方法,也有常用的递归拷贝方法,和ES5中的 Object.create() 方法。

1.2 方法二:Object.assign()

0.2.5. javascript判断对象是否相等

在javascript中相等运算包括’==’,’===’.那么如何判断两个对象是否相等?你可能会认为,如果两个对象有相同的属性,以及她们的属性有相同的值,那么这两个对象就相等.那么下面通过一个实例来论证下:

var obj1 = {    name: 'Betty',    sex: 'male'};var obj2 = {    name: 'Betty',    sex: 'male'};var obj3 = obj1;console.log(obj1 == obj3); // trueconsole.log(obj1 === obj3); // trueconsole.log(obj2 == obj3); // falseconsole.log(obj2 === obj3); // false

上例返回true,是因为obj1和obj3的指针指向了内存中的同一地址.和面向对象的语言中值传递和引用传递的概念相似.因为,如果你想判断两个对象是否相等,你必须清楚,你想判断两个对象的属性相同,还是属性对应的值是否相同,还是怎样?

function Person  {    this.name = name;}var p1 = new Person("p1");var p2 = new Person("p2");console.log(p1 == p2); // falsePerson.prototype.sayHi = function () {    // do sayHi here}console.log(p1.sayHi() == p2.sayHi; // trueconsole.log(p1.sayHi() === p2.sayHi; // true

2.1 方法一:使用 JSON.parse() 方法

要实现深拷贝有很多办法,比如最简单的办法是使用 JSON.parse()

/* ================ 深拷贝 ================ */
function deepClone(initalObj) {
    var obj = {};
    try {
        obj = JSON.parse(JSON.stringify(initalObj));
    }
    return obj;
}

/* ================ 客户端调用 ================ */
var obj = {
    a: {
        a: "world",
        b: 21
    }
}
var cloneObj = deepClone(obj);
cloneObj.a.a = "changed";

console.log(obj.a.a); // "world"

这种方法简单易用。

但是这种方法也有不少坏处,譬如它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被
json 直接表示的数据结构。RegExp对象是无法通过这种方式深拷贝。

Object.assign()
方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是
Object.assign()
进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

0.3. 数组的拷贝

2.2 方法二:递归拷贝

代码如下:

/* ================ 深拷贝 ================ */
function deepClone(initalObj, finalObj) {
    var obj = finalObj || {};
    for (var i in initalObj) {
        if (typeof initalObj[i] === 'object') {
            obj[i] = (initalObj[i].constructor === Array) ? [] : {};
            arguments.callee(initalObj[i], obj[i]);
        } else {
            obj[i] = initalObj[i];
        }
    }
    return obj;
}

上述代码确实可以实现深拷贝。但是当遇到两个互相引用的对象,会出现死循环的情况。

为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否相互引用对象,如果是则退出循环。

改进版代码如下:

/* ================ 深拷贝 ================ */
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) ? [] : {};
            arguments.callee(prop, obj[i]);
        } else {
            obj[i] = prop;
        }
    }
    return obj;
}
var obj = { a: {a: "hello", b: 21} };var initalObj = Object.assign;initalObj.a.a = "changed";console.log; // "changed"

0.3.1. js的slice函数

Array对象的slice函数,返回一组数组我的一段

arrayobj.slice(start,end);

参数:

  1. arrayobj:必选项,一个array对象
  2. start:必选项,arrayobj中所指定的部分的开始元素是从零开始计算的下标
  3. end:可选项.arrayobj中所指定的部分的结束元素是从零开始计算的下标

说明:

  1. slice方法返回一个Array对象,其中包含了arrayobj的指定部分
  2. slice方法一致复制到end所指定的元素,但是不包括该元素.如果start为负,将它作为length+start处理,如果end为负,就将它作为length+end处理,此处length为数组的长度.如果省略end,那么slice方法将一直复制到arrayobj的结尾.如果end出现在start之前,不复制任何元素到新数组中

代码如下:

var arr = ['one', 'two', 'three'];var arrtoo = arr.slice(0);arrtoo[1] = 'set Map';console.log("arr:"+arr); // arr:one,two,threeconsole.log("arrtoo:"+arrtoo); // arrtoo:one,set Map,three

2.3 方法三:使用Object.create()方法

直接使用var newObj = Object.create(oldObj),可以达到深拷贝的效果。

/* ================ 深拷贝 ================ */
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;
}

二. 深拷贝的实现

0.3.2. js的concat方法

concat()方法用于连接两个或多个数组,该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本

语法:

arrayObject.concat(arrayX,arrayX,......,arrayX)

说明:返回一个新的数组。该数组是通过把所有 arrayX 参数添加到 arrayObject
中生成的。如果要进行 concat()
操作的参数是数组,那么添加的是数组中的元素,而不是数组。

代码如下:

var arr = ['one', 'two', 'three'];var arrtoo = arr.concat();arrtoo[1] = 'set map';console.log("arr:"+arr); // arr:one,two,threeconsole.log("arrtoo:"+arrtoo); // arrtoo:one,set map,three

三. 参考:jQuery.extend()方法的实现

jQuery.js的jQuery.extend()也实现了对象的深拷贝。下面将官方代码贴出来,以供参考。

官方链接地址:。

jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[ 0 ] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situation
    if ( typeof target === "boolean" ) {
        deep = target;

        // Skip the boolean and the target
        target = arguments[ i ] || {};
        i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
        target = {};
    }

    // Extend jQuery itself if only one argument is passed
    if ( i === length ) {
        target = this;
        i--;
    }

    for ( ; i < length; i++ ) {

        // Only deal with non-null/undefined values
        if ( ( options = arguments[ i ] ) != null ) {

            // Extend the base object
            for ( name in options ) {
                src = target[ name ];
                copy = options[ name ];

                // Prevent never-ending loop
                if ( target === copy ) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                    ( copyIsArray = jQuery.isArray( copy ) ) ) ) {

                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && jQuery.isArray( src ) ? src : [];

                    } else {
                        clone = src && jQuery.isPlainObject( src ) ? src : {};
                    }

                    // Never move original objects, clone them
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // Don't bring in undefined values
                } else if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }
    }

    // Return the modified object
    return target;
};

要实现深拷贝有很多办法,有最简单的 JSON.parse()
方法,也有常用的递归拷贝方法,和ES5中的 Object.create() 方法。

2.1 方法一:使用 JSON.parse() 方法

要实现深拷贝有很多办法,比如最简单的办法是使用 JSON.parse():

/* ================ 深拷贝 ================ */function deepClone {var obj = {};try {obj = JSON.parse(JSON.stringify;}return obj;}

/* ================ 客户端调用 ================ */var obj = {a: {a: "world",b: 21}}var cloneObj = deepClone;cloneObj.a.a = "changed";console.log; // "world"

但是这种方法也有不少坏处,譬如它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

这种方法能正确处理的对象只有 Number, String, Boolean, Array,
扁平对象,即那些能够被 json
直接表示的数据结构。RegExp对象是无法通过这种方式深拷贝。

2.2 方法二:递归拷贝

/* ================ 深拷贝 ================ */function deepClone {var obj = finalObj || {};for  {if (typeof initalObj[i] === 'object') {obj[i] = (initalObj[i].constructor === Array) ? [] : {};arguments.callee;} else {obj[i] = initalObj[i];}}return obj;}

上述代码确实可以实现深拷贝。但是当遇到两个互相引用的对象,会出现死循环的情况。

为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否相互引用对象,如果是则退出循环。

/* ================ 深拷贝 ================ */function deepClone {var obj = finalObj || {};for  {var prop = initalObj[i];// 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况if {continue;}if (typeof prop === 'object') {obj[i] = (prop.constructor === Array) ? [] : {};arguments.callee;} else {obj[i] = prop;}}return obj;}

2.3 方法三:使用Object.create()方法

直接使用var newObj = Object.create,可以达到深拷贝的效果。

/* ================ 深拷贝 ================ */function deepClone {var obj = finalObj || {};for  {var prop = initalObj[i];// 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况if {continue;}if (typeof prop === 'object') {obj[i] = (prop.constructor === Array) ? [] : Object.create;} else {obj[i] = prop;}}return obj;}

三. 参考:jQuery.extend()方法的实现

jQuery.js的jQuery.extend()也实现了对象的深拷贝。下面将官方代码贴出来,以供参考。

官方链接地址:

jQuery.extend = jQuery.fn.extend = function() {var options, name, src, copy, copyIsArray, clone,target = arguments[ 0 ] || {},i = 1,length = arguments.length,deep = false;// Handle a deep copy situationif ( typeof target === "boolean" ) {deep = target;// Skip the boolean and the targettarget = arguments[ i ] || {};i++;}// Handle case when target is a string or something (possible in deep copy)if ( typeof target !== "object" && !jQuery.isFunction {target = {};}// Extend jQuery itself if only one argument is passedif  {target = this;i--;}for  {// Only deal with non-null/undefined valuesif ( ( options = arguments[ i ] ) != null ) {// Extend the base objectfor  {src = target[ name ];copy = options[ name ];// Prevent never-ending loopif  {continue;}// Recurse if we're merging plain objects or arraysif ( deep && copy && ( jQuery.isPlainObject ||( copyIsArray = jQuery.isArray {if  {copyIsArray = false;clone = src && jQuery.isArray ? src : [];} else {clone = src && jQuery.isPlainObject ? src : {};}// Never move original objects, clone themtarget[ name ] = jQuery.extend;// Don't bring in undefined values} else if  {target[ name ] = copy;}}}}// Return the modified objectreturn target;};

这篇文章主要是介绍js关于深拷贝的内容,其它的内容可以查看脚本之家以前发表的文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注