對象的深拷貝和淺拷貝

来源:https://www.cnblogs.com/dabingqi/archive/2018/03/03/8502932.html
-Advertisement-
Play Games

整理自互聯網 整理做隨筆 如有相似純屬抄襲 淺拷貝和深拷貝都是對於JS中的引用類型而言的,淺拷貝就只是複製對象的引用(堆和棧的關係,簡單類型Undefined,Null,Boolean,Number和String是存入堆,直接引用,object array 則是存入桟中,只用一個指針來引用值),如果 ...


整理自互聯網 整理做隨筆 如有相似純屬抄襲

淺拷貝和深拷貝都是對於JS中的引用類型而言的,淺拷貝就只是複製對象的引用(堆和棧的關係,簡單類型Undefined,Null,Boolean,Number和String是存入堆,直接引用,object array 則是存入桟中,只用一個指針來引用值),如果拷貝後的對象發生變化,原對象也會發生變化。只有深拷貝才是真正地對對象的拷貝。

淺拷貝

  淺拷貝的意思就是只複製引用(指針),而未複製真正的值。

const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

const cloneArray = originArray;
const cloneObj = originObj;

console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}

cloneArray.push(6);
cloneObj.a = {aa:'aa'};

console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]

console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}

  

上面的代碼是最簡單的利用 = 賦值操作符實現了一個淺拷貝,可以很清楚的看到,隨著 cloneArray 和 cloneObj 改變,originArray 和 originObj 也隨著發生了變化。

深拷貝

深拷貝就是對目標的完全拷貝,不像淺拷貝那樣只是複製了一層引用,就連值也都複製了。

只要進行了深拷貝,它們老死不相往來,誰也不會影響誰。

目前實現深拷貝的方法不多,主要是兩種:

  1. 利用 JSON 對象中的 parsestringify
  2. 利用遞歸來實現每一層都重新創建對象並賦值

JSON.stringify/parse的方法

先看看這兩個方法吧:

The JSON.stringify() method converts a JavaScript value to a JSON string.

  JSON.stringify 是將一個 JavaScript 值轉成一個 JSON 字元串。

The JSON.parse() method parses a JSON string, constructing the JavaScript value or object described by the string.

  

JSON.parse 是將一個 JSON 字元串轉成一個 JavaScript 值或對象。

很好理解吧,就是 JavaScript 值和 JSON 字元串的相互轉換。

它能實現深拷貝呢?我們來試試。

const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

 確實是深拷貝,也很方便。但是,這個方法只能適用於一些簡單的情況。比如下麵這樣的一個對象就不適用:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "axuebin"}

 

  發現在 cloneObj 中,有屬性丟失了。。。那是為什麼呢?

undefinedfunctionsymbol 會在轉換過程中被忽略。。。

If undefined, a function, or a symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).

  明白了吧,就是說如果對象中含有一個函數時(很常見),就不能用這個方法進行深拷貝

遞歸的方法

遞歸的思想就很簡單了,就是對每一層的數據都實現一次 創建對象->對象賦值 的操作,簡單粗暴上代碼:

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判斷複製的目標是數組還是對象
  for(let keys in source){ // 遍歷目標
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是對象,就遞歸一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接賦值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

  我們來試試:

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

可以。那再試試帶有函數的:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}

  也可以。搞定。

JavaScript中的拷貝方法

我們知道在 JavaScript 中,數組有兩個方法 concatslice 是可以實現對原數組的拷貝的,這兩個方法都不會修改原數組,而是返回一個修改後的新數組。

同時,ES6 中 引入了 Object.assgn 方法和 ... 展開運算符也能實現對對象的拷貝。

那它們是淺拷貝還是深拷貝呢?

concat

 
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.

  

該方法可以連接兩個或者更多的數組,但是它不會修改已存在的數組,而是返回一個新數組。

看著這意思,很像是深拷貝啊,我們來試試:

const originArray = [1,2,3,4,5];
const cloneArray = originArray.concat();

console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];

  

看上去是深拷貝的。

我們來考慮一個問題,如果這個對象是多層的,會怎樣。

const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]

originArray 中含有數組 [1,2,3] 和對象 {a:1},如果我們直接修改數組和對象,不會影響 originArray,但是我們修改數組 [1,2,3] 或對象 {a:1} 時,發現 originArray 也發生了變化。

結論:concat 只是對數組的第一層進行深拷貝。

slice

 
The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.

解釋中都直接寫道是 a shallow copy 了 ~

但是,並不是!

const originArray = [1,2,3,4,5]; const cloneArray = originArray.slice(); console.log(cloneArray === originArray); // false cloneArray.push(6); // [1,2,3,4,5,6] console.log(originArray); [1,2,3,4,5];

  同樣地,我們試試多層的數組。

const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]

  

果然,結果和 concat 是一樣的。

結論:slice 只是對數組的第一層進行深拷貝。

Object.assign()

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target obj

