【學習筆記】JavaScript - 深淺拷貝

拷貝的問題主要是針對引用類型javascript

淺拷貝和深拷貝區別

對於這個問題,首先讓咱們先簡單回顧一下 JavaScript 的基本知識vue

一、JavaScript 包含兩種不一樣數據類型的值:基本類型(原始值)引用類型java

基本類型有如下幾種,具體以下:
string、number、boolean、null、undefined、symbol、bigIntgit

引用類型具體有:
Object(Object、Array、Function...)github

在將一個值賦給變量時,解析器必須肯定這個值是基本類型仍是引用類型算法

  • 基本數據類型是按值訪問的,由於能夠操做保存在變量中的實際的值
  • 引用類型的值是保存在內存中的對象,棧內存存儲的是變量的標識符以及對象在堆內存中的存儲地址。JavaScript 不容許直接訪問內存中的位置,即不能直接操做對象的內存空間。所以在操做對象時其實是對操做對象的引用而不是實際的對象。當須要訪問引用類型(如對象、數組等)的值時,首先從棧中得到該對象的地址指針,而後再從對應的堆內存中取得所需的數據

二、JavaScript 的變量存儲方式 -- 棧(stack)堆(heap)segmentfault

  • :自動分配內存空間,系統自動釋放,裏面存放的是基本類型的值和引用類型的地址指針
  • :動態分配內存,大小不定,也不會自動釋放,裏面存放引用類型的值

image.png

三、JavaScript 值傳遞與址傳遞
基本類型與引用類型最大的區別實際就是傳值與傳址的區別數組

  • 值傳遞:基本類型採用的是值傳遞
let a = 1;
let b = a;
b++;
console.log(a, b) // 1, 2
複製代碼
  • 址傳遞:引用類型則是地址傳遞,將存放在棧內存中的地址賦值給接收的變量
let a = ['a', 'b', 'c'];
let b = a; 
b.push('d');
console.log(a) // ['a', 'b', 'c', 'd']
console.log(b) // ['a', 'b', 'c', 'd']
複製代碼

分析:markdown

  • a 是數組是引用類型,賦值給 b 就是將 a 的地址賦值給 b,所以 a 和 b 指向同一個地址(該地址都指向了堆內存中引用類型的實際的值)
  • 當 b 改變了這個值的同時,由於 a 的地址也指向了這個值,故 a 的值也跟着變化,就比如 a 租了一間房,將房間的地址給了 b,b 經過地址找到了房間,那麼 b 對房間作的任何改變對 a 來講確定一樣是可見的

那麼如何解決上面出現的問題,這裏就引出了淺拷貝或者深拷貝了。JS 的基本類型不存在淺拷貝仍是深拷貝的問題,主要是針對引用類型數據結構

淺拷貝:拷貝的級別淺。淺拷貝是指複製對象時只對第一層鍵值對進行復制,若對象內還有對象則只能複製嵌套對象的地址指針

  • 缺點:當有一個屬性是引用值(數組或對象)時,按照這種克隆方式,只是把這個引用值的指向賦給了新的目標對象,即一旦改變了源對象或目標對象的引用值屬性,另外一個也會跟着改變

深拷貝:拷貝級別更深。深拷貝是指複製對象時是徹底拷貝,即便嵌套了對象,拷貝後二者也相互不影響,修改一個對象的屬性不會影響另外一個。原理實際上是遞歸把那些值是對象的屬性再次進入對象內部進行復制

淺拷貝

sliceconcat
如果數組,數組元素均爲基本數據類型,可利用數組的一些方法如 sliceconcat 返回一個新數組的特性來實現拷貝(此時至關於深拷貝)

若數組的元素是引用類型(Object,Array),sliceconcat 對對象數組的拷貝仍是淺拷貝,拷貝以後數組各個元素的指針仍是指向相同的存儲地址

let arr = ['one', 'two', 'three'];
let newArr = arr.concat();
newArr.push('four')

console.log(arr)    // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]

let arr = ['one', 'two', 'three'];
let newArr = arr.slice();
newArr.push('four')

console.log(arr)    // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]

