《JavaScript权威指南第7版》第11章 JavaScript标准库

有些数据类型,如数字和字符串(第3章)、对象(第6章)和数组(第7章)是JavaScript的基础,我们可以将它们视为语言本身的一部分。本章介绍了其他重要但不太基础的API,它们可以被认为是JavaScript的“标准库”:这些是JavaScript内置的有用类和函数,可用于web浏览器和Node中的所有JavaScript程序。1

这一章的章节是相互独立的,你可以按任何顺序阅读。它们包括:

  • 集合类Set和映射类Map,用于表示一组值以及从一组值到另一组值的映射。
  • 称为TypedArrays的类数组对象,表示二进制数据的数组,以及从非二进制数据数组中提取值的相关类。
  • 正则表达式和RegExp类,它们定义文本模式,对文本处理很有用。本节还将详细介绍正则表达式语法。
  • 用于表示和操作日期和时间的Date类。
  • Error类及其各种子类,当JavaScript程序中发生错误时,将抛出它们的实例。
  • JSON对象,其方法支持由对象、数组、字符串、数字和布尔值组成的JavaScript数据结构的序列化和反序列化。
  • Intl对象及其定义的类可以帮助您本地化JavaScript程序。
  • Console对象,其中的方法主要输出字符串,对调试程序和记录这些程序的行为特别有用。
  • URL类,它简化了解析和操作URL的任务。本节还介绍了对URL及其组件进行编码和解码的全局函数。
  • setTimeout()和相关函数,用于指定经过一定时间间隔后要执行的代码。

本章中的一些部分,特别是,关于类型化数组和正则表达式的部分相当长,因为在有效地使用这些类型之前,您需要了解一些重要的背景信息。然而,其他许多部分都很短:它们只是介绍了一个新的API并展示了一些使用它的示例。

11.1 Set和Map

JavaScript的Object类型是一种通用的数据结构,可以用来将字符串(对象的属性名)映射到任意值。当映射到的值是固定的,比如true,那么对象实际上就是一组字符串。

在JavaScript编程中,对象实际上经常被用作映射和集合,但这又受到属性必须是字符串的限制,并且由于对象通常继承名为“toString”的属性而变得复杂,这些属性通常不是映射或集合的一部分。

因此,ES6引入了真正的Set和Map类,我们将在下面的小节中介绍这些类。

11.1.1 Set类

集合是值的集合,就像数组一样。但是,与数组不同的是,集合不被排序或编制索引,并且它们不允许重复:一个值要么是集合的成员,要么不是一个成员;不可能询问一个值在一个集合中出现了多少次。

使用Set()构造函数创建Set对象:

let s = new Set(); // 一个新的空集合
let t = new Set([1, s]); // 一个有2个成员的新集合

Set()构造函数的参数不必是数组:允许任何可迭代对象(包括其他Set对象):

let t = new Set(s); // 复制s元素的新集合。
let unique = new Set("Mississippi"); // 4个元素:“M”、“i”、“s”和“p”

集合的size属性类似于数组的length属性:它告诉您集合包含多少个值:

unique.size // => 4

集合创建时不需要必须初始化。可以使用add()、delete()和clear()随时添加和删除元素。请记住,集合不能包含重复项,因此在集合中已包含该值时向其添加值不会产生任何效果:

let s = new Set(); // 空集合
s.size // => 0
s.add(1); // 添加一个数字
s.size // => 1; 现在集合有一个成员
s.add(1); // 再添加同样的数字
s.size // => 1; 大小不变
s.add(true); // 添加另一个值;注意可以添加混合类型
s.size // => 2
s.add([1, 2, 3]); // 添加数组值
s.size // => 3; 数组已添加,而不是其元素
s.delete(1) // => true: 已成功删除元素1
s.size // => 2: 大小减小为2
s.delete("test") // => false: “test”不是成员,删除失败
s.delete(true) // => true: 删除成功
s.delete([1, 2, 3]) // => false: 与集合中的数组不是同一个
s.size // => 1: 集合中仍然有一个数组
s.clear(); // 清空集合
s.size // => 0

