一、棧內(nèi)存與堆內(nèi)存
棧內(nèi)存與堆內(nèi)存 、淺拷貝與深拷貝,可以說(shuō)是前端程序員的內(nèi)功,要知其然,知其所以然。
1、棧與棧內(nèi)存

棧的定義
- 后進(jìn)者先出,先進(jìn)者后出,簡(jiǎn)稱(chēng) 后進(jìn)先出(LIFO),這就是典型的
棧結(jié)構(gòu)。 - 新添加的或待刪除的元素都保存在棧的末尾,稱(chēng)作
棧頂,另一端就叫棧底。 - 在棧里,新元素都靠近棧頂,舊元素都接近棧底。
- 從棧的操作特性來(lái)看,是一種
操作受限的線(xiàn)性表,只允許在一端插入和刪除數(shù)據(jù)。 - 不包含任何元素的棧稱(chēng)為
空棧。 - 棧也被用在編程語(yǔ)言的編譯器和內(nèi)存中保存變量、方法調(diào)用等,比如函數(shù)的調(diào)用棧。
棧內(nèi)存
- 它們存儲(chǔ)的值都有固定的大小,保存在棧內(nèi)存空間,通過(guò)按值訪(fǎng)問(wèn),并由系統(tǒng)自動(dòng)分配和自動(dòng)釋放。
- 這樣帶來(lái)的好處就是,內(nèi)存可以及時(shí)得到回收,相對(duì)于堆來(lái)說(shuō),更加容易管理內(nèi)存空間。
- 主要用于存儲(chǔ)各種基本數(shù)據(jù)類(lèi)型的變量,以及對(duì)象變量的指針。
- JavaScript 中的
Boolean、Null、Undefined、Number、String、Symbol、BigInt都是基本數(shù)據(jù)類(lèi)型。
2、堆與堆內(nèi)存
堆的定義
- 堆數(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)釋放。
- 棧,線(xiàn)性結(jié)構(gòu),后進(jìn)先出,便于管理。
- 堆,一個(gè)混沌,雜亂無(wú)章,方便存儲(chǔ)和開(kāi)辟內(nèi)存空間。
堆內(nèi)存
引用類(lèi)型(如對(duì)象、數(shù)組、函數(shù)等)是保存在堆內(nèi)存中的對(duì)象,值大小不固定,棧內(nèi)存中存放的該對(duì)象的訪(fǎng)問(wèn)地址指向堆內(nèi)存中的對(duì)象,JavaScript 不允許直接訪(fǎng)問(wèn)堆內(nèi)存中的位置,因此操作對(duì)象時(shí),實(shí)際操作對(duì)象的引用。
JavaScript 中的 Object、Array、Function、RegExp、Date 是引用類(lèi)型。
堆內(nèi)存與棧內(nèi)存比較
| 棧內(nèi)存 | 堆內(nèi)存 |
|---|---|
| 存儲(chǔ)基礎(chǔ)數(shù)據(jù)類(lèi)型 | 存儲(chǔ)引用數(shù)據(jù)類(lèi)型 |
| 按值訪(fǎng)問(wèn) | 按引用訪(fǎng)問(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ù)引用直接獲取 |

