javascript對象深拷貝和淺拷貝

若有錯誤歡迎指出會在第一時間改正前端

基本類型的值和引用類型的值

JavaScript的變量中包含兩種類型的值jquery

  1. 基本類型值 基本類型值指的是存儲在棧中的一些簡單的數據段
var str = 'a';
var num = 1;
複製代碼複製代碼

在JavaScript中基本數據類型有String,Number,Undefined,Null,Boolean,在ES6中,又定義了一種新的基本數據類型Symbol,因此一共有6種git

基本類型是按值訪問的,從一個變量複製基本類型的值到另外一個變量後這2個變量的值是徹底獨立的,即便一個變量改變了也不會影響到第二個變量github

var str1 = 'a';
var str2 = str1;
str2 = 'b';
console.log(str2); //'b'
console.log(str1); //'a'
複製代碼複製代碼
  1. 引用類型值 引用類型值是引用類型的實例,它是保存在堆內存中的一個對象,引用類型是一種數據結構,最經常使用的是Object,Array,Function類型,另外還有Date,RegExp,Error等,ES6一樣也提供了Set,Map2種新的數據結構

JavaScript是如何複製引用類型的

JavaScript對於基本類型和引用類型的賦值是不同的數組

var obj1 = {a:1};
var obj2 = obj1;
obj2.a = 2;
console.log(obj1); //{a:2}
console.log(obj2); //{a:2}
複製代碼複製代碼

在這裏只修改了obj1中的a屬性,卻同時改變了ob1和obj2中的a屬性bash

當變量複製引用類型值的時候,一樣和基本類型值同樣會將變量的值複製到新變量上,不一樣的是對於變量的值,它是一個指針,指向存儲在堆內存中的對象(JS規定放在堆內存中的對象沒法直接訪問,必需要訪問這個對象在堆內存中的地址,而後再按照這個地址去得到這個對象中的值,因此引用類型的值是按引用訪問)數據結構

變量的值也就是這個指針是存儲在棧上的,當變量obj1複製變量的值給變量obj2時,obj1,obj2只是一個保存在棧中的指針,指向同一個存儲在堆內存中的對象,因此當經過變量obj1操做堆內存的對象時,obj2也會一塊兒改變 函數

保存在於棧中的變量和堆內存中對象的關係

再舉個例子,小明(obj1變量)知道他家的地址(對象{a:1}),而後小明告訴了小剛(obj2變量)他家的地址(複製變量),小剛這個時候就知道了小明家的地址,而後小剛去小明家把小明家的門給拆了(修改對象),小明回家一看就會發現門沒了,這時小明和小剛去這個地址的時候都會看到一個沒有門的家-.-(對象的修改反映到變量)oop

淺拷貝

對於淺拷貝的定義能夠理解爲ui

建立一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。若是屬性是基本類型,拷貝的就是基本類型的值,若是屬性是引用類型,拷貝的就是內存地址 ,因此若是其中一個對象改變了這個地址,就會影響到另外一個對象。

如下是一些JavaScript提供的淺拷貝方法

Object.assign()

ES6中拷貝對象的方法,接受的第一個參數是拷貝的目標,剩下的參數是拷貝的源對象(能夠是多個)

語法:Object.assign(target, ...sources)

var target = {};
var source = {a:1};
Object.assign(target ,source);
console.log(target); //{a:1}
source.a = 2;
console.log(source); //{a:2}
console.log(target); //{a:1}
複製代碼複製代碼

Object.assign是一個淺拷貝,它只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是還是對象的話依然是淺拷貝,

Object.assign還有一些注意的點是:

  1. 不會拷貝對象繼承的屬性
  2. 不可枚舉的屬性
  3. 屬性的數據屬性/訪問器屬性
  4. 能夠拷貝Symbol類型

能夠理解爲Object.assign就是使用簡單的=來賦值,遍歷從右往左遍歷源對象(sources)的全部屬性用 = 賦值到目標對象(target)上

var obj1 = {
    a:{
        b:1
    },
    sym:Symbol(1)
};
Object.defineProperty(obj1,'innumerable',{
    value:'不可枚舉屬性',
    enumerable:false
});
var obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1); 
console.log('obj2',obj2); 
複製代碼複製代碼

能夠看到Symbol類型能夠正確拷貝,可是不可枚舉的屬性被忽略了而且改變了obj1.a.b的值,obj2.a.b的值也會跟着改變,說明依舊存在訪問的是堆內存中同一個對象的問題

擴展運算符

