JavaScript 弱引用强引用底层示例详解

目录
  • 正文
  • 1. 弱引用
  • 2. 强引用
  • 3. JavaScript 的垃圾收集
    • 3.1 可达性
  • 4. Set VS WeakSet
  • 5. Map VS WeakMap
  • 6. WeakMap 的应用
    • 6.1 缓存
    • 6.2 额外的数据存储
  • 7. 小结

正文

内存和性能管理是软件开发的重要方面,也是每个软件开发人员都应该注意的方面。虽然弱引用很有用,但在 JavaScript 中并不经常使用。在 ES6 版本中,JavaScript 引入了 WeakSetWeakMap

1. 弱引用

与强引用不同,弱引用并不阻止被引用的对象被垃圾收集器回收或收集,即使它是内存中对对象的唯一引用。

在讨论强引用、WeakSetSetWeakMapMap 之前,让我们用下面的代码片段来演示弱引用:

// 创建 WeakMap 对象的实例
let human = new WeakMap();
// 创建一个对象,并将其赋值给名为 man 的变量
let man = { name: "xiaan" };
// 调用 human 的 set 方法,并传递两个参数(键和值)给它
human.set(man, "done")
console.log(human)

以上代码的输出如下:

WeakMap {{…} => 'done'}
man = null;
console.log(human)

当我们将 man 变量重新赋值为 null 时,内存中对原始对象的唯一引用是弱引用,它来自我们前面创建的 WeakMap。当 JavaScript 引擎运行垃圾收集过程时,man 对象将从内存和我们分配给它的 WeakMap 中删除。这是因为它是一个弱引用,并且它不阻止垃圾收集。接下来我们谈谈强引用。

2. 强引用

JavaScript 中的强引用是防止对象被垃圾回收的引用。它将对象保存在内存中。

下面的代码片段说明了强引用的概念:

let man = {name: "xiaan"};
let human = [man];
man =  null;
console.log(human);

以上代码的结果如下:

// 长度为 1 的对象数组
[{…}]

由于 human 数组和对象之间存在强引用。对象被保留在内存中,可以通过以下代码访问:

console.log(human[0])

这里要注意的重要一点是,弱引用不会阻止对象被垃圾回收,而强引用却会阻止对象被垃圾回收。

3. JavaScript 的垃圾收集

与所有编程语言一样,在编写 JavaScript 时,内存管理是需要考虑的关键因素。与 C 语言不同,JavaScript 是一种高级编程语言,它在创建对象时自动分配内存,在不再需要对象时自动清除内存。当不再使用对象时清除内存的过程称为垃圾收集。在谈论 JavaScript 中的垃圾收集时,几乎不可能不触及可达性的概念。

3.1 可达性

在特定作用域中的所有值或在作用域中使用的所有值都被称为在该作用域中的“可达”,并被称为“可达值”。可访问的值总是存储在内存中。

在以下情况下,值被认为是可达的:

  • 程序根中的值或从根中引用的值,如全局变量或当前执行的函数、它的上下文和回调。
  • 通过引用或引用链从根中访问的值(例如,全局变量中的对象引用另一个对象,该对象也引用另一个对象——这些都被认为是可访问的值)。

下面的代码片段说明了可达性的概念:

var person = {name: "xiaan"};

这里我们有一个对象,它的键值对(name"xiaan")引用全局变量 person。如果我们通过赋值 null 来覆盖 person 的值:

person = null;

那么对象将被垃圾回收,"xiaan" 值将无法再次访问。下面是另一个例子:

var person = {name: "xiaan"};
var programmer = person;

从上面的代码片段中,我们可以从 person 变量和 programmer 变量访问 object 属性。然而,如果我们将person 设置为 null

person = null;

那么对象仍然在内存中,因为它可以通过 programmer 变量访问。简单地说,这就是垃圾收集的工作方式。

注意:默认情况下,JavaScript 对其引用使用强引用。要在 JavaScript 中实现弱引用,可以使用 WeakMapWeakSetWeakRef

