在 JS 中,内存分两种:堆内存和栈内存。简单理解,栈内存就是给代码的执行提供一个环境,也叫上下文,它的大小是固定的(存在栈内存溢出的情况);而堆内存是用来存储引用类型值的,比如对象、数组等,它的大小不固定。
内存生命周期
- 分配:在变量刚被定义时就被分配了内存
- 使用:在使用变量时其实是对其内存进行读取、写入
- 回收:内存不是无限使用的,JS 有垃圾回收机制
堆内存的访问
let b = [1, 2, 3, 4];
let c = b;
c.pop();
console.log(b);//[1, 2, 3]
第一行代码在内存中声明了一个变量 b ,它的值是一个数组的引用,这个数组的值存储在堆内存中,在访问这个数组的时候不是通过值直接访问的而是通过访问它在内存中的地址(堆内存中的每个变量对象都有一个16进制地址)。所以变量 b 中存储的就是该数组在堆内存中的地址,变量 c 获得了地址后对其进行操作,其结果是改变了堆内存中那个数组对象的值,而变量 b 和变量 c 的值未变——堆内存中的地址。
一个容易错的问题
let a = {};
let b = [1, 2, 3, 4];
let c = b;
c = a;
console.log(b);// [1, 2, 3, 4]
console.log(c);// {}
b 和 c 同时获得了数组对象的引用后,将 a 的值赋值给 c,这时候就比较容易想错:变量 a 会不会将数组对象在内存中覆盖?答案是不会。因为赋值操作永远是操作基本类型值,而不直接操作引用类型,即“赋”的是16进制地址。
单线程
为什么 JavaScript 是单线程的呢?想象一下,如果它是多线程的,两个线程同时操作同一个 DOM ,必然会造成混乱,所以 JavaScript 必然是单线程的。它的异步和多线程是通过 Event Loop 来实现的。
事件循环机制(Event Loop)
它有三个部分组成:
-
调用栈(Call Stack)
-
消息队列(Message Queue)
-
微任务队列(Microtask Queue)
- 首先从全局代码开始,一行行压入调用栈中执行并弹出,当遇到函数时,函数中的代码也被一行行压入栈顶执行完出栈,被压入的函数叫做帧(Frame)。当所有代码执行完后调用栈为空。
- 执行代码时遇到 fetch、setTimeOut()、setInterval() 中的回调函数会入队到消息队列中,然后继续往下执行代码,当遇到 promise、await、async 时,会将它们创建的异步操作入队到微任务队列中。
- 当调用栈清空时才先将微任务队列中的代码入栈执行,再执行消息队列中的代码。