专业编程基础技术教程

网站首页 > 基础教程 正文

一文掌握:掌握 JavaScript 中的内存生命周期。

ccvgpt 2024-12-22 14:48:23 基础教程 1 ℃

一、JavaScript 内存生命周期概述

JavaScript 内存生命周期和大多数程序语言一样,分为三个阶段:分配内存、使用内存、释放内存。不同的编程语言对于这三个阶段的实现方式有所不同,而 JavaScript 的内存管理是自动的,由 JavaScript 引擎帮助开发者处理。

一、JavaScript 内存生命周期概述

一文掌握:掌握 JavaScript 中的内存生命周期。

JavaScript 的内存生命周期,和大多数程序语言一样,分为三个阶段:

  1. 分配内存
    • JavaScript 在定义变量时就完成了内存分配。例如,var n = 123;为数值变量分配内存,var s = "azerty";为字符串分配内存,var o = { a:1, b: null };为对象及其包含的值分配内存,var a = [1, null, "abra"];为数组及其包含的值分配内存。
    • 有些函数调用结果是分配对象内存,如var d = new Date();分配一个 Date 对象,var e = document.createElement('div');分配一个 DOM 元素。
    • 有些方法分配新变量或者新对象,如var s = "azerty";var s2 = s.substr(0,3);,因为字符串是不变量,JavaScript 可能决定不分配内存,只是存储了 [0 - 3] 的范围;var a = ["ouais ouais", "nan nan"];var a2 = ["generation", "nan nan"];var a3 = a.concat(a2);新数组有四个元素,是 a 连接 a2 的结果。
  1. 使用内存
    • 使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
  1. 释放内存
    • 大多数内存管理的问题都在这个阶段。高级语言解释器嵌入了 “垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。但要知道是否仍然需要某块内存是无法判定的,所以这只能是一个近似的过程。

二、内存分配阶段

JavaScript 的内存分配阶段主要涉及值的初始化分配内存以及通过函数调用分配内存。

  1. 值的初始化分配内存

JavaScript 在定义变量时就完成了内存分配,例如为数字、字符串、对象、数组、函数等分配内存。

    • 对于数字类型,如var n = 123;,为数值变量分配内存。
    • 对于字符串类型,var s = "azerty";为字符串分配内存。
    • 对象类型,var o = { a:1, b: null };为对象及其包含的值分配内存。
    • 数组类型,var a = [1, null, "abra"];为数组及其包含的值分配内存。
    • 函数类型,function f(a) { return a + 2; }为函数分配内存。
  1. 通过函数调用分配内存

有些函数调用结果会分配对象内存,如:

    • var d = new Date();分配一个 Date 对象。
    • var e = document.createElement('div');分配一个 DOM 元素。

一些方法也会分配新变量或新对象,如:

    • 字符串的substr方法,var s = "azerty"; var s2 = s.substr(0,3);,因为字符串是不变量,JavaScript 可能决定不分配内存,只是存储了 [0 - 3] 的范围。
    • 数组的concat方法,var a = ["ouais ouais", "nan nan"]; var a2 = ["generation", "nan nan"]; var a3 = a.concat(a2);新数组有四个元素,是 a 连接 a2 的结果。

三、内存使用阶段

使用内存实际上是对分配的内存进行读写操作,包括读取和写入变量、对象属性值以及传递函数参数等。

对于基本数据类型,如数字、字符串等,访问时直接从栈内存中进行,速度较快。例如:

let num = 10;
let str = "Hello";
console.log(num); // 读取变量值
num = 20; // 写入变量值
console.log(str.charAt(0)); // 读取字符串属性值

对于引用数据类型,如对象、数组等,访问时先从栈内存中获取对象的引用地址,再通过地址去堆内存中找到对应的值,速度相对较慢。例如:

let obj = { name: "John" };
console.log(obj.name); // 读取对象属性值
obj.age = 30; // 写入对象属性值
let arr = [1, 2, 3];
console.log(arr[1]); // 读取数组元素值
arr.push(4); // 写入数组元素值

在函数参数传递过程中,基本数据类型是按值传递,函数内部的修改不会影响外部变量。例如:

function add(n) {
  n += 1;
  return n;
}
let num = 2;
let result = add(num);
console.log(num); // 2
console.log(result); // 3

而引用数据类型是按引用传递地址,函数内部对对象的修改会影响外部变量指向的对象。例如:

function setName(obj) {
  obj.name = "123";
}
let p = new Object();
setName(p);
console.log(p.name); // "123"

但如果在函数内部重新给参数赋值为一个新的对象,不会影响外部变量。例如:

function setName(obj) {
  obj.name = "123";
  obj = new Object();
  obj.name = "321";
}
let p = new Object();
setName(p);
console.log(p.name); // "123"

四、内存释放阶段

  1. 由于内存大小有限,当内存不再需要时需要释放。高级语言通常有垃圾回收器来自动执行这个任务。JavaScript 的垃圾回收器判断一个对象是否可以被回收的重要标准是引用。如果一个对象被其他对象引用,那么它不能被回收。

