JavaScript 數據結構與算法之美 - 棧內存與堆內存 、淺拷貝與深拷貝

女神鎮樓javascript

前言

想寫好前端,先練好內功。前端

棧內存與堆內存 、淺拷貝與深拷貝,能夠說是前端程序員的內功,要知其然,知其因此然。java

筆者寫的 JavaScript 數據結構與算法之美 系列用的語言是 JavaScript ,旨在入門數據結構與算法和方便之後複習。git

棧

定義程序員

  1. 後進者先出,先進者後出,簡稱 後進先出(LIFO),這就是典型的結構。
  2. 新添加的或待刪除的元素都保存在棧的末尾,稱做棧頂,另外一端就叫棧底
  3. 在棧裏,新元素都靠近棧頂,舊元素都接近棧底。
  4. 從棧的操做特性來看,是一種 操做受限的線性表,只容許在一端插入和刪除數據。
  5. 不包含任何元素的棧稱爲空棧

棧也被用在編程語言的編譯器和內存中保存變量、方法調用等,好比函數的調用棧。es6

定義github

  • 堆數據結構是一種樹狀結構。 它的存取數據的方式,與書架與書很是類似。咱們不關心書的放置順序是怎樣的,只需知道書的名字就能夠取出咱們想要的書了。 比如在 JSON 格式的數據中,咱們存儲的 key-value 是能夠無序的,只要知道 key,就能取出這個 key 對應的 value。

堆與棧比較算法

  • 堆是動態分配內存,內存大小不一,也不會自動釋放。
  • 棧是自動分配相對固定大小的內存空間,並由系統自動釋放。
  • 棧,線性結構,後進先出,便於管理。
  • 堆,一個混沌,雜亂無章,方便存儲和開闢內存空間。

棧內存與堆內存

JavaScript 中的變量分爲基本類型和引用類型。編程

  • 基本類型是保存在棧內存中的簡單數據段,它們的值都有固定的大小,保存在棧空間,經過按值訪問,並由系統自動分配和自動釋放。 這樣帶來的好處就是,內存能夠及時獲得回收,相對於堆來講,更加容易管理內存空間。 JavaScript 中的 Boolean、Null、Undefined、Number、String、Symbol 都是基本類型。segmentfault

  • 引用類型(如對象、數組、函數等)是保存在堆內存中的對象,值大小不固定,棧內存中存放的該對象的訪問地址指向堆內存中的對象,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] 做爲對象存在於堆中
複製代碼

棧/堆內存空間

當咱們要訪問堆內存中的引用數據類型時

    1. 從棧中獲取該對象的地址引用
    1. 再從堆內存中取得咱們須要的數據

基本類型發生複製

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
複製代碼

複製前

複製後

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: 2, 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。

文章輸出計劃

JavaScript 數據結構與算法之美 的系列文章,堅持 3 - 7 天左右更新一篇,暫定計劃以下表。

標題 連接
時間和空間複雜度 github.com/biaochenxuy…
線性表(數組、鏈表、棧、隊列) github.com/biaochenxuy…
實現一個前端路由,如何實現瀏覽器的前進與後退 ? github.com/biaochenxuy…
棧內存與堆內存 、淺拷貝與深拷貝 github.com/biaochenxuy…
非線性表(樹、堆) 精彩待續
遞歸 精彩待續
冒泡排序 精彩待續
插入排序 精彩待續
選擇排序 精彩待續
歸併排序 精彩待續
快速排序 精彩待續
計數排序 精彩待續
基數排序 精彩待續
桶排序 精彩待續
希爾排序 精彩待續
堆排序 精彩待續
十大經典排序彙總 精彩待續

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。

7. 最後

歡迎 star github,對做者也是一種鼓勵。

關注個人公衆號,第一時間接收最新的精彩博文。

文章能夠轉載,但須註明做者及出處,須要轉載到公衆號的,喊我加下白名單就好了。

參考文章:

JavaScript棧內存和堆內存

JavaScript實現淺拷貝與深拷貝的方法分析

淺拷貝與深拷貝(JavaScript)

筆芯
相關文章
相關標籤/搜索