JavaScript 資料結構與演算法之美 - 棧記憶體與堆記憶體 、淺拷貝與深拷貝
前言
想寫好前端,先練好內功。
棧記憶體與堆記憶體 、淺拷貝與深拷貝,可以說是前端程式設計師的內功,要知其然,知其所以然。
筆者寫的 JavaScript 資料結構與演算法之美 系列用的語言是 JavaScript ,旨在入門資料結構與演算法和方便以後複習。
棧
定義
- 後進者先出,先進者後出,簡稱 後進先出(LIFO),這就是典型的
棧
結構。 - 新新增的或待刪除的元素都儲存在棧的末尾,稱作
棧頂
,另一端就叫棧底
。 - 在棧裡,新元素都靠近棧頂,舊元素都接近棧底。
- 從棧的操作特性來看,是一種
操作受限
的線性表,只允許在一端插入和刪除資料。 - 不包含任何元素的棧稱為
空棧
。
棧也被用在程式語言的編譯器和記憶體中儲存變數、方法呼叫等,比如函式的呼叫棧。
堆
定義
- 堆資料結構是一種樹狀結構。
它的存取資料的方式,與書架與書非常相似。我們不關心書的放置順序是怎樣的,只需知道書的名字就可以取出我們想要的書了。
好比在 JSON 格式的資料中,我們儲存的 key-value 是可以無序的,只要知道 key,就能取出這個 key 對應的 value。
堆與棧比較
- 堆是動態分配記憶體,記憶體大小不一,也不會自動釋放。
- 棧是自動分配相對固定大小的記憶體空間,並由系統自動釋放。
- 棧,線性結構,後進先出,便於管理。
- 堆,一個混沌,雜亂無章,方便儲存和開闢記憶體空間。
棧記憶體與堆記憶體
JavaScript 中的變數分為基本型別和引用型別。
基本型別是儲存在棧記憶體中的簡單資料段,它們的值都有固定的大小,儲存在棧空間,通過按值訪問,並由系統自動分配和自動釋放。
這樣帶來的好處就是,記憶體可以及時得到回收,相對於堆來說,更加容易管理記憶體空間。
JavaScript 中的Boolean、Null、Undefined、Number、String、Symbol
引用型別(如物件、陣列、函式等)是儲存在堆記憶體中的物件,值大小不固定,棧記憶體中存放的該物件的訪問地址指向堆記憶體中的物件,JavaScript 不允許直接訪問堆記憶體中的位置,因此操作物件時,實際操作物件的引用。
JavaScript 中的Object、Array、Function、RegExp、Date
是引用型別。
結合例項說明
let a1 = 0; // 棧記憶體 let a2 = "this is string" // 棧記憶體 let a3 = null; // 棧記憶體 let b = { x: 10 }; // 變數 b 存在於棧中,{ x: 10 } 作為物件存在於堆中 let c = [1, 2, 3]; // 變數 c 存在於棧中,[1, 2, 3] 作為物件存在於堆中
當我們要訪問堆記憶體中的引用資料型別時
- 從棧中獲取該物件的地址引用
- 再從堆記憶體中取得我們需要的資料
基本型別發生複製
let a = 20;
let b = a;
b = 30;
console.log(a); // 20
在棧記憶體中的資料發生複製行為時,系統會自動為新的變數分配一個新值,最後這些變數都是 相互獨立,互不影響的。
引用型別發生複製
let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
- 引用型別的複製,同樣為新的變數 b 分配一個新的值,儲存在棧記憶體中,不同的是,這個值僅僅是引用型別的一個地址指標。
- 他們兩個指向同一個值,也就是地址指標相同,在堆記憶體中訪問到的具體物件實際上是同一個。
- 因此改變 b.x 時,a.x 也發生了變化,這就是引用型別的特性。
結合下圖理解
總結
棧記憶體 | 堆記憶體 |
---|---|
儲存基礎資料型別 | 儲存引用資料型別 |
按值訪問 | 按引用訪問 |
儲存的值大小固定 | 儲存的值大小不定,可動態調整 |
由系統自動分配記憶體空間 | 由程式碼進行指定分配 |
空間小,執行效率高 | 空間大,執行效率相對較低 |
先進後出,後進先出 | 無序儲存,可根據引用直接獲取 |
淺拷貝與深拷貝
上面講的引用型別的複製
就是淺拷貝,複製得到的訪問地址都指向同一個記憶體空間
。所以修改了其中一個的值,另外一個也跟著改變了。
深拷貝:複製得到的訪問地址指向不同的記憶體空間,互不相干
。所以修改其中一個值,另外一個不會改變。
平時使用陣列複製時,我們大多數會使用 =
,這只是淺拷貝,存在很多問題。比如:
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]
很明顯,淺拷貝下,拷貝和被拷貝的陣列會相互受到影響。
所以,必須要有一種不受影響的方法,那就是深拷貝。
深拷貝的的複製過程
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
陣列
一、for 迴圈
//for 迴圈 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實現深拷貝
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實現深拷貝
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 擴充套件運算
//es6 擴充套件運算實現深拷貝
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]
注意:該方法在資料量比較大時,會有效能問題。
物件
一、物件的迴圈
// 迴圈 copy 物件
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}}
可實現多維物件的深拷貝。
注意:進行JSON.stringify() 序列化的過程中,undefined、任意的函式以及 symbol 值,在序列化過程中會被忽略(出現在非陣列物件的屬性值中時)或者被轉換成 null(出現在陣列中時)。
三、es6 擴充套件運算
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() 只能實現一維物件的深拷貝。
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}}
通用深拷貝方法
簡單版
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;
}
// 測試
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]
但上面的深拷貝方法遇到迴圈引用,會陷入一個迴圈的遞迴過程,從而導致爆棧,所以要避免。
let obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
let obj2 = clone(obj1);
console.log(obj2)
結果如下:
總結:深刻理解 javascript 的深淺拷貝,可以靈活的運用陣列與物件,並且可以避免很多 bug。
7. 最後
文中所有的程式碼及測試事例都已經放到我的 GitHub 上了。
如果你覺得有用或者喜歡,就點收藏,順便點個贊吧,你的支援是我最大的鼓勵 !
參考文章:
JavaScript棧記憶體和堆記憶體
JavaScript實現淺拷貝與深拷貝的方法分析
淺拷貝與深拷貝(JavaScript