javascript學習總結之對象的深拷貝和淺拷貝

  • 2019 年 10 月 27 日
  • 筆記

前言

最近在寫ES6的文章的時候發現重複遇到關於javascript深拷貝和淺拷貝的問題,然後查找了一些資料,根據資料和自己的理解做了以下筆記,畢竟javascript關於深拷貝和淺拷貝的問題在一些面試的時候有些面試官可能會進行提問,一起來看看吧!

數據類型

在了解淺拷貝和深拷貝之前,我們先回顧一下javascript中的數據類型,因為在講淺拷貝和深拷貝的時候就是就是對原始數據類型(基本數據類型)和對象數據類型(引用數據類型)的拷貝

在javascript中,我們將數據類型分為兩種,原始數據類型(基本數據類型)和對象類型(引用數據類型)

基本數據類型

基本數據類型的值是按值訪問的,基本數據類型的值是不可變的

常見的基本數據類型:Number,String,Boolean,Undefined,Null

引用數據類型

引用類型的值是按引用訪問的,引用類型的值是動態可變的

常見的引用類型:Object,Function,Array

由於數據類型的訪問方式不同,它們的比較方式也是不一樣的,我們來看一下下面的示例

(1)基本數據類型和引用數據類型的比較

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>深拷貝和淺拷貝入門</title>      </head>      <body>          <script type="text/javascript">              var a=100;              var b=100;              console.log(a===b);//true              var c={a:1,b:2};              var d={a:1,b:2};              console.log(c===d);//false          </script>      </body>  </html>

總結

  • 基本數據類型的比較是值的比較,所以在示例中a===b為true
  • 引用類型的比較是引用地址的比較,所以在示例c===d為false,因為c和d的地址不同

鑒於綜上兩點我們大概知道所謂的淺拷貝和深拷貝可能就是對於值的拷貝和引用的拷貝(基本數據類型都是對值的拷貝),在這裡主要講解關於引用類型的拷貝

淺拷貝

淺拷貝是對象共用一個記憶體地址,對象的變化相互影響。比如常見的賦值引用就是淺拷貝

(1)簡單對象的淺拷貝

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>對象的淺拷貝</title>      </head>      <body>          <script type="text/javascript">              var obj1={name:'cat'};              var obj2=obj1;              obj2.name='dog';              console.log(obj1);//{name:'dog'}              console.log(obj2);//{name:'dog'}          </script>      </body>  </html>

我們發現當我們改變obj2的值的時候obj1的值也會發生改變,這裡到底發生了什麼,請看圖解

當我們將obj2的值賦值給obj1的時候,僅僅只是將obj2的地址給了obj1而不是obj1重新在記憶體中開闢空間,所以obj1的地址和obj2的地址指向相同,改變obj2的時候obj1也會發生改變。

(2)使用循環實現淺拷貝

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>使用循環實現淺拷貝</title>      </head>      <body>          <script type="text/javascript">              var person={                  name:'tt',                  age:18,                  friends:['aa','bb','cc']              }              function shallowCopy(source){                  if(!source||typeof source!=='object'){                      throw new Error('error');                  }                  var targetObj=source.constructor===Array?[]:{};                  for(var keys in source){                      if(source.hasOwnProperty(keys)){                          targetObj[keys]=source[keys]                      }                  }                  return targetObj;              }              var p1=shallowCopy(person);              console.log(p1);//{name:'tt',age:18,friends:['aa','bb','cc']}          </script>      </body>  </html>

在上面的程式碼中,我們創建了shallowCopy函數,它接收一個參數也就是被拷貝的對象,步驟分別是

(1):首先創建了一個對象
(2):然後for…in循環傳進去的對象為了避免循環到原型上面會被遍歷到的屬性,使用 hasOwnProperty 限制循環只在對象自身,將被拷貝對象的每一個屬性和值添加到創建的對象當中
(3):最後返回這個對象

那麼看到這裡,我們發現p1拿到了和person一樣的對象,那麼p1=person又有什麼區別了,我們看下下面的示例

(3)簡單對象的淺拷貝

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>簡單對象的淺拷貝</title>      </head>      <body>          <script type="text/javascript">              var person={                  name:'tt',                  age:18,                  friends:['oo','cc','yy']              }              function shallowCopy(source){                  if(!source||typeof source!=='object'){                      throw new Error('error');                  }                  var targetObj=source.constructor===Array?[]:{};                  for(var keys in source){                      if(source.hasOwnProperty(keys)){                          targetObj[keys]=source[keys]                      }                  }                  return targetObj;              }              var p1=shallowCopy(person);              var p2=person;              //這個時候我們修改person的數據              person.name='tadpole';              person.age=19;              person.friends.push('tt');              console.log(p2.name);//tadpole              console.log(p2.age);//19              console.log(p2.friends);//['oo','cc','yy','tt']              console.log(p1.name);//tt              console.log(p1.age);//18              console.log(p1.friends);//['oo','cc','yy','tt']          </script>      </body>  </html>

上面創建了一個新變數p2,將person的值賦值給p2,然後比較這兩個值

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

 

深拷貝