4. Set VS WeakSet

set 对象是一个只有一次出现的唯一值的集合。像数组一样,集合没有键值对。我们可以使用for...of.forEach 的数组方法遍历。

让我们用以下片段来说明这一点:

let setArray = new Set(["Joseph", "Frank", "John", "Davies"]);
for (let names of setArray){
  console.log(names)
}// Joseph Frank John Davies

我们也可以使用 .forEach 遍历:

 setArray.forEach((name, nameAgain, setArray) =>{
   console.log(names);
 });

WeakSet 是唯一对象的集合。正如其名称一样,弱集使用弱引用。以下是 WeakSet() 的特性:

  • 它可能只包含对象。
  • 集合中的对象可以在其他地方访问。
  • 它不能循环遍历。
  • Set() 一样,WeakSet()addhasdelete 方法。

下面的代码说明了如何使用 WeakSet() 和一些可用的方法:

const human = new WeakSet();
let person = {name: "xiaan"};
human.add(person);
console.log(human.has(person)); // true
person = null;
console.log(human.has(person)); // false

在第 1 行,我们创建了 WeakSet() 的一个实例。在第 3 行,我们创建了对象并将它分配给变量 person。在第 5 行,我们将 person 添加到 WeakSet() 中。在第 9 行,我们将 person 引用设为空。第 11 行代码返回false,因为 WeakSet() 将被自动清除,因此,WeakSet() 不会阻止垃圾回收。

5. Map VS WeakMap

我们从上面关于垃圾收集的部分了解到,只要可以访问,JavaScript 引擎就会在内存中保留一个值。让我们用一些片段来说明这一点:

let person = {name: "xiaan"};
// 对象可以从引用中访问
// 覆盖引用 person.
person = null;
// 该对象不能被访问

当数据结构在内存中时,数据结构的属性被认为是可访问的,并且它们通常保存在内存中。如果将对象存储在数组中,那么只要数组在内存中,即使没有其他引用,也仍然可以访问对象。

let person = {name: "xiaan"};
let arr = [person];
// 覆盖引用
person = null;
console.log(array[0]) // {name: 'xiaan'}

即使引用被覆盖,我们仍然能够访问这个对象因为对象被保存在数组中。因此,只要数组仍然在内存中,它就保存在内存中。因此,它没有被垃圾回收。由于我们在上面的例子中使用了数组,我们也可以使用 map。当 map 仍然存在时,存储在其中的值将不会被垃圾回收。

let map = new Map();
let person = {name: "xiaan"};
map.set(person, "person");
// 覆盖引用
person = null;
// 还能访问对象
console.log(map.keys());

与对象一样,map 可以保存键—值对,我们可以通过键访问值。但是对于 map,我们必须使用 .get() 方法来访问值。

根据 Mozilla Developer Network,Map 对象保存键—值对并记住键的原始插入顺序。任何值(包括对象值和原语值)都可以用作键或值。

map 不同,WeakMap 保存弱引用。因此,如果这些值在其他地方没有被强引用,它不会阻止垃圾回收删除它引用的值。除此之外,WeakMapmap 是相同的。由于弱引用,WeakMap 不可枚举。

对于 WeakMap,键必须是对象,值可以是数字或字符串。

下面的代码片段说明了 WeakMap 的工作原理和其中的方法:

// 创建一个 WeakMap
let weakMap = new WeakMap();
let weakMap2 = new WeakMap();
// 创建一个对象
let ob = {};
// 使用 set 方法
weakMap.set(ob, "Done");
// 你可以将值设置为一个对象甚至一个函数
weakMap.set(ob, ob)
// 可以设置为undefined
weakMap.set(ob, undefined);
// WeakMap 也可以是值和键
weakMap.set(weakMap2, weakMap)
// 要获取值,使用 get 方法
weakMap.get(ob) // Done
// 使用 has 方法
weakMap.has(ob) // true
weakMap.delete(ob)
weakMap.has(ob) // false