let arr = [{a:1}, 'two', 'three'];
let newArr = arr.concat();
newArr[0].a = 2;

console.log(arr)    // [{a: 2},"two","three"]
console.log(newArr) // [{a: 2},"two","three"]
複製代碼

Object assign()
該方法能夠把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,而後返回目標對象。Object assign() 對對象的拷貝仍是淺拷貝

let arr = {
  a: 'one', 
  b: 'two', 
  c: 'three'
};

let newArr = Object.assign({}, arr)
newArr.d = 'four'
console.log(arr);    // {a: "one", b: "two", c: "three"}
console.log(newArr); // {a: "one", b: "two", c: "three", d: "four"}

let arr = {
  a: 'one', 
  b: 'two', 
  c: {a: 1}
};

let newArr = Object.assign({}, arr)
newArr.c.a = 3;
console.log(arr);    // {a: "one", b: "two", c: {a: 3}}
console.log(newArr); // {a: "one", b: "two", c: {a: 3}}
複製代碼

Object.assign 原理及其實現

淺拷貝封裝

原理:遍歷對象,而後把屬性和屬性值放在一個新對象並返回

function clone(obj) {
  // 只拷貝對象
  if (typeof src !== 'object') return;
  // 根據 obj 的類型判斷是新建一個數組仍是對象
  let newObj = Obejct.prototype.toString.call(obj) == '[object Array]' ? [] : {};
  for(let prop in newObj) {
    if(newObj.hasOwnProperty(prop)) {
      newObj[prop] = obj[src];
    }
  }
  return newObj;
}
複製代碼

深拷貝

JSON.parse(JSON.stringify(arr)):不只適用於數組還適用於對象

let a = {
  name: "tn",
  book: {
    title: "JS",
    price: "45"
  }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "JS", price: "55"}
// } 

console.log(b);
// {
// name: "tn",
// book: {title: "JS", price: "45"}
// } 
複製代碼

改變變量 a 中的引用屬性後對 b 沒有任何影響,這就是深拷貝的魔力

對數組深拷貝以後,改變原數組頁不會影響到拷貝以後的數組

// 木易楊
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b); // ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a); // [0, "99", [4, 3]]

console.log(b); // ["1", [2, 3]]
複製代碼

但該方法有侷限性

  • 會忽略 undefined
  • 會忽略 symbol
  • 不能序列化函數
  • 不能解決循環引用的對象
  • 不能正確處理 new Date()
  • 不能處理正則

undefinedsymbol函數 會被直接忽略

// 木易楊
let obj = {
  name: "tn",
  a: undefined,
  b: Symbol("tn"),
  c: function() {}
}

let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn"}
複製代碼

循環引用狀況下會報錯

let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3
  }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
複製代碼

new Date 狀況下轉換結果不正確

new Date(); // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date()); // ""2018-12-24T02:59:25.776Z""

JSON.parse(JSON.stringify(new Date())); // "2018-12-24T02:59:41.523Z"
複製代碼

解決方法轉成字符串或者時間戳

let date = (new Date()).valueOf();
JSON.stringify(date); // "1625905818735"
JSON.parse(JSON.stringify(date)); // 1625905818735
複製代碼

正則狀況下

let obj = {
  name: "tn",
  a: /'123'/
}
console.log(obj); // {name: "tn", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "tn", a: {}}
複製代碼

ES6 擴展運算符[...]:不只適用於數組還適用於對象,只有原始值能夠深拷貝,當含有引用值時進行淺拷貝

lodash 的深拷貝函數

深拷貝封裝

原理:在拷貝時判斷一下屬性值的類型,如果對象則遞歸調用深拷貝函數,深拷貝是徹底拷貝了原對象的內容並寄存在新的內存空間,指向新的內存地址

function deepClone1(src, target) {
  var target = target || {};
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] !== null && typeof(src[prop]) === 'object') {
        target[prop] = Object.prototype.toString.call(src[prop]) == '[object Array]' ? [] : {};
        deepClone(src[prop], target[prop]);
      } else {
        target[prop] = src[prop];
      }
    }
  }
  return target;
}

