淺探js深拷貝和淺拷貝

Fundebug發表於2018-11-15

物件和陣列的拷貝對我來說一直都是一個比較模糊的概念,一直有點一知半解,但是在實際工作中又偶爾會涉及到,有時候還會一不小心掉坑裡,不知道大家有沒有同樣的感受,因此,準備對js物件和陣列拷貝一探究竟。提到js的物件和陣列拷貝,大家一定會想深拷貝和淺拷貝,但是為什麼會有深拷貝和淺拷貝呢?下面就讓我簡單介紹一下為什麼拷貝會有深淺之分以及有什麼區別?

原因及區別

我們都知道js中有兩種資料型別,一種是基本資料型別,一種是引用資料型別,基本資料型別是按值訪問的,即在操作基本型別的變數時,是直接修改變數的值,而引用資料型別的值是按引用訪問的,什麼叫按引用訪問的呢?js的引用型別,也叫物件型別,是儲存在記憶體中的,而在js中又無法直接操作記憶體中的物件,實際上操作的是物件的引用,因此在引用型別變數在進行復制操作時,並不是對物件值的直接複製,而是將物件的引用複製給了另一個變數,實際上變數指向的是同一個記憶體地址中物件的值,因此只要改變其中一個物件變數的值另外一個就會一起改變,這就是我們常說的淺拷貝。而在深拷貝中,會開闢一個新的記憶體地址用來存放新物件的值,兩個物件對應兩個不同的記憶體地址 ,修改一個物件並不會對另外一個物件產生影響。接下來就讓我們更細緻的探究js中的深淺拷貝。

淺拷貝

實現淺拷貝的方法有多種,讓我們先來看看js中提供的幾個自帶方法實現淺拷貝的的例子:

  • Object.assign()方法用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件。它將返回目標物件。注意:Object.assign()拷貝的是屬性值,假如源物件的屬性值是一個指向物件的引用,它也只拷貝那個引用值,來看個例子:
var a = {a : 'old', b : { c : 'old'}}
var b = Object.assign({}, a)
b.a = 'new'
b.b.c = 'new'
console.log(a) // { a: 'old', b: { c: 'new' } }
console.log(b) // { a: 'new', b: { c: 'new' } }
複製程式碼

如上面例子,當拷貝的源物件的屬性值是一個物件時,拷貝的只是物件的引用值,因此當修改屬性值的時候兩個物件的屬性值都會發生更新

  • Array.prototype.slice()方法提取並返回一個新的陣列,如果源陣列中的元素是個物件的引用,slice會拷貝這個物件的引用到新的陣列,來看個例子:
var arr = ['a', 'b', {d: 'old'}]
var arr1 = arr.slice(1)
arr1[1].d = 'new'
console.log(arr[2].d) // new
複製程式碼

如上例所示,但源陣列中的元素是物件引用時,slice拷貝的是這個物件的引用,因此當修改其中一個的值時,兩個陣列中的值都會發生改變

  • Array.prototype.concat()用於合併多個陣列,並返回一個新的陣列,和slice方法類似,當源陣列中的元素是個物件的引用,concat在合併時拷貝的就是這個物件的引用,來看個例子:
var arr1 = [{a: 'old'}, 'b', 'c']
var arr2 = [{b: 'old'}, 'd', 'e']
var arr3 = arr1.concat(arr2)
arr3[0].a = 'new'
arr3[3].b = 'new'
console.log(arr1[0].a) // new
console.log(arr2[0].b) // new
複製程式碼

除了上述js中自帶方法實現的淺拷貝外,我們自己如何簡單實現一個淺拷貝呢?來看個例子:

function copy(obj) {
  if (!obj || typeof obj !== 'object') {
    return
  }

  var newObj = obj.constructor === Array ? [] : {}
  for (var key in obj) {
    newObj[key] = obj[key]
  }
  return newObj
}
var a = {b: 'bb', c: 'cc',  d: {e: 'ee'}}
var b = copy(a)
console.log(b) // { b: 'bb', c: 'cc', d: { e: 'ee' } }
複製程式碼

實現一個淺拷貝,就是遍歷源物件,然後在將物件的屬性的屬性值都放到一個新物件裡就ok了,是不是很簡單呢?

深拷貝

先來介紹一個做深拷貝最簡單粗暴的方法JSON.stringify()和JSON.parse()的混合配對使用,相信大家對這兩個方法都是非常熟悉的,來看個例子:

var obj = {a: {b: 'old'}}
var newObj = JSON.parse(JSON.stringify(obj))
newObj.a.b = 'new'
console.log(obj) // { a: { b: 'old' } }
console.log(newObj) // { a: { b: 'new' } }
複製程式碼

上述例子可以看出,使用JSON.stringify()和JSON.parse()確實可以實現深拷貝,在新物件中修改物件的引用時,並不會影響老物件裡面的值,那麼,這麼個方法是否就沒有缺陷了呢?在JSON.stringify()做序列時,undefined、任意的函式以及symbol值,在序列化過程中會被忽略,這會在物件複製的時候導致什麼後果呢?來看一個例子:

var obj = {a: {b: 'old'}, c:undefined, d: function () {}, e:  Symbol('')}
var newObj = JSON.parse(JSON.stringify(obj))
newObj.a.b = 'new'
console.log(obj) // { a: { b: 'old' }, c: undefined, d: [Function: d], e: Symbol() }
console.log(newObj) // { a: { b: 'new' } }
複製程式碼

從例子中可以看到,當源物件中有undefine、function、symbol時,在序列化操作的時候會被忽略,導致拷貝生成的物件中沒有對應屬性及屬性值。那麼怎麼自己去實現一個深拷貝呢?比較常見的方法就是通過遞迴,來看個例子:

function copy(obj) {
  if (!obj || typeof obj !== 'object') {
    return
  }
  var newObj = obj.constructor === Array ? [] : {}
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (typeof obj[key] === 'object') {
        newObj[key] = copy(obj[key])
      } else {
        newObj[key] = obj[key]
      }
    }
  }
  return newObj
}

var old = {a: 'old', b: {c: 'old'}}
var newObj = copy(old)
newObj.b.c = 'new'
console.log(old) // { a: 'old', b: { c: 'old' } }
console.log(newObj) // { a: 'old', b: { c: 'new' } }
複製程式碼

通過對需要拷貝的物件的屬性進行遞迴遍歷,如果物件的屬性不是基本型別時,就繼續遞迴,知道遍歷到物件屬性為基本型別,然後將屬性和屬性值賦給新物件。

總結

以上對js深拷貝和淺拷貝做了簡單的介紹,在深拷貝的實現上也只介紹了最簡單的實現形式,並未考慮複雜情況以及相應優化,想要對深拷貝有更深入的瞭解,需要大家花時間去深入研究,或者可以關注我後續文章的動態。

這篇文章如果有錯誤或不嚴謹的地方,歡迎批評指正,如果喜歡,歡迎點贊收藏。

原文地址: segmentfault.com/u/liwenjie_…

相關文章