狠狠滴深拷貝

前言

對象的深淺拷貝,一直是老生常談的話題,平臺上的文章數量可謂是汗牛充棟,要從這塊素材裏找突破幾乎是不可能。索性我就寫一篇文章,積累一下本身的學習心得,以便後續複習的時候,能有一個比較清晰的思路。javascript

定義

淺拷貝:將數據中全部的數據引用下來,並指向同一個存放地址,拷貝的數據修改以後,會對原數據產生反作用。前端

深拷貝:將數據中全部的數據拷貝下來,對拷貝以後的數據進行修改不會對原始數據產生反作用。java

非深拷貝

業務中,不少時候你作的是淺拷貝,若是不影響業務邏輯,你可能不關心這些東西。面試

等號賦值

引用類型的等號賦值是最多見淺拷貝,以下所示:數組

var obj = {
  name: 'Nick'
}

var newObj = obj
複製代碼

此時你修改 newObj.name = 'Chen',則會使得 obj 也會跟着變化,這是由於聲明的 obj 屬於引用類型的變量,存在了全局做用域下的堆內存中。賦值給 newObj,只是將內存的地址賦值給了它,因此修改 newObj 的屬性,也就是修改了堆內存中數據的屬性,從而 obj 也會跟着改變。性能優化

var obj = {
  name: 'Nick'
}

var newObj = obj

newObj.name = 'Chen'

console.log(obj.name) // 'Chen'
console.log(newObj.name) // 'Chen'
複製代碼

Object.assign

你覺得 Object.assign 是深拷貝方法,其實否則。它也是淺拷貝,只不過是第一級的原始類型的數據,不受牽連,引用類型仍是會被篡改,咱們用數聽說話:markdown

var obj = {
  name: 'Nick',
  hobby: ['code', 'movie', 'travel', { a: 1 }]
}

var newObj = Object.assign({}, obj)

newObj.name = 'Chen'
newObj.hobby[0] = 'codeing'
newObj.hobby[3].a = 2

console.log('obj', obj)
console.log('newObj', newObj)
複製代碼

打印結果以下:函數

image.png

綠色箭頭表明原始類型,沒有被篡改。紅色箭頭表明的是引用類型,都隨着 newObj 的修改而變化。工具

... 擴展運算符

它比較特殊,若是要拷貝的對象,第一層是原始類型,則爲深拷貝。若是是引用類型,則爲淺拷貝,不妨作個小實驗:post

var obj = {
  name: 'Nick',
  salary: {
  	high: 1,
    mid: 2,
    low: 3
  }
}

var newObj = { ...obj }

newObj.name = 'Chen'
newObj.salary.high = 2

console.log(obj)
console.log(newObj)
複製代碼

image.png

objname 屬性沒有被改變,salary 中的 high 被改爲了 2。

因此咱們若是想用 ... 擴展運算符完成深拷貝,就得這樣操做:

var obj = {
  name: 'Nick',
  salary: {
  	high: 1,
    mid: 2,
    low: 3
  }
}

var newObj = {
	...obj,
  salary: {
  	...obj.salary
  }
}
複製代碼

我以爲這樣操做的人,確定是有毛病。

JSON.parse + JSON.stringify

不少有志之士,會在代碼中使用這種方式去作深拷貝。固然,多數業務場景中,這種方式仍是比較香的,可是仍是會有那麼些狀況,會出現大大小小的問題。

對象中存在函數:

var obj = {
  name: 'Nick',
  hobby: ['code', 'movie', 'travel', { a: 1 }],
  callback: function() {
    console.log('test')
  }
}

var newObj = JSON.parse(JSON.stringify(obj))

newObj.name = 'Chen'
newObj.hobby[0] = 'codeing'
newObj.hobby[3].a = 2

console.log('obj', obj)
console.log('newObj', newObj)
複製代碼

image.png

確實沒有被關聯到,數據已經脫離了控制,可是函數 callback 麼的了。

對象中存在時間對象 Date

var obj = {
  name: 'Nick',
  date: [new Date(1621259998866), new Date(1621259998866)],
};

var newObj = JSON.parse(JSON.stringify(obj))
複製代碼

image.png

obj 中的 date 內的時間對象被執行了。

對象中存在 RegExp、Error

var obj = {
  name: 'Nick',
  date: new RegExp('\\s+'),
};

var newObj = JSON.parse(JSON.stringify(obj));
obj.name = 'Chen'
複製代碼

image.png

拷貝以後,date 變成了一個空值。

對象中存在 undefined 值

var obj = {
	name: undefiend
}

var newObj = JSON.parse(JSON.stringify(obj));
複製代碼

image.png

undefiend 在拷貝的過程當中,被丟失了。

對象中存在 NaN、Infinity、-Infinity

