很多前端开发对js的内存机制模模糊糊,我们在本篇文章详细的聊聊js的底层内存机制,首先我们引出一个简单的问题,面试官问你js分为几种数据类型,同学们可能会说八种(es6)也有人会说两种,我们来今天就来看看两种是怎么个事
我们首先来看一段简单代码
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
console.log(m.a)
同学们来看看这会输出什么
答案也很简单当然是15,但是同学们想想这在内存里具体发生了什么,这就引出了我们今天的话题--js的两种数据类型一种为储存在栈空间,一种储存在堆空间即原始类型与引用类型---引用类型只有一种 Object(原型链知识可以先跳过)
我们先来介绍这两种空间
-
栈(Stack) :
- 栈是一个后进先出(LIFO)的数据结构,用于存储执行上下文(Execution Context)。
- 当一个函数被调用时,它的执行上下文会被推入栈中。
- 栈中的变量通常存储在栈帧(Stack Frame)中,每个栈帧代表一个执行上下文。
- 栈的特点是:出口跟入口是同一个,遵循着
先进后出、后进先出的原则。数据只能顺序的入栈,顺序的出栈
-
堆(Heap) :
- 堆是一个用于存储动态分配的内存区域。
- 在JavaScript中,几乎所有的数据(如数组、对象、函数等)都存储在堆内存中。
- 堆中的对象是通过引用(Reference)来访问的,而不是通过值。
- 堆的特点是 :
无序的key-value键值对存储方式。
var m = { a: 10, b: 20 };
这行代码创建了一个对象`m`,并将其存储在堆内存中。栈中保存了对该对象的引用。
var n = m;
这行代码将`m`的引用赋值给`n`,这意味着`n`和`m`都引用堆中的同一个对象。
n.a = 15;
这行代码修改了`n`引用的对象的`a`属性的值。因为`n`和`m`都引用同一个对象,所以`m`引用的对象的`a`属性也会被修改。
console.log(m.a);
最后,我们打印`m`引用的对象的`a`属性的值,输出结果是15。
这里注意此时n与m的引用都是同一个地址
在 JavaScript 中,对象(Objects)通常存储在堆内存中。这些对象可以包含各种类型的数据,包括其他对象、数组、原始值等。由于堆内存的动态性和动态分配的特性,这些对象在堆中的存储位置是动态变化的,因此它们的顺序并不是固定的,也就是说,堆是无序的。
同时,对象在 JavaScript 中是通过键(key)来访问其值的。这些键通常是字符串,但也可能是符号(Symbol)。这意味着,每个对象都可以有一个或多个键值对,并且可以通过这些键来访问或修改对应的值。
现在有没有更加深入理解js的内存分配 我们现在来解决一个新问题-----函数也是引用类型,为什么又在栈内存中呢?(上篇闭包的文章详细讲解了调用栈与作用域链--函数调用的时候会在调用栈创建一个执行上下文)
别急我们来慢慢了解
在JavaScript中,当一个函数被调用时,其执行上下文会被创建,并且这个执行上下文包含了一系列与函数执行相关的信息。这些信息被称为函数的执行上下文信息或上下文帧。
执行上下文信息主要包括:
- 函数的作用域链(Scope Chain) :这包含了函数内部的变量、参数以及函数外部的变量(通过闭包可以访问)。
- 变量对象(Variable Object) :这包含了函数内部的变量和参数。
this值:这取决于函数的调用方式。arguments对象:这包含了传递给函数的参数。
这些信息对于函数的执行是必需的,它们用于确定函数在运行时如何访问和修改变量、参数等。
当函数被调用时,这些信息会被推入栈中,以便在函数执行期间能够访问和修改这些信息。这些信息存储在栈帧(Stack Frame)中,每个栈帧代表一个执行上下文。
而函数本身(即函数对象)是存储在堆中的。栈中的引用(通常是一个指针或引用地址)允许我们在函数执行期间访问和修改函数内的变量和参数。
这种设计允许JavaScript引擎有效地管理内存和执行函数,同时保持函数执行上下文的隔离性。
所以,尽管函数是引用类型,但它们的相关信息(如执行上下文信息)会被存储在栈中,以便在函数执行期间能够访问和修改这些信息。
同学们我们来小小总结一下
栈(Stack)
- 存储基础数据类型:栈主要用于存储基本数据类型(如整数、浮点数、布尔值等)。
- 按值访问:当从栈中取出一个元素时,它会被直接取出并可以使用,不需要通过引用。
- 存储的值大小固定:栈中的元素大小是固定的,每个元素占据的空间大小相同。
- 由系统自动分配内存空间:当创建栈时,系统会自动分配内存空间,通常不需要手动管理。
- 空间小,运行效率高:由于栈的空间有限,所以其内存管理相对简单,从而提高了执行效率。
- 先进后出,后进先出:栈的操作遵循后进先出(LIFO)的原则,即最后进入的元素最先被取出。
- 与事件循环和队列:在JavaScript中,栈与事件循环紧密相关。DOM事件、Ajax响应和
setTimeout回调等通常会被放入任务队列中,当栈中的代码执行完毕后,这些任务会被依次取出并执行。 - 微任务和宏任务:在JavaScript中,微任务(如
Promise.then、MutationObserver等)会优先于宏任务(如setTimeout、setInterval、DOM事件等)执行。微任务会在当前栈中的代码执行完毕后立即执行,而宏任务会等待当前栈中的代码执行完毕后再进入事件循环。
堆(Heap)
- 存储引用数据类型:堆主要用于存储引用数据类型(如对象、数组、函数等)。
- 按引用访问:当从堆中取出一个元素时,通常是通过引用访问的,而不是直接取出值。
- 存储的值大小不定,可动态调整:堆中的元素大小是不固定的,可以根据需要动态调整。
- 主要用来存放对象:由于堆的特性,它主要用于存储对象。
- 空间大,但是运行效率相对较低:由于堆的空间大且动态分配,所以其内存管理相对复杂,从而导致执行效率相对较低。
- 无序存储,可根据引用直接获取:堆中的元素是无序存储的,但可以通过引用直接访问和获取。
同时使用栈和堆可以提高内存管理的效率和灵活性。栈和堆的分离使得代码更加灵活,因为你可以根据需要分配和释放内存。此外,栈和堆的互补特性使得它们成为内存管理的重要部分,共同构成了计算机系统的内存模型,为程序的执行提供了必要的支持。
总的来说,栈和堆的互补性使得它们成为内存管理的关键组件,它们各自的特点和用途使得程序能够高效、安全地执行。