javascript中的淺拷貝ShallowCopy與深拷貝DeepCopy

拷貝,在js中,分爲淺拷貝和深拷貝。這二者是如何區分的呢?又是如何實現的呢?jquery

深淺拷貝的區分

首先說下,在js中,分爲基礎數據類型和複雜數據類型,算法

基礎數據類型:Undefined、Null、Boolean、Number、String、Symboljson

複雜數據類型:Object、Array、Function、Date等數組

基礎數據類型值,存儲在棧(stack)中,拷貝的話,會從新在棧中開闢一個相同的空間存儲數據。而複雜數據類型,值存儲在堆(heap)中,棧中存儲對值的引用地址。深淺拷貝,只針對複雜數據類型來講的。函數

淺拷貝ShallowCopy,是一個對象的逐位副本。建立一個新對象,該對象具備原始對象中的精確副本。若是對象的任何字段是對其餘對象的引用,則只複製引用地址,即只複製內存地址,而不復制對象自己,新舊對象仍是共享同一塊堆內存。改變其中一個對象,另外一個也會受影響。若是有修改,會失去原始數據。oop

深拷貝DeepCopy,複製出一個全新的對象實例,新對象跟原對象不共享內存,二者操做互不影響。測試

簡單點區分,ui

淺拷貝拷貝引用;this

深拷貝拷貝實例。prototype

ShallowCopy淺拷貝的實現方式

1. 賦值

先來講說,簡單的賦值狀況,

var o1 = { a : 1, b : 2 }
var o2 = o1
console.log(o2 === o1) // true
o1.a = 2
console.log(o1) // {a: 2, b: 2}
console.log(o2) // {a: 2, b: 2}

賦值,這裏是對對象地址的引用,改變一個對象的值,拷貝的另外一個對象的值也跟着變化,因此這是淺拷貝。

2. Array.concat()

concat方法用於合併兩個或多個數組。該方法不會更改現有的數組,而僅僅會返回被鏈接數組的一個副本。

var o1 =  [1, [2], 3]

var o2 = o1.concat([]) // 這裏會返回一個o1對象的淺拷貝對象

console.log(o2) //  [1, [2], 3]

console.log(o1 === o2) // false

o2數組就是一個新數組。若是改變o1數組對象,會不會影響o2數組對象呢?

o1[0] = 11

console.log(o1) // [11, [2], 3]

console.log(o2) // [1, [2], 3]

以上這種狀況,沒有改變o2數組值。這是由於,o2中第一個元素和o1中的第一個元素,不是同一個內存地址。

o1[1][0] = 22

console.log(o1) // [11, [22], 3]

console.log(o2) // [1, [22], 3]

而修改o1變量中的引用的值,o2數組值也跟隨着變化。這說明,o2中第二個元素和o1中的第二個元素引用相同的內存地址。

根據以上的說明,能夠得出結論,若是數組是一維數組,則能夠算是深拷貝。若是是多維數組,則是淺拷貝。

3. Array.slice()

slice方法可從已有的數組中返回選定的元素。

var o1 = [1, [2], 3]

var o2 = o1.slice(0)

console.log(o1) // [1, [2], 3]

console.log(o2) // [1, [2], 3]

該方法不會修改數組,而是返回一個子數組。

o1[0] = 11

console.log(o1) // [11, [2], 3]

console.log(o2) // [1, [2], 3]

從結果看出,只修改了o1的值,o2的值沒有修改。

o1[1][0] = 22

console.log(o1) // [11, [22], 3]

console.log(o2) // [1, [22], 3]

從結果看出,o一、o2兩個變量的值,都發生了變化。說明,二者引用指向同一個內存地址。

以上,說明是淺拷貝。

4. Object.assign()

Object.assign()方法用於將全部可枚舉的自有屬性的值從一個或多個源對象複製到目標對象。 它將返回目標對象

var o1 =  { a : 1, b : { c : 2, d : 3} }

var o2 = Object.assign({}, o1)

console.log(o1) // { a : 1, b : { c : 2, d : 3} }

console.log(o2) // { a : 1, b : { c : 2, d : 3} }

console.log(o2 === o1) // false    說明實現了淺拷貝



o1.a = 11

console.log(o2) // { a : 1, b : { c : 2, d : 3} }    o1和o2內部包含的基本類型值,拷貝的是其實例,不會相互影響



o1.b.c = 22

console.log(o1) // { a : 11, b : { c : 22, d : 3} }

console.log(o2) // { a : 1, b : { c : 22, d : 3} }    o1和o2內部包含的引用類型值,拷貝的是其引用,會相互影響

5. 使用jQuery中的extend函數

// Shallow copy

jQuery.extend({},OriginalObject)
// Deep copy 
jQuery.extend(true, {},OriginalObject)

