J

JavaScript 中的内存生命周期、堆、栈和调用栈

2025-05-25

JavaScript 中的内存生命周期、堆、栈和调用栈

JavaScript 中有一些主题你作为一名开发者可能还不了解。了解这些主题可以帮助你写出更好的代码。内存生命周期、堆、堆栈和调用堆栈就是其中的一部分。在本教程中,你将学习这些主题以及 JavaScript 的一些工作原理。

简要介绍

JavaScript 是一种非常宽容的编程语言。它允许你以多种方式做很多事情。它也为你做了很多工作。内存管理就是其中之一。问问自己:你有多少次不得不考虑为变量或函数分配内存?

当你不再需要这些变量或函数时,你有多少次想过要释放内存?很可能一次都没有。同样的道理也适用于了解堆、栈和调用栈的工作原理,或者它们到底是什么。然而,你仍然可以使用 JavaScript。你仍然可以编写每天正常工作的代码。

这些内容你不必知道,也并非必须知道。然而,了解它们以及它们的工作原理可以帮助你理解 JavaScript 的工作原理。反过来,这可以帮助你编写更好的代码,成为更优秀的 JavaScript 开发者。

内存生命周期

让我们从最简单的部分开始。什么是内存生命周期?它的含义是什么?它在 JavaScript 中是如何运作的?内存生命周期指的是编程语言如何处理内存。无论使用哪种语言,内存生命周期几乎总是相同的。它由三个步骤组成。

第一步是内存分配。当你赋值一个变量,或者创建一个函数或对象时,必须为其分配一定量的内存。第二步是内存使用。当你在代码中处理数据(读取或写入)时,你就在使用内存。读取变量或更改变量值就是读取内存,写入内存也是。

第三步是内存释放。当你不再使用某个函数或对象时,该内存就可以被释放。释放后,它就可以再次使用。简而言之,这就是内存生命周期。JavaScript 的优点在于它能帮你完成这三个步骤。

JavaScript 会根据您的需求分配内存。这让您能够更轻松地使用分配的内存。最后,它还会处理繁琐的工作,清理所有杂乱。它使用垃圾回收机制持续检查内存,并在不再使用时将其释放。结果如何?

作为 JavaScript 开发者,您无需担心为变量或函数分配内存。您也无需担心在读取数据之前选择正确的内存地址。此外,您也无需担心释放之前使用过的内存。

堆栈和内存堆

现在您了解了内存生命周期的各个步骤。您了解了内存的分配、使用和释放。您可能会问,这些变量、函数和对象实际上存储在哪里?答案是:视情况而定。JavaScript 不会将所有这些东西存储在同一个地方。

JavaScript 的做法是使用两个地方。这两个地方分别是栈和内存堆。具体使用哪个地方取决于你当前正在处理什么。

堆栈

JavaScript 栈仅用于存储静态数据。这包括原始数据类型的值,例如数字、字符串、布尔值undefinednull。这些静态数据也包含引用,这些引用指向你创建的对象和函数。

这些数据有一个共同点:它们的大小是固定的,并且 JavaScript 在编译时就知道这个大小。这也意味着 JavaScript 知道应该分配多少内存,并且会分配该大小。这种内存分配称为“静态内存分配”。它发生在代码执行之前。

关于静态数据和内存,有一点很重要。这些原始值的大小是有限制的。堆栈本身也是如此,它也有限制。这些限制的具体大小取决于具体的浏览器和引擎。

// Declare and assign some variables
// and assign them primitive data types
// All these variables are stored in stack
const firstName = 'Jill'
const lastName = 'Stuart'
const age = 23
const selfEmployed = true
const dateOfMarriage = null

// The stack after declaring
// and assigning those variables:

// Stack:
// dateOfMarriage = null
// selfEmployed = true
// age = 23
// lastName = 'Stuart'
// firstName = 'Jill'
Enter fullscreen mode Exit fullscreen mode

内存堆

JavaScript 存储数据的第二个地方是内存堆。这种存储方式更加动态。对于内存堆,JavaScript 不会分配固定大小的内存,而是根据当前需要分配内存。这种内存分配方式称为“动态内存分配”。