結論:Object.assign() 拷貝的是屬性值。假如源對象的屬性值是一個指向對象的引用,它也只拷貝那個引用值。

... 展開運算符

const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};

const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]

const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}

  結論:... 實現的是對象第一層的深拷貝。後面的只是拷貝的引用值。

首層淺拷貝

我們知道了,會有一種情況,就是對目標對象的第一層進行深拷貝,然後後面的是淺拷貝,可以稱作“首層淺拷貝”。

我們可以自己實現一個這樣的函數:

function shallowClone(source) {
  const targetObj = source.constructor === Array ? [] : {}; // 判斷複製的目標是數組還是對象
  for (let keys in source) { // 遍歷目標
    if (source.hasOwnProperty(keys)) {
      targetObj[keys] = source[keys];
    }
  }
  return targetObj;
}

  我們來測試一下:

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = shallowClone(originObj);
console.log(cloneObj === originObj); // false
cloneObj.a='aa';
cloneObj.c=[1,1,1];
cloneObj.d.dd='surprise';

  

經過上面的修改,cloneObj 不用說,肯定是 {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}} 了,那 originObj 呢?剛剛我們驗證了 cloneObj === originObjfalse,說明這兩個對象引用地址不同啊,那應該就是修改了 cloneObj 並不影響 originObj
 
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}}
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'surprise'}}

  

What happend?

originObj 中關於 ac都沒被影響,但是 d 中的一個對象被修改了。。。說好的深拷貝呢?不是引用地址都不一樣了嗎?

原來是這樣:

  1. shallowClone 的代碼中我們可以看出,我們只對第一層的目標進行了 深拷貝 ,而第二層開始的目標我們是直接利用 = 賦值操作符進行拷貝的。
  2. so,第二層後的目標都只是複製了一個引用,也就是淺拷貝。
 

總結

  1. 賦值運算符 = 實現的是淺拷貝,只拷貝對象的引用值;
  2. JavaScript 中數組和對象自帶的拷貝方法都是“首層淺拷貝”;
  3. JSON.stringify 實現的是深拷貝,但是對目標對象有要求(非 undefined,function);
  4. 若想真正意義上的深拷貝,請遞歸。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 之前看源碼都是在Windows下用SourceInsight看,雖然達到了研究源碼的效果,但終究還是有遺憾。。。趁著周末,準備在Ubuntu虛擬機上下載編譯源碼。 之前下源碼時,有瞭解一些Android源碼的情況。網上的教程很多也是從谷歌官網下源碼,但是最近藍燈不好用,翻牆效率有點低,而且翻牆的網速 ...
  • 接上篇《Android進階之光》--Android新特性 No1: 組件: 1)底部工作條-Bottom Sheets 2)卡片-Cards 3)提示框-Dialogs 4)菜單-Menus 5)選擇器 6)滑塊控制項-Sliders 7)進度和動態 8)Snackbar(底部可操作彈出框)與Toas ...
  • Android 5.0新特性 1)全新的Material Design設計風格 2)支持多種設備 3)全新的通知中心設計--按照優先順序顯示 4)支持64位ART虛擬機 5)多任務視窗Overview 6)設備識別解鎖--比如附近信任設備 7)Ok Google語音指令 8)Face unlock面部 ...
  • 阿裡推薦原因:使用線程池可以減少創建和銷毀線程上所花的時間以及系統資源的開銷,然後之所以不用Executors自定義線程池,用ThreadPoolExecutor是為了規範線程池的使用,還有讓其他人更好懂線程池的運行規則。先說一下關於線程的概念任務:線程需要執行的代碼,也就是Runnable任務隊列 ...
  • 1. 什麼是面向對象? 以下一段話是我在百度上找的解釋: 面向對象(Object Oriented,OO)是軟體開發方法。面向對象的概念和應用已超越了程式設計和軟體開發,擴展到如資料庫系統、互動式界面、應用結構、應用平臺、分散式系統、網路管理結構、CAD技術、人工智慧等領域。面向對象是一種對現實世界 ...
  • UI線程很忙,忙著繪製界面,忙著響應用戶操作,忙著執行App程式員書寫的**多數**代碼 ...
  • 創建對象 第一種:基於Object對象 第二種:對象字面量方式(比較清楚的查找對象包含的屬性及方法) 使用Object構造函數或對象字面量都可以創建對象,但缺點是創建多個對象時,會產生大量的重覆代碼,因此下麵介紹可解決這個問題的創建對象的方法 1、工廠模式 缺點:創建對象交給一個工廠方法來實現,可以 ...
  • 今天終於開始寫代碼了。 希望明天也能恢復更新吧,寒假在家裡實在太過放鬆了,關於寒假,明天拿出時間來好好總結一下,到底幹了些什麼。 用readyState輸出狀態碼是0,未初始化?什麼意思,不太懂…… 問題暫時先留著這裡,應該是很簡單的問題,11點要斷電,不多想了。 大家晚安。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...