棧內(nèi)存中的變量一般都是已知大小或者有范圍上限的,算作一種簡(jiǎn)單存儲(chǔ)。而堆內(nèi)存存儲(chǔ)的對(duì)象類(lèi)型數(shù)據(jù)對(duì)于大小這方面,一般是未知的。這就是為什么null作為一個(gè)object類(lèi)型的變量卻存儲(chǔ)在棧內(nèi)存中的原因吧。
當(dāng)我們定義一個(gè)const obj ={name:'king'}對(duì)象的時(shí)候,我們說(shuō)的常量obj其實(shí)是指針,就是變量obj的值不允許改變,也就是變量obj指向的堆內(nèi)存的地址不能改變,但是,該堆內(nèi)存地址內(nèi)數(shù)據(jù)本身的大小或?qū)傩允强梢愿淖兊摹?/p>
既然知道了const在內(nèi)存中的存儲(chǔ),那么const、let定義的變量不能二次定義的流程也就比較容易猜出來(lái)了,每次使用const或者let去初始化一個(gè)變量的時(shí)候,會(huì)首先遍歷當(dāng)前的內(nèi)存棧,看看有沒(méi)有重名變量,有的話(huà)就返回錯(cuò)誤。
二、數(shù)據(jù)類(lèi)型
在JS中,數(shù)據(jù)分為基本數(shù)據(jù)類(lèi)型(String、Number、Boolean、Null、Undefined、Symbol、BigInt)和引用數(shù)據(jù)類(lèi)型(Object、Function、class)。
- 基本數(shù)據(jù)類(lèi)型是直接存儲(chǔ)在棧(Stack)內(nèi)存中的數(shù)據(jù)
-
引用數(shù)據(jù)類(lèi)型棧內(nèi)存存儲(chǔ)的是該對(duì)象的引用地址,對(duì)象的真實(shí)數(shù)據(jù)存儲(chǔ)在堆內(nèi)存中
引用數(shù)據(jù)類(lèi)型在棧內(nèi)存中存儲(chǔ)了引用地址(也叫指針),該引用地址或指針指向了堆內(nèi)存中實(shí)際數(shù)據(jù)的地址。
引用數(shù)據(jù)類(lèi)型存儲(chǔ)
1、基本數(shù)據(jù)類(lèi)型的存儲(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沒(méi)有任何關(guān)系。注意 棧內(nèi)存的賦值都是值賦值,newnum = num是指將變量num的值賦值給新變量newnum;而不是把變量num賦值給變量newnum。
基本數(shù)據(jù)類(lèi)型值是不可變的
let a = 1;
console.log(++a);
其實(shí)這個(gè)時(shí)候并不是變量a 指向的 1 直接加了 1,而是 新建了一個(gè) 1+1 = 2 的值,再將 a 指向這個(gè)新建出來(lái)的 2,原來(lái)的那個(gè) 1 并沒(méi)有發(fā)生改變,留由垃圾回收機(jī)制處理。也就是說(shuō)不是 a 指向的值發(fā)生了改變,而是 a 變量指針指向了一個(gè)新的值,這就是基本類(lèi)型的值是不可變的。
2、引用數(shù)據(jù)類(lèi)型的存儲(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ù)類(lèi)型值是可變的
let obj = { name: 'king' };
obj.name = '偶爾平凡';
console.log(obj)
//{ name: '偶爾平凡' }
引用數(shù)據(jù)類(lèi)型占據(jù)空間大,大小不固定,儲(chǔ)存在堆內(nèi)存,但是指向該引用數(shù)據(jù)類(lèi)型的變量指針「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,出來(lái)的是對(duì)象,而直接字面量賦值和工廠(chǎng)模式出來(lái)的都是字符串。
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ù)類(lèi)型和引用數(shù)據(jù)類(lèi)型的銷(xiāo)毀
基本類(lèi)型在當(dāng)前執(zhí)行環(huán)境結(jié)束時(shí)銷(xiāo)毀,而引用類(lèi)型不會(huì)隨執(zhí)行環(huán)境結(jié)束而銷(xiāo)毀,只有當(dāng)所有引用它的變量不存在時(shí)這個(gè)對(duì)象才被垃圾回收機(jī)制回收。
三、淺拷貝和深拷貝的區(qū)別
基本數(shù)據(jù)類(lèi)型都是深拷貝,無(wú)需討論。引用數(shù)據(jù)類(lèi)中的淺拷貝只復(fù)制指向某個(gè)對(duì)象的指針,而不復(fù)制對(duì)象本身,新舊對(duì)象還是共享同一塊堆內(nèi)存。但深拷貝會(huì)另外創(chuàng)造一個(gè)一模一樣的對(duì)象,新對(duì)象跟原對(duì)象不共享堆內(nèi)存,修改新對(duì)象不會(huì)改到原對(duì)象。淺拷貝和深拷貝,都是圍繞堆棧內(nèi)存展開(kā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ì)它不了解的見(jiàn)ES6對(duì)象新增方法,它可以實(shí)現(xiàn)第一層的“深拷貝”,但無(wú)法實(shí)現(xiàn)多層的深拷貝。
第一層“深拷貝”:就是對(duì)于A(yíng)對(duì)象下所有的屬性和方法都進(jìn)行了深拷貝,但是當(dāng)A對(duì)象下的屬性如data是對(duì)象時(shí),它拷貝的是地址,也就是淺拷貝,這種拷貝方式還是屬于淺拷貝。
多層深拷貝:能將A對(duì)象下所有的屬性,及時(shí)屬性是對(duì)象,也能夠深拷貝出來(lái),讓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)于A(yíng)rray的slice和concat方法的補(bǔ)充說(shuō)明: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ā)生改變,則新的和原來(lái)的數(shù)組中的這個(gè)元素也會(huì)發(fā)生改變。
- 對(duì)于字符串、數(shù)字及布爾值來(lái)說(shuō)(不是 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、手寫(xiě)遞歸函數(shù)實(shí)現(xiàn)
第1步:假設(shè)遞歸函數(shù)已寫(xiě)好。
const deepClone = (value) => {
}
第2步:處理第一層的情況,也就是一般的情況。比如對(duì)方傳遞了一個(gè)null或者undefined進(jìn)來(lái)可以處理的。
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ù)類(lèi)型或許函數(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è)正則類(lèi)型或許時(shí)間類(lèi)型的值,那么返回一個(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)來(lái)的是對(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í)候,才開(kāi)始考慮遞歸即可。因?yàn)榈谝粚拥那闆r我們已經(jīng)處理好了,第二層重復(fù)第一層就OK了。
//深拷貝
const deepClone = (value) => {
if (value == null) {
//排除null 和 undefine的情況,直接返回
return value;
}
if (typeof value !== "object") {
//基本數(shù)據(jù)類(lèi)型和函數(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)了深拷貝,感覺(jué)很完美,但是,遇到下面這種情況,會(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ù)類(lèi)型和函數(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 中查詢(xún)一下是否存在過(guò),如果存在就把以前拷貝的返回
return hash.get(value);
}
hash.set(value, instance); //沒(méi)有存過(guò)就放進(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ì)象外部人工銷(xiāo)毀了,該對(duì)象在Map中并沒(méi)有銷(xiāo)毀
//堆內(nèi)存存放的 { name: "king" } 的地址為 OXFF00
// 地址OXFF00 分別賦值給了 obj以及map中去
//變量obj重新賦值了null;但是map中的地址還在,所以堆內(nèi)存中的對(duì)象不會(huì)銷(xiāo)毀
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)用自己。通過(guò)某個(gè)條件判斷跳出結(jié)構(gòu),有了跳出才有結(jié)果。
2、遞歸寫(xiě)法的步驟
- 假設(shè)遞歸函數(shù)已經(jīng)寫(xiě)好了。
- 尋找遞推關(guān)系。
- 將遞推關(guān)系的結(jié)構(gòu)轉(zhuǎn)換為遞歸體
- 將臨界條件加入到遞歸體中(一定要加臨界條件,某則陷入死循環(huán),內(nèi)存泄漏)
3、遞歸示例
求1-100的和,用遞歸該怎么寫(xiě)呢?按照遞歸的步驟來(lái)即可
1、假設(shè)遞歸函數(shù)已經(jīng)寫(xiě)好了,即為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
