前言
想寫(xiě)好前端逐纬,先練好內(nèi)功。
棧內(nèi)存與堆內(nèi)存 肮街、淺拷貝與深拷貝风题,可以說(shuō)是前端程序員的內(nèi)功,要知其然嫉父,知其所以然沛硅。
筆者寫(xiě)的 JavaScript 數(shù)據(jù)結(jié)構(gòu)與算法之美 系列用的語(yǔ)言是 JavaScript ,旨在入門(mén)數(shù)據(jù)結(jié)構(gòu)與算法和方便以后復(fù)習(xí)绕辖。
棧
定義
- 后進(jìn)者先出摇肌,先進(jìn)者后出,簡(jiǎn)稱(chēng) 后進(jìn)先出(LIFO)仪际,這就是典型的
棧
結(jié)構(gòu)围小。 - 新添加的或待刪除的元素都保存在棧的末尾,稱(chēng)作
棧頂
树碱,另一端就叫棧底
肯适。 - 在棧里,新元素都靠近棧頂成榜,舊元素都接近棧底框舔。
- 從棧的操作特性來(lái)看,是一種
操作受限
的線性表赎婚,只允許在一端插入和刪除數(shù)據(jù)刘绣。 - 不包含任何元素的棧稱(chēng)為
空棧
。
棧也被用在編程語(yǔ)言的編譯器和內(nèi)存中保存變量挣输、方法調(diào)用等纬凤,比如函數(shù)的調(diào)用棧。
堆
定義
- 堆數(shù)據(jù)結(jié)構(gòu)是一種樹(shù)狀結(jié)構(gòu)撩嚼。
它的存取數(shù)據(jù)的方式停士,與書(shū)架與書(shū)非常相似。我們不關(guān)心書(shū)的放置順序是怎樣的完丽,只需知道書(shū)的名字就可以取出我們想要的書(shū)了向瓷。
好比在 JSON 格式的數(shù)據(jù)中,我們存儲(chǔ)的 key-value 是可以無(wú)序的舰涌,只要知道 key,就能取出這個(gè) key 對(duì)應(yīng)的 value你稚。
堆與棧比較
- 堆是動(dòng)態(tài)分配內(nèi)存瓷耙,內(nèi)存大小不一朱躺,也不會(huì)自動(dòng)釋放。
- 棧是自動(dòng)分配相對(duì)固定大小的內(nèi)存空間搁痛,并由系統(tǒng)自動(dòng)釋放长搀。
- 棧,線性結(jié)構(gòu)鸡典,后進(jìn)先出源请,便于管理。
- 堆彻况,一個(gè)混沌谁尸,雜亂無(wú)章,方便存儲(chǔ)和開(kāi)辟內(nèi)存空間纽甘。
棧內(nèi)存與堆內(nèi)存
JavaScript 中的變量分為基本類(lèi)型和引用類(lèi)型良蛮。
基本類(lèi)型是保存在棧內(nèi)存中的簡(jiǎn)單數(shù)據(jù)段,它們的值都有固定的大小悍赢,保存在椌鐾空間,通過(guò)按值訪問(wèn)左权,并由系統(tǒng)自動(dòng)分配和自動(dòng)釋放皮胡。
這樣帶來(lái)的好處就是,內(nèi)存可以及時(shí)得到回收赏迟,相對(duì)于堆來(lái)說(shuō)屡贺,更加容易管理內(nèi)存空間。
JavaScript 中的Boolean瀑梗、Null烹笔、Undefined、Number抛丽、String谤职、Symbol
都是基本類(lèi)型。引用類(lèi)型(如對(duì)象亿鲜、數(shù)組允蜈、函數(shù)等)是保存在堆內(nèi)存中的對(duì)象,值大小不固定蒿柳,棧內(nèi)存中存放的該對(duì)象的訪問(wèn)地址指向堆內(nèi)存中的對(duì)象饶套,JavaScript 不允許直接訪問(wèn)堆內(nèi)存中的位置,因此操作對(duì)象時(shí)垒探,實(shí)際操作對(duì)象的引用妓蛮。
JavaScript 中的Object、Array圾叼、Function蛤克、RegExp捺癞、Date
是引用類(lèi)型。
結(jié)合實(shí)例說(shuō)明
let a1 = 0; // 棧內(nèi)存
let a2 = "this is string" // 棧內(nèi)存
let a3 = null; // 棧內(nèi)存
let b = { x: 10 }; // 變量 b 存在于棧中构挤,{ x: 10 } 作為對(duì)象存在于堆中
let c = [1, 2, 3]; // 變量 c 存在于棧中髓介,[1, 2, 3] 作為對(duì)象存在于堆中
當(dāng)我們要訪問(wèn)堆內(nèi)存中的引用數(shù)據(jù)類(lèi)型時(shí)
- 從棧中獲取該對(duì)象的地址引用
- 再?gòu)亩褍?nèi)存中取得我們需要的數(shù)據(jù)
基本類(lèi)型發(fā)生復(fù)制
let a = 20;
let b = a;
b = 30;
console.log(a); // 20
在棧內(nèi)存中的數(shù)據(jù)發(fā)生復(fù)制行為時(shí),系統(tǒng)會(huì)自動(dòng)為新的變量分配一個(gè)新值筋现,最后這些變量都是 相互獨(dú)立唐础,互不影響的。
引用類(lèi)型發(fā)生復(fù)制
let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
- 引用類(lèi)型的復(fù)制矾飞,同樣為新的變量 b 分配一個(gè)新的值一膨,保存在棧內(nèi)存中,不同的是凰慈,這個(gè)值僅僅是引用類(lèi)型的一個(gè)地址指針汞幢。
- 他們兩個(gè)指向同一個(gè)值,也就是地址指針相同微谓,在堆內(nèi)存中訪問(wèn)到的具體對(duì)象實(shí)際上是同一個(gè)森篷。
- 因此改變 b.x 時(shí),a.x 也發(fā)生了變化豺型,這就是引用類(lèi)型的特性仲智。
結(jié)合下圖理解
總結(jié)
棧內(nèi)存 | 堆內(nèi)存 |
---|---|
存儲(chǔ)基礎(chǔ)數(shù)據(jù)類(lèi)型 | 存儲(chǔ)引用數(shù)據(jù)類(lèi)型 |
按值訪問(wèn) | 按引用訪問(wèn) |
存儲(chǔ)的值大小固定 | 存儲(chǔ)的值大小不定,可動(dòng)態(tài)調(diào)整 |
由系統(tǒng)自動(dòng)分配內(nèi)存空間 | 由代碼進(jìn)行指定分配 |
空間小姻氨,運(yùn)行效率高 | 空間大钓辆,運(yùn)行效率相對(duì)較低 |
先進(jìn)后出,后進(jìn)先出 | 無(wú)序存儲(chǔ)肴焊,可根據(jù)引用直接獲取 |
淺拷貝與深拷貝
上面講的引用類(lèi)型的復(fù)制
就是淺拷貝前联,復(fù)制得到的訪問(wèn)地址都指向同一個(gè)內(nèi)存空間
。所以修改了其中一個(gè)的值娶眷,另外一個(gè)也跟著改變了似嗤。
深拷貝:復(fù)制得到的訪問(wèn)地址指向不同的內(nèi)存空間,互不相干
届宠。所以修改其中一個(gè)值烁落,另外一個(gè)不會(huì)改變。
平時(shí)使用數(shù)組復(fù)制時(shí)豌注,我們大多數(shù)會(huì)使用 =
伤塌,這只是淺拷貝,存在很多問(wèn)題轧铁。比如:
let arr = [1,2,3,4,5];
let arr2 = arr;
console.log(arr) //[1, 2, 3, 4, 5]
console.log(arr2) //[1, 2, 3, 4, 5]
arr[0] = 6;
console.log(arr) //[6, 2, 3, 4, 5]
console.log(arr2) //[6, 2, 3, 4, 5]
arr2[4] = 7;
console.log(arr) //[6, 2, 3, 4, 7]
console.log(arr2) //[6, 2, 3, 4, 7]
很明顯每聪,淺拷貝下,拷貝和被拷貝的數(shù)組會(huì)相互受到影響。
所以熊痴,必須要有一種不受影響的方法他爸,那就是深拷貝。
深拷貝的的復(fù)制過(guò)程
let a = { x: 10, y: 20 }
let b = JSON.parse(JSON.stringify(a));
b.x = 5;
console.log(a.x); // 10
console.log(b.x); // 5
數(shù)組
一果善、for 循環(huán)
//for 循環(huán) copy
function copy(arr) {
let cArr = []
for(let i = 0; i < arr.length; i++){
cArr.push(arr[i])
}
return cArr;
}
let arr3 = [1,2,3,4];
let arr4 = copy(arr3) //[1,2,3,4]
console.log(arr4) //[1,2,3,4]
arr3[0] = 5;
console.log(arr3) //[5,2,3,4]
console.log(arr4) //[1,2,3,4]
二、slice 方法
//slice實(shí)現(xiàn)深拷貝
let arr5 = [1,2,3,4];
let arr6 = arr5.slice(0);
arr5[0] = 5;
console.log(arr5); //[5,2,3,4]
console.log(arr6); //[1,2,3,4]
三系谐、concat 方法
//concat實(shí)現(xiàn)深拷貝
let arr7 = [1,2,3,4];
let arr8 = arr7.concat();
arr7[0] = 5;
console.log(arr7); //[5,2,3,4]
console.log(arr8); //[1,2,3,4]
四巾陕、es6 擴(kuò)展運(yùn)算
//es6 擴(kuò)展運(yùn)算實(shí)現(xiàn)深拷貝
let arr9 = [1,2,3,4];
let [...arr10] = arr9;
arr9[0] = 5;
console.log(arr9) //[5,2,3,4]
console.log(arr10) //[1,2,3,4]
五、JSON.parse 與 JSON.stringify
let arr9 = [1,2,3,4];
let arr10 = JSON.parse(JSON.stringify(arr9))
arr9[0] = 5;
console.log(arr9) //[5,2,3,4]
console.log(arr10) //[1,2,3,4]
注意:該方法在數(shù)據(jù)量比較大時(shí)纪他,會(huì)有性能問(wèn)題鄙煤。
對(duì)象
一、對(duì)象的循環(huán)
// 循環(huán) copy 對(duì)象
let obj = {
id:'0',
name:'king',
sex:'man'
}
let obj2 = copy2(obj)
function copy2(obj) {
let cObj = {};
for(var key in obj){
cObj[key] = obj[key]
}
return cObj
}
obj2.name = "king2"
console.log(obj) // {id: "0", name: "king", sex: "man"}
console.log(obj2) // {id: "0", name: "king2", sex: "man"}
二茶袒、JSON.parse 與 JSON.stringify
var obj1 = {
x: 1,
y: {
m: 1
},
a:undefined,
b:function(a,b){
return a+b
},
c:Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ?, c: Symbol(foo)}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ?, c: Symbol(foo)}
console.log(obj2) //{x: 1, y: {m: 2}}
可實(shí)現(xiàn)多維對(duì)象的深拷貝梯刚。
注意:進(jìn)行JSON.stringify() 序列化的過(guò)程中,undefined薪寓、任意的函數(shù)以及 symbol 值亡资,在序列化過(guò)程中會(huì)被忽略(出現(xiàn)在非數(shù)組對(duì)象的屬性值中時(shí))或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時(shí))。
三向叉、es6 擴(kuò)展運(yùn)算
let obj = {
id:'0',
name:'king',
sex:'man'
}
let {...obj4} = obj
obj4.name = "king4"
console.log(obj) //{id: "0", name: "king", sex: "man"}
console.log(obj4) //{id: "0", name: "king4", sex: "man"}
四锥腻、Object.assign()
Object.assign() 只能實(shí)現(xiàn)一維對(duì)象的深拷貝。
var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) // {x: 1, y: 2}
console.log(obj2) // {x: 1, y: 2}
obj2.x = 2; // 修改 obj2.x
console.log(obj1) // {x: 1, y: 2}
console.log(obj2) // {x: 2, y: 2}
var obj1 = {
x: 1,
y: {
m: 1
}
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) // {x: 1, y: {m: 1}}
console.log(obj2) // {x: 1, y: {m: 1}}
obj2.y.m = 2; // 修改 obj2.y.m
console.log(obj1) // {x: 1, y: {m: 2}}
console.log(obj2) // {x: 2, y: {m: 2}}
通用深拷貝方法
簡(jiǎn)單版
let clone = function (v) {
let o = v.constructor === Array ? [] : {};
for(var i in v){
o[i] = typeof v[i] === "object" ? clone(v[i]) : v[i];
}
return o;
}
// 測(cè)試
let obj = {
id:'0',
name:'king',
sex:'man'
}
let obj2 = clone(obj)
obj2.name = "king2"
console.log(obj) // {id: "0", name: "king", sex: "man"}
console.log(obj2) // {id: "0", name: "king2", sex: "man"}
let arr3 = [1,2,3,4];
let arr4 = clone(arr3) // [1,2,3,4]
arr3[0] = 5;
console.log(arr3) // [5,2,3,4]
console.log(arr4) // [1,2,3,4]
但上面的深拷貝方法遇到循環(huán)引用母谎,會(huì)陷入一個(gè)循環(huán)的遞歸過(guò)程瘦黑,從而導(dǎo)致爆棧,所以要避免奇唤。
let obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
let obj2 = clone(obj1);
console.log(obj2)
結(jié)果如下:
總結(jié):深刻理解 javascript 的深淺拷貝幸斥,可以靈活的運(yùn)用數(shù)組與對(duì)象,并且可以避免很多 bug咬扇。
文章輸出計(jì)劃
JavaScript 數(shù)據(jù)結(jié)構(gòu)與算法之美 的系列文章甲葬,堅(jiān)持 3 - 7 天左右更新一篇,暫定計(jì)劃如下表冗栗。
標(biāo)題 | 鏈接 |
---|---|
時(shí)間和空間復(fù)雜度 | https://github.com/biaochenxuying/blog/issues/29 |
線性表(數(shù)組演顾、鏈表、棧隅居、隊(duì)列) | https://github.com/biaochenxuying/blog/issues/34 |
實(shí)現(xiàn)一個(gè)前端路由挠乳,如何實(shí)現(xiàn)瀏覽器的前進(jìn)與后退 ? | https://github.com/biaochenxuying/blog/issues/30 |
棧內(nèi)存與堆內(nèi)存 钱豁、淺拷貝與深拷貝 | https://github.com/biaochenxuying/blog/issues/35 |
非線性表(樹(shù)瓜喇、堆) | 精彩待續(xù) |
遞歸 | 精彩待續(xù) |
冒泡排序 | 精彩待續(xù) |
插入排序 | 精彩待續(xù) |
選擇排序 | 精彩待續(xù) |
歸并排序 | 精彩待續(xù) |
快速排序 | 精彩待續(xù) |
計(jì)數(shù)排序 | 精彩待續(xù) |
基數(shù)排序 | 精彩待續(xù) |
桶排序 | 精彩待續(xù) |
希爾排序 | 精彩待續(xù) |
堆排序 | 精彩待續(xù) |
十大經(jīng)典排序匯總 | 精彩待續(xù) |
如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤剑?qǐng)務(wù)必給予指正涕蚤,十分感謝宪卿。
7. 最后
關(guān)注我的公眾號(hào)的诵,第一時(shí)間接收最新的精彩博文。
文章可以轉(zhuǎn)載佑钾,但須注明作者及出處西疤,需要轉(zhuǎn)載到公眾號(hào)的,喊我加下白名單就行了休溶。
參考文章:
JavaScript棧內(nèi)存和堆內(nèi)存
JavaScript實(shí)現(xiàn)淺拷貝與深拷貝的方法分析
淺拷貝與深拷貝(JavaScript)