1. 程式人生 > 其它 >javascript 物件的深拷貝與淺拷貝

javascript 物件的深拷貝與淺拷貝

技術標籤:javascriptjavascriptjs

今天我們來談一談物件的深拷貝和淺拷貝吧

我們都知道,js資料型別主要分為兩大類 基礎資料型別和引用(複雜)資料型別。

  • 基礎資料型別存在於棧記憶體中,當被拷貝時,會建立一個完全相等的變數
  • 而引用資料型別存在於堆中,儲存的是一個記憶體空間,而賦值給變數的,僅僅是這個記憶體空間的一個引用而已。而就會出現一個問題,當我們將一個物件賦值給另一個變數時,賦值的是物件的引用,必然導致兩個變數都指向同一個記憶體空間,其中一個改變時,必然會影響到另一個。

淺拷貝和深拷貝的理解

什麼是淺拷貝

簡單理解就是
自己建立一個新的物件,來接受你要重新複製或引用的物件值。如果物件屬性是基本的資料型別,複製的就是基本型別的值給新物件;但如果屬性是引用資料型別,複製的就是記憶體中的地址,如果其中一個物件改變了這個記憶體中的地址,肯定會影響到另一個物件。

可以實現淺拷貝的方法
1、Object.assign()

  • object.assign 是es6新增的一個方法
  • 用於合併多個物件到目標物件中
  • 第一個引數是目標物件,後面的引數是源物件
var obj = {}
var source1 = {
	name: '張三',
	age: 18,
	like: {
		music: '老鼠愛大米'
	}
}
Object.assign(obj, source1)
console.log(obj)
source1.like.music= "沙漠駱駝"
console.log(obj)
// 輸出
{ name: '張三', age: 18, like:
{ music: '老鼠愛大米' } } { name: '張三', age: 18, like: { music: '沙漠駱駝' } }

從上面可以看出,修改source1的like屬性下的music屬性值時,會對obj產生影響

2、擴充套件運算子

  • 擴充套件運算子可以展開物件或者陣列中的選項 obj = { …obj2 }
var obj = {}
var source1 = {
	name: '張三',
	age: 18,
	like: {
		music: '老鼠愛大米'
	}
}
obj = { ...source1 }
console.log(obj)
source1.like.
music= "沙漠駱駝" console.log(obj) // 輸出 { name: '張三', age: 18, like: { music: '老鼠愛大米' } } { name: '張三', age: 18, like: { music: '沙漠駱駝' } }

可以發現,和Object.assign() 一樣的結果

同時,使用這兩種方式還需要注意

  • 它不會拷貝物件的繼承屬性
  • 它只能拷貝可列舉的屬性
  • 它可以拷貝 Symbol 型別的屬性
var obj = {
	name: '張三',
	age: 18,
	sym: Symbol(123)
}
Object.defineProperty(obj, 'aaa', {
    value: '這是一個不可列舉的屬性',
    enumerable: false // 設定為false代表此屬性不可列舉
})
var obj2 = { ...obj }
console.log(obj2)
// 輸出
{ name: '張三', age: 18, sym: Symbol(123) }

從上面可以看出,symbol屬性被拷貝了,但是屬性aaa並沒有被拷貝

3、陣列拷貝方法
concat ()
使用方法我就不多說了,不懂得看我上一篇文章吧

var arr = [{name: '張三'}, 2, {age: 18}]
var brr = arr.concat()
console.log(brr)
arr[0].name = '李四'
console.log(brr)
// 輸出
[ { name: '張三' }, 2, { age: 18 } ]
[ { name: '李四' }, 2, { age: 18 } ]

可以看出,concat()也是淺拷貝

slice()
使用方法見上一篇文章

var arr = [{name: '張三'}, 2, {age: 18}]
var brr = arr.slice()
console.log(brr)
arr[0].name = '李四'
console.log(brr)
// 輸出
[ { name: '張三' }, 2, { age: 18 } ]
[ { name: '李四' }, 2, { age: 18 } ]

和concat一樣,屬於淺拷貝

擴充套件運算子
陣列擴充套件運算子和物件得一模一樣,我就不另外說了

深拷貝

我們可能大多時候最常用到的深拷貝就是JSON.stringify() 了。相信大家都用過
原理就是先將物件轉為json字串,再轉換為物件。

