精彩的JS数据结构!
类型化数组
套
弱集
地图
弱映射
就这些吗?
资源
这篇文章摘自我的博客,记得去看看博客上的最新内容哦😉
JavaScript 的发展在 2015 年之前一直相当缓慢。没错,那一年ES6发布,整个 Web 开发领域真正腾飞,并呈指数级增长。📊 但每个 JS 爱好者可能都知道这一点——这个年份,这个确切的时间点,在世界各地的许多 JS 资源中都被反复提及。所以,让我们也来点创新,重现这一幕吧?😅
ES6 为 JavaScript 带来了许多新特性。不仅包括现在必备的箭头函数、Promise和语法糖,还有新的数据结构。🔢没错,我说的就是 Set、WeakMap 等等(如果你已经了解它们的话)。这些看似不起眼却非常有趣的特性,由于现代浏览器花了很长时间才完全支持新规范,所以一直被人们忽略。随着时间的推移⏳,人们开始使用新的语法和一些真正令人期待的新功能,但这些数据结构的重要性却逐渐降低。当然,这并非对所有人而言,但即使以我自己为例——我也几乎没用过它们。我一直坚持使用传统的数组和对象,并且局限于这种有限的框架内。不过,别担心,因为在本文中,我们将探讨这些数据结构的真正价值和实用性。凭借它们带来的新可能性和目前的支持……何乐而不为呢?😃
类型化数组
我想你肯定了解数组,毕竟谁不了解呢?数组提供的各种方法、函数式编程的可能性等等,都令人印象深刻。但是,如果你了解数组,那么类型化数组(TypedArrays)又是什么?我们为什么需要它呢?
TypedArrays 并非指一个独立的类,而是一个用来指代不同类型这类特定结构的名称。它们本质上是二进制数据缓冲区的自定义数组视图,这可能需要更多解释。😉
ArrayBuffer
ArrayBuffer类用于存储固定长度的原始二进制数据。💾 您可以使用其构造函数并传入长度参数来创建一个 ArrayBuffer 对象,该参数指定缓冲区的字节数。
const buffer = new ArrayBuffer(8);
ArrayBuffer 本身并没有太多属性。最值得一提的是 `length` 和 `slices` 属性byteLength——一个用于获取缓冲区长度(以字节为单位,slice()例如提供的值),另一个用于切片缓冲区的指定部分并创建新缓冲区。与 ArrayBuffer 交互的唯一方式是通过所谓的视图——TypedArray或DataView(但这又是另一个话题了)。
ArrayBuffer 的重要性在于它们表示数据的方式——原始二进制数据。一些底层 API(例如WebGL)需要这种数据形式,因为它的效率高,并且能够与其他代码部分(例如着色器)集成。
类型化数组[s]
既然我们知道 TypedArray 可以作为 ArrayBuffer 的视图,那么让我们先列出它们吧!
- Int[8/16/32]Array - 用于将缓冲区解释为整数数组,每个整数用给定位数的位数表示;
- Uint[8/16/32]数组-具有给定位数的无符号整数;
- Float[8/16/32/64]数组-具有给定位数的浮点数;
- BigInt64Array - 64 位整数(bigint);
- BigUint64Array -无符号整数(bigint),每个数字占64 位;
以上每种类型的 TypedArray 都具有相同的方法和属性集,唯一的区别在于数据的表示方式。TypedArray 实例可以指定长度(内部创建 ArrayBuffer)、另一个 TypedArray、一个对象(以长度和给定索引的值作为键)或先前实例化的ArrayBuffer来创建。👨💻
用法
现在,你的 TypedArray 已经准备就绪,你可以像编辑普通数组一样,使用类似的方法自由编辑它。👍
const typedArr = new Uint8Array([0,1,2,3,4]);
const mapped = typedArr.map(num => num * 2); // Uint8Array [0,2,4,6,8]
不过需要注意一点,由于底层操作的是 ArrayBuffer 的数据,你的 TypedArray 的大小是固定的。此外,所有在普通数组中可以修改数组大小的方法(例如删除、添加、剪切等)都受到限制,甚至完全不可用。🤐
const typedArr = new Uint8Array([0,1,2,3,4]);
typedArr.push(5) // Error! You must be kidding me!
您还可以随时对这些数组进行迭代,并根据需要来回转换它们与标准数组。
const typedArr = new Uint8Array([0,1,2,3,4]);
for(const num of typedArr){
// code
}
const arr = Array.from(typedArr); // [0,1,2,3,4]
TypedArrays 也提供了一些与其二进制部分相关的功能!例如,您可以使用属性访问底层的 ArrayBuffer 实例,并分别buffer使用 `bytelength` 和 `offset` 读取其字节长度和偏移量。🙂byteLengthbyteOffset
用例
正如我之前提到的,ArrayBuffer具有巨大的潜力,这得益于它们独特的数据表示方式。这种紧凑的数据形式可以轻松应用于许多场景。例如,它可以是服务器发送的向量🎨或其他压缩数据📦,在压缩、传输和解压缩的各个阶段都能实现最高的速度和性能。此外,正如我之前所说,一些Web API也充分利用了这种格式带来的高效性。👌
在 ArrayBuffer 之上使用 TypedArray,可以更轻松地操作其中的数据(绝对比直接设置位要好得多😅)。除了固定大小这个唯一的限制之外,你可以像操作普通数组一样操作这些紧凑的数据。
套
继续我们对类数组结构的研究,我们来到了集合(Set )。🗃 集合与数组非常相似——它们可以用来以类似的方式存储数据,只有一个重要的区别。集合中的所有值都必须是唯一的(当然也有一些特殊情况😵)——无论是基本类型值还是对象引用——重复值都会被自动移除。
用法
创建集合很简单——你只需要使用正确的构造函数,并带有一个可选参数,以便从一开始就提供数据。
const dataSet = new Set([1, 2, 3, 4, 5]);
集合本身提供了相当强大的 API。其中最重要的方法包括:
add()- 将给定值添加到集合的末尾;delete()- 从集合中移除给定的值;has()- 检查给定值是否存在于集合中;clear()- 从集合中移除所有值;
它们也可以转换为标准数组并随意迭代。
const dataSet = new Set([1,2,3]);
const values = [0,1,2,3,4];
for(const value of values) {
if(dataSet.has(value)){
dataSet.delete(value)
} else {
dataSet.add(value);
}
}
const result = Array.from(dataSet); // [0,4];
用例
集合的大多数应用场景显然都基于其仅存储唯一值的能力。⚡ 如果仅使用数组,则需要一些额外的样板代码。因此,在存储 ID等数据时,唯一值尤其有用。🆔
其次,在集合中删除元素要方便得多。只需提供要删除的值,而无需执行完整的查找、索引和拼接过程,这确实方便得多。👍 当然,对于标准数组中允许的重复值,这样做就没那么容易了。
弱集
现在,我们来谈谈另一种集合——弱集合(WeakSet)。🤨 弱集合很特殊——它们以不同的方式存储值,但也存在一些额外的限制,例如API 规模小得多。
记忆
首先,简单介绍一下 WeakSet 的存储方式。WeakSet的值只能是对象,不能是基本类型。这一点非常重要,因为WeakSet 的数据存储方式是“弱”的。“弱”意味着,如果某个对象没有其他引用(对象是通过引用访问的),那么它随时可能被垃圾回收。因此,要充分发挥弱结构的潜力,就必须深入理解引用机制以及对象之间的交互方式。
因为 WeakSet 本质上仍然是集合,所以它存储的所有值都必须是唯一的。但是,正如你可能知道的,对于对象来说,这并不是什么大问题——对象是 WeakSet 值的唯一类型。由于所有值都是通过引用存储的,即使属性完全相同的对象也会被视为不同的对象。
用法
与普通的 Set 相比,WeakSet 的 API 非常有限。其中最重要的一点是它们不可迭代。它们没有任何属性(例如 Set 的属性可以size指示存储的值的数量),只有三个主要方法:add()` get()`、`get()`delete()和 `get has()()`。构造函数看起来与 Set 类似,只是可选的数组参数只能存储对象。然而,使用这样的参数意义不大,因为所有存储的对象都需要在代码的其他地方被引用。
const weakDataSet = new WeakSet();
const obj = {a: 10};
weakDataSet.add(obj);
weakDataSet.add({b: 10}); // Pointless - will be removed soon
weakDataSet.has(obj); // True
weakDataSet.has({a: 10}); // False - objects are stored by reference
用例
实际上,WeakSet 的典型应用场景可能很难找到。这是因为,实际上它的应用场景并不多,而且都非常具体。最流行也可能是最好的应用场景是对象标记。你可以使用 WeakSet 对在代码其他地方被引用的特定对象进行分组和标记。标记(或者有些人称之为分组)如果使用得当,会是一种非常有用的技术。⚠
不过,你需要谨慎。记住,所有未被其他任何地方引用的对象都会被垃圾回收。但这并不意味着它们会立即被移除,而是在垃圾回收器的下一个周期⭕中被移除。你应该牢记这一点,不要过分依赖弱引用集(WeakSet)——某些值迟早会被移除。
地图
在我看来,Map 是一种兼具数组和对象优势的数据结构。在 Map 中,所有数据都以键值对的形式存储。🤝 这种存储方式与普通对象的区别在API中也有体现。更重要的是,在 Map 中,键和值是同等重要的,这意味着你可以进行一些非常巧妙的操作,例如将一个对象(但请记住,你需要一个引用才能在以后访问它)作为键来存储值!此外,与对象不同,Map 中存储的键值对具有特定的顺序,并且易于迭代。🔄
用法
您可以通过直接调用构造函数来创建 Map 实例。您也可以选择预先提供一个键值对数组作为 Map 的初始值。
const map = new Map([["key1", 10], [10, "value2"]]);
在 API 方面,Map 的优势就真正得以体现。它能让你以更快的速度、更易读的方式执行特定操作。
有一个特殊的属性size(在 Set 中也可用),可以让你快速了解当前键值对的数量。它的特别之处在于,在传统的对象中,没有类似且足够简单的方法可以实现相同的功能。😕
这种直观的 API 的好处远不止于此!如果您已经喜欢 Sets 的 API,那么您一定会很高兴地发现它与 Maps 的 API 有许多相似之处。所有用于编辑 Maps 值的方法都可以看作是 Sets 中基于新键值模式的方法。唯一的区别add()在于,出于显而易见的理性思考原因,方法本身发生了变化set()。😅 除此之外,要更改和访问 Maps 数据,您主要操作的是键而不是值。
此外,就像 Set 和对象一样(对于更像数组的 Set 来说可能不太相关),Map 提供了 3 种方法来读取其数据的特定组:
entries()- 以数组的数组形式返回 Map 的键值对;values()- 将 Map 中的所有值以数组形式返回;keys()- 返回 Map 的所有键,并将其存储在一个数组中;
这些方法(尤其是在你练习函数式编程时)很可能在与对象交互时被广泛使用,因为当时没有其他更便捷的方式。但对于 Maps 来说,情况完全不同。借助 Maps 的 API 和优秀的数据结构,你一定会感到工作轻松许多。🌈
const map = new Map([['key', 10], ['key2', 10]])
map.forEach((value,key) => {
map.delete(key);
map.set(key, 10);
});
用例
如您所见,Map 为标准对象提供了一种绝佳的替代方案。当您需要同时访问键及其值并能够遍历它们时,Map 可能是您的最佳选择。
这种兼具可迭代性和类对象特性的巧妙组合显然有很多种实现方式。虽然用普通对象也能轻松实现同样的效果,但何必呢?🤔 Maps API的便捷性以及它作为行业标准的地位,使其成为许多不同场景下的理想选择。👍
弱映射
WeakMap是我们遇到的第二种弱结构。WeakSet 的许多特性也适用于 WeakMap!这包括数据存储方式、仅限对象规则、有限的 API以及不支持迭代(没有方法可以返回这些弱存储键的列表)。
如您所知,Map(以及 WeakMap)以键值对模式存储数据。这意味着在这个结构中实际上存在两组数据——键和值。“弱”字仅指键,因为键负责允许我们访问值。值则以常规方式存储,或者如果您喜欢,也可以称之为强方式。💪 因此,尽管听起来可能有些奇怪,但在 WeakMap 中,只有对象才能用作有效的键。
用法
就像 WeakSet 一样,WeakMap 的 API 也非常有限。你只能使用 `get` get()、set()`get`delete()和`get` 这四个方法has()。同样,不支持迭代。😭 但是,如果你考虑一下可能的用例以及这种结构的工作原理,你就会更好地理解这些限制。你不能对弱存储的数据进行迭代。你需要对键进行引用,所以这四个基本方法是最佳选择。等等等等。😏
当然,构造函数还接受一个额外的参数,但这个参数对于初始化数据来说用处不大。
const weakMap = new WeakMap();
const value = {a: 10}
weakMap.set({}, value); /* Key will be garbage-collected, but value
will still be accessible through variable. */
weakMap.set(value, 10) // Values don't have to be object-only
用例
WeakMap 和 WeakSet 的用例类似——都支持标记。所有这些操作都发生在键(key)端。然而,值(value)作为不同类型的强存储数据,无需与特定键一起被垃圾回收。如果之前已保存到变量中,仍然可以自由使用。这意味着您不仅可以标记数据的键,还可以标记数据的另一面(值),并依赖于两者之间的关系。🙌
就这些吗?
目前来看——是的。🤯 希望这篇文章能帮助你学到一些新知识,或者至少复习一下基础知识。你的 JS 代码不必只依赖对象和数组,尤其是在现代浏览器市场份额不断增长的情况下。📊 此外,除了结构本身的缺陷和内部行为之外,上面提到的所有结构都有非常简单易用的polyfill 选项。这样,即使只是为了它们优秀的 API,你也可以自由地使用它们。
那么,你觉得这篇文章怎么样?请在下方点赞或评论分享你的看法。这真的能帮助我写出更好的文章——你知道的,就是那种你们喜欢读的文章!😀 哦,也别忘了分享这篇文章,让更多人看到!此外,欢迎在Twitter🐦或Facebook上关注我 ,也可以访问我的个人博客,随时了解本博客的最新内容。再次感谢你的阅读,希望下次还能见到你!✌
资源
- 来自“2ality.com”的ECMAScript 6 中的类型化数组;
- 来自“javascript.info”的Map、Set、WeakMap 和 WeakSet;
- ES6 集合:使用 Map、Set、WeakMap、WeakSet(来自“sitepoint.com”);