前言
想寫好前端,先練好內功。javascript
棧內存與堆內存 、淺拷貝與深拷貝,能夠說是前端程序員的內功,要知其然,知其因此然。前端
筆者寫的 JavaScript 數據結構與算法之美 系列用的語言是 JavaScript ,旨在入門數據結構與算法和方便之後複習。java
棧
定義git
- 後進者先出,先進者後出,簡稱 後進先出(LIFO),這就是典型的
棧
結構。 - 新添加的或待刪除的元素都保存在棧的末尾,稱做
棧頂
,另外一端就叫棧底
。 - 在棧裏,新元素都靠近棧頂,舊元素都接近棧底。
- 從棧的操做特性來看,是一種
操做受限
的線性表,只容許在一端插入和刪除數據。 - 不包含任何元素的棧稱爲
空棧
。
棧也被用在編程語言的編譯器和內存中保存變量、方法調用等,好比函數的調用棧。程序員
堆
定義es6
- 堆數據結構是一種樹狀結構。 它的存取數據的方式,與書架與書很是類似。咱們不關心書的放置順序是怎樣的,只需知道書的名字就能夠取出咱們想要的書了。 比如在 JSON 格式的數據中,咱們存儲的 key-value 是能夠無序的,只要知道 key,就能取出這個 key 對應的 value。
堆與棧比較github
- 堆是動態分配內存,內存大小不一,也不會自動釋放。
- 棧是自動分配相對固定大小的內存空間,並由系統自動釋放。
- 棧,線性結構,後進先出,便於管理。
- 堆,一個混沌,雜亂無章,方便存儲和開闢內存空間。
棧內存與堆內存
JavaScript 中的變量分爲基本類型和引用類型。算法
-
基本類型是保存在棧內存中的簡單數據段,它們的值都有固定的大小,保存在棧空間,經過按值訪問,並由系統自動分配和自動釋放。 這樣帶來的好處就是,內存能夠及時獲得回收,相對於堆來講,更加容易管理內存空間。 JavaScript 中的
Boolean、Null、Undefined、Number、String、Symbol
都是基本類型。編程 -
引用類型(如對象、數組、函數等)是保存在堆內存中的對象,值大小不固定,棧內存中存放的該對象的訪問地址指向堆內存中的對象,JavaScript 不容許直接訪問堆內存中的位置,所以操做對象時,實際操做對象的引用。 JavaScript 中的
Object、Array、Function、RegExp、Date
是引用類型。segmentfault
結合實例說明
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
數組
1、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]
2、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]
3、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]
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]
5、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]
注意:該方法在數據量比較大時,會有性能問題。
對象
1、對象的循環
// 循環 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"}
2、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(出如今數組中時)。
3、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"}
4、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)