var obj = {
    name: '張三',
    like: {
        music: '老鼠愛大米',
        color: 'red',
        animate: {
            name: '狗'
        }
    }
}
var obj2 = JSON.parse(JSON.stringify(obj))
obj.like.music = '沙漠駱駝'
obj.like.animate.name = '熊貓'
console.log(obj)
console.log(obj2)
// 輸出
{
  name: '張三',
  like: { music: '沙漠駱駝', color: 'red', animate: { name: '熊貓' } }
}
{
  name: '張三',
  like: { music: '老鼠愛大米', color: 'red', animate: { name: '狗' } }
}

可以看出,更改obj 不會影響到拷貝出來的obj2

但是這種方法也存在很多問題

  1. 拷貝的物件的值中如果有函式、undefined、symbol 這幾種型別,經過 JSON.stringify 序列化之後的字串中這個鍵值對會消失,導致無法拷貝到
  2. 拷貝 Date 引用型別會變成字串
  3. 無法拷貝不可列舉的屬性
  4. 拷貝 RegExp 引用型別會變成空物件
  5. 物件中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的結果會變成 null
var obj = {
    base: {
        name: '張三'
    }
}
obj.date = new Date(0)
obj.un = undefined
obj.nu = null
obj.infinity = Infinity
obj.nan = NaN
obj.sym = Symbol(123)
obj.reg = /^123/
Object.defineProperty(obj, 'aaa', {
    value: '這是一個不可列舉的屬性',
    enumerable: false // 設為false代表不可列舉
})
var obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj)
console.log(obj2)

// 輸出
{
  base: { name: '張三' },
  date: 1970-01-01T00:00:00.000Z,
  un: undefined,
  nu: null,
  infinity: Infinity,
  nan: NaN,
  sym: Symbol(123),
  reg: /^123/
}
{
  base: { name: '張三' },
  date: '1970-01-01T00:00:00.000Z',
  nu: null,
  infinity: null,
  nan: null,
  reg: {}
}

從上面可以看出

  • Date物件變成字串了
  • infinity 和 NaN 變成null了
  • un屬性的值是undefined,並沒有被拷貝到,因為這個屬性在序列號時消失了,sym屬性也是一樣消失了
  • RegExp引用型別變成空物件了
  • aaa這個不可列舉的屬性也沒有被拷貝到

那麼,如何實現一個完整的深拷貝方法呢,那就要看我們該如何一一來解決以上問題了

  • 拷貝引用型別屬性時,我們可以通過遞迴來迴圈拷貝
  • 如果屬性是簡單資料型別,包括undefined,infinity 和 NaN, 則直接遞迴賦值
  • 拷貝Date物件時,我們可以直接重新生成一個新的Date例項返回
  • 拷貝RegExp型別時,也是一樣,直接生成一個新的RegExp例項返回
  • 拷貝Symbol屬性和不可列舉的屬性時,使用Reflect.ownKeys (什麼是Reflect.ownKeys)方法
function isObject (obj) {
    if ((typeof obj === 'object' || typeof obj === 'function') && (obj !== null)) {
        return true
    }
}
function deepClone (obj) {
    if (obj.constructor === Date) {
        return new Date(obj) // 返回一個新例項
    }
    
    if (obj.constructor === RegExp) {
        return new RegExp(obj) // 返回一個新例項
    }

    let allDesc = Object.getOwnPropertyDescriptors(obj)
    // 遍歷傳入引數所有鍵的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

    for (let key of Reflect.ownKeys(obj)) { 
        cloneObj[key] = (isObject(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key]) : obj[key]
    }

    return cloneObj
}
var obj = {
    base: {
        name: '張三'
    }
}
obj.date = new Date(0)
obj.un = undefined
obj.nu = null
obj.infinity = Infinity
obj.nan = NaN
obj.sym = Symbol(123)
obj.reg = /^123/
Object.defineProperty(obj, 'aaa', {
    value: '這是一個不可列舉的屬性',
    enumerable: false // 設為false代表不可列舉
})

var obj2 = deepClone(obj)
console.log(obj)
console.log(obj2)

// 輸出
{
  base: { name: '張三' },
  date: 1970-01-01T00:00:00.000Z,
  un: undefined,
  nu: null,
  infinity: Infinity,
  nan: NaN,
  sym: Symbol(123),
  reg: /^123/
}
{
  base: { name: '張三' },
  date: 1970-01-01T00:00:00.000Z,
  un: undefined,
  nu: null,
  infinity: Infinity,
  nan: NaN,
  sym: Symbol(123),
  reg: /^123/
}

從結果可以看出,我們實現的深拷貝函式確實實現了物件的一個深拷貝功能。

深拷貝與淺拷貝就寫到這了。有問題歡迎指出