JavaScript的内存管理

一、JS的数据类型

基本数据类型:String、Number、Boolean、undefined、null、Symbol(ES6)、BigInt(ES10)

引用数据类型:Object(包括Array、Function和如RegExp、Date、Math等内置对象)。

1.两种数据类型的区别

1.1基本数据类型

1、存储于栈内存中。
2、值不可变

let number = 10; // 变量number指向内存中的地址A,地址A中存储着值10  
console.log(number); // 输出10  
  
number = 20; // 变量number现在指向内存中的新地址B,地址B中存储着值20,同时地址A中的值10保持不变,但此时已无法通过变量number访问它  
console.log(number); // 输出20

3、创建变量时,直接存储变量的值,只要值相等,变量的内就相等。

let a = '1';  
let b = '1';  
console.log(a === b); // 输出 true
1.2引用数据类型

1、存储于堆内存中。
2、属性值是可变的。

// 创建一个对象,并将其引用赋给变量obj,此时obj指向内存中的地址A,地址A中存储着对象{name: 'Jack'}  
const obj = {name: 'Jack'};  
// 输出变量obj所引用的对象,即输出地址A中的对象  
console.log(obj); // 输出:{name: 'Jack'}  
  
// 修改变量obj所引用的对象的name属性为'Alice',此时地址A中的对象内容被改变,但地址A保持不变,obj仍然指向它  
obj.name = 'Alice';  
// 再次输出变量obj所引用的对象,即再次输出地址A中的对象,此时对象的name属性已经被修改  
console.log(obj); // 输出:{name: 'Alice'}

3、创建变量时,存储对象的引用,对象的引用相同,变量就相等。

let obj1 = { name: 'John' };
let obj2 = obj1; // 这里obj2并不是复制了obj1,而是复制了对obj1的引用

console.log(obj1 === obj2); // 输出 true,因为它们指向的是同一个对象
const obj1 = {};
const obj2 = {};

console.log(obj1 === obj2); // 输出false,因为它们的引用不同

二、内存管理

1.引用的定义

JS中的引用即对象的变量。对象通过const、let或var创建对象的变量后,该变量存储在栈内存中,存储了指向堆内存中对象的指针,指针指向了该对象的内存地址,而不是像C++中的变量一样直接存储对象的内存地址。JS没有指针的概念,无法直接操作内存,但可以通过变量操作对象,间接操作内存。

2.装箱机制与内存占用

2.1基本数据类型

基本数据类型可以视为一个值,直接存储在栈内存中。这个值不是对象,因此它们无法像对象一样调用方法,也没有自己的属性(除了Symbol类型可以有一个可选的description属性)。然而,JS引擎提供了装箱(boxing)机制,使基本数据类型可以调用很多内置方法。

装箱机制

JS的装箱机制分为隐式装箱和显式装箱。在隐式装箱中,对象是临时创建的,而在显式装箱中,对象是持续存在的,直到被垃圾回收。

我们一般使用隐式装箱,因为隐式装箱占用更少的内存空间,能避免内存泄漏。

隐式装箱
const num = 12.3333
num.toFixed(2) // 输出 '12.33'

当调用基本数据类型的方法时(num.toFixed(2)),JS引擎会在堆内存中临时创建一个对象包装器,将该值(num)装箱为一个对象,将基本数据类型的值(12.3333)赋给这个对象。调用方法结束后,临时创建的对象就被销毁,从堆内存中移除,由垃圾收集器回收。最后,该基本数据类型不再是对象的值,又变为了一个纯粹的值。

显式装箱
const numObj = new Number(12.3333)
numObj.toFixed(2) // 输出 '12.33'

当执行const numObj = new Number(12.3333)时,相当于在堆内存中创建了一个Number对象,将基本数据类型的值(12.3333)赋给这个对象,并将这个对象的引用存储在numObj中。

这样numObj就成为了真正的 Number 对象,而不是临时创建的对象包装器。没有其他代码操作,使该对象被垃圾收集器回收,用显式装箱创建的对象就一直占用内存空间。

2.2引用数据类型

引用数据类型指对象,对象会对应一个内存地址,当创建对象后,如果没有任何引用指向它,它会被垃圾回收机制回收。如果存在引用,对象由于没有装箱机制,不手动销毁,会一直占用内存。

手动销毁对象的方式
对象离开作用域
function createObject() {
    let obj = { name: "Jack" };  // obj 在这个函数的作用域中
}
createObject();  // 当函数执行完后,obj 无法再被访问

