如何實現深淺拷貝

本文github地址,歡迎star前端

1.deepclone.jpg

在平時開發中,通常處理兩種數據類型:git

  • 基本數據類型
  • 引用數據類型

基本數據類型存在棧中,引用數據類型存放在堆中。github

在談到深淺拷貝時,也是圍繞這兩種數據類型展開的。數組

淺拷貝

建立一個對象,從新複製或引用的源對象的值。函數

  • 若是對象屬性是基本的數據類型,複製的就是基本類型的值給新對象。
  • 若是屬性是引用數據類型,拷貝出來的目標對象的指針和源對象的指針指向的內存空間是同一塊空間, 修改源對象同時也會對目標對象產生影響。

JS 提供了以下方法來對象和數據進行淺拷貝。oop

展開運算符

能夠在函數調用/數組構造時, 將數組表達式或者 string 在語法層面展開;還能夠在構造字面量對象時, 將對象表達式按 key-value 的方式展開。測試

// 數據的拷貝
let arr = [1, 2, 3, 4, {
    a: 1
}]
let arrCopy = [...arr]
arrCopy[4].a = 100
console.log(arrCopy[4].a) // 100
console.log(arr[4].a) // 100

// 對象的拷貝
let obj = {
    a: 1,
    b: {
        c: 3
    }
}
console.log(obj.b.c) // 3
let objCopy = {
    ...obj
}
objCopy.b.c = 4
console.log(obj.b.c) // 4
console.log(objCopy.b.c) // 4

不難發現數據類型都是基本類型,使用展開運算符拷貝很是方便。spa

Object.assign

Object.assign 方法用於將全部可枚舉屬性的值從一個或多個源對象分配到目標對象。它將返回目標對象。3d

let t = {}
let s = {
    a: {
        b: 100
    }
}
Object.assign(t, s)
console.log(s.a.b) // 100
t.a.b = 200
console.log(s.a.b) // 200
console.log(t.a.b) // 200

須要注意的幾個地方指針

  • 不會拷貝對象的繼承屬性
  • 不會拷貝對象的不可枚舉的屬性
  • 能夠拷貝 Symbol 類型的屬性

Object.assign 會遍歷原對象的屬性,經過複製的方式將其賦值給目標對象的相應屬性(包括 Symbol 類型的對象)

手寫實現淺拷貝

對於基本類型直接進行拷貝複製,對於引用類型,在內存中開闢一塊新的空間進行拷貝複製。

const isObject = (obj) => typeof obj === 'object' && obj !== null;

const shallowClone = (obj) => {
    if (!is(obj)) return obj
    const cloneObj = Array.isArray(obj) ? [] : {};
    for (let prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            cloneObj[prop] = obj[prop];
        }
    }
    return cloneObj;
}

深拷貝

將一個對象從內存中完整地拷貝出來一份給目標對象,並從堆內存中開闢一個全新的空間存放新對象,且新對象的修改並不會改變原對象,兩者實現真正的分離。

JSON.stringfy

JSON.stringfy 是前端中最簡單的深拷貝方式,把對象序列化成爲 JSON 的字符串,並將對象裏面的內容轉換成字符串,再用 JSON.parseJSON 字符串生成一個新的對象。

let obj1 = {
    a: 1,
    b: [1, 2, 3]
}
let str = JSON.stringify(obj1);
let 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.stringfy 在深拷貝會出現如下的問題:

  • 拷貝的對象的值中若是有函數、undefinedsymbol 這幾種類型,通過 JSON.stringify 序列化以後的字符串中這個鍵值對會消失。
  • 拷貝 Date 引用類型會變成字符串。
  • 沒法拷貝不可枚舉的屬性。
  • 沒法拷貝對象的原型鏈。
  • 拷貝 RegExp 引用類型會變成空對象。
  • 對象中含有 NaNInfinity 以及 -InfinityJSON 序列化的結果會變成 null
  • 沒法拷貝對象的循環應用。
let testObj = {
      [Symbol('test')]: 1,
      number: -1,
      string: 'Hello World',
      boolean: true,
      undefined: undefined,
      nul: null,
      obj: {
          name: '我是一個對象',
          id: 1
      },
      list: [11, 22, 33],
      fn: function() {
          console.log('Hello World')
      },
      date: new Date(),
      regExp: new RegExp('/[\w]/ig'),
  };
  Object.defineProperty(testObj, 'innumerable', {
      enumerable: false,
      value: 'innumerable'
  });
  obtestObjj = Object.create(testObj, Object.getOwnPropertyDescriptors(testObj))
  // 設置循環引用, 不能拷貝,將會報錯
  // testObj.loop = testObj
  let cloneObj = JSON.parse(JSON.stringify(testObj))
  cloneObj.list.shift()
  console.log('testObj', testObj)
  console.log('cloneObj', cloneObj)

js-2.png

手寫深拷貝

對於上述 JSON.stringfy 缺點,在實現深拷貝進行以下的改善:

  • 當對象爲 DateRegExp 類型,直接返回一個新的實例。
  • 對象的不可枚舉和symbol屬性,採用 Reflect.ownKeysReflect.ownKeys方法用於返回對象的全部屬性,基本等同於Object.getOwnPropertyNamesObject.getOwnPropertySymbols之和。
  • 沒法拷貝對象的原型鏈對象,能夠Object.getPrototypeOf獲取對象的原型對象,以及對象的屬性描述對象Object.getOwnPropertyDescriptors
  • 對於循環引用,能夠採用WeakMap, WeakMap 是弱引用類型,能夠有效防止內存泄漏。
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function(obj, hash = new WeakMap()) {
    // 處理 RegExp 對象
    if (obj.constructor === RegExp) return new RegExp(obj)
    // 處理 Date 對象
    if (obj.constructor === Date) return new Date(obj)
    // 處理循環引用
    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)
    // Reflect.ownKeys 能夠獲取不可枚舉和 symbol 屬性
    for (let key of Reflect.ownKeys(obj)) {
        cloneObj[key] = (isComplexDataType(obj[key]) ? deepClone(obj[key], hash) : obj[key]
        }
        return cloneObj
    }
}

接下來深拷貝測試

// 測試 對象
let testObj = {
    [Symbol('test')]: 1,
    number: -1,
    string: 'Hello World',
    boolean: true,
    undefined: undefined,
    nul: null,
    obj: {
        name: '我是一個對象',
        id: 1
    },
    list: [11, 22, 33],
    fn: function() {
        console.log('Hello World')
    },
    date: new Date(),
    regExp: new RegExp('/[\w]/ig'),
};
Object.defineProperty(testObj, 'innumerable', {
    enumerable: false,
    value: 'innumerable'
});
obtestObjj = Object.create(testObj, Object.getOwnPropertyDescriptors(testObj))
// 設置循環引用
testObj.loop = testObj
let cloneObj = deepClone(testObj)
cloneObj.list.shift()
console.log('cloneObj', testObj)
console.log('cloneObj', cloneObj)

js-1.png

能夠看出解決了 JSON.stringfy 在進行深拷貝的時候的缺點。

總結

在平時開發過程,咱們能夠採用第三方庫來實現深拷貝,好比 loadsh.cloneDeep

相關文章
相關標籤/搜索