在 JavaScript 中,内存释放是一个关键环节。因为内存资源是有限的,当某些对象不再被使用时,就需要释放它们所占用的内存空间,以避免内存泄漏和资源浪费。高级语言通常会内置垃圾回收器来自动处理这个任务。在 JavaScript 中,垃圾回收器主要通过判断对象的引用情况来决定是否回收该对象。如果一个对象被其他对象引用,那么它被认为是仍然在使用中的,不能被回收。

  1. 垃圾回收算法
    • 引用计数法:当一个对象有引用指向它时,引用计数加 1;当引用为 0 时,对象可以被销毁。但该算法存在循环引用的问题。

引用计数法是一种早期的垃圾回收算法。在这种算法中,每个对象都有一个引用计数,当有其他对象引用它时,引用计数就会加 1。反之,当一个引用被解除时,引用计数就会减 1。当一个对象的引用计数变为 0 时,就说明这个对象不再被任何其他对象引用,可以被销毁了。然而,引用计数法存在一个严重的问题,那就是循环引用。如果两个对象相互引用,即使它们不再被程序的其他部分使用,它们的引用计数也不会变为 0,从而导致这两个对象无法被回收。例如:

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1;
    return "Cycle reference!";
}
cycle();

在这个例子中,函数执行完毕后,o1和o2相互引用,导致它们的引用计数始终不为 0,无法被垃圾回收器回收。

  • 标记清除法:将 “不再使用的对象” 定义为 “无法到达的对象”。从根部出发定时扫描内存中的对象,能从根部到达的对象保留,其他对象回收。现代浏览器大多使用基于标记清除法的改进算法。

标记清除法是目前主流的垃圾回收算法。它将 “不再使用的对象” 定义为 “无法到达的对象”。具体来说,垃圾回收器会从根部(在 JavaScript 中通常是全局对象)出发,定时扫描内存中的对象。凡是能从根部到达的对象,都被认为是还在使用中的,会被保留下来。而那些无法从根部到达的对象,就被标记为不再使用的对象,稍后会被回收。这种算法有效地解决了引用计数法中的循环引用问题。例如:

function objGroup(obj1, obj2) {
    obj1.next = obj2;
    obj2.prev = obj1;
    return { o1: obj1, o2: obj2 };
}
let obj = objGroup({ name: 'obj1' }, { name: 'obj2' });
console.log(obj);

在这个例子中,当函数执行完毕后,从全局对象出发无法访问到obj1和obj2,所以它们会被标记为不再使用的对象,最终被垃圾回收器回收。现代浏览器大多使用基于标记清除法的改进算法,以提高垃圾回收的效率和性能。

五、栈内存与堆内存

  1. JS 数据类型与内存存储机制

JavaScript 的数据类型分为基本类型和引用类型。基本类型包括 String、Number、Boolean、null、undefined、Symbol、Bigint,这些类型大小固定、体积轻量,存放在栈内存中。引用类型如 Object、Array、Function,大小不定、占用空间较大,存放在堆内存中,而栈内存中存放的是其引用地址。

例如,当我们定义一个基本类型的变量var num = 123;时,数字 123 就直接存储在栈内存中。而对于引用类型,如var obj = { name: 'John' };,对象{ name: 'John' }存储在堆内存中,栈内存中存放的是指向这个对象的引用地址。

  1. 引用数据类型内存访问机制

基本数据类型可以直接从栈内存中访问变量的值,而引用数据类型要先从栈内存中找到它对应的引用地址,再去堆内存中查找才能拿到变量的值。

以var num = 123;为例,我们可以直接从栈内存中获取数字 123 这个值。但对于var obj = { name: 'John' };,当我们要访问obj的name属性时,首先要从栈内存中找到指向对象的引用地址,然后根据这个地址去堆内存中查找对象,进而获取name属性的值。

六、内存泄漏问题

  1. 意外的全局变量以及显式的全局变量可能导致内存泄漏,解决方案是在 JavaScript 文件开头添加 “use strict” 使用严格模式,防止意外的全局变量产生。对于显式的全局变量,使用完后赋值为 null 或重新分配。
    • 在 JavaScript 中,一个未声明变量的使用,会在全局对象中创建一个新变量。在浏览器环境下,全局对象就是 window。例如,函数内部忘记使用var声明变量,或者使用this创建变量,都可能导致意外的全局变量产生。这种意外的全局变量如果不被及时清理,会一直占用内存,导致内存泄漏。
    • 为了避免这种情况,可以在 JavaScript 文件开头添加 “use strict”,开启严格模式。这样,当出现未声明变量的使用时,程序会报错,而不是创建全局变量。对于显式的全局变量,在使用完后可以将其赋值为null或者重新分配,以便垃圾回收器能够回收其占用的内存。
  1. timers 与 callbacks 可能导致内存泄漏,如计时器引用不再需要的节点或数据。在观察者模式下,要在不再需要的时候显式地删除相关对象或让其变为不可达。
    • 计时器(如setInterval和setTimeout)和回调函数如果在不再需要的时候没有被正确清理,可能会导致内存泄漏。例如,使用setInterval设置了一个循环计时器,如果回调函数引用了不再需要的节点或数据,那么这些节点或数据将无法被垃圾回收器回收,从而导致内存泄漏。
    • 在观察者模式下,当观察者对象不再需要观察目标对象时,应该显式地删除相关对象或让其变为不可达。对于计时器,可以在不再需要的时候使用clearInterval或clearTimeout来清除计时器。对于回调函数,可以在不再需要的时候将其设置为null或者重新分配,以便垃圾回收器能够回收其占用的内存。

Tags:

最近发表
标签列表