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