// test
var a = {
  name: "tn",
  book: {
    title: "JS",
    price: "45"
  },
  a1: undefined,
  a2: null,
  a3: 123
}

var b = deepClone(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {
// title: "JS", 
// price: "45"
// },
// name: "tn"
// }
複製代碼

循環引用

咱們知道 JSON 沒法深拷貝循環引用,遇到這種狀況會拋出異常

使用哈希表

其實就是循環檢測,設置一個數組或哈希表存儲已拷貝過的對象,當檢測到當前對象已存在於哈希表中時,取出該值並返回

function deepClone2(src, hash = new WeakMap()) {
  var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
  if (hash.has(src)) return hash.get(src); // 新增代碼,查哈希表
  hash.set(src, target); // 新增代碼,哈希表設值
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] !== null && typeof(src[prop]) === 'object') {
        target[prop] = deepClone2(src[prop], hash);
      } else {
        target[prop] = src[prop];
      }
    }
  }
  return target;
}

var a = {
  name: "tn",
  book: {
    title: "JS",
    price: "45"
  },
  a1: undefined,
  a2: null,
  a3: 123
};
a.circleRef = a;
var b = deepClone2(a);
console.log(b);
// {
// a1: undefined,
// a2: null,
// a3: 123,
// book: {title: "JS", price: "45"},
// circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
// name: "tn"
// }
複製代碼

使用數組

上面使用了 ES6 中的 WeakMap 來處理,在 ES5 下可使用數組來處理

function deepClone2(src, uniqueList) {
  var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
  if (!uniqueList) uniqueList = []; // 新增代碼,初始化數組
  // 數據已經存在,返回保存的數據
  var uniqueData = find(uniqueList, src);
  if (uniqueData) {
     return uniqueData.target;
  };
  // 數據不存在,保存源數據,以及對應的引用
  uniqueList.push({
      source: src,
      target: target
  });
  for (let prop in src) {
    if (src.hasOwnProperty(prop)) {
      if(src[prop] !== null && typeof(src[prop]) === 'object') {
        target[prop] = deepClone2(src[prop], uniqueList);
      } else {
        target[prop] = src[prop];
      }
    }
  }
  return target;
}

// 用上面用例測試 OK
複製代碼

如今已經很完美的解決了循環引用的狀況,但其實還有一種狀況是引用丟失,咱們看下面的例子

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

obj2.a === obj2.b; // true

var obj3 = deepClone1(obj2);
obj3.a === obj3.b; // false
複製代碼

引用丟失在某些狀況下是有問題的,如上面的對象 obj2,obj2 的鍵值 a 和 b 同時引用了同一個對象 obj1,使用 deepClone1 進行深拷貝後就丟失了引用關係變成了兩個不一樣的對象

其實上面的 deepClone2 已經解決了這個問題,由於存儲了已拷貝過的對象

var obj3 = deepClone2(obj2);
obj3.a === obj3.b; // true
複製代碼

拷貝 Symbol

