一、棧內(nèi)存與堆內(nèi)存
棧內(nèi)存與堆內(nèi)存 玖姑、淺拷貝與深拷貝愕秫,可以說是前端程序員的內(nèi)功,要知其然焰络,知其所以然戴甩。
1、棧與棧內(nèi)存
棧的定義
- 后進(jìn)者先出闪彼,先進(jìn)者后出甜孤,簡(jiǎn)稱 后進(jìn)先出(LIFO),這就是典型的
棧
結(jié)構(gòu)。 - 新添加的或待刪除的元素都保存在棧的末尾缴川,稱作
棧頂
茉稠,另一端就叫棧底
。 - 在棧里二跋,新元素都靠近棧頂战惊,舊元素都接近棧底。
- 從棧的操作特性來看扎即,是一種
操作受限
的線性表吞获,只允許在一端插入和刪除數(shù)據(jù)。 - 不包含任何元素的棧稱為
空棧
谚鄙。 - 棧也被用在編程語言的編譯器和內(nèi)存中保存變量各拷、方法調(diào)用等,比如函數(shù)的調(diào)用棧闷营。
棧內(nèi)存
- 它們存儲(chǔ)的值都有固定的大小烤黍,保存在棧內(nèi)存空間,通過按值訪問傻盟,并由系統(tǒng)自動(dòng)分配和自動(dòng)釋放速蕊。
- 這樣帶來的好處就是,內(nèi)存可以及時(shí)得到回收娘赴,相對(duì)于堆來說规哲,更加容易管理內(nèi)存空間。
- 主要用于存儲(chǔ)各種基本數(shù)據(jù)類型的變量诽表,以及對(duì)象變量的指針唉锌。
- JavaScript 中的
Boolean
、Null
竿奏、Undefined
袄简、Number
、String
泛啸、Symbol
绿语、BigInt
都是基本數(shù)據(jù)類型。
2平痰、堆與堆內(nèi)存
堆的定義
- 堆數(shù)據(jù)結(jié)構(gòu)是一種樹狀結(jié)構(gòu)汞舱。
- 它的存取數(shù)據(jù)的方式,與書架與書非常相似宗雇。我們不關(guān)心書的放置順序是怎樣的昂芜,只需知道書的名字就可以取出我們想要的書了。
- 好似在
JSON
格式的數(shù)據(jù)中赔蒲,我們存儲(chǔ)的key-value
是可以無序的泌神,只要知道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è)混沌篙挽,雜亂無章,方便存儲(chǔ)和開辟內(nèi)存空間镊靴。
堆內(nèi)存
引用類型(如對(duì)象铣卡、數(shù)組、函數(shù)等)是保存在堆內(nèi)存中的對(duì)象偏竟,值大小不固定煮落,棧內(nèi)存中存放的該對(duì)象的訪問地址指向堆內(nèi)存中的對(duì)象,JavaScript 不允許直接訪問堆內(nèi)存中的位置踊谋,因此操作對(duì)象時(shí)州邢,實(shí)際操作對(duì)象的引用。
JavaScript 中的 Object褪子、Array、Function骗村、RegExp嫌褪、Date
是引用類型。
堆內(nèi)存與棧內(nèi)存比較
棧內(nèi)存 | 堆內(nèi)存 |
---|---|
存儲(chǔ)基礎(chǔ)數(shù)據(jù)類型 | 存儲(chǔ)引用數(shù)據(jù)類型 |
按值訪問 | 按引用訪問 |
存儲(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)先出 | 無序存儲(chǔ)缨伊,可根據(jù)引用直接獲取 |
棧內(nèi)存中的變量一般都是已知大小或者有范圍上限的,算作一種簡(jiǎn)單存儲(chǔ)进宝。而堆內(nèi)存存儲(chǔ)的對(duì)象類型數(shù)據(jù)對(duì)于大小這方面刻坊,一般是未知的。這就是為什么null作為一個(gè)object類型的變量卻存儲(chǔ)在棧內(nèi)存中的原因吧党晋。
當(dāng)我們定義一個(gè)const obj ={name:'king'}
對(duì)象的時(shí)候谭胚,我們說的常量obj
其實(shí)是指針徐块,就是變量obj
的值不允許改變,也就是變量obj
指向的堆內(nèi)存的地址不能改變灾而,但是胡控,該堆內(nèi)存地址內(nèi)數(shù)據(jù)本身的大小或?qū)傩允强梢愿淖兊摹?/p>
既然知道了const
在內(nèi)存中的存儲(chǔ),那么const
旁趟、let
定義的變量不能二次定義的流程也就比較容易猜出來了昼激,每次使用const
或者let
去初始化一個(gè)變量的時(shí)候,會(huì)首先遍歷當(dāng)前的內(nèi)存棧锡搜,看看有沒有重名變量橙困,有的話就返回錯(cuò)誤。
二余爆、數(shù)據(jù)類型
在JS
中纷宇,數(shù)據(jù)分為基本數(shù)據(jù)類型(String
、Number
蛾方、Boolean
像捶、Null
、Undefined
桩砰、Symbol
拓春、BigInt
)和引用數(shù)據(jù)類型(Object
、Function
亚隅、class
)硼莽。
- 基本數(shù)據(jù)類型是直接存儲(chǔ)在棧(Stack)內(nèi)存中的數(shù)據(jù)
-
引用數(shù)據(jù)類型棧內(nèi)存存儲(chǔ)的是該對(duì)象的引用地址,對(duì)象的真實(shí)數(shù)據(jù)存儲(chǔ)在堆內(nèi)存中
引用數(shù)據(jù)類型在棧內(nèi)存中存儲(chǔ)了引用地址(也叫指針)煮纵,該引用地址或指針指向了堆內(nèi)存中實(shí)際數(shù)據(jù)的地址懂鸵。
引用數(shù)據(jù)類型存儲(chǔ)
1、基本數(shù)據(jù)類型的存儲(chǔ)
變量名和值都儲(chǔ)存在棧內(nèi)存中
let num = 10;
let newnum = num;
那么該num
變量在棧內(nèi)存中的存儲(chǔ)如下
變量num
和值10
都存儲(chǔ)在棧內(nèi)存中行疏,num =10
;讓變量num
指向數(shù)字10
;而newnum =num
是將變量newnum
存儲(chǔ)在棧內(nèi)存中匆光,并且讓該變量指向新的值數(shù)字10
。此時(shí)酿联,變量num
和變量newnum
沒有任何關(guān)系终息。注意 棧內(nèi)存的賦值都是值賦值,newnum = num
是指將變量num
的值賦值給新變量newnum
;而不是把變量num
賦值給變量newnum
贞让。
基本數(shù)據(jù)類型值是不可變的
let a = 1;
console.log(++a);
其實(shí)這個(gè)時(shí)候并不是變量a
指向的 1
直接加了 1
周崭,而是 新建了一個(gè) 1+1 = 2 的值,再將 a 指向這個(gè)新建出來的 2喳张,原來的那個(gè) 1 并沒有發(fā)生改變续镇,留由垃圾回收機(jī)制處理。也就是說不是 a 指向的值發(fā)生了改變蹲姐,而是 a 變量指針指向了一個(gè)新的值磨取,這就是基本類型的值是不可變的人柿。
2、引用數(shù)據(jù)類型的存儲(chǔ)
變量名儲(chǔ)存在棧內(nèi)存中忙厌,值儲(chǔ)存在堆內(nèi)存中凫岖,但是堆內(nèi)存中會(huì)提供一個(gè)引用地址指向堆內(nèi)存中的值,而這個(gè)地址是儲(chǔ)存在棧內(nèi)存中的逢净。
let ary = [1, 2, 3];
let newary = ary;
那么該ary
變量在內(nèi)存中的存儲(chǔ)如下
引用數(shù)據(jù)類型值是可變的
let obj = { name: 'king' };
obj.name = '偶爾平凡';
console.log(obj)
//{ name: '偶爾平凡' }
引用數(shù)據(jù)類型占據(jù)空間大哥放,大小不固定,儲(chǔ)存在堆內(nèi)存爹土,但是指向該引用數(shù)據(jù)類型的變量指針「a」是儲(chǔ)存在棧內(nèi)存中甥雕。 當(dāng)解釋器尋找引用值時(shí),會(huì)首先檢索其在棧中的地址胀茵,取得地址后從堆中獲得實(shí)體社露。
var a = new String("123");
var b = String("123");
var c = "123";
console.log(a == b, a === b, b == c, b === c, a == c, a === c);
// true false true true true false
我們可以看到new
一個(gè)String
,出來的是對(duì)象琼娘,而直接字面量賦值和工廠模式出來的都是字符串峭弟。
let a = new String("123");
let b = new String("123");
console.log(a == b, a === b);
console.log(null === null);
//false false
//true
我們知道,變量a
和變量b
都是存儲(chǔ)在棧內(nèi)存中的脱拼,但是變量a
指向一個(gè)對(duì)象瞒瘸,假設(shè)該對(duì)象的堆內(nèi)存地址AA00
,變量b
執(zhí)行一個(gè)新的對(duì)象,該新對(duì)象的堆內(nèi)存地址BB00
熄浓,也就是變量a
的值為AA00
情臭,變量b
的值為BB00
,所以變量a
和變量b
的值不同。而null
是存儲(chǔ)在棧內(nèi)存的赌蔑,所以null === null
為true
俯在。
let a = null;
let b = null;
console.log(a === b);//true
3、基本數(shù)據(jù)類型和引用數(shù)據(jù)類型的銷毀
基本類型在當(dāng)前執(zhí)行環(huán)境結(jié)束時(shí)銷毀娃惯,而引用類型不會(huì)隨執(zhí)行環(huán)境結(jié)束而銷毀朝巫,只有當(dāng)所有引用它的變量不存在時(shí)這個(gè)對(duì)象才被垃圾回收機(jī)制回收。
三石景、淺拷貝和深拷貝的區(qū)別
基本數(shù)據(jù)類型都是深拷貝,無需討論拙吉。引用數(shù)據(jù)類中的淺拷貝只復(fù)制指向某個(gè)對(duì)象的指針潮孽,而不復(fù)制對(duì)象本身,新舊對(duì)象還是共享同一塊堆內(nèi)存筷黔。但深拷貝會(huì)另外創(chuàng)造一個(gè)一模一樣的對(duì)象往史,新對(duì)象跟原對(duì)象不共享堆內(nèi)存,修改新對(duì)象不會(huì)改到原對(duì)象佛舱。淺拷貝和深拷貝椎例,都是圍繞堆棧內(nèi)存展開的挨决,一個(gè)是處理值,一個(gè)是處理指針订歪。
四脖祈、淺拷貝的實(shí)現(xiàn)方式
1、直接賦值
let A = {
name: "king",
data: {
age: 20,
},
};
let B = {};
B = A;
B.name = "偶爾平凡";
console.log(A);
//{ name: '偶爾平凡', data: { age: 20 } }
2刷晋、Object.assign()
這是ES6中新增的對(duì)象方法盖高,對(duì)它不了解的見ES6對(duì)象新增方法,它可以實(shí)現(xiàn)第一層的“深拷貝”眼虱,但無法實(shí)現(xiàn)多層的深拷貝喻奥。
第一層“深拷貝”:就是對(duì)于A對(duì)象下所有的屬性和方法都進(jìn)行了深拷貝,但是當(dāng)A對(duì)象下的屬性如data是對(duì)象時(shí)捏悬,它拷貝的是地址撞蚕,也就是淺拷貝,這種拷貝方式還是屬于淺拷貝过牙。
多層深拷貝:能將A對(duì)象下所有的屬性甥厦,及時(shí)屬性是對(duì)象,也能夠深拷貝出來抒和,讓A和B相互獨(dú)立矫渔,這種叫才叫深拷貝。
let A = {
name: "king",
data: {
age: 20,
},
};
let B = {};
Object.assign(B, A);
B.name = "偶爾平凡";
B.data.age = 15;
console.log(A);
console.log(B);
//{ name: 'king', data: { age: 15 } }
//{ name: '偶爾平凡', data: { age: 15 } }
3摧莽、擴(kuò)展運(yùn)算符
let obj = { name: "king", age: 10, se: { sex: 0 } };
let newobj = { ...obj };
newobj.name = "偶爾平凡";
newobj.se.sex = 1;
console.log(obj);
console.log(newobj);
//{ name: 'king', age: 10, se: { sex: 1 } }
//{ name: '偶爾平凡', age: 10, se: { sex: 1 } }
4庙洼、Array.prototype.concat()
let arr = [
1,
3,
{
name: "king",
},
];
let arr2 = arr.concat();
arr2[1] = 10;
arr2[2].name = "偶爾平凡";
console.log(arr);
console.log(arr2);
//[ 1, 3, { name: '偶爾平凡' } ]
//[ 1, 10, { name: '偶爾平凡' } ]
5、Array.prototype.slice()
let arr = [
1,
3,
{
name: "king",
},
];
let arr2 = arr.slice();
arr2[1] = 10;
arr2[2].name = "偶爾平凡";
console.log(arr);
console.log(arr2);
//[ 1, 3, { name: '偶爾平凡' } ]
//[ 1, 10, { name: '偶爾平凡' } ]
關(guān)于Array的slice和concat方法的補(bǔ)充說明:Array的slice和concat方法不修改原數(shù)組镊辕,只會(huì)返回一個(gè)淺復(fù)制了原數(shù)組中的元素的一個(gè)新數(shù)組油够。
原數(shù)組的元素會(huì)按照下述規(guī)則拷貝:
- 如果該元素是個(gè)對(duì)象引用(不是實(shí)際的對(duì)象),slice 會(huì)拷貝這個(gè)對(duì)象引用到新的數(shù)組里征懈。兩個(gè)對(duì)象引用都引用了同一個(gè)對(duì)象石咬。如果被引用的對(duì)象發(fā)生改變,則新的和原來的數(shù)組中的這個(gè)元素也會(huì)發(fā)生改變卖哎。
- 對(duì)于字符串鬼悠、數(shù)字及布爾值來說(不是 String、Number 或者 Boolean 對(duì)象)亏娜,slice 會(huì)拷貝這些值到新的數(shù)組里焕窝。在別的數(shù)組里修改這些字符串或數(shù)字或是布爾值,將不會(huì)影響另一個(gè)數(shù)組维贺。
五它掂、深拷貝的實(shí)現(xiàn)方式
1、JSON.parse(JSON.stringify())
let arr = [1, 2, { name: "king" }];
let arr2 = JSON.parse(JSON.stringify(arr));
arr2[1] = 10;
arr2[2].name = "偶爾平凡";
console.log(arr);
console.log(arr2);
//[ 1, 2, { name: 'king' } ]
//[ 1, 10, { name: '偶爾平凡' } ]
這種方法雖然可以實(shí)現(xiàn)數(shù)組或?qū)ο笊羁截愃萜荒芴幚砗瘮?shù)虐秋。這是因?yàn)?JSON.stringify()
方法是將一個(gè)JavaScript
值(對(duì)象或者數(shù)組)轉(zhuǎn)換為一個(gè)JSON
字符串榕茧,不能接受函數(shù)。
let arr = [1, 2, { name: "king", fn: function() {} }];
let arr2 = JSON.parse(JSON.stringify(arr));
console.log(arr);
console.log(arr2);
//[ 1, 2, { name: 'king', fn: [Function: fn] } ]
//[ 1, 2, { name: 'king' } ]
2客给、手寫遞歸函數(shù)實(shí)現(xiàn)
第1步:假設(shè)遞歸函數(shù)已寫好用押。
const deepClone = (value) => {
}
第2步:處理第一層的情況,也就是一般的情況起愈。比如對(duì)方傳遞了一個(gè)null
或者undefined
進(jìn)來可以處理的只恨。
const deepClone = (value) => {
if (value == null) {
return value;
}
};
let result = deepClone(null);
let result1 = deepClone(undefined);
console.log(result,result1);//null undefined
第3步:還是處理一般的情況,比如對(duì)方傳遞了一個(gè)普通數(shù)據(jù)類型或許函數(shù)抬虽,直接返回該值即可搞定官觅。
const deepClone = (value) => {
if (value == null) {
return value;
}
if (typeof value !== "object") {
return value;
}
};
let result = deepClone('abc');
console.log(result);//abc
第4步:還是處理一般的情況,比如對(duì)方傳遞了一個(gè)正則類型或許時(shí)間類型的值阐污,那么返回一個(gè)新正則或時(shí)間即可休涤。
const deepClone = (value) => {
if (value == null) {
return value;
}
if (typeof value !== "object") {
return value;
}
if (value instanceof RegExp) {
return new RegExp(value);
}
if (value instanceof Date) {
return new Date(value);
}
};
第5步:處理傳進(jìn)來的是對(duì)象或者數(shù)組的情況了。
const deepClone = (value) => {
if (value == null) {
return value;
}
if (typeof value !== "object") {
return value;
}
if (value instanceof RegExp) {
return new RegExp(value);
}
if (value instanceof Date) {
return new Date(value);
}
let instance = new value.constructor(); //創(chuàng)建一個(gè)新的空對(duì)象或數(shù)組
for (let key in value) {
instance[key] = value[key];
}
return instance;
};
let obj = { name: "king", age: 10 };
let result = deepClone(obj);
result.name = "偶爾平凡";
console.log(result);//{ name: '偶爾平凡', age: 10 }
console.log(obj);//{ name: 'king', age: 10 }
第6步:我們上面處理的對(duì)象是單層對(duì)象笛辟,如果對(duì)象內(nèi)的值 value[key]
又是一個(gè)對(duì)象怎么辦呢功氨,這時(shí)候,才開始考慮遞歸即可手幢。因?yàn)榈谝粚拥那闆r我們已經(jīng)處理好了捷凄,第二層重復(fù)第一層就OK了。
//深拷貝
const deepClone = (value) => {
if (value == null) {
//排除null 和 undefine的情況,直接返回
return value;
}
if (typeof value !== "object") {
//基本數(shù)據(jù)類型和函數(shù)的情況直接返回即可
return value;
}
if (value instanceof RegExp) {
//正則的情況,返回新的正則即可
return new RegExp(value);
}
if (value instanceof Date) {
return new Date(value);
}
//處理對(duì)象或者數(shù)組的情況,new 創(chuàng)建新的空對(duì)象或數(shù)組
let instance = new value.constructor();
for (let key in value) {
if (value.hasOwnProperty(key)) {
//排除原型鏈上的屬性或方法
instance[key] = deepClone(value[key]);
}
}
return instance;
};
let obj = { name: "king", age: 10, se: { sex: 0 }, fn: () => {} };
let newobj = deepClone(obj);
newobj.name = '偶爾平凡';
newobj.se.sex = 1;
console.log(newobj);
console.log(obj);
//{ name: '偶爾平凡', age: 10, se: { sex: 1 }, fn: [Function: fn] }
//{ name: 'king', age: 10, se: { sex: 0 }, fn: [Function: fn] }
確實(shí)围来,實(shí)現(xiàn)了深拷貝跺涤,感覺很完美,但是监透,遇到下面這種情況桶错,會(huì)直接跑死,進(jìn)入死循環(huán)了胀蛮。
let obj = { a: 1 };
obj.b = obj;
let newobj =deepClone(obj);
//RangeError: Maximum call stack size exceeded 內(nèi)存爆裂而亡
最終修改后的內(nèi)容
const deepClone = (value, hash = new WeakMap => {
if (value == null) {
//排除null 和 undefine的情況,直接返回
return value;
}
if (typeof value !== "object") {
//基本數(shù)據(jù)類型和函數(shù)的情況直接返回即可
return value;
}
if (value instanceof RegExp) {
//正則的情況,返回新的正則即可
return new RegExp(value);
}
if (value instanceof Date) {
return new Date(value);
}
//處理對(duì)象或者數(shù)組的情況,new 創(chuàng)建新的空對(duì)象或數(shù)組
let instance = new value.constructor();
if (hash.has(value)) {
//在hash 中查詢一下是否存在過院刁,如果存在就把以前拷貝的返回
return hash.get(value);
}
hash.set(value, instance); //沒有存過就放進(jìn)去
for (let key in value) {
if (value.hasOwnProperty(key)) {
//排除原型鏈上的屬性或方法
instance[key] = deepClone(value[key], hash);
//將hash繼續(xù)傳遞下去,保證每次能拿到以前拷貝的結(jié)果
}
}
return instance;
};
let obj = { a: 1 };
obj.b = obj;
let newobj = deepClone(obj);
console.log(newobj);
//{ a: 1, b: [Circular] }
注意小知識(shí)點(diǎn):
//字典 Map 和 WeakMap 區(qū)別
//Map 中的key為對(duì)象粪狼,如果該對(duì)象外部人工銷毀了退腥,該對(duì)象在Map中并沒有銷毀
//堆內(nèi)存存放的 { name: "king" } 的地址為 OXFF00
// 地址OXFF00 分別賦值給了 obj以及map中去
//變量obj重新賦值了null;但是map中的地址還在再榄,所以堆內(nèi)存中的對(duì)象不會(huì)銷毀
let obj = { name: "king" };
let map = new Map();
map.set(obj, 0);
obj = null;
console.log(map);
console.log(obj);
//Map { { name: 'king' } => 0 }
//null
//obj 重新賦值后阅虫,WeakMap中的obj也改變,也就是若引用
let obj = { name: "king" };
let map = new WeakMap();
map.set(obj, 0);
obj = null;
console.log(map);
console.log(obj);
//WeakMap { <items unknown> }
//null
六不跟、遞歸基礎(chǔ)知識(shí)
1、什么是遞歸
在JavaScript程序中米碰,函數(shù)直接或間接調(diào)用自己窝革。通過某個(gè)條件判斷跳出結(jié)構(gòu)购城,有了跳出才有結(jié)果。
2虐译、遞歸寫法的步驟
- 假設(shè)遞歸函數(shù)已經(jīng)寫好了瘪板。
- 尋找遞推關(guān)系。
- 將遞推關(guān)系的結(jié)構(gòu)轉(zhuǎn)換為遞歸體
- 將臨界條件加入到遞歸體中(一定要加臨界條件漆诽,某則陷入死循環(huán)侮攀,內(nèi)存泄漏)
3、遞歸示例
求1-100的和厢拭,用遞歸該怎么寫呢兰英?按照遞歸的步驟來即可
1、假設(shè)遞歸函數(shù)已經(jīng)寫好了供鸠,即為sum(100)
,就是求1-100
的和畦贸。
2、尋找遞推關(guān)系楞捂,就是n
和n-1
的關(guān)系 sum(n) ==sum(n-1) +n
薄坏。
let result = sum(100);
let result = sum(99) +100;
3、將遞歸結(jié)構(gòu)轉(zhuǎn)換為遞歸體寨闹。
function sum(n) {
return sum(n - 1) + n;
}
4胶坠、加入臨界條件,防止死循環(huán)繁堡。求100 轉(zhuǎn)換為 求99 求99 轉(zhuǎn)換為 求98 求98 轉(zhuǎn)換為 求97 … 求2 轉(zhuǎn)換為 求1 求1 轉(zhuǎn)換為 求1 即 sum(1) = 1
function sum(n) {
if (n == 1) return 1;
return sum(n - 1) + n;
}
console.log(sum(100));//5050