将引用设置为不可指向的状态

将对象分配的引用设置为null或undefined

let obj = { name: "Jack" };
obj = null;  // 现在原来的对象没有任何引用指向它了,可以被回收
页面重载

页面重载会导致当前的页面及其JavaScript执行环境被完全销毁,并且浏览器会重新加载页面,创建一个全新的JavaScript执行环境。

除了JavaScript原生的对象(如全局对象window、navigator、document等)和继续运行的Web Workers中的对象,其余内存中的对象都将被垃圾回收。

// 假设你有一个对象  
const myObject = {  
  name: 'Example Object',  
  sayHello: function() {  
    console.log('Hello!');  
  }  
};  
  
// 使用对象  
myObject.sayHello(); // 输出: Hello!  
  
// 现在你想要通过页面重载来销毁这个对象  
// 页面重载后,myObject以及页面中的其他所有JavaScript对象都将被销毁  
location.reload();
循环引用

两个或者多个对象之间相互引用,如果没有外部引用指向这些对象时,垃圾回收器也可以识别这样的循环引用,并将其释放。这通常需要现代垃圾回收器的支持。

可以被垃圾回收的循环引用

在这个特定的例子中,由于 obj1 和 obj2 都是局部变量,并且 createCircularReference 函数执行完毕后没有外部引用指向这两个对象,因此它们最终会被垃圾回收器回收,即使它们之间存在循环引用。

function createCircularReference() {
    let obj1 = {};
    let obj2 = {};
    obj1.ref = obj2;  // obj1 引用 obj2
    obj2.ref = obj1;  // obj2 引用 obj1
    // 如果没有外部引用指向 obj1 或 obj2,最终会被回收,否则会内存泄漏
}
createCircularReference();

不可被垃圾回收的循环引用

全局变量引用

let globalObj1;  
let globalObj2;  

function createCircularReference() {  
    let obj1 = {};  
    let obj2 = {};  
    obj1.ref = obj2;  
    obj2.ref = obj1;  
    globalObj1 = obj1; // 现在obj1有一个外部引用  
    globalObj2 = obj2; // 现在obj2有一个外部引用  
}  
createCircularReference();

闭包引用

function createCircularReference() {  
    let obj1 = {};  
    let obj2 = {};  
    obj1.ref = obj2;  
    obj2.ref = obj1;  

    return function() {  
        console.log(obj1); // 闭包捕获了obj1的引用  
    };  
}  

let closure = createCircularReference();

3、堆和栈与内存占用

通常基本数据类型比引用数据类型占用更小内存,这取决于堆和栈的特点。

3.1基本数据类型

1、值大小是固定的。例如,一个Number类型的数据在JavaScript中通常是64位的浮点数,占用固定的内存空间。
2、变量被销毁时,其占用的栈内存空间会立即被释放

3.2引用数据类型

1、值大小是不固定的。这是因为它们可以包含任意数量的属性和方法,以及嵌套的对象和数组。
2、变量被销毁时,其占用的栈内存空间会被释放,但堆内存中的实际数据可能不会被立即释放,这取决于垃圾回收机制的实现。
3、由于引用数据类型需要在栈内存中存储一个指向堆内存的引用,这增加了额外的内存开销。此外,堆内存中的对象还可能包含一些额外的信息,如原型链、隐藏类等,这些也会占用一定的内存空间。

4、避免内存泄漏的编码规范

1、清理无用的数据:当数据不再需要时,确保将其设置为 null 或 undefined,以断开引用,使垃圾回收器能够回收内存。
2、避免使用全局变量:尽量减少全局变量的使用,因为全局变量在页面的生命周期内都不会被释放。
3、使用weakMap和weakSet:当需要存储私有数据但又不想阻止垃圾回收时,可以使用 WeakMap 和 WeakSet。
4、事件监听器、定时器和异步操作:对于事件监听器、定时器和异步操作,确保在不需要时及时移除,避免内存泄漏。
5、少用闭包:闭包可以保持外部函数的变量,但如果不当使用,也可能导致内存泄漏。确保只在必要时使用闭包,并在不需要时断开闭包对外部变量的引用。
6、避免过度操作DOM:频繁的DOM操作会导致内存泄漏,因为每次操作都可能产生新的DOM节点,而这些节点如果不及时清理,会占用大量内存。
7、使用内存泄漏的检测工具:使用浏览器的开发者工具(如 Chrome 的 DevTools)中的内存泄漏检测工具,定期检查你的应用是否存在内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AAA`

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值