SymbolES6 下才有,須要一些方法來檢測出 Symble 類型

  • 方法一:Object.getOwnPropertySymbols(...)

    該方法能夠查找一個給定對象的符號屬性時返回一個 ?symbol 類型的數組。注意,每一個初始化的對象都是沒有本身的 symbol 屬性的,所以這個數組可能爲空,除非你已經在對象上設置了 symbol 屬性(來自MDN)

    var obj = {};
    var a = Symbol("a"); // 建立新的 symbol 類型
    var b = Symbol.for("b"); // 從全局的 symbol 註冊?表設置和取得symbol
    
    obj[a] = "localSymbol";
    obj[b] = "globalSymbol";
    
    var objectSymbols = Object.getOwnPropertySymbols(obj);
    
    console.log(objectSymbols.length); // 2
    console.log(objectSymbols)         // [Symbol(a), Symbol(b)]
    console.log(objectSymbols[0])      // Symbol(a)
    複製代碼

    思路就是先查找有沒有 Symbol 屬性,若是查找到則先遍歷處理 Symbol 狀況,而後再處理正常狀況

    function deepClone3(src, hash = new WeakMap()) {
      var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
      if (hash.has(src)) return hash.get(src); // 新增代碼,查哈希表
      hash.set(src, target); // 新增代碼,哈希表設值
    
      let symKeys = Object.getOwnPropertySymbols(src); // 查找
      if (symKeys.length) { // 查找成功
        symKeys.forEach(symKey => {
          if (src[prop] !== null && typeof(src[prop]) === 'object') {
            target[symKey] = deepClone3(src[symKey], hash); 
          } else {
            target[symKey] = src[symKey];
          }    
        });
      }
    
      for (let prop in src) {
        if (src.hasOwnProperty(prop)) {
          if(src[prop] !== null && typeof(src[prop]) === 'object') {
            target[prop] = deepClone3(src[prop], hash);
          } else {
            target[prop] = src[prop];
          }
        }
      }
      return target;
    }
    
    var a = {
      name: "tn",
      book: {
        title: "JS",
        price: "45"
      },
      a1: undefined,
      a2: null,
      a3: 123
    };
    var sym1 = Symbol("a"); // 建立新的symbol類型
    var sym2 = Symbol.for("b"); // 從全局的symbol註冊?表設置和取得symbol
    
    a[sym1] = "localSymbol";
    a[sym2] = "globalSymbol";
    
    var b = deepClone3(a);
    console.log(b);
    
    // {
    // a1: undefined
    // a2: null
    // a3: 123,
    // book: {title: "JS", price: "45"},
    // circleRef: {name: "tn", book: {…}, a1: undefined, a2: null, a3: 123, …},
    // name: "tn",
    // [Symbol(a)]: "localSymbol",
    // [Symbol(b)]: "globalSymbol"
    // }
    複製代碼
  • 方法二:Reflect.ownKeys(...)

    返回一個由目標對象自身的屬性鍵組成的數組。它的返回值等同於Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))(來自MDN)

    Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
    Reflect.ownKeys([]); // ["length"]
    
    var sym = Symbol.for("comet");
    var sym2 = Symbol.for("meteor");
    var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
              [sym2]: 0, "-1": 0, "8": 0, "second str": 0};
    Reflect.ownKeys(obj); // [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
    // 注意順序
    // Indexes in numeric order, 
    // strings in insertion order, 
    // symbols in insertion order
    複製代碼
    function deepClone3(src, hash = new WeakMap()) {
      var target = Object.prototype.toString.call(src) == '[object Array]' ? [] : {};
      if (hash.has(src)) return hash.get(src); 
      hash.set(src, target);
      
      Reflect.ownKeys(src).forEach(key => { // 改動
        if (src[key] !== null && typeof(src[key]) === 'object') {
          target[key] = deepClone3(src[key], hash); 
        } else {
          target[key] = src[key];
        }  
      });
      return target;
    }
    // 測試 ok
    複製代碼

    這裏使用了 Reflect.ownKeys() 獲取全部的鍵值,同時包括 Symbol,對 src 遍歷賦值便可

破解遞歸爆棧

上面使用的都是遞歸方法,可是有個問題是可能會爆棧,錯誤提示以下

// RangeError: Maximum call stack size exceeded
複製代碼

詳情請參考這篇文章:深拷貝的終極探索(99%的人都不知道)

庫實現

上面的方式能夠知足基本的場景的需求,如有更復雜的需求可本身實現。一些框架和庫的也有對應的解決方案,如:jQuery.extend()lodash

應用場景

淺拷貝
對於一層結構的 ArrayObject 想要拷貝一個副本時使用 vuemixin 是淺拷貝的一種複雜型式

深拷貝
複製深層次的 object 數據結構,如想對某個數組或對象的值進行修改,但又要保留原數組或對象的值不被修改,此時就能夠用深拷貝來建立一個新的數組或對象

參考資料

javascript中的深拷貝和淺拷貝?
JavaScript 如何完整實現深度Clone對象?
ithub lodash源碼
MDN 結構化克隆算法 jQuery v3.2.1 源碼

相關文章
相關標籤/搜索