关于此代码,有几点需要注意:

  • add()方法接受一个参数;如果传递一个数组,它将数组本身添加到集合中,而不是单个数组元素。add()总是返回调用它的集合,因此如果要向一个集合添加多个值,可以使用链式方法调用,如s.add(‘a’).add(‘b’).add(‘c’);。
  • delete()方法一次也只删除一个集合元素。但是,与add()不同,delete()返回一个布尔值。如果指定的值实际上是集合的成员,那么delete()将删除它并返回true。否则,它什么也不做,返回false。
  • 最后,了解集合成员资格是基于严格的相等性检查的,这一点非常重要,就像===运算符所执行的那样。集合可以同时包含数字1和字符串“1”,因为它认为它们是不同的值。当值是对象(或数组或函数)时,它们也会进行比较,就像使用===。这就是为什么我们不能从这段代码中的集合中删除数组元素。我们在集合中添加了一个数组,然后试图通过向delete()方法传递一个不同的数组(尽管元素相同)来删除该数组。为了使其工作,我们必须传递一个完全相同数组的引用。

Python程序员要注意:这是JavaScript和Python集之间的一个显著区别。Python集比较成员的相等性,而不是引用的相等性,但是折衷的是Python集只允许添加不可变的成员(如元组),而不允许将列表和词典添加到集合中。

实际上,我们对集合做的最重要的事情不是在集合中添加或删除元素,而是检查指定的值是否是集合的成员。我们使用has()方法执行此操作:

let oneDigitPrimes = new Set([2, 3, 5, 7]);
oneDigitPrimes.has(2) // => true: 2是一个一位数的素数
oneDigitPrimes.has(3) // => true: 3也是
oneDigitPrimes.has(4) // => false: 4 不是素数
oneDigitPrimes.has("5") // => false: "5" 甚至不是一个数字

关于集合最重要的一点是,它们是为成员资格测试而优化的,无论集合有多少成员,has()方法都会非常快。数组的includes()方法也执行成员资格测试,但所需的时间与数组的大小成正比,使用数组作为集合可能比使用真正的集合对象慢得多。

Set类是可迭代的,这意味着您可以使用for/of循环来枚举集合的所有元素:

let sum = 0;
for (let p of oneDigitPrimes) {
    // 循环一位数素数的数组
    sum += p; // 求和
}
sum // => 17: 2 + 3 + 5 + 7

由于集合是可迭代的,你可以使用…展开运算符把它们转化成数组或者参数列表:

[...oneDigitPrimes] // => [2,3,5,7]: 转换为数组的集合
Math.max(...oneDigitPrimes) // => 7: 集合元素作为函数参数传递

集合通常被描述为“无序集合”。然而,对于JavaScript集合类来说,这并不完全正确。JavaScript集合没有索引:不能像数组那样要求集合的第一个或第三个元素。但是JavaScript Set类总是记住元素插入的顺序,并且在迭代集合时总是使用这个顺序:插入的第一个元素将是第一个迭代的元素(假设您没有首先删除它),最近插入的元素将是最后一个迭代的元素。2

除了可迭代之外,Set类还实现了一个与同名的与数组方法类似的forEach()方法:

let product = 1;
oneDigitPrimes.forEach(n => {
    product *= n; });
product // => 210: 2 * 3 * 5 * 7

数组的forEach()将数组索引作为第二个参数传递给指定的函数。集合没有索引,因此Set类的这个方法只将元素值同时作为第一个和第二个参数传递。

11.1.2 Map类

映射Map对象表示一组称为键的值,其中每个键都有另一个与之关联(或“映射到”)的值。从某种意义上说,映射就像数组,但它不使用一组连续的整数作为键,而是允许我们使用任意值作为“索引”。与数组一样,映射速度很快:无论映射有多大,查找与键相关联的值都会很快(尽管不如索引数组那么快)。

使用Map()构造函数创建新映射:

let m = new Map(); // 创建一个新的空映射
let n = new Map([ // 一个新映射,使用字符串作为键,数字作为值来初始化
    ["one", 1],
    ["two", 2]
]);

Map()构造函数的可选参数应该是一个可生成两元素[key,value]数组的可迭代对象。在实践中,这意味着如果您想在创建映射时初始化它,您通常会将所需的键和相关联的值写成数组。但也可以使用Map()构造函数复制其他映射或从现有对象复制属性名称和值:

let copy = new Map(n); // 与映射n具有相同键和值的新映射
let o = {
    x: 1, y: 2}; // 具有两个属性的对象