深拷貝是將對象放到一個新的記憶體中,兩個對象的改變不會相互影響或者你可以理解為淺拷貝由於只是複製一層對象的屬性,當遇到有子對象的情況時,子對象就會互相影響。所以,深拷貝是對對象以及對象的所有子對象進行拷貝

(1)遞歸調用淺拷貝實現深拷貝

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>遞歸實現深拷貝</title>      </head>      <body>          <script type="text/javascript">              var obj1={                  name:'cat',                  show:function(){                      console.log('名稱:'+this.name);                  }              }              var obj2=deepClone(obj1);              obj2.name='pig';              obj1.show();//cat              obj2.show();//pig              function deepClone(obj){                  var objClone=Array.isArray(obj)?[]:{};                  if(obj&&typeof obj==='object'){                      for(key in obj){                          if(obj.hasOwnProperty(key)){                              //判斷obj子元素是否為對象,如果是,遞歸複製                              if(obj[key]&&typeof obj[key]==='object'){                                  objClone[key]=deepClone(obj[key])                              }else{                                  //如果不是,簡單複製                                  objClone[key]=obj[key]                              }                          }                      }                  }                  return objClone;              }            </script>      </body>  </html>

對於深拷貝的對象,改變源對象不會對得到的對象有影響。只是在拷貝的過程中源對象的方法丟失了,這是因為在序列化 JavaScript 對象時,所有函數和原型成員會被有意忽略

(2)利用 JSON 對象中的 parse 和 stringify實現深拷貝

JOSN 對象中的 stringify 可以把一個 js 對象序列化為一個 JSON 字元串,parse 可以把 JSON 字元串反序列化為一個 js 對象,通過這兩個方法,也可以實現對象的深拷貝

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>利用 JSON 對象中的 parse 和 stringify</title>      </head>      <body>          <script type="text/javascript">              var obj1={                  name:'cat',                  show:function(){                      console.log(this.name);                  }              }              var obj2=JSON.parse(JSON.stringify(obj1));              obj2.name='dog';              console.log(obj1.name);//cat              console.log(obj2.name);//dog              obj1.show();//cat              obj2.show();//TypeError: obj2.show is not a function          </script>      </body>  </html>

注意:JSON.parse()和JSON.stringify()能正確處理的對象只有Number、String、Array等能夠被json表示的數據結構,因此函數這種不能被json表示的類型將不能被正確處理,經過轉換之後,function丟失了,因此JSON.parse()和JSON.stringify()還是需要謹慎使用

(3)使用Object.assgin()方法實現深拷貝

這種方法我在javascript學習總結之Object.assign()方法詳解有過講解,但是我在看資料的時候有發現了一點點的問題,所以在這裡補充一下

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

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>使用Object.assgin()方法</title>      </head>      <body>          <script type="text/javascript">              let srcObj = {'name': 'lilei', 'age': '20'};              let copyObj2 = Object.assign({}, srcObj, {'age': '21'});              copyObj2.age = '23';              console.log(srcObj);//{name:'lilei',age:20}              console.log(copyObj2);//{name:'lilei',age:23}          </script>      </body>  </html>

看起來好像是深拷貝了,那其實這裡let copyObj2 = Object.assign({}, srcObj, {'age': '21'}); 我們把srcObj 給了一個新的空對象。同樣目標對象為 {},我們再來測試下

<!DOCTYPE html>  <html>      <head>          <meta charset="UTF-8">          <title>使用Object.assgin()方法</title>      </head>      <body>          <script type="text/javascript">              let srcObj = {'name': 'lilei', 'age': '20'};              let copyObj2 = Object.assign({}, srcObj, {'age': '21'});              copyObj2.age = '23';              console.log(srcObj);//{name:'lilei',age:20}              console.log(copyObj2);//{name:'lilei',age:23}              srcObj = {'name': '', grade: {'chi': '50', 'eng': '50'} };              copyObj2 = Object.assign({}, srcObj);              copyObj2.name = '';              copyObj2.grade.chi = '60';              console.log(srcObj);//{name:'紅',grade:{chi:60,eng:50}}          </script>      </body>  </html>

從例子中可以看出,改變複製對象的name 和 grade.chi ,源對象的name沒有變化,但是grade.chi卻被改變了。因此我們可以看出Object.assign()拷貝的只是屬性值,假如源對象的屬性值是一個指向對象的引用,它也只拷貝那個引用值。 
也就是說,對於Object.assign()而言, 如果對象的屬性值為簡單類型(string, number),通過Object.assign({},srcObj);得到的新對象為‘深拷貝’;如果屬性值為對象或其它引用類型,那對於這個對象而言其實是淺拷貝的。這是Object.assign()特別值得注意的地方,補充一句,Object.assig({},src1,src2) 對於scr1和src2之間相同的屬性是直接覆蓋的,如果屬性值為對象,是不會對對象之間的屬性進行合併的。

總結

本篇部落格主要講解了數據類型,淺拷貝的實現方式,深拷貝的實現方式,從數據類型的講解中一步一步引入到關於淺拷貝和深拷貝的實現方式,在這裡我們必須學會關於遞歸實現深拷貝的實現方式,這個有可能在面試的時候會實現手寫程式碼。