js中,内存主要分为两种类型:栈内存(stack)、堆内存(heap),两种内存区域在存储和管理数据时有各自的特点和用途。
栈内存
访问顺序
栈是先进后出、后进先出的数据结构,栈内存是内存用于存放临时变量的一片内存块,是一种特殊的列表,栈内元素只能通过列表的一端访问,这一端成为栈顶,另一端称为栈底。
存储数据
栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null、Symbol、BigInt以及对象变量的指针。
比如乒乓球盒子,盒子顶层的乒乓球,是最后放进去的,但可以最先被使用,如果想要取最底层的乒乓球,必须将上面的乒乓球都取出才行,这就是栈先进后出、后进先出的特点。
注意:闭包中的基本数据类型变量是在堆中,而不是在栈中;
通过new String类似这种new出来的实例对象,也是在堆中,而不是在栈中;
堆内存
访问顺序
堆内存不同于栈,虽然都是存储的一片空间,但堆中存储变量没什么规律,只会用一块足够大的空间存储变量。js不允许直接访问堆内存中的位置。
存储数据
堆内存主要用于存储像对象Object变量类型的的存储,堆内存存储的对象类型数据对于大小这方面,一般都是未知的。
注意:闭包中的基本数据类型变量是在堆中,而不是在栈中;
通过new String类似这种new出来的实例对象,也是在堆中,而不是在栈中;
// 基本数据类型-栈内存
let a1 = 0;
// 基本数据类型-栈内存
let a2 = 'this is string';
// 基本数据类型-栈内存
let a3 = null;
// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中
let b = { m: 20 };
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let c = [1, 2, 3];
变量复制
基本类型复制
let a = 20;
let b = a;
b = 30;
console.log(a); // 此时 a 的值是多少,是 30?还是 20?
// 答案是20
a、b都是基本类型,值是存在栈中的,a、b有各自独立的栈空间,所以修改了b的值后,a不会变化。
引用类型复制
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) // 此时 m.a 的值是多少,是10?还是15?
// 答案是15
m、n都是引用类型,栈内存中存放的地址指向堆内存中的对象,引用类型的复制会为新的变量自动分配一个新的值保存在变量中,但只是引用类型的一个地址指针而已,实际指向的是同一个对象。
常常问到的面试题:const定义的值是否可以改
答案:部分能改,部分不能改。
- 定义基本数据类型后,是不可修改的;
- 定义对象时,不可修改的是指向堆内存中的地址,对象内部的属性和方法是可以修改的;
栈内存、堆内存优缺点
栈:
- 存储基本数据类型:Boolean、Number、String、Undefined、Null、Symbol、BigInt以及对象变量的指针。
- 固定大小:栈内存的大小是固定的,由操作系统在程序运行时分配,数据进栈,栈顶指针移动,分配空间,数据出栈,栈顶指针反向移动,释放空间。
- 快速访问:栈内存采用线性结构,访问数据速度非常快。
- 自动管理:js引擎会自动管理栈内存,分配和回收空间,不需要手动介入。
- 函数执行的时候是放在栈里执行的。
堆:
- 存储引用数据类型:Object(包括普通对象、数组、函数)、String(通过拼接方式创建)
- 动态大小:堆内存大小是动态的,可以根据程序的运行需求进行扩展和收缩。
- 较慢访问:堆内存的数据是通过栈中的引用地址访问的,所以相对较慢。
- 垃圾回收:堆内容中的变量只有在所有的引用都结束后,才会被回收。
- 闭包中的局部变量存在堆中。
浏览器的事件机制
对象是放在堆里的,常见的基础类型和函数放在栈里,函数执行的时候,在栈里执行,栈里函数执行的时候,可能会调一些Dom操作,ajax,settimeout,这时候,要等栈里所有程序先走,走完再执行ajax,ajax执行完后,结果放在回调队列里(队列中代码,先进先执行),也就是当栈里程序走完,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行。
- 所有同步任务都在主线程上执行,形成一个执行栈;
- 主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件;
- 一旦执行栈里所有的同步任务执行完毕,胸痛会读取任务队列中的事件,放到执行栈中,依次执行;
- 主线程从任务队列中读取事件,这个过程是循环不断的;
- 宏任务、微任务属于队列,并不是放在栈中,具体看【JS】Promise与setTimeout执行顺序_js settimeout 同步执行-CSDN博客