let p = new Map(Object.entries(o)); // 等同于new Map([["x", 1], ["y", 2]])

创建映射对象后,可以使用get()查询与给定键关联的值,也可以使用set()添加新的键/值对。不过,请记住,映射是一组键,每个键都有一个关联的值。这与一组键/值对不太一样。如果使用映射中已存在的键调用set(),则将更改与该键关联的值,而不是添加新的键/值映射。除了get()和set()之外,Map类还定义了与set方法类似的方法:使用has()检查映射是否包含指定的键;使用delete()从映射中删除键(及其关联的值);使用clear()从映射中删除所有键/值对;使用size属性确定映射包含多少个键。

let m = new Map(); // 从一个空映射开始
m.size // => 0: 空映射没有键
m.set("one", 1); // 键"one" 映射到值 1
m.set("two", 2); // 键"two" 映射到值 2.
m.size // => 2: 映射有2个键
m.get("two") // => 2: 返回与键“two”关联的值
m.get("three") // => undefined: 这个键不在映射中
m.set("one", true); // 更改现有键的映射值
m.size // => 2: 大小不变
m.has("one") // => true: 映射含有键"one"
m.has(true) // => false: 映射不含有键true
m.delete("one") // => true: 键存在,删除成功
m.size // => 1
m.delete("three") // => false: 无法删除不存在的键
m.clear(); // 清空映射

与Set的add()方法一样,Map的Set()方法也可以链接,这样就可以在不使用数组的数组的情况下初始化映射:

let m = new Map().set("one", 1).set("two", 2).set("three", 3);
m.size // => 3
m.get("two") // => 2

与Set一样,任何JavaScript值都可以用作映射中的键或值。这包括null、undefined和NaN,以及对象和数组等引用类型。与Set类一样,Map按标识(引用)而不是相等性来比较键,因此如果使用对象或数组作为键,则会认为它与其他所有对象和数组都不同,即使这些对象和数组具有完全相同的属性或元素:

let m = new Map(); // 一个空映射
m.set({
   }, 1); // 把一个空对象关联值1
m.set({
   }, 2); // 把另一个空对象关联值2
m.size // => 2: 映射中有2个键
m.get({
   }) // => undefined: 但是这个空对象不是键
m.set(m, undefined); // 映射自身到值undefined
m.has(m) // => true: m是一个键
m.get(m) // => undefined: 如果m不是键,我们也会得到相同的值

Map对象是可迭代的,每个迭代值都是一个两元素数组,其中第一个元素是键,第二个元素是与该键关联的值。如果在Map对象中使用展开操作符,您将得到一个数组的数组,就像我们传递给Map()构造函数的数组一样。当使用for/of循环迭代映射时,惯用的做法是使用解构赋值将键和值分配给单独的变量:

let m = new Map([["x", 1], ["y", 2]]);
[...m] // => [["x", 1], ["y", 2]]
for (let [key, value] of m) {
   
    // 在第一次迭代中,key为“x”,value为1
    // 在第二次迭代中,key为“y”,value为2
}

像Set类一样,Map类按插入顺序迭代。迭代的第一个键/值对将是最早添加到映射中的,迭代的最后一个将是最近添加的一个。

如果只想迭代映射的键或关联值,请使用keys()和values()方法:这些方法返回可迭代对象,这些对象按插入顺序迭代键和值。(entries()方法返回迭代键/值对的可迭代对象,但这与直接迭代映射完全相同。)

[...m.keys()] // => ["x", "y"]: 只有键
[...m.values()] // => [1, 2]: 只有值
[...m.entries()] // => [["x", 1], ["y", 2]]: 类似 [...m]

映射对象也可以使用数组类首先实现的forEach()方法进行迭代。

m.forEach((value, key) => {
    // 注意是 value, key, 不是 key, value
	// 在第一次迭代中, value 为 1,key 为 "x"
	// 在第二次迭代中, value 为 2,key 为 "y"
});

在上面的代码中,value参数出现在key参数之前似乎很奇怪,因为在for/of迭代中,key在前面。如本节开头所述,可以将映射视为一个通用数组,其中整数数组索引被任意键替换。数组的forEach()方法首先传递数组元素,然后传递数组索引,因此,通过类比,映射的forEach()方法首先传递映射值,然后传递映射健。

