面試如何寫出一個滿意的深拷貝(適合初級前端)

前言

已經有不少關於深拷貝與淺拷貝的文章,爲何本身還要寫一遍呢💯javascript

學習就比如是座大山,人們沿着不一樣的路爬山,分享着本身看到的風景。你不必定能看到別人看到的風景,體會到別人的心情。只有本身去爬山,才能看到不同的風景,體會才更加深入。java

分享一個不錯的思惟導圖👇git

深拷貝
深拷貝

經過文本的總結,但願能夠明白:github

  • 什麼是深拷貝/淺拷貝,他們與賦值有什麼區別
  • 深拷貝/淺拷貝實現方式有哪些

本章節直接從拷貝開始提及,對於基本數據類型,引用數據類型以前的區別,能夠看看上面的思惟導圖👆web

或者看看我以前的章節補一補基礎,有寫的不對的地方歡迎指出!面試


引用數據類型拷貝

對於引用數據類型的話,細分能夠分爲下面三個方面segmentfault

  • 賦值
  • 淺拷貝
  • 深拷貝

賦值

引用類型的賦值是傳址。只是改變指針的指向,例如,引用類型的賦值是對象保存在棧中的地址的賦值,這樣的話兩個變量就指向同一個對象,所以二者之間操做互相有影響。例如:數組

var a = {}; // a保存了一個空對象的實例
var b = a; // a和b都指向了這個空對象  a.name = 'jozo'; console.log(a.name); // 'jozo' console.log(b.name); // 'jozo'  b.age = 22; console.log(b.age);// 22 console.log(a.age);// 22  console.log(a == b);// true 複製代碼
拷貝2
拷貝2

這樣子的狀況,會致使a和b指向同一份數據,對其中一個進行修改數據的話,會影響到另一個,實際開發中,這不是咱們預期中的結果,這會照成某種程度上的bug。編輯器

那麼咱們如何不讓相互之間產生影響呢?一種簡單的辦法就是拷貝一份a變量的數據,因此根據拷貝的層次不一樣能夠分爲淺拷貝和深拷貝,淺拷貝的話知識進行一層拷貝,深拷貝的話是無限層次的拷貝!函數

咱們先來實現一個淺拷貝

let shallowClone = source => {
 let target = {}  for(let i in source) {  if( source.hasOwnProperty(i) )  target[i] = source[i];  }  return target  }  let demo = {  b:{  c : {  }  }  }  let demo2 = shallowClone(demo)  let demo3 = demo;  console.log(demo3 === demo ) // true  console.log(demo2.b.c === demo.b.c ) // true  console.log(demo2.b === demo.b ) // true  console.log(demo2 === demo ) // false 複製代碼

demo3 = demo 賦值的話,是地址的賦值,也就是說指向同一個對象,那麼不是咱們想要的結果,咱們來看看shallowClone函數,這個是淺拷貝的一種實現方式,那麼demo2變量應該就是實現了一層的拷貝,正如20行效果,demo2變量是在堆中開了一個新內存,因此二者指向不一樣對象,demo2.b === demo.b 爲 true 說明 這就是淺拷貝效果,簡單的拷貝一層,那麼咱們是否是能夠遞歸的思想去完成深拷貝呢?


淺拷貝的實現方式

Object.assign()

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

let demo = {
 name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = Object.assign({}, demo)  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price);  // dayday 100 複製代碼

修改上面代碼demo變量以後,對象clone_demo基本屬性沒有改變,可是修改demo對象中book引用屬性時,對象clone_demo相應位置屬性值也發生改變,一樣的接下來展開運算符也是同樣效果👇

展開運算符...

let demo = {
 name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = {...demo}  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price);  // dayday 100 複製代碼

咱們能夠看到展開運算… 效果跟Object.assign() 效果是同樣的。

Array.prototype.slice()

slice() 方法返回一個新的數組對象,這一對象是一個由 beginend(不包括end)決定的原數組的淺拷貝。原始數組不會被改變。

let a = [0, "1", [2, 3]];
let b = 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", [4, 3]] 複製代碼

能夠看出,改變 a[1] 以後 b[0] 的值並無發生變化,但改變 a[2][0] 以後,相應的 b[1][0] 的值也發生變化。說明 slice() 方法是淺拷貝,相應的還有concat等,在工做中面對複雜數組結構要額外注意。


深拷貝實現方式

深拷貝會拷貝全部的屬性,並拷貝屬性指向的動態分配的內存。當對象和它所引用的對象一塊兒拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢而且花銷較大。拷貝先後兩個對象互不影響。

JSON.parse(JSON.stringify(obj))

let demo = {
 name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = JSON.parse(JSON.stringify(demo))  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price);  // dayday 45 複製代碼

徹底改變變量 demo 以後對 clone_demo 沒有任何影響,這就是深拷貝的魔力。

一樣的對於數組使用該方法也是能夠達到深拷貝的。

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

對於undefined symbol 函數三種狀況會直接忽略

let demo = {
 name : 'dayday',  h1 : undefined,  h2 : Symbol('dayday'),  h3 : function () {},  }  let clone_demo = JSON.parse(JSON.stringify(demo))  console.dir(clone_demo)  // { name : 'dayday' } 複製代碼

