前言
為什么寫拷貝這篇文章?同事有一天提到了拷貝,他說賦值就是一種淺拷貝方式,另一個同事說賦值和淺拷貝并不相同。
我也有些疑惑,于是我去MDN搜一下拷貝相關內容,發現并沒有關于拷貝的實質概念,沒有辦法只能通過實踐了,同時去看一些前輩們的文章總結了這篇關于拷貝的內容,本文也屬于公眾號【程序員成長指北】學習路線中【JS必知必會】內容。
基本類型:undefined,null,Boolean,String,Number,Symbol
引用類型:Object,Array,Date,Function,RegExp等
存儲方式基本類型:基本類型值在內存中占據固定大小,保存在棧內存中(不包含閉包中的變量)
引用類型:引用類型的值是對象,保存在堆內存中。而棧內存存儲的是對象的變量標識符以及對象在堆內存中的存儲地址(引用),引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址后從堆中獲得實體。
注意:
閉包中的變量并不保存在棧內存中,而是保存在堆內存中。這一點比較好想,如果閉包中的變量保存在了棧內存中,隨著外層中的函數從調用棧中銷毀,變量肯定也會被銷毀,但是如果保存在了堆內存中,內存函數仍能訪問外層已銷毀函數中的變量。看一段對應代碼理解下:
function A() { let a = "koala" function B() { console.log(a) } return B }
本篇所講的淺拷貝和深拷貝都是對于引用類型的,對于基礎類型不會有這種操作。
賦值操作 基本數據類型復制看一段代碼
let a ="koala"; let b = a; b="程序員成長指北"; console.log(a); // koala
基本數據類型復制配圖:
結論:在棧內存中的數據發生數據變化的時候,系統會自動為新的變量分配一個新的之值在棧內存中,兩個變量相互獨立,互不影響的。
引用數據類型復制看一段代碼
let a = {x:"kaola", y:"kaola1"} let b = a; b.x = "程序員成長指北"; console.log(a.x); // 程序員成長指北
引用數據類型復制配圖:
結論:引用類型的復制,同樣為新的變量b分配一個新的值,報錯在棧內存中,不同的是這個變量對應的具體值不在棧中,棧中只是一個地址指針。兩個變量地址指針相同,指向堆內存中的對象,因此b.x發生改變的時候,a.x也發生了改變。
不知道的api我一般比較喜歡看MDN,淺拷貝的概念MDN官方并沒有給出明確定義,但是搜到了一個函數Array.prototype.slice,官方說它可以實現原數組的淺拷貝。
對于官方給的結論,我們通過兩段代碼驗證一下,并總結出淺拷貝的定義。
第一段代碼:
var a = [ 1, 3, 5, { x: 1 } ]; var b = Array.prototype.slice.call(a); b[0] = 2; console.log(a); // [ 1, 3, 5, { x: 1 } ]; console.log(b); // [ 2, 3, 5, { x: 1 } ];
從輸出結果可以看出,淺拷貝后,數組a[0]并不會隨著b[0]改變而改變,說明a和b在棧內存中引用地址并不相同。
第二段代碼
var a = [ 1, 3, 5, { x: 1 } ]; var b = Array.prototype.slice.call(a); b[3].x = 2; console.log(a); // [ 1, 3, 5, { x: 2 } ]; console.log(b); // [ 1, 3, 5, { x: 2 } ];
從輸出結果可以看出,淺拷貝后,數組中對象的屬性會根據修改而改變,說明淺拷貝的時候拷貝的已存在對象的對象的屬性引用。
淺拷貝定義
通過這個官方的slice淺拷貝函數分析淺拷貝定義:
新的對象復制已有對象中非對象屬性的值和對象屬性的引用。如果這種說法不理解換一種一個新的對象直接拷貝已存在的對象的對象屬性的引用,即淺拷貝。淺拷貝實例 Object.assign
語法:
語法:Object.assign(target, ...sources)
ES6中拷貝對象的方法,接受的第一個參數是拷貝的目標target,剩下的參數是拷貝的源對象sources(可以是多個)
舉例說明:
let target = {}; let source = {a:"koala",b:{name:"程序員成長指北"}}; Object.assign(target ,source); console.log(target); // { a: "koala", b: { name: "程序員成長指北" } } source.a = "smallKoala"; source.b.name = "程序員成長指北哦" console.log(source); // { a: "smallKoala", b: { name: "程序員成長指北哦" } } console.log(target); // { a: "koala", b: { name: "程序員成長指北哦" } }
從打印結果可以看出,Object.assign是一個淺拷貝,它只是在根屬性(對象的第一層級)創建了一個新的對象,但是對于屬性的值是對象的話只會拷貝一份相同的內存地址。
Object.assign注意事項
只拷貝源對象的自身屬性(不拷貝繼承屬性)
它不會拷貝對象不可枚舉的屬性
undefined和null無法轉成對象,它們不能作為Object.assign參數,但是可以作為源對象
Object.assign(undefined) // 報錯 Object.assign(null) // 報錯 let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true
屬性名為 Symbol 值的屬性,可以被Object.assign拷貝。
Array.prototype.slice這個函數在淺拷貝概念定義的時候已經進行了分析,看上文。
Array.prototype.concat語法
var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
參數:將數組和/或值連接成新數組
舉例說明
let array = [{a: 1}, {b: 2}]; let array1 = [{c: 3},{d: 4}]; let array2=array.concat(array1); array1[0].c=123; console.log(array2);// [ { a: 1 }, { b: 2 }, { c: 123 }, { d: 4 } ] console.log(array1);// [ { c: 123 }, { d: 4 } ]
Array.prototype.concat也是一個淺拷貝,只是在根屬性(對象的第一層級)創建了一個新的對象,但是對于屬性的值是對象的話只會拷貝一份相同的內存地址。
...擴展運算符語法
var cloneObj = { ...obj };
舉例說明
let obj = {a:1,b:{c:1}} let obj2 = {...obj}; obj.a=2; console.log(obj); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c = 2; console.log(obj); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
擴展運算符也是淺拷貝,對于值是對象的屬性無法完全拷貝成2個不同對象,但是如果屬性都是基本類型的值的話,使用擴展運算符也是優勢方便的地方。
補充說明:以上4中淺拷貝方式都不會改變原數組,只會返回一個淺拷貝了原數組中的元素的一個新數組。
自己實現一個淺拷貝實現原理:新的對象復制已有對象中非對象屬性的值和對象屬性的引用,也就是說對象屬性并不復制到內存。
實現代碼:
function cloneShallow(source) { var target = {}; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } return target; }
for in與hasOwnProperty函數說明,怕有些人小伙伴可能不清楚具體內容
for in
for...in語句以任意順序遍歷一個對象自有的、繼承的、可枚舉的、非Symbol的屬性。對于每個不同的屬性,語句都會被執行。
hasOwnProperty
語法:obj.hasOwnProperty(prop)
prop是要檢測的屬性字符串名稱或者Symbol
該函數返回值為布爾值,所有繼承了 Object 的對象都會繼承到 hasOwnProperty 方法,和 in 運算符不同,該函數會忽略掉那些從原型鏈上繼承到的屬性和自身屬性。
深拷貝操作說了賦值操作和淺拷貝操作,大家是不是已經能想到什么是深拷貝了,下面直接說深拷貝的定義。
深拷貝定義深拷貝會另外拷貝一份一個一模一樣的對象,從堆內存中開辟一個新的區域存放新對象,新對象跟原對象不共享內存,修改新對象不會改到原對象。深拷貝實例 JSON.parse(JSON.stringify())
JSON.stringify()是前端開發過程中比較常用的深拷貝方式。原理是把一個對象序列化成為一個JSON字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()反序列化將JSON字符串變成一個新的對象
舉例說明:
let arr = [1, 3, { username: " koala" }]; let arr4 = JSON.parse(JSON.stringify(arr)); arr4[2].username = "smallKoala"; console.log(arr4);// [ 1, 3, { username: "smallKoala" } ] console.log(arr);// [ 1, 3, { username: " koala" } ]
實現了深拷貝,當改變數組中對象的值時候,原數組中的內容并沒有發生改變。JSON.stringify()雖然可以實現深拷貝,但是還有一些弊端比如不能處理函數等。
JSON.stringify()實現深拷貝注意點
拷貝的對象的值中如果有函數,undefined,symbol則經過JSON.stringify()序列化后的JSON字符串中這個鍵值對會消失
無法拷貝不可枚舉的屬性,無法拷貝對象的原型鏈
拷貝Date引用類型會變成字符串
拷貝RegExp引用類型會變成空對象
對象中含有NaN、Infinity和-Infinity,則序列化的結果會變成null
無法拷貝對象的循環應用(即obj[key] = obj)
自己實現一個簡單深拷貝深拷貝,主要用到的思想是遞歸,遍歷對象、數組直到里邊都是基本數據類型,然后再去復制,就是深度拷貝。
實現代碼:
//定義檢測數據類型的功能函數 function isObject(obj) { return typeof obj === "object" && obj != null; } function cloneDeep(source) { if (!isObject(source)) return source; // 非對象返回自身 var target = Array.isArray(source) ? [] : {}; for(var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (isObject(source[key])) { target[key] = cloneDeep(source[key]); // 注意這里 } else { target[key] = source[key]; } } } return target; }
該簡單深拷貝未考慮內容:
遇到循環引用,會陷入一個循環的遞歸過程,從而導致爆棧
// RangeError: Maximum call stack size exceeded
小伙伴們有沒有什么好辦法呢,可以寫下代碼在評論區一起討論哦!
第三方深拷貝庫該函數庫也有提供_.cloneDeep用來做 Deep Copy(lodash是一個不錯的第三方開源庫,有好多不錯的函數,也可以看具體的實現源碼)
var _ = require("lodash"); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false拷貝內容總結
用一張圖總結
今天就分享這么多,如果對分享的內容感興趣,可以關注公眾號「程序員成長指北」,或者加入技術交流群,大家一起討論。
進階技術路線
加入我們一起學習吧!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105920.html
摘要:所以,深拷貝是對對象以及對象的所有子對象進行拷貝實現方式就是遞歸調用淺拷貝對于深拷貝的對象,改變源對象不會對得到的對象有影響。 上一篇 JavaScript中的繼承 前言 文章開始之前,讓我們先思考一下這幾個問題: 為什么會有淺拷貝與深拷貝 什么是淺拷貝與深拷貝 如何實現淺拷貝與深拷貝 好了,問題出來了,那么下面就讓我們帶著這幾個問題去探究一下吧! 如果文章中有出現紕漏、錯誤之處...
摘要:所以,深拷貝是對對象以及對象的所有子對象進行拷貝實現方式就是遞歸調用淺拷貝對于深拷貝的對象,改變源對象不會對得到的對象有影響。 為什么會有淺拷貝與深拷貝什么是淺拷貝與深拷貝如何實現淺拷貝與深拷貝好了,問題出來了,那么下面就讓我們帶著這幾個問題去探究一下吧! 如果文章中有出現紕漏、錯誤之處,還請看到的小伙伴多多指教,先行謝過 以下↓ 數據類型在開始了解 淺拷貝 與 深拷貝 之前,讓我們先...
摘要:也就是說,深拷貝與淺拷貝最主要的區別在引用類型的拷貝上。方法二遞歸拷貝深拷貝與淺拷貝相比不就是多拷貝幾層的事嘛,這不就是遞歸常干的事嘛。 什么是淺拷貝和深拷貝 淺拷貝 淺拷貝:將一個對象自身的屬性拷貝給另一個對象,如果源對象的屬性是基本類型則直接進行值賦值,如果是引用類型則進行引用賦值,也就是說只進行一層賦值。 深拷貝 深拷貝:將一個對象自身的屬性拷貝給另一個對象,如果源對象的屬性是基...
閱讀 3601·2021-11-23 09:51
閱讀 1473·2021-11-04 16:08
閱讀 3547·2021-09-02 09:54
閱讀 3616·2019-08-30 15:55
閱讀 2594·2019-08-30 15:54
閱讀 958·2019-08-29 16:30
閱讀 2047·2019-08-29 16:15
閱讀 2317·2019-08-29 14:05