11.1.3 WeakMap和WeakSet

WeakMap类是Map类的一个变体(但不是实际的子类),它不会阻止其键值被垃圾回收。垃圾回收是JavaScript解释器回收不再“可访问”且程序无法使用的对象的内存的过程。一个常规的映射包含对其键值的“强”引用,即使对它们的所有其他引用都不存在,它们仍然可以通过映射访问。相反,WeakMap保留对其键值的“弱”引用,因此它们无法通过WeakMap访问,并且它们在映射中的存在不会阻止它们的内存被回收。

WeakMap()构造函数与Map()构造函数类似,但WeakMap和Map之间存在一些显著差异:

  • WeakMap键必须是对象或数组;原始值不受垃圾回收的影响,不能用作键。
  • WeakMap只实现get()、set()、has()和delete()方法。特别是,WeakMap是不可迭代的,并且不定义keys()、values()或forEach()。如果WeakMap是可迭代的,那么它的键就可以访问,那它就不是弱引用。
  • 类似地,WeakMap不实现size属性,因为WeakMap的大小在对象被垃圾回收时随时可能更改。

WeakMap的预期用途是允许您将值与对象关联,而不会导致内存泄漏。例如,假设您正在编写一个函数,该函数接受对象参数,并且需要对该对象执行一些耗时的计算。为了提高效率,您希望缓存计算的值,以便以后重用。如果使用映射对象实现缓存,则将防止任何对象被回收,但是通过使用WeakMap,可以避免此问题。(通常可以使用私有符号属性直接缓存对象上的计算值,从而获得类似的结果。见§6.10.3.)

WeakSet实现了一组不阻止这些对象被垃圾回收的对象。WeakSet()构造函数的工作方式与Set()构造函数类似,但WeakSet对象与Set对象的不同之处与WeakMap对象与Map对象的不同之处相同:

  • WeakSet不允许原始值作为成员。
  • WeakSet只实现add()、has()和delete()方法,不可迭代。
  • WeakSet没有size属性。

WeakSet并不常用:它的应用与WeakMap的应用类似。例如,如果要将对象标记(或“标识”)为具有某些特殊属性或类型,可以将其添加到WeakSet中。然后,在其他地方,当您要检查该属性或类型时,可以测试该WeakSet中的成员资格。使用普通集合执行此操作将防止所有标记的对象被垃圾回收,但在使用WeakSet时,这不是一个问题。

11.2 类型数组和二进制数据

常规JavaScript数组可以有任何类型的元素,并且可以动态地增长或收缩。JavaScript实现执行许多优化,因此JavaScript数组的普通操作非常快。然而,它们仍然与C和Java等低级语言的数组类型有很大的不同。类型化数组是ES6中的新特性3,它更接近于这些语言的低级数组。类型化数组在技术上不是数组(Array.isArray()返回false),但它们实现了§7.8中描述的所有数组方法以及它们自己的一些方法。但是,它们在一些非常重要的方面与常规数组不同:

  • 类型化数组的元素都是数字。但是,与常规JavaScript数字不同,类型化数组允许您指定要存储在数组中的数字的类型(有符号和无符号整数与IEEE-754浮点数)和大小(8位到64位)。
  • 创建类型化数组时必须指定其长度,并且该长度永远不会更改。
  • 类型化数组的元素在创建数组时总是初始化为0。

11.2.1 类型数组的类型

JavaScript不定义TypedArray类。相反,有11种类型的数组,每种类型都有不同的元素类型和构造函数:

构造函数 数字类型
Int8Array() 有符号字节
Uint8Array() 无符号字节
Uint8ClampedArray() 不带取模的无符号字节
Int16Array() 有符号16位短整数
Uint16Array() 无符号16位短整数
Int32Array() 有符号32位整数
Uint32Array() 无符号32位整数
BigInt64Array() 带符号64位BigInt值(ES2020)
BigUint64Array() 无符号64位BigInt值(ES2020)
Float32Array() 32位浮点值
Float64Array() 64位浮点值:常规JavaScript数字