利用擴展運算符能夠在構造字面量對象時,進行克隆或者屬性拷貝

語法:var cloneObj = { ...obj };

var obj = {a:1,b:{c:1}}
var obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
複製代碼複製代碼

擴展運算符Object.assign()有一樣的缺陷,對於值是對象的屬性沒法徹底拷貝成2個不一樣對象,可是若是屬性都是基本類型的值的話,使用擴展運算符更加方便

Array.prototype.slice()

slice() 方法返回一個新的數組對象,這一對象是一個由 begin和 end(不包括end)決定的原數組的淺拷貝。原始數組不會被改變。

語法: arr.slice(begin, end);

在ES6之前,沒有剩餘運算符,Array.from的時候能夠用 Array.prototype.slice將arguments類數組轉爲真正的數組,它返回一個淺拷貝後的的新數組

Array.prototype.slice.call({0: "aaa", length: 1}) //["aaa"]

let arr = [1,2,3,4]
console.log(arr.slice() === arr); //false
複製代碼複製代碼

深拷貝

淺拷貝只在根屬性上在堆內存中建立了一個新的的對象,複製了基本類型的值,可是複雜數據類型也就是對象則是拷貝相同的地址,而深拷貝則是對於複雜數據類型在堆內存中開闢了一塊內存地址用於存放複製的對象而且把原有的對象複製過來,這2個對象是相互獨立的,也就是2個不一樣的地址

將一個對象從內存中完整的拷貝一份出來,從堆內存中開闢一個新的區域存放新對象,且修改新對象不會影響原對象

一個簡單的深拷貝

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

obj2.a = {}
obj2.c = obj1.c
obj2.a.b = obj1.a.b;
console.log(obj1); //{a:{b:1},c:1};
console.log(obj2); //{a:{b:1},c:1};
obj1.a.b = 2;
console.log(obj1); //{a:{b:2},c:1};
console.log(obj2); //{a:{b:1},c:1};

複製代碼複製代碼

在上面的代碼中,咱們新建了一個obj2對象,同時根據obj1對象的a屬性是一個引用類型,咱們給obj2.a的值也新建一個新對象(即在內存中新開闢了一塊內存地址),而後把obj1.a.b屬性的值數字1複製給obj2.a.b,由於數字1是基本類型的值,因此改變obj1.a.b的值後,obj2.a不會收到影響,由於他們的引用是徹底2個獨立的對象,這就完成了一個簡單的深拷貝

JSON.stringify()

JSON.stringify()是目前前端開發過程當中最經常使用的深拷貝方式,原理是把一個對象序列化成爲一個JSON字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()反序列化將JSON字符串變成一個新的對象

var obj1 = {
    a:1,
    b:[1,2,3]
}
var str = JSON.stringify(obj1)
var obj2 = JSON.parse(str)
console.log(obj2); //{a:1,b:[1,2,3]}
obj1.a=2
obj1.b.push(4);
console.log(obj1); //{a:2,b:[1,2,3,4]}
console.log(obj2); //{a:1,b:[1,2,3]}
複製代碼複製代碼

經過JSON.stringify實現深拷貝有幾點要注意

  1. 拷貝的對象的值中若是有函數,undefined,symbol則通過JSON.stringify()序列化後的JSON字符串中這個鍵值對會消失
  2. 沒法拷貝不可枚舉的屬性,沒法拷貝對象的原型鏈
  3. 拷貝Date引用類型會變成字符串
  4. 拷貝RegExp引用類型會變成空對象
  5. 對象中含有NaN、Infinity和-Infinity,則序列化的結果會變成null
  6. 沒法拷貝對象的循環應用(即obj[key] = obj)
function Obj() {
    this.func = function () {
        alert(1) 
    };
    this.obj = {a:1};
    this.arr = [1,2,3];
    this.und = undefined;
    this.reg = /123/;
    this.date = new Date(0);
    this.NaN = NaN
    this.infinity = Infinity
    this.sym = Symbol(1)
}
var obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
    enumerable:false,
    value:'innumerable'
})
console.log('obj1',obj1);
var str = JSON.stringify(obj1);
var obj2 = JSON.parse(str);
console.log('obj2',obj2);
複製代碼複製代碼

打印出來的結果以下


能夠看到除了Object對象和數組其餘基本都和原來的不同,obj1的constructor是Obj(),而obj2的constructor指向了Object(),而對於循環引用則是直接報錯了

雖然說經過JSON.stringify()方法深拷貝對象也有不少沒法實現的功能,可是對於平常的開發需求(對象和數組),使用這種方法是最簡單和快捷的

