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