深拷贝与浅拷贝的实现(一)

最近的学习中,仔细研究了下深拷贝和浅拷贝,下面就来简单的总结下。

数据类型

首先我们了解下两种数据类型
1、基本类型:像Number、String、Boolean等这种为基本类型
2、复杂类型:Object和Array

浅拷贝与深拷贝的概念

接着我们分别来了解下浅拷贝和深拷贝,深拷贝和浅拷贝是只针对Object和Array这样的复杂类型的。
浅拷贝

var a = {
    myname: 'yana'
};
var b = a;
b.myname = '小雅';
console.log(b.myname);    // 小雅
console.log(a.myname);    // 小雅
var a = ['myname', 'yana'];
var b = a;
b[1] = '小雅';
console.log(a);    // ["myname", "小雅"]
console.log(b);    // ["myname", "小雅"]

可以看出,对于对象或数组类型,当我们将a赋值给b,然后更改b中的属性,a也会随着变化。也就是说a和b指向了同一块内存,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝。

深拷贝
刚刚我们了解了什么是浅拷贝,那么相应的,如果给b放到新的内存中,将a的各个属性都复制到新内存里,就是深拷贝。
也就是说,当b中的属性有变化的时候,a内的属性不会发生变化。

浅拷贝

那么除了上面简单的赋值引用,还有哪些方法使用了浅拷贝呢?
Object.assign()
在MDN上介绍Object.assign():”Object.assign() 方法用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。”
复制一个对象

var target = {a: 1, b: 1};
var copy1 = {a: 2, b: 2, c: {ca: 21, cb: 22, cc: 23}};
var copy2 = {c: {ca: 31, cb: 32, cd: 34}};
var result = Object.assign(target, copy1, copy2);
console.log(target);    // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 33}}
console.log(target === result);    // true

可以看到,Object.assign()拷贝的只是属性值,假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。所以Object.assign()只能用于浅拷贝或是合并对象。这是Object.assign()值得注意的地方。

深拷贝

那么下面我们就来说说复杂的深拷贝
jQuery.extend()
说到深拷贝,第一想到的就是jQuery.extend()方法,下面我们简单看下jQuery.extend()的使用。
jQuery.extend( [deep ], target, object1 [, objectN ] ),其中deep为Boolean类型,如果是true,则进行深拷贝。
我们还是用上面的数据来看下extend()方法。

var target = {a: 1, b: 1};
var copy1 = {a: 2, b: 2, c: {ca: 21, cb: 22, cc: 23}};
var copy2 = {c: {ca: 31, cb: 32, cd: 34}};
var result = $.extend(true, target, copy1, copy2);   // 进行深拷贝
console.log(target);    // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 23, cd: 34}}
 
var target = {a: 1, b: 1};
var copy1 = {a: 2, b: 2, c: {ca: 21, cb: 22, cc: 23}};
var copy2 = {c: {ca: 31, cb: 32, cd: 34}};
var result = $.extend(target, copy1, copy2);   // 不进行深拷贝
console.log(target);    // {a: 1, b: 1, c: {ca: 31, cb: 32, cd:34}}
 

通过上面的对比可以看出,当使用extend()进行深拷贝的时候,对象的所有属性都添加到target中了。
我们知道了extend()可以进行深拷贝,那么extend()是如何实现深拷贝的呢?
先来看下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 = Array.isArray( copy ) ) ) ) {
 
                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && Array.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;
};

主要看下关于深拷贝的部分,取第一个参数,如果是boolean类型的,就赋值给deep,下面如果deep为true(也就是进行深拷贝),就递归调用extend(),这样就将对象的所有属性都添加到了target中实现了深拷贝。

JSON.parse()和JSON.stringify()

上面的jQuery源码是否让你眼花缭乱?有没有什么办法无脑实现深拷贝呢?JSON.parse()和JSON.stringify()给了我们一个基本的解决办法。

var target = {a: 1, b: 1, c: {ca: 11, cb: 12, cc: 13}};
var targetCopy = JSON.parse(JSON.stringify(target));
targetCopy.a = 2;
targetCopy.c.ca = 21;
console.log(target);   // {a: 1, b: 1, c: {ca: 11, cb: 12, cc: 13}}
console.log(targetCopy);    // {a: 2, b: 1, c: {ca: 21, cb: 12, cc: 13}}
console.log(target === targetCopy);  // false

可以看到改变targetCopy并没有改变原始的target,继承的属性也没有丢失,因此实现了基本的深拷贝。
但是用JSON.parse()和JSON.stringify()会有一个问题。
JSON.parse()和JSON.stringify()能正确处理的对象只有Number、String、Array等能够被json表示的数据结构,因此函数这种不能被json表示的类型将不能被正确处理。

var target = {
    a: 1,
    b: 2,
    hello: function() { 
            console.log("Hello, world!");
    }
};
var copy = JSON.parse(JSON.stringify(target));
console.log(copy);   // {a: 1, b: 2}

上面的例子可以看出,hello这个属性由于是函数类型,使用JSON.parse()和JSON.stringify()后丢失了。
因此JSON.parse()和JSON.stringify()还是需要谨慎使用。

转载自AlloyTeam:http://www.alloyteam.com/2017/08/12978/

推荐阅读更多精彩内容