使用第三方庫實現對象的深拷貝

1.lodash

2.jQuery

以上2個第三方的庫都很好的封裝的深拷貝的方法,有興趣的同窗能夠去深刻研究一下

本身來實現一個深拷貝函數

遞歸

這裏簡單封裝了一個deepClone的函數,for in遍歷傳入參數的值,若是值是引用類型則再次調用deepClone函數,而且傳入第一次調用deepClone參數的值做爲第二次調用deepClone的參數,若是不是引用類型就直接複製

var obj1 = {
    a:{
        b:1
    }
};
function deepClone(obj) {
    var cloneObj = {}; //在堆內存中新建一個對象
    for(var key in obj){ //遍歷參數的鍵
       if(typeof obj[key] ==='object'){ 
          cloneObj[key] = deepClone(obj[key]) //值是對象就再次調用函數
       }else{
           cloneObj[key] = obj[key] //基本類型直接複製值
       }
    }
    return cloneObj 
}
var obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); //{a:{b:1}}
複製代碼複製代碼

可是還有不少問題

  • 首先這個deepClone函數並不能複製不可枚舉的屬性以及Symbol類型
  • 這裏只是針對Object引用類型的值作的循環迭代,而對於Array,Date,RegExp,Error,Function引用類型沒法正確拷貝
  • 對象循環引用成環了的狀況

本人總結的深拷貝的方法

看過不少關於深拷貝的博客,本人總結出了一個可以深拷貝ECMAScript的原生引用類型的方法

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {

    if (obj.constructor === Date) return new Date(obj);   //日期對象就返回一個新的日期對象
    if (obj.constructor === RegExp) return new RegExp(obj);  //正則對象就返回一個新的正則對象

    //若是成環了,參數obj = obj.loop = 最初的obj 會在WeakMap中找到第一次放入的obj提早返回第一次放入WeakMap的cloneObj
    if (hash.has(obj)) return hash.get(obj)

    let allDesc = Object.getOwnPropertyDescriptors(obj);     //遍歷傳入參數全部鍵的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc); //繼承原型鏈

    hash.set(obj, cloneObj)

    for (let key of Reflect.ownKeys(obj)) {   //Reflect.ownKeys(obj)能夠拷貝不可枚舉屬性和符號類型
        // 若是值是引用類型(非函數)則遞歸調用deepClone
        cloneObj[key] =
            (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ?
                deepClone(obj[key], hash) : obj[key];
    }
    return cloneObj;
};


let obj = {
    num: 0,
    str: '',
    boolean: true,
    unf: undefined,
    nul: null,
    obj: {
        name: '我是一個對象',
        id: 1
    },
    arr: [0, 1, 2],
    func: function () {
        console.log('我是一個函數')
    },
    date: new Date(0),
    reg: new RegExp('/我是一個正則/ig'),
    [Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
    enumerable: false,
    value: '不可枚舉屬性'
});

obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))

obj.loop = obj

let cloneObj = deepClone(obj);

console.log('obj', obj);
console.log('cloneObj', cloneObj);


for (let key of Object.keys(cloneObj)) {
    if (typeof cloneObj[key] === 'object' || typeof cloneObj[key] === 'function') {
        console.log(`${key}相同嗎? `, cloneObj[key] === obj[key])
    }
}
複製代碼複製代碼

這個函數有幾個要點

  1. 利用Reflect.ownKeys()方法,可以遍歷對象的不可枚舉屬性和Symbol類型
  2. 當參數爲Date,RegExp類型則直接生成一個新的實例
  3. 使用Object.getOwnPropertyDescriptors()得到對象的全部屬性對應的特性,結合Object.create()建立一個新對象繼承傳入原對象的原型鏈
  4. 利用WeekMap()類型做爲哈希表,WeekMap()由於是弱引用的能夠有效的防止內存泄露,做爲檢測循環引用頗有幫助,若是存在循環引用直接返回WeekMap()存儲的值

這裏我用全等判斷打印了2個對象的屬性是否相等,經過打印的結果能夠看到,雖然值是同樣的,可是在內存中是兩個徹底獨立的對象

上述的深拷貝函數中Null和Function類型引用的仍是同一個對象,由於deepClone函數對於對象的值是函數或者null時直接返回,這裏沒有深拷貝函數,若是須要深拷貝一個函數,能夠考慮使用Function構造函數或者eval?這裏還有待研究

相關文章
相關標籤/搜索