在没有其他引用的 WeakMap 中使用对象作为键的一个主要副作用是,它们将在垃圾收集期间自动从内存中删除。

6. WeakMap 的应用

WeakMap 可以用于 web 开发的两个领域:缓存和额外的数据存储。

6.1 缓存

这是一种 web 技术,它涉及到保存(即存储)给定资源的副本,并在请求时返回它。可以缓存函数的结果,以便在调用函数时重用缓存的结果。

让我们来看看实际情况。

let cachedResult = new WeakMap();
// 存储结果的函数
function keep(obj){
	if(!cachedResult.has(obj){
    let result = obj;
    cachedResult.set(obj, result);
  }
	return cachedResult.get(obj);
}
let obj = {name: "xiaan"};
let resultSaved = keep(obj)
obj = null;
// console.log(cachedResult.size); Possible with map, not with WeakMap

如果我们在上面的代码中使用 Map() 而不是 WeakMap(),并且对 keep() 函数有多次调用,那么它只会在第一次调用时计算结果,并在其他时候从 cachedResult 检索结果。副作用是,每当对象不需要时,我们就需要清理 cachedResult。使用 WeakMap(),一旦对象被垃圾回收,缓存的结果就会自动从内存中删除。缓存是提高软件性能的一种很好的方法——它可以节省数据库使用、第三方 API 调用和服务器对服务器请求的成本。通过缓存,请求结果的副本被保存在本地。

6.2 额外的数据存储

WeakMap() 的另一个重要用途是额外的数据存储。想象一下,我们正在建立一个电子商务平台,我们有一个计算访客数量的程序,我们希望能够在访客离开时减少计数。这个任务在 Map 中要求很高,但在 WeakMap() 中很容易实现:

let visitorCount = new WeakMap();
function countCustomer(customer){
  let count = visitorCount.get(customer) || 0;
  visitorCount.set(customer, count + 1);
}
let person = {name: "xiaan"};
// 统计访问人数
countCustomer(person)
// 访客离开
person = null;

使用 Map(),我们必须在客户离开时清除 visitorCount,否则,它将在内存中无限增长,占用空间。但是使用 WeakMap(),我们不需要清理 visitorCount,一旦一个人(对象)变得不可访问,它就会自动被垃圾回收。

7. 小结

在本文中,我们了解了弱引用、强引用和可达性的概念,并尽可能地将它们与内存管理联系起来。

更多关于JavaScript 弱引用强引用的资料请关注我们其它相关文章!

(0)

相关推荐

  • JavaScript 中的引用类型Date 和RegExp的详细介绍

    目录 引用类型 & 引用值的理解 Date 引用类型 Date.parse()方法 Date.UTC()方法 继承的方法 RegExp RegExp 实例方法 exec() test() 继承的方法 引用类型 & 引用值的理解 引用值(或者对象)是某个特定引用类型的实例. 在ECMAScript中,引用类型是把数据和功能组织到一起的结构,经常被人错误的称作“类”.虽然从技术上讲JavaScript 是一门面向对象语言,但ECMAScrtipt 缺少传统的面相对象语言所具备的某些基本结构,包

  • JavaScript垃圾回收机制(引用计数,标记清除,性能优化)

    目录 一.前言 二.何为垃圾 三.垃圾回收 四.可达性(Reachability) 五.可达性举例 层次关联 相互关联 可达孤岛 六.垃圾回收算法 引用计数 标记清除 七.性能优化 分代回收 增量收集 空闲收集 八.总结 一.前言 垃圾回收是JavaScript的隐藏机制,我们通常无需为垃圾回收劳心费力,只需要专注功能的开发就好了.但是这并不意味着我们在编写JavaScript的时候就可以高枕无忧了,伴随着我们实现的功能越来越复杂,代码量越积越大,性能问题就变的越来越突出.如何写出执行速度更快,

  • js中值类型和引用类型的区别介绍

    1.JavaScript中的变量类型有哪些? (1)值类型(基本类型):字符串(string).数值(number).布尔值(boolean).undefined.null  .symbol (2)引用类型:对象(Object).数组(Array).函数(Function) 2.值类型和引用类型的区别 (1)值类型: 1.占用空间固定,保存在栈中(当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了.因

  • JavaScript中原始值和引用值深入讲解

    目录 值和引用相关内容 1. 简单值(原始值) 2. 复杂值(引用值) 3. 访问方式 4. 比较方式 5. 动态属性 6. 变量赋值 灵魂拷问 总结 值和引用相关内容 在 JavaScript 中,数据类型整体上来讲可以分为两大类:基本类型和引用数据类型 基本数据类型,一共有 6 种: string,symbol,number,boolean,undefined,null 其中 symbol 类型是在 ES6 里面新添加的基本数据类型. 引用数据类型,就只有 1 种: object 基本数据类

  • JavaScript引用赋值与传值赋值总结

    目录 前言 基本数据类型 复合数据类型 前言 在JavaScript中基本数据类型都是传值赋值,复合数据类型都是引用赋值(传地址)也叫引用传址 基本数据类型的变量名和数据是直接存在"快速内存"(栈内存)中. 基本的数据类型有:undefined,boolean,number,string,null. 基本类型存放在栈区,访问是按值访问的,就是说你可以操作保存在变量中的实际的值. 而复合数据类型(对象和数组)的存储分两个部分: ①具体的数据存在“慢速内存”“堆内存”中: ②将变量名和数据

  • JavaScript 引用类型之原始值包装类型String

    目录 String 原始值包装类型 String 原始值包装类型 操作方法 1.字符串编码常规化函数 normalize()方法 2.字符串拼接函数concat() 3.字符串提取子字符串方法:slice(),substr(),substring() 4.字符串位置方法 indexOf(),lastIndexOf() 5.字符串包含方法:startsWith(),endsWith()和includes() 6.去除字符串前后空格的方法 trim(),trimLeft(),trimRight()

  • JavaScript console对象与控制台使用示例详解

    目录 1. console对象 2. console的静态方法 3. 自定义console 4. 控制台命令行API 4.1 $_ 4.2 $0-$4 4.3 $(selector) 4.4 $x(path) 4.5 inspect(obj) 4.6 keys()和values() 4.7 其它的命令 1. console对象 console对象是JavaScript的原生对象,提供了很多用于调试的方法,如console.log输出信息,console.count记录执行次数 console.l

  • JavaScript事件的委托(代理)的用法示例详解

    目录 简介 示例:事件委托 写法1:事件委托 写法2:每个子元素都绑定事件 示例:新增元素 写法1:事件委托 写法2:每个子元素都绑定事件 简介 说明 本文用示例介绍JavaScript中的事件(Event)的委托(代理)的用法. 事件委托简介 事件委托,也叫事件代理,是JavaScript中绑定事件的一种常用技巧.就是将原本需要绑定在子元素的响应事件委托给父元素或更外层元素,让外层元素担当事件监听的职务. 事件代理的原理是DOM元素的事件冒泡. 事件委托的优点 1.节省内存,减少事件的绑定 原

  • JavaScript稀疏数组与孔hole示例详解

    目录 稀疏数组是什么 JavaScript数组天生就是稀疏数组 JavaScript数组稀疏特性带来的“怪异现象” slice会复制孔 forEach.every会跳过孔(不对孔调用回调函数) map不对孔调用回调函数,但是孔会保留 filter不对孔调用回调函数,但是孔会被过滤掉 join会将孔转化为一个空字符串进行拼接,与undefined一样 初始化无孔数组的方法 Array.apply(null, Array(n))的原理 稀疏数组是什么 在绝大多数JavaScript的实现中,数组是稀

  • JavaScript实现基础排序算法的示例详解

    目录 前言 正文 1.冒泡排序 2.选择排序 3.插入排序 4.快速排序 前言 文本来总结常见的排序算法,通过 JvavScript  来实现 正文 1.冒泡排序 算法思想:比较相邻两个元素的大小,如果第一个比第二个大,就交换它们.从头遍历到尾部,当一轮遍历完后,数组最后一个元素是最大的.除去最后一个元素,对剩下的元素重复执行上面的流程,每次找出剩余元素中最大的,遍历完后,数组是升序的 算法分析:总共需要进行length * (length - 1) / 2 次比较,所以时间复杂度为O(n^2)

  • JavaScript 弱引用强引用底层示例详解

    目录 正文 1. 弱引用 2. 强引用 3. JavaScript 的垃圾收集 3.1 可达性 4. Set VS WeakSet 5. Map VS WeakMap 6. WeakMap 的应用 6.1 缓存 6.2 额外的数据存储 7. 小结 正文 内存和性能管理是软件开发的重要方面,也是每个软件开发人员都应该注意的方面.虽然弱引用很有用,但在 JavaScript 中并不经常使用.在 ES6 版本中,JavaScript 引入了 WeakSet 和 WeakMap. 1. 弱引用 与强引用

  • JavaScript中工厂函数与构造函数示例详解

    前言 当谈到JavaScript语言与其他编程语言相比时,你可能会听到一些令人困惑东西,其中之一是工厂函数和构造函数. 工厂函数 所谓工厂函数,就是指这些内建函数都是类对象,当你调用他们时,实际上是创建了一个类实例".意思就是当我调用这个函数,实际上是先利用类创建了一个对象,然后返回这个对象.由于 Javascript 本身不是严格的面向对象的语言(不包含类),实际上来说,Javascript 并没有严格的"工厂函数",但是在 Javascript中,我们能利用函数模拟类.来

  • JavaScript中new操作符的原理示例详解

    new的用处 new的作用是通过构造函数来创建一个实例对象,该实例与原型和构造函数之间的关系如下图所示: 先来总结一下 创建一个空对象 空对象的内部属性 __proto__ 赋值为构造函数的 prototype 属性 将构造函数的 this 指向空对象 执行构造函数内部代码 返回该新对象 详细说明 执行 new 操作时会依次经过以下步骤: 1.创建一个空对象 空对象是 Object 的实例,即 {} . let obj = {} 2.空对象的内部属性 __proto__ 赋值为构造函数的 pro

  • JavaScript相等运算符的九条规则示例详解

    简介 Java的等号有=,==,===三个运算符虽然都读成"等于",但概念是不一样的,=指的是赋值,在这篇文章中我们不会进行更多的介绍:==称为相等运算符,比较的是两个操作数值是否相等:===称为严格相等运算符,比较的是两个操作数是否是"同一个值". 针对相等运算符==和===,有两个对应的!=和!==运算符,这两个不相等运算符的运算结果是==和===运算结果的相反值. 严格相等运算符 严格相等运算符在对操作数进对比运算时,是不进行类型转换的.对比规则如下: 1.

  • JavaScript学习笔记之惰性函数示例详解

    前言 本文主要给大家介绍了关于JavaScript惰性函数的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 需求 我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次. 解决一:普通方法 var t; function foo() { if (t) return t; t = new Date() return t; } 问题有两个,一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断. 解决二:闭包 我们很容易想到用闭

  • JS中原始值和引用值的储存方式示例详解

    在ECMAscript中,变量可以存放两种类型的值,即原始值和引用值 原始值指的是代表原始数据类型的值,也叫基本数据类型,包括:Number.Stirng.Boolean.Null.Underfined 引用值指的是复合数据类型的值,包括:Object.Function.Array.Date.RegExp 根据数据类型不同,有的变量储存在栈中,有的储存在堆中.具体区别如下: 原始变量及他们的值储存在栈中,当把一个原始变量传递给另一个原始变量时,是把一个栈房间的东西复制到另一个栈房间,且这两个原始

随机推荐