循環引用狀況下,會報錯。

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();
// Wed Jul 01 2020 16:19:07 GMT+0800 (中國標準時間) {}  JSON.stringify(new Date()); // ""2020-07-01T08:19:19.860Z""  JSON.parse(JSON.stringify(new Date())); // "2020-07-01T08:19:35.569Z"  複製代碼

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

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

正則狀況下

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

PS:爲何會存在這些問題能夠學習一下 JSON

除了上面介紹的深拷貝方法,經常使用的還有jQuery.extend()lodash.cloneDeep(),因爲文章篇幅的問題,這裏就很少介紹了,有興趣的能夠本身去了解了解

面試如何實現一個深拷貝

面試官叫你實現一個深拷貝的話,你只要記得淺拷貝+遞歸,淺拷貝的時候,去判斷是否是一個對象就行的,是對象的話,就進行遞歸操做。

以前的簡單淺拷貝:

let shallowClone = source => {
 let target = {}  for(let key in source) {  if(Object.prototype.hasOwnProperty.call(source, key)){  target[key] = typeof source[key] === 'object' ? shallowClone(source[key]) : source[key];  }  }  return target  }  let demo = {  name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  }  }  let clone_demo = shallowClone(demo);  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  console.log(clone_demo.name,clone_demo.book.price)  // dayday 45 複製代碼

寫到這裏,至少一個簡單的深克隆實現了,可是仍是有些問題沒有解決!

  • 沒有考慮數組的寫法
  • 對對象的判斷邏輯不嚴謹,由於 typeof null === object
  • 沒有對傳入參數校驗,好比傳入null 應該返回 null 而不是 {}

首先的寫一個兼容數組而且判斷null方法的函數

let isObject = obj => typeof obj === 'object' && obj !== null ;
複製代碼

那麼進一步完善了深度拷貝的方法

// 保留數組 而且判斷是否是null
 let isObject = obj => typeof obj === 'object' && obj !== null ;  let shallowClone2 = source => {   if(!isObject(source)) return source // 非對象返回自身  let target = Array.isArray(source) ? [] : {}  for(let key in source) {  if(Object.prototype.hasOwnProperty.call(source, key)){  target[key] = isObject(source[key]) ? shallowClone2(source[key]) : source[key];  }  }  return target  }  let demo = {  name : 'dayday',  book : {  title : 'Do you really Know JS',  price : "45"  },  h1 : null,  h2 : [1,2,3],  h3 : undefined  }  let clone_demo = shallowClone2(demo);  console.log(clone_demo);  demo.name = 'new name'  demo.book.price = '100'  demo.h2[1] = 'new data'  console.log(clone_demo.name,clone_demo.book.price)  // dayday 45  console.log(clone_demo);  // 修改demo值爲能影響clone_demo 複製代碼

這篇文章寫的很好:深拷貝的終極探索(99%的人都不知道)

它還對深度拷貝有了新的優化,好比JSON.parse(JSON.stringify(obj))循環引用拋出異常的問題,作出了優化,那咱們試着去優化這個小問題。

  • 哈希表

對於循環檢測的話,咱們可使用哈希檢測的方法,好比設置一個數組或者是已經拷貝的對象,當檢測到對象已經存在哈希表時,就取出該值🤭

let isObject = obj => typeof obj === 'object' && obj !== null;
 let shallowClone3 = (source, hash = new WeakMap()) => {   if (!isObject(source)) return source // 非對象返回自身  if (hash.has(source)) return hash.get(source) // 新增檢測, 查哈希表  let target = Array.isArray(source) ? [] : {}  hash.set(source, target) // 設置哈希表值  for (let key in source) {  if (Object.prototype.hasOwnProperty.call(source, key)) {  target[key] = isObject(source[key]) ? shallowClone3(source[key], hash) : source[key]; // 傳入哈希表  }  }  return target  }  let obj = {  a: 1,  b: {  c: 2,  d: 3  }  }  obj.a = obj.b;  obj.b.c = obj.a;  let clone_obj = shallowClone3(obj)  console.log(clone_obj) 複製代碼

寫完這段代碼的話,至少面試實現一個這樣子的深拷貝馬馬虎虎過的去,固然了仍是有不少的問題須要解決的:

  • 好比拷貝一個Symbol類型的值該這麼解決
  • 這麼解決遞歸爆棧問題

固然了有興趣的讀者能夠深刻的瞭解吶🚀

總結

-- 和原數據是否指向同一對象 第一層數據爲基本數據類型 原數據中包含子對象
賦值 改變會使原數據一同改變 改變會使原數據一同改變
淺拷貝 改變會使原數據一同改變 改變會使原數據一同改變
深拷貝 改變會使原數據一同改變 改變會使原數據一同改變

參考

JavaScript深拷貝的一些坑

面試題之如何實現一個深拷貝

深刻剖析 JavaScript 的深複製

MDN展開語法

MDN之Object.assign()

深拷貝的終極探索(99%的人都不知道)

本文使用 mdnice 排版

相關文章
相關標籤/搜索