名称以Int开头的类型包含1、2或4字节(8、16或32位)的有符号整数。名称以Uint开头的类型包含相同长度的无符号整数。“BigInt”和“BigUint”类型保存64位整数,用JavaScript表示为BigInt值(见§3.2.5)。以Float开头的类型保留浮点数。Float64Array的元素与普通JavaScript数字的类型相同。Float32Array的元素精度较低,范围较小,但只需要一半的内存。(这种类型在C和Java中称为float)

Uint8ClampedArray是Uint8Array上的一个特殊情况变体。这两种类型都包含无符号字节,可以表示0到255之间的数字。在Uint8Array中,如果将大于255或小于零的值存储到数组元素中,它将“取模舍入”并获得其他值。这就是计算机内存在低级模式下的工作方式,所以这是非常快的。Uint8ClampedArray执行一些额外的类型检查,这样,如果存储的值大于255或小于0,它将“固定”到255或0,并且不会"取模舍入"。(这个’Clamped’行为是HTML<canvas>元素的低级API所必需的,用于操作像素颜色)

每个类型化数组构造函数都有一个BYTES_PER_ELEMENT属性,值为1、2、4或8,具体取决于类型。

11.2.2 创建类型数组

创建类型化数组的最简单方法是使用一个数字参数调用适当的构造函数,该参数指定您希望在数组中使用的元素数量:

let bytes = new Uint8Array(1024); // 1024 字节
let matrix = new Float64Array(9); // 一个 3x3 矩阵
let point = new Int16Array(3); // 三维空间中的点
let rgba = new Uint8ClampedArray(4); // 4字节RGBA像素值
let sudoku = new Int8Array(81); // 9x9数独板

以这种方式创建类型化数组时,保证数组元素都初始化为0、0n或0.0。但是,如果知道类型化数组中需要的值,则也可以在创建数组时指定这些值。每个类型化数组构造函数都有静态from()和of()工厂方法,其工作方式类似于Array.from()和Array.of():

let white = Uint8ClampedArray.of(255, 255, 255, 0); // RGBA不透明白色

回想一下Array.from()工厂方法需要一个类似数组或可迭代对象作为其第一个参数。对于类型化数组变量也是如此,只是可迭代或类似数组的对象也必须有数字元素。例如,字符串是可迭代的,但是将它们传递给类型化数组的from()工厂方法是没有意义的。

如果只使用from()的单参数版本,则可以删除.from并将可迭代或类似数组的对象直接传递给构造函数,后者的行为完全相同。请注意,构造函数和from()工厂方法都允许您复制现有类型化数组,同时可以更改类型:

let ints = Uint32Array.from(white); // 相同的4个数字,但作为整数

从现有数组、可迭代对象或类似数组的对象创建新的类型化数组时,这些值可能会被截断以适应数组的类型约束。发生这种情况时不会出现警告或错误:

// 浮点被截断为整数,较长的整数被截断为8位
Uint8Array.of(1.23, 2.99, 45000) // => new Uint8Array([1, 2, 200])

最后,还有一种方法可以创建包含ArrayBuffer类型的类型化数组。ArrayBuffer是对内存块的不透明引用。可以使用构造函数创建一个;只需传入要分配的内存字节数:

let buffer = new ArrayBuffer(1024*1024);
buffer.byteLength // => 1024*1024; 一兆字节内存

ArrayBuffer类不允许读取或写入已分配的任何字节。但是您可以创建类型化数组,这些数组使用缓冲区的内存,并且允许您读写内存。为此,调用类型化数组构造函数,第一个参数是ArrayBuffer,第二个参数是数组缓冲区内的字节偏移量,第三个参数是数组长度(以元素为单位,而不是字节)。第二个和第三个参数是可选的。如果两者都省略,那么数组将使用数组缓冲区中的所有内存。如果只省略length参数,则数组将使用数组开始位置和结束位置之间的所有可用内存。关于这种类型化数组构造函数,还要记住一件事:数组必须与内存对齐,因此如果指定字节偏移量,则值应该是类型大小的倍数。例如,Int32Array()构造函数需要4的倍数,Float64Array()需要8的倍数。

给定前面创建的ArrayBuffer,可以创建如下类型的数组:

let asbytes = new Uint8Array(buffer); //以字节形式查看
let asints = new Int32Array(buffer); // 以32位有符号整数形式查看
let lastK = new Uint8Array(buffer, 1023*1024); // 最后1K字节(字节)
let ints2 = new Int32Array(buffer, 1024, 256);<
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值