哪些数据存储在内存堆中?JavaScript 的栈用于存储静态数据,而内存堆则用于存储对象和函数。因此,请记住,当你使用原语创建对象时,你处理的是静态数据。JavaScript 将这些静态数据存储在栈中。

这些数据始终拥有固定分配的内存。另一方面,当你创建对象或函数时,JavaScript 会将它们存储在内存堆中。这些对象的内存分配并不固定,而是根据需要动态分配。

// Declare a variable and assign it an object
const terryP = {
  firstName: 'Terry',
  lastName: 'Pratchett',
  profession: 'author'
}

function introduceTerry() {
  return `Hi, my name is ${terryP.firstName}.`
}

const series = ['Discworld', 'Johnny Maxwell', 'Long Earth']

const isDone = true

// Stack:
// isDone = true
// introduceTerry (reference to function)
// terryP (reference to "terryP" object)
// series (reference to "series" array)


// Memory heap:
//  {
//    firstName: 'Terry',
//    lastName: 'Pratchett',
//    profession: 'author
//  }
//  function introduceTerry() {
//    return `Hi, my name is ${terryP.firstName}.`
// }
//  ['Discworld', 'Johnny Maxwell', 'Long Earth']

// NOTE:
// the "terryP" in stack points
// to the "terryP" object in memory heap
// the "introduceTerry" in stack points
// to introduceTerry() function in memory heap
// the "series" in stack points
// to the "series" array in memory heap
// arrays are objects in JavaScript
Enter fullscreen mode Exit fullscreen mode

栈、堆和引用

当你创建一个变量并赋予其原始值时,它将存储在堆栈中。当你使用对象尝试相同的操作时,情况会有所不同。如果你声明一个变量并赋予其对象,则会发生两件事。首先,JavaScript 会在堆栈中为该变量分配内存。

当涉及到对象本身时,JavaScript 会将其存储在内存堆中。堆栈中存在的变量只会指向内存堆中的这个对象。该变量将是该对象的引用。你可以将引用视为现有事物的快捷方式或别名。

这些引用并非指代那些事物本身。它们只是指向那些“真实”事物的链接。您可以使用这些链接访问它们所引用(或链接到)的事物,并对其进行操作。

// Declare variable and assign it an object
// The "cat" variable will be stored in stack
// It will hold the reference to the "cat" object
const cat = {
  name: 'Kitty'
  breed: 'Abyssinian'
}

// The "cat" object itself will be stored in memory heap.

// Memory heap:
//  {
//    name: 'Kitty',
//    breed: 'Abyssinian'
//  }
Enter fullscreen mode Exit fullscreen mode

复制对象和基元

这也是为什么在 JavaScript 中创建对象副本实际上并非那么简单。尝试通过引用来创建存储在变量中的对象的副本,并不会创建真正的副本。它不会复制对象本身,而只会复制对该对象的引用。这称为浅拷贝

当您更改原始对象时,副本也会随之更改。这是因为仍然只有一个对象。但是,该对象有两个引用(别名或链接)。当您使用其中一个引用更改对象时,另一个引用仍然指向同一个对象,即您刚刚更改的对象。

// Declare a variable and assign it an object
const bookShelf = {
  read: 'Colour Of Magic',
  reading: 'Night Watch',
  toRead: 'Going Postal'
}

// Create a copy of the "bookShelf"
const newBookShelf = bookShelf

// Update the "bookShelf"
bookShelf.reading = 'Mort'
bookShelf.justFinished = 'Night Watch'

// Log the value of "bookShelf"
console.log(bookShelf)
// Output:
// {
//   read: 'Colour Of Magic',
//   reading: 'Mort',
//   toRead: 'Going Postal',
//   justFinished: 'Night Watch'
// }

// Log the value of "newBookShelf"
// Since "newBookShelf" and "bookShelf"
// points to the same object
// the output will be the same
console.log(newBookShelf)
// Output:
// {
//   read: 'Colour Of Magic',
//   reading: 'Mort',
//   toRead: 'Going Postal',
//   justFinished: 'Night Watch'
// }
Enter fullscreen mode Exit fullscreen mode

