JavaScript中的淺拷貝與深拷貝

上一篇 JavaScript中的繼承

前言

文章開始之前,讓我們先思考一下這幾個問題:

  • 爲什麼會有淺拷貝與深拷貝
  • 什麼是淺拷貝與深拷貝
  • 如何實現淺拷貝與深拷貝

好了,問題出來了,那麼下面就讓我們帶着這幾個問題去探究一下吧!

如果文章中有出現紕漏、錯誤之處,還請看到的小夥伴多多指教,先行謝過

以下↓

數據類型

在開始瞭解 淺拷貝深拷貝 之前,讓我們先來回顧一下 JavaScript 的數據類型(可以參考這裏 JavaScript中的數據類型

JavaScript 中,我們將數據分爲 基本數據類型(原始值)引用類型

  • 基本數據類型的值是按值訪問的,基本類型的值是不可變的
  • 引用類型的值是按引用訪問的,引用類型的值是動態可變的

由於數據類型的訪問方式不同,它們的比較方式也是不一樣的

var a = 100;
var b = 100;

a === b // true

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

c == d // false 兩個不同的對象
  • 基本數據類型的比較是值得比較
  • 引用類型的比較是引用地址的比較

鑑於以上數據類型的特點,我們可以初步想到:所謂 淺拷貝深拷貝 可能就是對於值的拷貝和引用的拷貝(簡單數據類型都是對值的拷貝,不進行區分)

一般來說,我們所涉及的拷貝對象,也都是針對引用類型的。由於引用類型屬性層級可能也會有多層,這樣也就引出了我們所要去了解的 淺拷貝深拷貝

淺拷貝

顧名思義,所謂淺拷貝就是對對象進行淺層次的複製,只複製一層對象的屬性,並不包括對象裏面的引用類型數據

想象一下,如果讓你自己去實現這個功能,又會有怎麼的思路呢

首先,我們需要知道被拷貝對象有哪些屬性吧,然後還需要知道這些屬性都對應了那些值或者地址的引用吧。那麼,答案已經呼之欲出了,是的,循環

var person = {
    name: 'tt',
    age: 18,
    friends: ['oo', 'cc', 'yy']
}

function shallowCopy(source) {
    if (!source || typeof source !== 'object') {
        throw new Error('error');
    }
    var targetObj = source.constructor === Array ? [] : {};
    for (var keys in source) {
        if (source.hasOwnProperty(keys)) {
            targetObj[keys] = source[keys];
        }
    }
    return targetObj;
}

var p1 = shallowCopy(person);

console.log(p1)

在上面的代碼中,我們創建了一個 shallowCopy 函數,它接收一個參數也就是被拷貝的對象。

  • 首先創建了一個對象
  • 然後 for...in 循環傳進去的對象,爲了避免循環到原型上面會被遍歷到的屬性,使用 hasOwnProperty 限制循環只在對象自身,將被拷貝對象的每一個屬性和值添加到創建的對象當中
  • 最後返回這個對象

通過測試,我們拿到了和 person 對象幾乎一致的對象 p1。看到這裏,你是不是會想那這個結果和 var p1 = person 這樣的賦值操作又有什麼區別呢?

我們再來測試一波

var p2 = person;

// 這個時候我們修改person對象的數據
person.name = 'tadpole';
person.age = 19; 
person.friends.push('tt')

p2.name // tadpole
p2.age // 19
p2.friends // ["oo", "cc", "yy", "tt"]

p1.name // tt
p1.age // 18
p1.friends // ["oo", "cc", "yy", "tt"]

上面我們創建了一個新的變量 p2 ,將 person 賦值給 p2 ,然後比較兩個變量

-- 和原數據是否指向同一對象 第一層數據爲基本數據類型 原數據中包含子對象
賦值 改變會使原數據一同改變 改變會使原數據一同改變
淺拷貝 改變不會使原數據一同改變 改變會使原數據一同改變

深拷貝

瞭解完淺拷貝,相信小夥伴們對於深拷貝也應該瞭然於胸了

淺拷貝由於只是複製一層對象的屬性,當遇到有子對象的情況時,子對象就會互相影響。所以,深拷貝是對對象以及對象的所有子對象進行拷貝

實現方式就是遞歸調用淺拷貝

function deepCopy(source){
   if(!source || typeof source !== 'object'){
     throw new Error('error');
   }
   var targetObj = source.constructor === Array ? [] : {};
   for(var 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 obj1 = {
    arr: [1, 2, 3],
    key: {
        id: 22
    },
    func: function() {
        console.log(123)
    }
}

var obj2 = deepCopy(obj1);

obj1.arr.push(4);

obj1.arr // [1, 2, 3, 4]
obj2.arr // [1, 2, 3]
obj1.key === obj2.key // false
obj1.func === obj2.func // true

對於深拷貝的對象,改變源對象不會對得到的對象有影響。只是在拷貝的過程中源對象的方法丟失了,這是因爲在序列化 JavaScript 對象時,所有函數和原型成員會被有意忽略

還有一種實現深拷貝的方式是利用 JSON 對象中的 parsestringifyJOSN 對象中的 stringify 可以把一個 js 對象序列化爲一個 JSON 字符串,parse 可以把 JSON 字符串反序列化爲一個 js 對象,通過這兩個方法,也可以實現對象的深複製

// 利用JSON序列化實現一個深拷貝
function deepCopy(source){
  return JSON.parse(JSON.stringify(source));
}
var o1 = {
  arr: [1, 2, 3],
  obj: {
    key: 'value'
  },
  func: function(){
    return 1;
  }
};
var o2 = deepCopy(o1);
console.log(o2); // => {arr: [1,2,3], obj: {key: 'value'}}

實現拷貝的其他方式

淺拷貝

  • Array.prototype.slice()
  • Array.prototype.concat()
  • Object.assign
  • 拓展操作符...
  • ...

深拷貝

很多框架或者庫都提供了深拷貝的方式,比如 jQuerylodash 函數庫等等,基本實現方式也就和我們前面介紹的大同小異

後記

根據需求的不同,比如有時候我們需要一個全新的對象,在修改它的時候不去影響到源對象,那麼這個時候我們就可能需要深拷貝;反之,淺拷貝就能實現我們的需求

只是,我們需要注意到一點,那就是因爲實現深拷貝使用遞歸的方式,就增加了性能的消耗

相信在不斷使用的過程中,你一定會對它越來越熟悉

最後,推薦一波前端學習歷程,不定期分享一些前端問題和有意思的東西歡迎 star 關注 傳送門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章