var obj = {
  name1: NaN,
  name2: Infinity,
  name3: -Infinity
}

var newObj = JSON.parse(JSON.stringify(obj))
複製代碼

image.png 直接所有變成 null,不跟你嘻嘻哈哈,可是這種狀況應該也很少。

對象中存在經過構造函數生產的對象

function Animal(name) {
	this.name = name
}

var animal = new Animal('dog')

var obj = {
	test: animal
}

var newObj = JSON.parse(JSON.stringify(obj))
複製代碼

image.png

直接就把構造函數給丟了,拷貝以後,直接指向了 Object

諸如上述種種的狀況,在真實開發環境中遇到的可能不是不少,可是你真的遇到了,在不知情的狀況下,可能會耗費一些沒必要要的時間去找出問題所在。

狠狠滴深拷貝

首先,大可使用 lodash.cloneDeep 這類工具實現深拷貝,有工具不用,哎,放着玩兒?

這裏我要手動寫一個深拷貝,從中能夠學習到一些小知識點,愛看不看吧,我寫給本身看。

var obj = {
  name: 'Nick',
  date: [new Date(1621261792177)],
  callback: function() { console.log('shadiao') },
  link: undefined
}

function deepClone(origin) {
  if(origin === null) return null 
  if(typeof origin !== 'object') return origin;
  if(origin.constructor === Date) return new Date(origin); 
	// 接受兩個參數,origin 是原對象
  var _target = origin.constructor() //保持繼承鏈
  // 循環 origin
	for(var key in origin) {
    //不遍歷其原型鏈上的屬性
    if (origin.hasOwnProperty(key)) {
    	// 若是 origin[key] 是一個引用類型的值,則進入遞歸邏輯
      if (typeof origin[key] === 'object' && origin[key] !== null) {
        // 進入遞歸,此時原始值就是 origin[key],被賦值的對象是 _target[key]
        // 注意,上述第一次聲明的 _target 將會貫穿整個遞歸,後續全部的賦值,都將會被 return 到 _target
        _target[key] = deepClone(origin[key])
      } else {
        // 若是不是對象或數組,則進入此邏輯,直接賦值給 _target[key]
        _target[key] = origin[key]
      }
    }
  }
  // for...in 循環結束後,return 當前上下文的 _target 值
  return _target
}

const newObj = deepClone(obj)


複製代碼

image.png

上述 obj 對象的屬性都被完整的拷貝下來了。

上述代碼中,有一個關鍵步驟,若是理解了它,基本上你就理解爲何能夠實現遞歸賦值,咱們來看下面這段代碼:

function test() {
	var obj = {}
  const _obj = test1(obj)
  console.log('obj', obj)
  console.log('_obj', _obj)
	console.log(_obj === obj)
}

function test1(_obj) {
	_obj.a = 1
  return _obj
}

test()
複製代碼

image.png

上述代碼,在函數 test 內部聲明 obj 對象,並將其以參數的形式,傳遞給 test1 方法。test1 內部的操做是給傳進來的 _obj 參數賦值一個 a 屬性,而且 return _obj

此時查看打印結果,obj 被也被添加了 a 屬性,而且 _obj 全等於 obj。這說明它們指向了同一個內存地址,就是 test 內的函數做用域。在《JavaScript 高級程序設計》第 86 頁,對引用類型在函數之間的傳遞的知識有詳細的分析。

image.png

利用這個原理,上述 deepClone 方法內部,執行遞歸的時候,所傳進去的 _target[key] ,其實這個 _target 就是第一次執行 deepClone 的引用類型變量,後續遞歸操做對 _target[key] 的賦值,都將反映到最初的 _target。最後函數執行結束,return _target 即是最終遞歸深拷貝後的最終值。

總結

這個知識點很是細節,我不敢說會在業務開發中大量用到。但至少當你遇到這類問題的時候,你不會一頭霧水、傷春悲秋,以爲本身不適合這個行業。再一次強調,基礎知識很重要,不要小看這些平時不起眼的知識,真到了拼刺刀的時候,你一無所知。

往期好文推薦

打通任督二脈的前端環境變量 — env 點贊數👍 228

Vite 2.0 + React + Ant Design 4.0 搭建開發環境 點贊數👍 385

面不面試的,你都得懂原型和原型鏈 點贊數👍 593

Vue 3 和 Webpack 5 來了,手動搭建的知識該更新了 點贊數👍 521

換一個角度分析,網頁性能優化 點贊數👍 200

你好,談談你對前端路由的理解 點贊數👍 625

之前我沒得選,如今我只想用 Array.prototype.reduce 點贊數👍 588

無處不在的發佈訂閱模式 —— 此次必定 點贊數👍 164

聊聊 JSX 和虛擬 DOM 點贊數👍 110

相關文章
相關標籤/搜索