当你尝试复制原始值时,这种情况不会发生。当你尝试复制原始值并更改原始值时,副本将保持不变。原因:没有引用。你正在创建真正的副本,并且直接使用这些副本。

// Declare a variable with some primitive value
let book = 'Guards! Guards! (Paperback)'

// Create a copy of the "book"
const bookToRead = book

// Update the value of "book"
book = 'Guards! Guards! (Kindle Edition)'

// Log the value of "book"
// This will log the updated value
console.log(book)
// Output:
// 'Guards! Guards! (Kindle Edition)'

// Log the value of "bookToRead"
// This will log the old value because the "bookToRead"
// is a real copy of "book"
console.log(bookToRead)
// Output:
// 'Guards! Guards! (Paperback)'
Enter fullscreen mode Exit fullscreen mode

创建真正的副本(深层副本)稍微复杂一些。一种效率较低的方法是从头开始写入该对象。另一种方法是使用Object.assign()。还有一种是结合使用JSON.parse()JSON.stringify()

// Declare a variable and assign it an object
const bookShelf = {
  read: 'Colour Of Magic',
  reading: 'Night Watch',
  toRead: 'Going Postal'
}

// Create a copy of the "bookShelf"
const newBookShelf = Object.assign({}, bookShelf)

// Update the "bookShelf"
bookShelf.reading = 'Mort'
bookShelf.justFinished = 'Night Watch'

// Log the value of "bookShelf"
console.log(bookShelf)
// Output:
// {
//   read: 'Colour Of Magic',
//   reading: 'Mort',
//   toRead: 'Going Postal',
//   justFinished: 'Night Watch'
// }

// Log the value of "newBookShelf"
// The output will be different this time
// because the "newBookShelf" points
// to a different object than the "bookShelf"
console.log(newBookShelf)
// Output:
// {
//   read: 'Colour Of Magic',
//   reading: 'Night Watch',
//   toRead: 'Going Postal'
// }
Enter fullscreen mode Exit fullscreen mode

调用堆栈

您可能已经听说过“调用堆栈”。这与我们之前在本教程中讨论的堆栈不同。如您所知,堆栈是 JavaScript 用来存储赋值原始值的变量的地方。调用堆栈则有所不同。

调用栈是 JavaScript 用于跟踪函数的一种机制。当你调用一个函数时,JavaScript 会将该函数添加到调用栈中。如果该函数调用了另一个函数,JavaScript 也会将该函数添加到调用栈中,位于第一个函数的上方。

这个过程将重复执行任何将被前一个函数调用的其他函数。当一个函数执行完毕后,JavaScript 会将该函数从调用栈中移除。有两件重要的事情。首先,栈中的每个新函数都会被添加到调用栈的顶部。

第二点是,调用栈是从上到下执行的。最后加入栈的函数将最先执行。最先加入栈的函数将最后执行。这也被称为 LIFO 原则(后进先出)。我们用一个简单的示例代码来说明这一点。

function myFuncOne() {
  return 'This is the end.'
}

function myFuncTwo() {
  myFuncOne()

  return 'Knock knock.'
}

// Call stack is still empty here

myFuncTwo()

// Call stack:
// Step 1: myFuncTwo() is invoked
// Step 2: myFuncTwo() added to the call stack
// Step 3: myFuncTwo() calls myFuncOne()
// Step 4: myFuncOne() is added to the call stack
// Step 5: myFuncOne(), is executed
// Step 6: myFuncOne() removed from the stack
// Step 7: JavaScript goes back to myFuncTwo()
// Step 8: any code left inside myFuncTwo() after myFuncOne() call is executed
// Step 9: myFuncTwo() is removed from the stack
// Step 10: call stack is empty
Enter fullscreen mode Exit fullscreen mode

结论:JavaScript 中的内存生命周期、堆、栈和调用栈

内存生命周期、堆、栈和调用栈是不太常被讨论的主题。关于它们,可用的资料并不多。我希望本教程能帮助你理解什么是内存生命周期、堆、栈和调用栈,以及它们的工作原理。

文章来源:https://dev.to/alexdevero/memory-life-cycle-heap-stack-and-call-stack-in-javascript-5977
PREV
深入探究 JavaScript 中的函数式编程
NEXT
正确处理 React 组件中的 async/await