jQuery.extend( [deep ], target, object1 [, objectN ] ),其中deep爲Boolean類型,若是是true,則進行深拷貝。

var $ = require('jquery')

var o1 = { a : 1, b : { c : 2 } }

var o2 = $.extend({}, o1)

console.log(o1.b === o2.b) // true

console.log(o1.a === o1.a) // false

6. lodash中的 _.clone()

利用結構化拷貝算法。支持拷貝arrays,array buffers,booleans, data objects, maps,

numbers, Object objects, regexes, sets, strings, symbols, and typed
arrays. arguments對象的可枚舉屬性被拷貝爲普通對象。
爲不可拷貝的值(如錯誤對象、函數、DOM節點和弱映射)返回一個空對象。

淺拷貝: _.clone()

深拷貝:_.cloneDeep()

var objects = [{ 'a': 1 }, { 'b': 2 }]; 

var shallow = _.clone(objects);

console.log(shallow[0] === objects[0]); // true

objects[0].a = 11

console.log(shallow[0]) // { a : 11}

DeepCopy深拷貝的實現方式

1. 手動複製

要實現拷貝出來的副本,不受本來影響,那麼能夠這麼實現

var o1 = { a : 1, b : 2 }
var o2 = { a : o1.a, b : o1.b }
console.log(o2 === o1) // false
o1.a = 2
console.log(o1) // {a: 2, b: 2}
console.log(o2) // {a: 1, b: 2}

將每一個引用對象都經過複製值來實現深拷貝。

2. JSON.parse(JSON.stringify(object_array))

  • JSON.stringify(): 把對象轉換爲字符串

  • JSON.parse():把字符串轉換爲對象

var o1 = { a : 1, b : { c : 2} }

var o2 = JSON.parse(JSON.stringify(o1))

console.log(o1 === o2) // false
console.log(o1.b === o2.b) // false

o1.b.c = 22

o1.a = 11

console.log(o1) //   { a : 11, b : { c : 22} }

console.log(o2) //   { a : 1, b : { c : 2} }

這種方式,只針對能夠轉換爲JSON對象的類型,好比Array,Object。若是遇到Function就不適用了。

3. 再次碰見jQuery.extend()方法

jQuery.extend( [deep ], target, object1 [, objectN ] ),其中deep爲Boolean類型,若是是true,則進行深拷貝。

// 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
        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
    // 若是隻傳遞一個參數,則擴展jQuery自己
    if ( i === length ) {
        target = this;
        i--;
    }
 
    for ( ; i < length; i++ ) {
 
        // Only deal with non-null/undefined values
        // 只處理non-null/undefined的值
        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;
};

4. lodash中的_.cloneDeep()

利用第三方庫lodash,它的深拷貝函數cloneDeep(),這個函數仍是比較靠譜的,大多數需求都能知足。

var o1 =  { a : 1, b : { c : 2} }
var o2 = _.cloneDeep(o1)
console.log(o1 === o2) // false
o1.a = 11
o1.b.c = 22
console.log(o1) // { a : 11, b : { c : 22} }
console.log(o2) // { a : 1, b : { c : 2} }

5. 本身實現深拷貝

針對Array和Object兩種複雜類型,本身實現深拷貝。本身實現的深拷貝,對比jquery的。沒有考慮undefined和null值。

// 檢測數據類型的函數

function typeString(obj) {

    var cons =   Object.prototype.toString.call(obj).slice(8, -1) 

    return (cons === 'Array' || cons === 'Object')

}

// 實現深度拷貝 Array/Object

function deepClone(oldObj) {

    if(typeString(oldObj)) {

        var newObj = oldObj.constructor()

        for(let i in oldObj) {

            if (oldObj.hasOwnProperty(i)) {

                newObj[i] = typeString(oldObj[i]) ? deepClone(oldObj[i]) : oldObj[i]

            }

        }

        return newObj;

    } else {

        return oldObj

    }

}



// 測試

var o1 = [1, 2, [3, 4]]

var o2 = deepClone(o1)

console.log(o1 === o2) // false

o1[2][0] = 2018

console.log(o2) // [1, 2, [3, 4]]

console.log(o1) // [1, 2, [2018, 4]]

深淺拷貝總結

拷貝以後,是否會相互影響,是個重要的指標。以上討論的深淺拷貝,針對的範圍比較小,大部分只考慮了Object和Array類型,可是能在大多數場合使用。 Function、Data、RegExp等沒有考慮。若是須要考慮這些,能夠針對特定的狀況,來具體實現,好比lodash的_.clone,就是使用的結構化拷貝算法,針對不一樣狀況來拷貝的。

相關文章
相關標籤/搜索