JavaScript权威指南 第11章JavaScript标准库

JavaScript权威指南 第11章JavaScript标准库

  • 第11章 JavaScript标准库
    • 11.1 集合与映射
      • 11.1.1 Set类
      • 11.1.2 Map类
      • 11.1.3 WeakMap和WeakSet
    • 11.2 定型数组与二进制数据
      • 11.2.1 定型数组的类型
      • 11.2.2 创建定型数组
      • 11.2.3 使用定型数组
      • 11.2.4 定型数组的方法与属性
      • 11.2.5 DateView与字节序
    • 11.3 正则表达式与模块匹配
      • 11.3.1 定义正则表达式
        • 字面量字符
        • 字符类
        • 重复
        • 非贪婪重复
        • 任选、分组和引用
        • 指定匹配位置
        • 标志
          • g
          • i
          • m
          • s
          • u
          • y
      • 11.3.2 模式匹配的字符串方法
        • search()
        • replace()
        • match()
        • matchAll()
        • split()
      • 11.3.3 RegExp类
        • RegExp属性
          • sourse
          • flags
          • global
          • ignoreCase
          • multiline
          • dotAll
          • unicode
          • sticky
          • lastIndex
        • test()
        • exec()
    • 11.4 日期和时间
      • 11.4.1 时间戳
      • 11.4.2 日期计算
      • 11.4.3 格式化与解析日期字符串
        • toString()
        • toUTCString()
        • toISOString()
        • toLocaleString()
        • toDateString()
        • toLocaleDateString()
        • toTimeString()
        • toLocaleTimeString()
    • 11.5 Error类
    • 11.6 JSON序列化与解析
      • 11.6.1 JSON自定义
    • 11.7 国际化API
      • 11.7.1 格式化数值
        • style
        • currency
        • currencyDisplay
        • useGrouping
        • minimumIntegerDigits
        • minimunFractionDigits、maxmumFractionDigits
        • minimumSignificantDigits、maximumSignificantDigits
      • 11.7.2 格式化日期和时间
        • year
        • month
        • day
        • weekday
        • era
        • hour、minute、second
        • timeZone
        • timeZoneName
        • hour12
        • hourCycle
      • 11.7.3 比较字符串
        • usage
        • sensitivity
        • ignorePunctuation
        • numeric
        • caseFirst
    • 11.8 控制台API
        • console.log()
        • console.debug()、console.warn()、console.error()
        • console.assert()
        • console.clear()
        • console.table()
        • console.trace()
        • console.count()
        • console.countReset()
        • console.group()
        • console.groupCollapsed()
        • console.groupEnd()
        • console.time()
        • console.timelog()
        • console.timeEnd()
      • 11.8.1 通过控制台格式化输出
        • %s
        • %i和%d
        • %f
        • %o和%0
        • %c
    • 11.9 URL API
      • 11.9.1 遗留URL函数
        • encodeURI()和decodeURI()
        • encodeUEIComponent()和decodeURIComponent()
    • 11.10 计时器

第11章 JavaScript标准库

11.1 集合与映射

11.1.1 Set类

集合就是一组值,与数组类似。但与数组不同的是,集合没有索引或顺序,也不允许重复:一个值要么是集合的成员,要么不是;这个值不可能在一个集合中出现多次。

可以使用Set()构造函数创建集合对象:

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

Set()构造函数的参数不一定是数组,但必须是一个可迭代对象(包括其他集合):

let t=new Set(s)                      //复刻了s
let unique=new Set("Mississippi")     //4个元素:"M""i""s"和"p"
unique
=>Set(4) {"M", "i", "s", "p"}
t
=>Set(0) {}

集合的size属性类似数组的length属性,保存着集合包含多少个值:

unique.size
=>4

集合不一定在创建时初始化,可以在创建之后再通过add()、delete()和clear()方法给它添加元素或从中删除元素。记住,集合不能包含重复的值,因此添加集合中已经存在的值没有效果。

let s=new Set();       //创建空集合
s.size
=>0
s.add(1)               //添加一个数值
=>Set(1) {1}
s.size
=>1
s.add(1)
=>Set(1) {1}
s.size
=>1
s.add(true)
=>Set(2) {1, true}
s.size
=>2
s.add([1,2,3])
=>Set(3) {1, true, Array(3)}
s.size
=>3
s.delete(1)
=>true
s.size
=>2
s.delete("test")      //“test”不是成员,删除失败
=>false
s.delete(true)
=>true
s.delete([1,2,3])    //数组是对象,引用值不同,不是同一个数组
=>false
s.size
=>1
s.clear()
s.size
=>0

关于这段代码有几个地方需要着重说明一下。

  • add()方法接收一个参数,如果传入一个数组,它会把数组而不是数组的元素添加到集合中。add()始终返回调用它的集合,因此如果想给集合添加多个值,可以连缀调用add(),如s.add(‘s’).add(‘b’).add(‘c’);。
  • delete()方法一次也只删除一个集合元素,与add()不同,delete()返回一个布尔值。如果指定的值确实是一个集合成员,那么delete()删除它并返回true;否则,delete()什么也不做并返回false。
  • 最后,很重要的一点就是要记住集合成员是根据严格相等来判断是否重复的,类似于使用===操作符。集合可以既包含数值1,也包含字符串“1”,因为它认为这两个值不同。如果值是对象(或数组、函数),那么也会像使用 ===一样进行比较。这也是以上代码中不能删除数组元素的原因。我们给集合添加的是一个数组,而传给delete()方法的则是另一个不同的数组(尽管两个数组包含相同的元素)。如果真想删除第一个数组,必需传入该数组的引用。

实践中,使用集合时最重要的不是添加和删除元素,而是检查某个值是不是集合的成员。为此要使用has()方法:

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

关于集合,最重要的是要知道它专门为成员测试做了优化,无论集合有多少成员,has()方法都非常快。数组的includes()方法也执行成员测试,但其执行速度与数组大小成反比。因此,使用数组作为集合比真正的Set对象要慢很多。

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

for(let p of oneDigitPrimes){  //循环遍历一位数字的素数
    sum+=p;                    //求它们的和
}
sum
=>17

一位Set对象是可迭代的,所以可以使用扩展操作符…把集合转换为数组或参数:

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

集合经常被称为“无序集合”,但对于JavaScript的Set类而言,这并不正确。JavaScript集合是无索引的:不能像对数组那样取得集合的第一个或第三个元素。但JavaScript的Set类会记住元素的插入顺序,而且始终按该顺序迭代集合:第一个元素第一个迭代(假定之前没有删除它),刚刚添加的元素最后一个迭代。

除了可以迭代,Set类也实现了一个foreach()方法,与数组的同名方法类似:

let product=1;
oneDigitPrimes.forEach(n=>{product*=n});
product
=>210

数组的forEach()方法把数组索引作为回调函数的第二个参数。但集合没有索引,所以这个方法的Set类版本传给回调的第一个和第二个参数都是元素的值。

11.1.2 Map类

Map对象表示一组被称为键的值,其中每个键都关联着(或映射到)另一个值。从某种角度看,映射类似数组,只不过它并不局限于用连续的整数作为键,而是允许使用任何值作为“索引”。同样与数组类似,映射速度也很快:无论映射有多大,查询与某个键关联的值都很快(虽然没有索引访问数组那么快)。

可以使用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]])

创建Map对象后,可以使用get()方法来查询关联的值,可以使用set()方法添加新的键/值对。不过,要记住,映射是一组键,每个键关联一个值。这跟一组键/值对不完全一样。如果调用set()传入一个映射中已经存在的键,将会修改与该键关联的值,而不是添加新的键/值映射。除了get()和set(),Map类也定义了与Set类似的方法,包括映射中是否包含指定键的has()、从映射中删除指定键(及其关联值)的remove()、删除映射中所有键/值对的clear()和保存映射中有多少个键的size()属性。

let m=new Map();    //开始创建一个空映射
m.size
=>0
m.set("one",1)
=>Map(1) {"one" => 1}
m.set("two",2)
=>Map(2) {"one" => 1, "two" => 2}
m.size
=>2
m.get('two')
=>2
m.get('three')
m.set("one",true)
=>Map(2) {"one" => true, "two" => 2}
m.size
=>2
m.has("one")
=>true
m.has(true)
=>false
m.delete("one")
=>true
m.size
=>1
m.delete("three")
=>false
m.clear()

与集合的add()方法类似,映射的set()方法可以连缀调用,这样不必再创建映射时用数组去初始化它了:

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

与集合一样,任何JavaScript值都可以在映射中作为键或值。这包括null、undefined和NaN,以及对象和数组等引用类型。同样与集合类一样,映射按照全等性而非相等性比较键。因此如果你在映射中使用对象或数组作为键,那么这样的键与其他任何对象或数组都不一样,即便它们有完全一样的属性或元素:

let m=new Map();                //开始先创建一个空映射
m.set({},1);                    //映射对象到值1
=>Map(1) {{} => 1}
m.set({},2)                    //映射另一个空对象到值2
=>Map(2) {{} => 1, {} => 2}
m.size                         //这个映射中有两个键
=>2
m.get({})                      //但是这个空对象不是映射的键
m.set(m,undefined)             //把映射自身映射到值undefined
=>Map(3) {{} => 1, {} => 2, Map(3) => undefined}
m.has(m)
=>true
m.get(m)
=>undefined

映射对象是可迭代的,迭代的每个值是一个两个元素的数组,其中第一个元素是键,第二个元素是与该键关联的值。如果对映射对象使用扩展操作符,会得到一个数组的数组,就像传给Map()构造函数的一样。在使用for/of循环迭代映射时,习惯上通过解构赋值把键和值赋给不同的变量:

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

与Set类一样,Map类也是按照插入顺序迭代的,即迭代第一个键/值是最早添加到映射中的,最后一个键/值对是最晚添加的。

如果只想迭代映射的键或关联的值,可以使用keys()和values()方法。这两个方法返回的可迭代对象可用于按照插入顺序迭代键和值(另外,entries()方法返回的可迭代对象用于迭代键/值对,与直接迭代映射一样)。

[...m.keys()]
=>(2) ["x", "y"]                //只有键
[...m.values()]
=>(2) [1, 2]                    //只有值
[...m.entries()]
=>(2) [Array(2), Array(2)]      //等价于[...m]

映射也实现了forEach()方法,通过这个最早由Array类实现的方法也可以迭代映射:

m.forEach((value,key)=>{
    //第一次迭代,键是“x”,值是1
    //第二次迭代,键是“y”,值是2
}

这里传给回调的参数值在前、键在后,而for/of循环则是键在前、值在后。本节开头提到过,可以把映射想象成一种通用数组,只不过整数索引被替换成任何键值。数组的forEach()方法是先传数组元素,后传数组索引。同样地,映射的forEach()方法也传映射的值,后传映射的键。

11.1.3 WeakMap和WeakSet

WeakMap(弱映射)类是Map类的一个变体(不是子类),它不会阻止键值被当作垃圾收集。垃圾收集是JavaScript解释器收回内存空间的过程,凡是已经“无法访问”因而无法被程序使用的对象,都会被当作垃圾收回。常规映射对自己的键值保持着“强”引用,即使对它们的所有其他引用都不存在了,仍然可以通过映射访问这些键。相对而言,WeakMap保持着对它们键值的“弱”引用,因而无法通过WeakMap访问这些键。也就是说,WeakMap的存在并不影响它们的键值被回收。

WeakMap()构造函数与Map()构造函数类似,但WeakMap与映射则有明确区别。

  • WeakMap的键必须是对象或数组,原始值不受垃圾回收控制,不能作为键。
  • WeakMap只实现了get()、set()、has()和delete()方法。特别地,WeakMap不是可迭代对象,所以没有定义keys()、values()和forEach()方法。如果WeakMap是可迭代的,那么它的键是可访问的,也就谈不上“弱”了。
  • 类似地,WeakMap没有实现size属性,因为弱映射地大小可能随着对象被当作垃圾来及收集而随时改变。

WeakMap的主要用途是实现值与对象的关联而不导致内存泄漏。

WeakSet(弱集合)实现了一组对象,不会妨碍这些对象被作为垃圾收集。WeakSet()构造函数与Set()构造函数也类似,但正如弱映射与映射一样,弱集合与集合也有着类似的区别。

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

WeakSet的使用场景不多,其主要应用场景与WeakMap类似。例如,如果你想把一个对象标记(或标注)为具有特殊属性或类型,可以把它添加到一个WeakSet中。然后,无论在哪里,只要想检查该属性或类型,就可以测试该WeakSet是否包含相应成员。如果使用常规集合来保存这些被标记的对象,就会妨碍它们被当作垃圾收集,而使用WeakSet则没有这个问题。

11.2 定型数组与二进制数据

常规JavaScript数组可以包含任意类型的元素,可以动态扩展或收缩。JavaScript实现进行了很多优化,因此JavaScript数组在典型的使用场景下速度非常快。然而,这种数组与C和Java等低级语言的数组类型还是有很大区别。ES6新增了定型数组,与这些语言的低级数组非常接近。定型数组严格来说并不是数组(Array.isArray()对它们返回false),但它们实现了7.8节描述的所有
数组方法,外加几个它们自己的方法。定型数组与常规数组存在如下几个非常重要的区别。

  • 定型数组的元素全部都是数值。与常规JavaScript数组不同,定型数组允许指定存储在数组中的数值的额类型(有符号和无符号数组以及IEEE-754浮点数)和大小(8位到64位)。
  • 创建定型数组时必须指定长度,且该长度不能再改变。
  • 定型数组的元素在创建时始终都会初始化为0。

11.2.1 定型数组的类型

JavaScript并未定义TypedArray类,而是定义了11种定型数组,每种都有自己的元素类型和构造函数。
JavaScript权威指南 第11章JavaScript标准库_第1张图片
名字以Int开头的类型保存有符号1、2、4字节(8、16、32位)整数。名字以Unit开头的类型保存1相同长度的无符号整数。BigInt和BigUnit类型保存64位整数,以JavaScript的BigInt值表示。名字以Float开头的类型保存浮点数。Float64Array的元素与常规JavaScript数值是同一种类型。Float32Array的元素精度较低、表示的范围也较小,但只占用一半内存(这个类型对应C和Java中的float)。

Unit8ClampedArray是Unit8Array的一种特殊变体。这两种类型都保存无符号字节,可表示的数值范围是0到255。对Unit8Array来说,如果要存储到数组元素的值大于255或小于0,这个值回“翻转”位其他值。这涉及计算机内存的底层工作机制,速度非常快。Unit8ClampedArray还会额外做一些类型检查,如果你要存储的值大于255或小于0,那它会“固定”为255或0,而不会翻转(这种固定行为对HTML< canvas >元素的低级API操作像素是必需的)。

上面每种定型数组构造函数都有一个BYTES_PER_ELEMENT属性,根据类型不同,这个属性的值可能是1、2、4、8。

11.2.2 创建定型数组

创建定型数组最简单的方式就是调用相应的构造函数,并传入一个表示数组元素个数的数值参数:

let bytes=new Uint8Array(1024);   //1024字节
let matrix=new Float64Array(9);   //3*3矩阵
let point=new Int16Array(3);      //3D空间中的一个店
let rgba=new Uint8ClampedArray(4);  //4字节的RGBA像素值
let sudoku=new Int8Array(81);       //9*9的数独网络

如果以这种方式创建定型数组,则数组元素一定会全部初始化为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个数值,但变成了整数
ints
=>Uint32Array(4) [255, 255, 255, 0]

在通过已有数组、可迭代或类数组对象创建新定型数组时,为适应类型限制,已有的值可能被截短。在此过程中,不会被警告,也不会报错:

//浮点数被截短尾整数,长整数被截短为8位
Uint8Array.of(1.23,2.99,45000)
=>Uint8Array(3) [1, 2, 200]

最后,还有一种创建定型数组的方式,该方式要用到ArrayBuffer类型。ArrayBuffer是对一块内存的不透明引用。可以通过构造函数创建ArrayBuffer,只要传入想分配内存的字节数即可:

let buffer=new ArrayBuffer(1024*1024);
buffer.byteLength
=>1048576         //1024*1024,1兆内存

ArrayBuffer类不允许读取或写入分配的任何字节。但是可以创建使用该缓冲区内存的定型数组,通过这个数组来读取或写入该内存。为此,在调用定型数组的构造函数时需要将ArrayBuffer作为第一个参数,将该缓冲区内的字节偏移量作为第二个参数,将数组的长度(单位是元素而非字节)作为第三个参数。第二个参数和第三个参数是可选的。如果省略第二个和第三个参数,则数组会使用缓冲区的所有内存。如果只省略长度参数,则数组会使用起点位置到缓冲区结束的所有可用内存。关于这种形式的定型数组构造函数,还要记住一点:数组的内存必须是对齐的,所以如果你指定了字节偏移量,那么这个只应该是类型大小的倍数。例如,Int32Array()构造函数要求必须是4的倍数,而Float64Array()则要求必须是8的倍数。

以前面创建的ArrayBuffer为例,可以像下面这样创建定型数组:

let asbytes=new Uint8Array(buffer);            //按字节查看
undefined
let asints=new Int32Array(buffer);             //按32位有符号整数查看
undefined
let lastK=new Uint8Array(buffer,1023*1024);   //按字节查看最后一千字节
undefined
let ints2=new Int32Array(buffer,1024,256);    //按256位整数查看第二个1千字节

这4个定型数组提供了对ArrayBuffer所表示内存的4个不同视图。关键是要知道,所有定型数组底层都有一个ArrayBuffer,即使你没有明确指定。如果调用定型数组构造函数时没有传缓冲区对象,则会自动以适当大小创建一个缓冲区。稍后也会介绍,所有定型数组都有一个buffer属性,引用自己底层的ArrayBuffer对象。之所以需要字节使用ArrayBuffer对象,是因为有时候可能需要一个缓冲区的多个定型数组视图。

11.2.3 使用定型数组

创建定型数组后,可以通过常规的中括号语法读取或写入其元素,与操作其他类数组对象一样:

function sieve(n){
    let a=new Uint8Array(n+1);          //如果x是合数,a[x]等于1
    let max=Math.floor(Math.sqrt(n));   //不分解大于这个数的数
    let p=2;                            //2是第一个素数
    while(p<=max){                      //对小于max的素数、
        for(let i=2*p;i<=n;i+=p)        //将p的倍数标记被合数
            a[i]=1;
        while(a[++p])  /* 空循环 */;     //下一个未标记的索引是素数
    }
    
    while(a[n]) n--;                    //向后循环查找另一个素数
    return n;
}

这个函数用于计算小于你指定数值的最大素数。代码中如果使用常规JavaScript数组也没问题,但按照我的测试,使用Uint8Array()而非Array()可以让代码快4倍以上, 且占用内存少8倍。

定型数组并不像真正的数组,但它们重新实现了多数数组方法,因此几乎可以像使用常规数组一样使用它们:

let ints=new Int16Array(10);              //10个短整数
ints.fill(3).map(x=>x*x).join("")
=>"9999999999"

记住,定型数组的长度是固定的,因此length属性是只读的,而定型数组并未实现改变数组长度的方法(如push()、pop()、unshift()、shift()和splice()),但实现了修改数组内容而不改变长度的方法(如sort()、reverse()和fill())。诸如map()和slice()等返回新数组的方法,则返回与调用它们的定型数组相同类型的数组。

11.2.4 定型数组的方法与属性

除了标准的数组方法,定型数组也是实现了它们自己的一些方法。其中,set()方法用于一次性设置定型数组的多个元素,即把其他常规数组或定型数组的元素复制到当前定型数组中:

let bytes=new Uint8Array(1024);         //1K缓冲区
let pattern=new Uint8Array([0,1,2,3]);   //4字节的数组
bytes.set(pattern);      //把它们复制到另一个字节数组的开头
bytes.set(pattern,4);    //使用不同的偏移量再复制一次
bytes.set([0,1,2,3],8);  //或者直接从一个常规数组复制值
bytes.slice(0,12);      
=>new Uint8Array(12) [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]

set()方法以一个数组或定型数组作为第一个参数,以一个元素偏移量作为其可选的第二个参数,如果不指定则默认为0。如果是从一个定型数组向另一个定型数组复制值,那么操作可能极快。

定型数组也有一个subarray方法,返回调用它的定型数组的一部分:

let ints=new Int16Array([0,1,2,3,4,5,6,7,8,9]);      //10个短整数
let last3=ints.subarray(ints.length-3,ints.length);  //其中最后3个
last3[0]
=>7

subarray()接收与slice()方法相同的参数,而且看起来行为方式也相同。但有一点重要区别:slice()以新的、独立的定型数组返回指定的元素,不与原始数组共享内存;而subarray()则不复制内存,只返回相同底层值得一个新视图:

ints[9]=-1            //修改原始数组中得一个值
last3[2]               
=>-1                  //子数组中也会反映这个变化

说到subarray()方法返回已有数组得新视图,就要再次提到ArrayBuffer。每个定型数组都有3个属性与底层得缓冲区相关:

last3.buffer                   //定型数组得ArrayBuffer对象
=>ArrayBuffer(20)
last3.buffer===ints.buffer     //都是一个缓冲区得视图
=>true
last3.byteOffset               //这个视图从缓冲区的字节14开始
=>14
last3.byteLength               //这个视图长度为6字节(3个16位整数长)
=>6
last3.buffer.byteLength
=>20                           //但底层缓冲区长度位20字节

buffer属性是数组的ArrayBuffer,byteOffset是数组数据在这个底层缓冲区的起点位置,而byteLength是数组数据的字节长度。对于任何定型数组a,以下不变式都成立:

a.length*a.BYTES_PER_ELEMENT===a.byteLength
=>true

ArrayBuffer只是不透明的字节块,通过定型数组可以访问其中的字节,但ArrayBuffer本身并不是定型数组。另外要小心,你可以像对任何JavaScript对象一样对ArrayBuffer使用数组索引。但这样做并不会访问缓冲区中的字节,只会导致难解的bug:

let bytes=new Uint8Array(8);
bytes[0]=1;                //将第1个字节设置为1
bytes.buffer[0]
=>undefined                //缓冲区没有索引0
bytes.buffer[1]=255        //尝试错误地设置缓冲区地字节
=>255
bytes.buffer[1]            //实际上这只是设置了一个常规JS属性
=>255
bytes[1]                  //上面那一行并未设置字节
=>0

前面为我们介绍过,可以通过ArrayBuffer()构造函数创建ArrayBuffer,然后再使用这个缓冲区来创建定型数组。另一种方式就是先创建一个初始化地定型数组,然后再使用该数组的缓冲区创建其他视图:

let bytes=new Uint8Array(1024);     //1024字节
let ints=new Uint32Array(bytes.buffer)   //或者256个整数
let floats=new Float64Array(bytes.buffer) //或者128个双精度浮点数

11.2.5 DateView与字节序

使用定型数组可以查看字节序列的8、16、32或64位视图。这就涉及“字节序”问题了。所谓字节序,就是多个字节排列为更长机器字的顺序。为效率考虑,定型数组使用底层硬件的原生字节序。在小端系统中,ArrayBuffer中的字节排列顺序为低字节到高字节。在大端系统中,字节排列顺序为高字节到低字节。可以使用以下代码确定底层平台的字节序:

//如果整数0x00000001在内存中排列为01 00 00 00
//则底层平台使用小端字节序。在大端字节序平台中,
//看到的字节排列应该是00 00 00 01
let littleEndian=new Int8Array(new Int32Array([1]).buffer)[0]===1
littleEndian
=>true

今天,市面上常见的CPU都是小端字节序。很多来自网络协议及某些二进制文件格式则要求使用大端字节序。如果你的定型数组要使用网络或文件的数据,可以假定平台字节序与数据字节序一致。通常,在使用外部数据时,可以使用Int8Array和Uint8Array来查看数组中的单个字节,但不应该使用字大侠为多字节的其他定型数组。此时,可以使用DataView类,这个类定义的方法可以显式指定读、写ArrayBuffer值时的字节序:

//假设要处理一个二进制字节的定型数组
//首先,创建DataView对象,以便从字节
//中灵活地读取值
let int=view.getInt32(0);     //从字节0开始按大端字节序读取有符号整数
int=view.getInt32(4,false);   //下一个整数还是大端字节序
int=view.getUint32(8,true);   //下一个整数是小端字节序且无序号
view.setUint32(8,int,false);  //将其以大端字节序写回缓冲区

DataView为10种定型数组类(不包括Uint8ClamedArray)定义了10个get方法。这些方法地名字类似getInt16、getUint32()、getBigInt64()和getFloat64()。它们的第一个参数是ArrayBuffer中的字节偏移量,表示读取值的开始位置。所有这些读取方法(除getInt8()和getUint8()之外)都接收一个可选的布尔值作为第二个参数。如果第二个参数被省略或是false,则使用大端字节序。如果第二个参数是true,则使用小端字节序。

DataView也定义了10个对应的设置方法,用于向底层ArrayBuffer写入值。这些方法的第一个参数是偏移量,表示写入值的开始位置。其中每个方法(除setInt8()和setUint8()之外)都接收一个可选的第三个参数。如果这个参数被省略或是false,则以大端字节序格式写入值,即有效字节在前。如果这个参数是true,则以小端字节序写入值,即最低有效字节在前。

定型数组和DataView提供了处理二进制数据所需的全部工具,可以让你编写能够解压的ZIP文件或者从JPEG文件中提取元数据之类的JavaScript程序。

11.3 正则表达式与模块匹配

11.3.1 定义正则表达式

在JavaScript中,正则表达式通过RegExp对象来表示。RegExp对象可以使用RegExp()构造函数来创建,但更多的是通过一种特殊的字面量语法来创建。与字符串字面量就是包含在引号中的字符串类似,正则表达式字面量就是包含在一对斜杆(/)字符之间的字符。因此,可以在JavaScript代码中这样声明一个正则表达式:

let pattern=/s$/;

这行代码创建了一个新的RegExp对象,并将它赋值给变量pattern。这个特殊的RegExp对象匹配任意以字母“s”结尾的字符串。同样的正则表达式也可以使用RegExp()构造函数像下面这样来创建:

let pattern=new RegExp("s$");

正则表达式有一系列字符构成。多数字符,包括所有字母数字字符,都只用来描述直接匹配的字符。因此,正则表达式/java/就匹配任何包含子串“java”的字符串。正则表达式中还有一些字符并不直接匹配字符本身,而是具有特殊的含义。例如,正则表达式/s$/包含两个字符,第一个“s”匹配自身,而第二个“ $”就是一个特殊字符,匹配字符串的末尾。因此,这个正则表达式就匹配任何以字母“s”作为最后一个字符的字符串。

后面还会看到,正则表达式也支持一个或多个标志字符,用于控制匹配的方式。在正则表达式字面量中,标志需要放在第二个斜杠字符后面,在RegExp()构造函数中,标志要作为第二个字符串参数。比如,要匹配以“s”或“S”结尾的字符串,可以给正则表达式添加i标志,表示希望匹配不区分大小写:

let pattern=/s$/i;
字面量字符

所有字母字符和数字在正则表达式中都匹配自身的字面值。JavaScript正则表达式语法通过反斜杠(\)开头的转义序列也支持一些非字母字符。例如,\n匹配字符串中换行字符的字面值。
JavaScript权威指南 第11章JavaScript标准库_第2张图片
有一些疑问标点符号在正则表达式中具有特殊含义,它们是:

^ $ . * + ? = ! : | \ / ( ) [ ] { }

这些字符的含义在下面各节中讨论。其中有的字符只在正则表达式的特定上下文中具有特殊含义,在其他上下文中仍然按字面值对待。但作为一个通用的规则,如果想在正则表达式中包含这些标点符号的字面值,必须在这些字符的前面加个反斜杆(\)。其他标点符号字符,如引号和@,在正则表达式不具有特殊含义,仅匹配自身的字面值。

如果记不住哪些标点符号需要使用反斜杠转义,可以给所有标点符号字符前都加上反斜杆。与此同时,也要知道很多字母和数字前面如果加了反斜杠也会具有特殊含义,因此任何想匹配任何字面值的字母和数字都不应该加反斜杠。当然,要在正则表达式中匹配任何包含一个反斜杠的字符串:/\/(如果使用RegExp()构造函数,则要记住,正则表达式中的任何斜杠都要写两次,因为字符串也是一年反斜杠作为转义字符)。

字符类

把个别字面值字符放在方括号中可以组合成字符类。字符类匹配方括号中包含的任意字符。因此,正则表达式/[abc]/匹配a、b或c中的任意一个字母。也可以定义排除性的字符类,匹配除方括号中包含的字符之外的任意字符。排除性字符类就是把插入符号( ^ )作为方括号的第一个字符。如正则表达式/[^abc]/匹配除a、b和c之外的任意一个字符。字符类可以使用连字符表示字符范围。要匹配拉丁字母表的任意一个小写字母,可以使用/[a-z]/(如果想通过字符类匹配真正的连字符,只要把它放到右方括号前面,作为字符类的最后一个字符即可)。

某些字符类很常用,JavaScript正则表达式语法正则表达式语法中包含一些特殊字符和转义序列来表示这些字符类。例如,\s匹配空格字符、制表字符和其他Unicode空白字符。表11-2列出了这些特殊字符并总结了字符类语法(注意,其中一些字符类转义序列只匹配ASCII字符,目前尚未匹配Unicode字符。不过,要匹配Unicode字符,也可以自己定义Unicode字符类。例如,要匹配任何西里而字母,可以定义Unicode字符类(/[\u0400-\u04FF]/)。
JavaScript权威指南 第11章JavaScript标准库_第3张图片
注意,所有特殊字符类转义本身也可以出现在方括号中。\s匹配任意空白字符,\d匹配任意数字,所以/[\s\d]/匹配任意空白字符或数字。不过有一个特例,即\b转义序列有一个特殊含义。如果出现在字符类中,\b表示退格字符。因此要在正则表达式中表示一个退格字符的字面量,就要使用只包含一个元素的字符类:/[\b]/。
JavaScript权威指南 第11章JavaScript标准库_第4张图片
JavaScript权威指南 第11章JavaScript标准库_第5张图片

重复

基于目前所学的正则表达式语法,我们可以用/\d\d/描述两位数字,用/\d\d\d\d/描述四位数字。但是没有方法描述任意位数的数值或者三个字母的字符串后跟一个可选的数字。这些更复杂的模式需要用到正则表达式中的某个元素可能重复多少次的语法。

指定重复的字符始终跟在应用它们的模式后面。由于某些重复的形式非常有用,还有有特殊字符表示这些情况。例如,+表示前面的模式出现一次或多次的情形。

表11-3总结了表示重复的正则表达式语法。
JavaScript权威指南 第11章JavaScript标准库_第6张图片
下面的代码展示了几个例子:

let r=/\d{2,4}/;     //匹配2到4位数字
r=/\w{3}\d?/;        //匹配3各字母后跟1个可选的数字
r=/s+java\s+/;       //匹配“java”且后有一个或多个空格
r=/[^(]*/;           //匹配零或多个非圆括号字符

注意,在所有这些例子中,重复说明符都应用给它们前面的一个字符或字符集。如果没有想匹配更复杂的表达式重复的情形,需要使用圆括号定义一个匹配组,后面几节会介绍。

使用*和?重复字符时要小心。因为这些字符匹配它们前面的字符或模式的零个实例,也就是它们可以匹配不存在。例如,正则表达式/a */实际上会匹配字符串“bbbb”,因为这个字符串包含零个字母“a”!

非贪婪重复

表11-3中列出的重复字符会尽可能多地匹配,捅死也允许正则表达式剩余的部分继续匹配。我们说这种重复是“贪婪的”。在重复字符后面简单地加个问号,就可以指定非贪婪地重复,如??、+?、*?,甚至{1,5}?。举个例子,正则表达式/a+/匹配一个或多个字母“a”。在应用到字符串“aaa”时,它匹配全部3个字母。/a+/同样也匹配一个或多个字符“a”,但在应用到字符串“aaa”时,它只匹配第一个字母“a”。

使用非贪婪重复不一定总能得到期待地结果。比如模式/a+b/匹配一个或多个“a”后跟字母“b”。在应用到字符串“aaab”时,它匹配整个字符串。而使用非贪婪版本地/a+?b/时似乎应该匹配“b”前面有最少的在字母“a”。在应用到同样的字符串“aaab”时,我们本意是希望它只匹配一个“a”和最后的字母“b”。但事实上,这个模式也会匹配整个字符串,与贪婪的版本一样。这是因为正则表达式模式的匹配会从字符串第一个位置开始查找匹配项。由于在字符串一开始就找到了匹配项,所以从后续字母开始的更短的匹配项就不在考虑之列了。

任选、分组和引用

正则表达式的语法也包含指定任选、分组子表达式和引用前面子表达式的特殊字符。竖线字符|用于分隔任选模式。例如,/ab|cd|ef/匹配字符串“ab”或字符串“cd”或字符串“ef”。而/\d{3}|[a-z]{4}/匹配三个数字或4个小写字母。

注意,在找到匹配项之前,会从左到右依次适配任选模式。如果左边的任选模式匹配,则忽略右边的模式,即使右边的模式可以得到“更好”的匹配。比如,/a|ab/应用到字符串“ab”只会匹配字母“a”。

圆括号在正则表达式中有几种不同的作用。一种作用是把独立的模式分组位子表达式,从而让这些模式可以被|、*、+、?等当成一个整体。例如,/java(script)?/匹配“java”后跟可选的“script”。而/(ab|cd)+|ef/匹配字符串“ef”,也匹配一个或多个字符串“ab”或“cd”。

圆括号在正则表达式中的另一个作用是在完整的模式中定义子模式。当正则表达式成功匹配一个目标字符串后,可以从目标字符串中提取出与圆括号包含的子模式对应的部分。例如,假设要查找一个或多个小写字母后跟一个或多个数字。可以使用模式/[a-z]+\d+/。但假设我们只关系每个匹配项中的数字部分。如果把匹配数字的模式放到一对圆括号中(/[a-z]+/(\d+)/),那么就可以从整个模式的匹配项中提取出相应的数字。

与圆括号分组的子表达式相关的一个用途是在同一个正则表达式中回引子表达式。回引前面的子表达式要使用\字符加上数字。这里的数字指的是圆括号分组的子表达式在整个正则表达式中的位置。例如,\1回引第一个子表达式,\3回引第三个。注意,由于子表达式可能会嵌套,所以它们的位置是按照左括号来计算的。例如,在下面的正则表达式中,嵌套的子表达式([Ss]script)要使用/2来引用。

/([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/

对正则表达式中前面子表达式的引用并不会引用该子表达式的模式,而是会引用该模式匹配的文本。因此,引用可以用来强制字符串中不同的部分包含完全相同的字符。例如,下面的正则表达式匹配位于一对单引号或双引号间的一个或多个字符。但是,它不要求开始和结尾的引号匹配(即必须是单引号或都是双引号):

/['"][^'"]*['"]/

如果想要求引号必须匹配,可以使用引用:

/(['"])[^'"]*\1/

这个\1匹配第一个圆括号分组的子表达式匹配的内容。在这个例子中,它强制结尾的引号必须匹配开始的引号。这个正则表达式不允许双引号字符串出现单引号或者单引号字符串中出现双引号(在字符类中使用引用是不合法的,因此不能这么写:/([’"]) [^\1] *\1/)。

在前面介绍RegExpAPI时我们会看到,这种对圆括号子表达式的引用在正则表达式搜索替换操作中是非常强大的特性。

如果不想让圆括号分组的子表达式生成数字引用,那么可以不用(和)分组,而是开头用(?:,结尾用)。来看下面的模式:

/([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/

在这个例子中,子表达式(?:[Ss]cript)?)仅仅是一个分组,从而让?重复字符可以应用到改组。这样修改后的圆括号不会产生引用,因此在这个正则表达式中\2引用的是(fun\w*)匹配的文本。
JavaScript权威指南 第11章JavaScript标准库_第7张图片
JavaScript权威指南 第11章JavaScript标准库_第8张图片

指定匹配位置

如前所述,正则表达式的很多组件匹配字符串中的一个字符。例如,\s匹配一个空白符。还有一些正则表达式组件匹配字符间的位置而非实际的字符。例如,\b匹配ASCII词边界,即\w(ASCII单词字符)与\W(非单词字符)边界,或者ASCII单词字符与字符串开头或末尾的边界。像\b这样的组件并不表示匹配的字符串中用到的任何字符,它们表示的是匹配可以发生的合法位置。有时候,这些组件也被称作正则表达式锚点,因为它们把模式锚定到被搜索字符串中特定的位置。最常用的锚点组件是^和$,分别把模式锚定字符串开头和末尾的位置。

例如,要匹配独占一行的“JavaScript”字符串,可以使用正则表达式/^JavaScript$/。如果想要搜索“Java”这个单词(不是作为前缀,因为它在“JavaScript”中),可以使用模式/\sJava\s/,即单词前后必须有一个空格。但这个方案存在两个问题。首先,它不匹配位于字符串开头或末尾的“Java”,只匹配两侧有空格的情况。其次,在这个模式找到匹配项之后,返回的匹配字符串前后都有空格,这通常并不是我们想要的。为此,可以把匹配实际空格的\s替换为匹配(或锚定)词边界的\b:/\bJava\b/。相应地,组件\B锚定非词边界匹配的位置,模式/\B[Ss]cript/匹配“JavaScript”和“postscript”,但不匹配“script”或“Scripting”。

可以使用任意正则表达式作为锚定条件。如果在(?=和)字符之间包含一个表达式,因为这些字符构成向前查找断言,所以就意味着其中的字符必须存在,但并不实际匹配。比如,要匹配常用编程语言的名字,但必须后面跟着一个冒号,可以使用/[Jj]ava([Ss]cript)?(?=:)/。这个模式匹配“JavaScript:The Definitive Guide”中的“JavaScript”,但不匹配“Java in a Nutshell”中的“Java”,因为它的后面没有冒号。

如果把前面提到的断言改为以(?!开头,那就变成了否定式向前查看断言,表示必须不存在断言中指定的字符。例如,/Java(?!Script)([A-Z]\w*)匹配“Java”后跟一个大写字母及任意数量的ASCII单词字符,但“Java”后面必须不能是“Script”。因此它匹配“JavaBeans”,不匹配“Javanese”,匹配“JavaScrip”,不匹配“JavaScript”或“JavaScripter”。
JavaScript权威指南 第11章JavaScript标准库_第9张图片
JavaScript权威指南 第11章JavaScript标准库_第10张图片

标志

每个正则表达式都可以带一个或多个标志,用于修改其行为。JavaScript定义了6个标志,每个标志都用一个字母表示。标志在正则表达式字面量中放在第二个斜杆后面,或者在使用RegExp()构造函数时要以字符串形式作为第二个参数。JavaScript正则表达式支持的标志及含义如下所示。

g

g标志表示正则表达式是“全局性的”,换句话说,使用这个标志意味着像要找到字符串中包含的所有匹配项,而不只是找到第一个匹配项。这个标志不改变模式匹配的方式,但正如后面会看到的,它会从重要的方面修改String的match()方法和RegExp的exec()方法的行为。

i

i标志表示模式匹配应该不区分大小写。

m

m标志表示匹配应该以“多行”模式进行。意思是这个RegExp要用于多行字符串,而且^和$锚点应该既匹配字符串的开头和末尾,也匹配字符串中任何一行的开头和末尾。

s

与m标志类似,s标志同样用在搜索的文本包含换行符的时候。正常情况下,句点“.”在正则表达式中匹配除行终止符之外的任何字符。但在使用s标志时,“.”将匹配任何字符,包括行终止符。s标志是ES2018引入JavaScript的,在2020年年初得到了除Firefox之外,包括Node、Chrom、Edge和Safari的支持。

u

u标志代表Unicode,可以让正则表达式匹配完整的码点而不是匹配16位值。这个标志是ES6新增的,如果没有特殊原因,应该对所有正则表达式都使用这个标志。如果你使用这个标志,那你的正则表达式将无法识别表情符号和其他需要16位以上表示的字符(包括很多中文字符)。没有u标志,“.”匹配任意一个UTF-1616位值。而有u标志,“.”匹配一个Unicode码点,包括超过16位编码的值。在正则表达式上添加u标志之后,就可以使用新的\u{…}转义序列表示Unicode字符,同时也可以使用\p{…P表示Unicode字符类。

y

y标志表示正则表达式是“有粘性的”(sticky),应该在字符串卡头匹配或在紧跟前一个匹配的第一个字符处匹配。在应用给只想查找一个匹配项时,这个标志的作用就类似给正则表达式加上了^锚点,将其锚定到字符串的开头。对于用来在字符串中反复查找所有匹配项的正则表达式,这个标志比较有用。这时候,它会导致String的match()方法和RegExp的exec()方法产生特殊行为,强制将每个后续匹配都锚定到前一个匹配(在字符串中的)的结束位置。

以上这些标志可以任意组合,顺序不分先后。例如,想让正则表达式识别Unicode、不区分大小写,同时还想用它查找一个字符串中匹配的所有匹配项,指定标志时可以用uig、gui,或者这3个字母的其他组合形式。

11.3.2 模式匹配的字符串方法

到现在为止,我们介绍了正则表达式的语法,但还没有解释怎么在JavaScript代码中使用这些正则表达式。从本节开始,我们就来解释使用RegExp对象的相关API。边界先介绍使用正则表达式执行模式匹配,以及搜索替换操作的字符串方法。后面几节将继续讨论RegExp对象的方法和属性,以及相关的JavaScript正则表达式模式匹配操作。

search()

String支持4个使用正则表达式的方法。最简单的就是search(),这个方法接收一个正则表达式参数,返回第一个匹配项起点字符的位置,如果没有找到匹配项,则返回-1:

"JavaScript".search(/script/ui)
=>4
"Python".search(/script/ui)
=>-1

如果search()方法的参数不是正则表达式,它会先把这个参数传给RegExp()构造函数,把它转换为正则表达式。search()方法不支持全局搜索,因此其正则表达式参数中包含的g标志会被忽略。

replace()

replace()方法执行搜索替换操作。它接收一个正则表达式作为第一个参数,接收一个替换字符串作为第二个参数。它搜索调用它的字符串,寻找与指定模式匹配的文本。如果正则表达式带g标志,replace()方法会用替换字符串中的所有匹配项;否则,它只替换第一个匹配项。如果replace()的第一个参数是字符串而非正则表达式,这个方法不会像search()那样将字符串通过RegExp()转换为正则表达式,而是会按照字面值搜索。比如,可以像下面这样使用replace()规范文本字符串中所有“JavaScript”的大小写:

//无论之前什么大小形式,都替换成规范的大小写形式
text.replace(/javascript/gi,"JavaScript")

不过,replace()的能力远不止这些。还记得吗,正则表达式中括号分组的子表达式是从左到右编号的,而且正则表达式能够记住每个子表达式匹配的文本。如果替换字符串中出现了$符号后跟一个数字,replace()会将两个字符替换为指定子表达式匹配的文本。这个特性非常厉害。比如,可以通过它将字符串中的引号替换成其他字符:

//quote表示一个引号后跟任意多个
//非引号字符,最后又是一个引号
let quote=/"([^"]*)"/g;
//将直引号替换成书名号,同时
//应用的文本不变(保存在$1中)
'He said "stop"'.replace(quote,'《$1》')
=>"He said 《stop》"

如果RegExp中使用的是命名捕获组,可以通过名字而非数字来引用匹配的文本:

let quote=/"(?[^"]*)"/g;
'He said "stop"'.replace(quote,'《$》')
=>"He said 《stop》"

除了给replace()传替换字符串,还可以传一个函数,这个函数会被调用以计算替换的值。这个替换函数在调用时会接收到几个参数。第一个是匹配的整个文本。然后,如果RegExp有捕获组,则后面几个参数分别捕获组匹配的子字符串。再接下来的参数是再字符串找到匹配项的位置。再然后一个参数是调用replace()方法的整个字符串。最后,如果RegExp包含命名捕获组,替换函数还会收到一个参数,这个参数是一个对象,其属性是捕获组的名字,属性值是匹配的文本。比如,下面的带啊吗使用替换函数将字符串中的十进制整数替换成了十六进制:

let s="15 times 15 in 255";
s.replace(/\d+/gu,n=>parseInt(n).toString(16))
=>"f times f in ff"
match()

match()是字符串最常用的正则表达式方法,它只有一个正则表达式参数(或者如果参数不是正则表达式,会把它传给RegExp()构造函数),返回一个数组,其中包含匹配的结果;如果没有找到匹配项,就返回null。如果正则表达式有g标志,这个方法返回的数组会包含再字符串中找到的所有匹配项。例如:

"7 plus 8 equals 15".match(/\d+/g)
=>(3) ["7", "8", "15"]

如果正则表达式没有g标志,match()不会执行全局搜索,只会查找第一个匹配项。在非全局搜索时,match()仍然返回数组,但数组元素完全不同。在没有g标志的情况下,返回数组的第一个元素是匹配的字符串,剩下的所有元素是正则表达式中括号分组的捕获组匹配的字符串,a[1]包含与第一个捕获组匹配的子字符串,以此类推。如果与replace()方法做个比较,则a[1]相当于$1,a[2]相当于域$2,以此类推。

比如,下面的代码是一个解析URL的例子:

//一个非常简单的解析URL的RegExp
let url=/(\w+):\/\/([\w.]+)\/(\S*)/;
let text="Visit my blog at http://www.example.com/~david";
let fullurl,protocol,host,path
let match=text.match(url)
if(match !== null){
    fullurl=match[0];    //fullurl=="http://www.example.com/~david"
    protocol=match[1];   //protocol=="http"
    host=match[2];       //host=="www.example.com"
    path=match[3];       //path=="~david"
}
"~david"

在非全局搜索的情况下,match()返回的数组除了可以通过数组索引的元素,也有一些对象属性。其中,input属性引用调用match()的字符串。index属性是匹配项在字符串中的起始位置。如果正则表达式包含命名捕获组,则返回的数组也有一个groups属性,其值是一个对象。这个对象的属性就是命名捕获组的名字,而属性的值就是匹配的文本。比如,可以将前面那个匹配URL的例子重写成下面这样:

let url=/(?\w+):\/\/(?[\w.]+)\/(?\S*)/;
let text="Visit my blog at http://www.example.com/~david"
let match=text.match(url)
match[0]
=>"http://www.example.com/~david"
match[1]
=>"http"
match.input
=>"Visit my blog at http://www.example.com/~david"
match.index
=>17
match.groups.protocol
=>"http"
match.groups.host
=>"www.example.com"
match.groups.path
=>"~david"

我们知道,match()的行为会因为RegExp是否带g标志而有很大不同。另外,是否设置y标志对match()的行为也有一些影响。前面介绍过了,y标志通过控制字符串匹配的开始位置让正则表达式“有粘性”。如果RegExp同时设置了g和y标志,match()返回包含所有匹配字符串的数组,就跟只设置了g而没有设置y一样。但第一个匹配项必须始于字符串开头,每个后续的匹配项必须从前一个匹配项的后一个字符开始。

如果只设置了y而没有设置g标志,match()会尝试知道第一个匹配项,且默认情况下,这个匹配项被限制在字符串开头。不过,这个默认的起始位置是可以被修改的,设置RegExp对象的lastIndex属性就可以指定匹配开始的位置。如果找到了匹配项,lastIndex属性会自动被更新为匹配项之后第一个字符的位置,因此如果你想这里一样再次调用match(),它会继续寻找后面的匹配项(lastIndex看起来并不像是一个可以指定下一次匹配开始位置的属性。在后面介绍RegExp的exec()方法时还会碰到这个属性,到时候这个名字会显得更有意义)。

let vowel=/[aeiou]/y;     //粘着元音匹配
"text".match(vowel)
=>null                    //"test"开头的字符不是元音字母
vowel.lastIndex=1         //指定一个不同的匹配位置
=>1
"test".match(vowel)
=>["e", index: 1, input: "test", groups: undefined]
vowel.lastIndex           //lastIndex会自动更新
=>2
"test".match(vowel)
=>null                    //位置2不是元音字符
vowel.lastIndex
=>0                       //匹配失败后,lastIndex会被重置

有一点值得注意,即给字符串的match()方法传一个非全局正则表达式,相当于把字符串传给正则表达式的exec()方法。这两种情况下返回的数组及其属性都相同。

matchAll()

matchAll()方法是ES2020中定义的,在2020年年初以及被现代浏览器和Node实现,matchAll()接收一个带g标志的正则表达式。但它并不像match()那样返回所有匹配项的数组,而是返回一个迭代器,每次迭代都产生一个与使用match()时传入非全局RegExp得到的匹配对象相同的对象。正因为如此,matchAll()成为循环遍历字符串中所有匹配项最简单和最通用的方式。

可以像下面这样使用matchAll()遍历字符串中包含的单词:

const words=/\b\p{Alphabetic}+\b/gu;
const testText="This is a native test of matchAll() method";
for(let word of testText.matchAll(words)){
    console.log(`Found '${word[0]}' at index ${word.index}`)
}
VM2088:2 Found 'This' at index 0
VM2088:2 Found 'is' at index 5
VM2088:2 Found 'a' at index 8
VM2088:2 Found 'native' at index 10
VM2088:2 Found 'test' at index 17
VM2088:2 Found 'of' at index 22
VM2088:2 Found 'matchAll' at index 25
VM2088:2 Found 'method' at index 36

也可以设置RegExp对象的IastIndex属性,告诉matchAll()从字符串中的哪个索引开始匹配。但是,与其他模式匹配方法不同的是,matchAll()不会修改传入RegExp的lastIndex属性,这也使得它不太可能在代码中导致bug。

split()

String对象的最后一个正则表达式方法是split()。这个方法使用传入的参数作为分隔符,将调用它的字符串拆分为子字符串保存到一个数组中。可以像这样给它传入一个字符串参数:

"123,456,789".split(",")
=>(3) ["123", "456", "789"]

split()方法也可以接收一个正则表达式参数,这样就可以指定更通用的分隔符。下面这个例子中指定的分隔符允许逗号两侧包含任意数量的空格:

"1, 2, 3, \n4, 5".split(/\s*,\s*/)
=>(5) ["1", "2", "3", "4", "5"]

出乎意料的是,如果调用split()时传入RegExp作为分隔符,且这个正则表达式中包含捕获组,则捕获匹配的文本也会包含在返回的数组中。比如:

const htmlTag=/<([^>]+)>/;     //<后跟一个或多个非>字符,再后跟》
"Testing
1,2,3"
.split(htmlTag) =>(3) ["Testing", "br/", "1,2,3"]

11.3.3 RegExp类

RegExp()构造函数接收一个或两个字符串参数,创建一个新RegExp对象。这个构造函数的第一个参数是包含正则表达式主体的表达式,即在正则表达式字面量中踹向那再斜杆中间的部分。注意,字符串字面量和正则表达式都是用\字符转义,因此再以字符串字面量形式给RegExp()传入正则表达式时,必须把所有\字符替换成\。RegExp()的第二个参数是可选的。如果提供了这个参数,则代表指定正则表达式的标志。这个参数应该是g、i、m、s、u、y或它们的任意组合。

例如:

//查找字符串中包含的所有5位数字。注意这里的双反斜杆\\
let zipcode=new RegExp("\\d{5}","g")

RegExp()构造函数主要用于动态创建正则表达式,即创建那些无法使用正则表达式字面量语法表示的正则表达式。例如,要搜索用户输入的字符串,就必须使用RegExp()在运行时创建正则表达式。

除了给RegExp()的第一个参数传字符串,也可以传一个RegExp对象。这样可以复制已有的正则表达式,并且修改它的标志:

let exactMatch=/JavaScript/;
let caseInsensitive=new RegExp(exactMatch,"i")
RegExp属性

RegExp对象有以下属性。

sourse

这是个只读属性,包含正则表达式的源文本,即出现在RegExp字面量的两个斜杆中间的字符。

flags

这是个只读属性,包含指定RegExp标题的一个或多个字母。

global

只读布尔属性,如果设置了g标志作为true。

ignoreCase

只读布尔属性,设置了i标志作为true。

multiline

只读布尔属性,如果设置了m标志则为true。

dotAll

只读布尔属性,如果设置了s标志则为true。

unicode

只读布尔属性,设置了u标志则为true。

sticky

只读布尔属性,如果设置了u标志则为true,

lastIndex

这是个可以读、写的整数属性。对于带有g或y标志的模式,这个属性用于指定下一次匹配的起始字符位置。接下来要介绍的RegExp类的exec()和test()方法都会用到这个属性。

test()

RegExp类的test()方法是使用正则表达式的最简单方式。该方法接收一个额字符串参数,如果字符串与模式匹配则返回true,如果没有找到匹配项则返回false。

test()方法的原理很简单,它会调用下面介绍的(更复杂的)exec()方法,如果exec()返回非空值就返回true。正因为如此,如果调用test()的RegExp使用了g或y标志,则这个方法的行为取决于RegExp对象的lastIndex属性的值,而这个属性的值可能会被意外修改。详细详细可以参考下面将介绍的“lastIndex属性与重用RegExp”。

exec()

RegExp的exec()方法是使用正则表达式最通常、最强大的方式。该方法接收一个字符串参数,并从这个字符串寻找匹配。如果没有找到匹配项,则返回null。而如果找到匹配项,则会返回一个数组,跟字符串的match()方法在非全局搜索时返回的数组一样。这个数组的元素0包含匹配整个正则表达式的字符串,后面的数组元素包含与正则表达式中捕获组匹配的子字符串。这个返回的数组也有对象属性:index属性包含匹配项起始字符的位置,input属性包含搜索的目标字符串,而groups属性(如果有捕获组)引用一个对象,保存与每个命名捕获组匹配的子字符串。

与String的match()方法不同,exec()方法无论正则表达式是否设置了g标志都会返回相同的数组。我们知道,match()方法在收到一个全局正则表达式时会返回所有匹配项的数组。相对而言,exec()始终返回一个匹配项,并提供关于该匹配项的完整信息。在通过设置了全局g或粘着y标志的正则表达式调用exec()时,exec()会根据RegExp对象的lastIndex属性来决定从哪里开始查找匹配(如果设置了y标志,那么也会限制匹配项必须从该位置开始)。对一个新创建的RegExp对象来说,它的lastIndex为0,因此搜索从字符串的起点开始。但每次exec()成功执行,知道匹配项,都会更新RegExp的lastIndex属性,将其改写为匹配文本之后第一个字符的索引。如果exec()没有找到匹配项,它会将lastIndex重置为0.这个特殊行为让我们得以重复调用exec(),从而逐个找到字符串中所有的匹配项(尽管像我们前面介绍的,ES2020及之后的版本为String新增了matchAll()方法。而matchAll()是遍历所有匹配的更简单的方式)。例如,下面代码中的循环会运行两次:

let pattern=/Java/g;
let text="JavaScript > Java";
let match;
while((match=pattern.exec(text))!==null){
    console.log(`Matched ${match[0]} at ${match.index}`);
    console.log(`Next search begins at ${pattern.lastIndex}`);
}
VM2828:2 Matched Java at 0
VM2828:3 Next search begins at 4
VM2828:2 Matched Java at 13
VM2828:3 

11.4 日期和时间

Date类是JavaScript中用于操作日期和时间的API。使用Date()构造函数可以创建一个日期对象。在不传参数的情况下,这个构造函数会返回一个表示当前日期和时间的Date对象:

let now=new Date();       //当前时间

如果传入一个数值参数,Date()构造函数会将其解释为自1970年至今经过的毫秒数:

let epoch=new Date(0);     //格林尼治标志时间1970年1月1日0时

如果传入一个或多个整数参数,它们会被解释为本地时区的年、月、日、时、分、秒和毫秒,如下所示:

let century=new Date(2100,
                     0,
                     1,
                     2,3,4,5);    //本地时间02:03:04.005                    

Date API有个奇怪的地方,即每年第一个月对应数值0,而每月第一天对应数值1。如果省略时间字段,Date()构造函数默认它们都为0,将时间设置为半夜12点。

注意,在使用多个参数调用时,Date()构造函数会使用本地计算机的时区来解释它们。如果想以UTC(Universal Coordinated Time,通过协调时间;也称GMT,即Greenwich Mean Time,格林尼治标准时间)指定日期和时间,可以使用Date.UTC()。这个静态方法接收与Date()构造函数同样的参数,但使用UTC来解释它们,并返回毫秒时间戳,可以传给Date()构造函数:

//英格兰2100年1月1日半夜12点
let century=new Date(Date.UTC(2100,0,1));

如果要打印日期(比如,使用console.log(century)),默认会以本地失去打印。如果想以UTC显示日期,应该先使用toUTCString()或toISOString()转换它。

最后,如果给Date()构造函数传入字符串,它会尝试按照日期和时间格式赖解析该字符串。这个构造函数可以解析toString()、toUTCString()和toISOString()方式产生的格式:

let century=new Date("2100-01-01T00:00:00Z");   //ISO格式的日期

有了一个Date对象后,可以通过很多方法获取或设置这个对象的年、月、日、时、分、秒和毫秒字段。这些方法都有两种形式,一种使用本地时间获取和设置,另一种使用UTC时间获取和设置。比如,要获取或设置一个Date对象的年份,可以使用getFullYear()、getUTCFullYear()、setFullYear()或setUTCFullYear():

let d=new Date();     
d
=>Thu Nov 04 2021 18:50:42 GMT+0800 (中国标准时间)
d.setFullYear(d.getFullYear()+1)
=>1667559042069
d
=>Fri Nov 04 2022 18:50:42 GMT+0800 (中国标准时间)

要获取或设置Date的其他字段,只要将前面方法中的“FullYear”替换成“Month”“Date”“Hours”“Minutes”“Seconds”或“Milliseconds”即可。其中一些日期设置方法允许我们一次性设置多个字段。setFullYear()和setUTCFullYear()也可选地允许同时设置月和日。而setHours()和setUTCHours()除了支持小时字段,还允许我们指定分钟、 秒和毫秒字段

注意,查询日的方法是getDate()和getUTCDate()。而名字听起来更自然的函数getDay()和getUTCDay()返回的是代表周几的数值(0表示周日,6表示周六)。周几字段是只读的,因此没有对应的setDay()方法。

11.4.1 时间戳

JavaScript在内部将日期表示为整数,代表自1970年1月1日半夜12点起(或之前)的毫秒数。最大支持的整数是8 640 000 000 000 000,因此JavaScript表示的时间不会超过27万年。

对于任何Date对象,getTime()方法返回这个内部值,而setTime()方法设置这个值。因此,可以像下面这样一样给Date对象添加30秒:

d.setTime(d.getTime()+30000)
=>1667559072069
d
=>Fri Nov 04 2022 18:51:12 GMT+0800 (中国标准时间)

这些毫秒值有时候也被称为时间戳,有时候直接使用这些值比使用Date对象更方便。静态的Date.now()方法返回当前时间的时间戳,经常用于度量代码运行时间。
JavaScript权威指南 第11章JavaScript标准库_第11张图片
JavaScript权威指南 第11章JavaScript标准库_第12张图片

11.4.2 日期计算

Date对象可以使用JavaScript标准的<、<=、>和>=等比较操作符进行比较。可以用一个Date对象减去另一个以确定两个日期相关的毫秒数(这本质上是因为Date类定义了valueOf()方法,这个方法返回的是日期的时间戳)。

如果想要给Date对象加或减指定数量的秒、分或小时,最简单的方式就是想前面例子中(给日期加上30秒)那样修改时间戳。但这种方式在涉及加天时就比较麻烦了,因为这个不适合所有月份和年份,不同月份和年份的天数也可能不一样。要完成涉及天数、月数和年数的计算,可以使用setDate()、setMonth()和setYear()。比如,下面的代码给当前日期加上了3个月和2周:

let d=new Date();
d.setMonth(d.getMonth()+3,d.getDate()+14);
1645182789392

日期设置方法即使在数值溢出的情况下也能正常工作。比如,在给当前月份加了3个月之后,最终值可能大于11(11表示12月)。setMonth()在遇到这种情况时会按照需要增加年份。类似地,在将天数设置为超过相应月份地天数时,月份也会相应递增。

11.4.3 格式化与解析日期字符串

如果使用Date类去记录日期和时间(而不只是度量时间),那很有可能需要通过代码向用户展示日期和时间。Date定义了一些方法,可以将日期对象转换为字符串。下面是几个例子:

let d=new Date(2020,0,1,17,10,30);  //2020年元旦5:10:30pm
d.toString()
=>"Wed Jan 01 2020 17:10:30 GMT+0800 (中国标准时间)"
d.toUTCString()
=>"Wed, 01 Jan 2020 09:10:30 GMT"
d.toLocaleDateString()
=>"2020/1/1"
d.toLocaleTimeString()
=>"下午5:10:30"
d.toISOString()
=>"2020-01-01T09:10:30.000Z"

下面分别介绍Date类定义的全部字符串格式化方法。

toString()

这个方法使用本地时区但不按照当地惯例格式化日期和时间。

toUTCString()

这个方法使用UTC时区但不按照当地惯例格式化日期。

toISOString()

这个方法以标准的ISO-8601“年-月-日时:分:秒:毫秒”格式打印日期和时间。字母“T”在输出分隔日期和时间部分。时间以UTC表示,可以通过输出末尾的字母“Z”看出来。

toLocaleString()

这个方法使用本地时区及与用户当地惯例一致的格式。

toDateString()

这个方法只格式化Date的日期部分,忽略时间部分。它使用本地时区,但不与当地惯例适配。

toLocaleDateString()

这个方法只格式化日期部分。它使用本地时区,也适配当地惯例

toTimeString()

这个方法只格式化时间部分。它使用本地时区,但不与当地惯例适配。

toLocaleTimeString()

这个方法只格式化时间部分。它使用本地时区,也适配当地惯例。

注意,这些日期到字符串的转换方法都可以用于格式化日期并展示给用户。11.7.2节介绍了更通用的适配当地惯例的日期和时间格式化技术。

最后,除了将Date对象转换为字符串的方法,还有一个静态的Date.parse()方法。该方法接收一个字符串参数,并尝试将其则为日期和时间来解析,返回一个表示该日期的时间戳。Date.parse()可以像Date()构造函数一样解析同样的字符串,也可以解析toISOString()、toUTCString()和toString()的输出。

11.5 Error类

JavaScript的throw和catch语句可以抛出和捕获任何JavaScript值,包括原始值。虽然没有用来表示错误的异常类型,但JavaScript定义了一个Error类。惯常的做法是使用Error类或其子类的实例作为throw抛出的错误。使用Error对象的一个主要原因就是在创建Error对象时,该对象能够捕获JavaScript的栈状态,如果异常未被捕获,则会显示包含错误信息的栈跟踪信息,这段排查错误很有帮助(注意,栈跟踪信息会展示创建Error对象的地方,而不是throw语句抛出它的地方。如果你始终在抛出之前创建该对象,如throw new Error(),就不会造成任何困惑)。

Error对象有两个属性:message和name,还有一个toString()方法。message属性的值是我们传给Error()构造函数的值,必须时会被转换为字符串。对使用Error()创建的错误对象,name属性的值始终是“Error”。toString()方法返回一个字符串,由name属性的值后跟一个冒号和一个空格,再后跟message属性的值构成。

虽然ECMAScript标准并没有定义,但Node和所有现代浏览器也都在Error对象上定义了stack属性。这个属性的值是一个多行字符串,包含创建错误对象时JavaScript调用栈的栈跟踪信息。在捕获到异常错误时,可以将这个属性的信息作为日志收集起来。

除了Error类,JavaScript还定义了一些它的子类,以便触发ECMAScript定义的一些特殊类型的错误。这些子类包括:EvalError,RangeError、ReferenceError、SyntaxError、TypeError和URIError。你可以按照自己认为合适的方式在代码中使用这些错误类。与基类Error一样,这些子类也都有一个构造函数,接收一个消息参数。每个子类的实例都有一个name属性,其值就是构造函数的名字。

作为开发者,我们可以自己定义Error的子类,以便更好地封装自己程序地错误信息。注意,自定义错误对象可以不限于message和name属性。在定义自己地子类时,可以任意添加新属性以提供更多地错误细节,例如,要写一个解析器,可以定义一个ParseError类,并为它定义line和column属性,以表示解析失败的具体位置。如果要使用HTTP请求,可能需要定义一个额HTTPError类,这个类通过status属性保存请求失败对应的HTTP状态码(例如404或405)。
eg:

class HTTPError extends Error{
    constructor(status,statusText,url){
        super(`${status} ${statusText}: ${url}`);
        this.status=status;
        this.statusText=statusText;
        this.url=url;
    }
    
    get name(){
        return "HTTPError";
    }
}
let error=new HTTPError(404,"Not Found","http://example.com/");
error.status
=>404
error.message
=>"404 Not Found: http://example.com/"
error.name
=>"HTTPError"

11.6 JSON序列化与解析

当程序需要保存数据或需要通过网络连接向另一个程序传输数据时,必须将内存中的数据转换为字节或字符的序列,才可以保存或传输。而且,之后可以再被解析或恢复为原来内存中的数据结构。这个将数据结构转换为字节或字符流的方式称为序列化,也称编排或制备。

JavaScript中序列化数据的最简单方式是使用一种称为JSON的序列化格式。JSON是“JavaScript Object Notation”(JavaScript对象表示法)的简写形式。顾名思义,这种格式使用JavaScript对象和数组字面量语法,将对象和数组形式的数据结构转换为字符串。JSON支持原始数值和字符串,也支持true、false和null值,以及在这些原始值基础上构建起来的对象和数组。JSON不支持其他JavaScript类型,如Map、Set、RegExp、Date或定型数组。但不管怎么说,实践已经证明JSON是一种非常通用的数据格式,就连很多非JavaScript程序都支持它。

JavaScript通过两个函数——JSON.stringify()和JSON.parse()支持JSON序列化和反序列化。这两个函数在6.8节简单介绍过。如果一个对象或数组,不包含任何无法序列化的值(如RegExp对象或定型数组),都可以把它传给JSON.stringify()进行序列化。顾名思义,JSON.stringify()返回一个字符串值。而给定JSON.stringify()返回的字符串,可以把它传给JSON.parse()再重建原始的数据结构:

let o={s:"",n:0,a:[true,false,null]};
let s=JSON.stringify(o)
s
=>"{\"s\":\"\",\"n\":0,\"a\":[true,false,null]}"
let copy=JSON.parse(s)
copy
=>{s: "", n: 0, a: Array(3)}

如果不考虑序列化之后的数据保存到文件中,或者通过网络发送出去,可以使用这对函数(以没有那么高效的方式)创建对象的深度副本:

//创建任何可序列化对象或数组的深度副本
function deepcopy(o){
    return JSON.parse(JSON.stringify(o));
}

JavaScript权威指南 第11章JavaScript标准库_第13张图片
通常,我们只会给JSON.stringify()和JSON.parse()传一个参数。这两个函数其实都可以接收可选的第二个参数,让我们能够扩展JSON格式,下一节会详细介绍。JSON.stringify()还接收可选的第三个参数,我们这里先来讨论它,如果你希望JSON格式字符串对人类友好(比如要用作配置文件),那可以在第二个参数传null,第三个参数传一个数值或字符串。JSON.stringify()的第三个参数告诉它应该把数据格式化为多行缩进格式。如果第三个参数是个数值,则该数值表示每级缩进的空格数。如果第三个参数是空白符(如’\t’)字符串,则每级缩进就使用该字符串。

a: (3) [true, false, null]n: 0s: ""[[Prototype]]: Object
let o={s:"test",n:0};
JSON.stringify(o,null,2)
=>"{\n  \"s\": \"test\",\n  \"n\": 0\n}"
JSON.stringify(o,null,">>")
=>"{\n>>\"s\": \"test\",\n>>\"n\": 0\n}"
JSON.stringify(o,null,"\t")
=>"{\n\t\"s\": \"test\",\n\t\"n\": 0\n}"

JSON.parse()忽略空白符,因此给JSON.stringify()传第三个参数不会影响其输出的字符串再转换为原型的数据结构。

11.6.1 JSON自定义

如果JSON.stringify()在序列化时碰到了JSON格式原生不支持的值,它会查找这个值是否有toJSON()方法。如果有这个方法,就会调用它,然后将其返回值字符串化以代表原始值。Date对象实现了toJSON()方法,这个方法返回与toISOString()方法相同的值。这意味着如果序列化的对象中包含Date,则该日期会自动转换为一个字符串。而在解析序列化之后的字符串时,重新创建的数据结构就不会与开始时的完全一样了,因为原来的Date值变成了字符串。

如果想重新创建这个Date对象(或以其他方式修改解析后的对象),可以给JSON.parse()的第二个参数传一个“复合”函数。如果指定了这个“复活”函数,该函数就会在解析输入字符串中的每个原始值时被调用(但解析包含这些原始值的对象和数组时不会调用)。调用这个函数时会给它传入两个参数。第一个是属性名,可能是对象属性名,也可能是转换为字符串的数组索引。第二个参数是该对象属性或数组元素对应的原始值。而且,这个函数会作为包含上述原始值的对象或数组的方法调用,因此可以在其中通过this关键字引用该包含对象。

复活函数的返回值会变成命名属性的新值。如果复活函数返回它的第二个参数,那么属性保持不变。如果它返回undefined,则相应的命名属性会从对象或数组中删除,即JSON.parse()返回给用户的对象中将不包含该属性。

下面来看一个例子。这个例子调用JSON.parse()是传入了复活函数,用于过滤某些属性并重新创建Date对象:

let data=JSON.parse(text,function(key,value){
    //删除以下划线开头的属性和值
    if(key[0]==="_")
        return undefined;
    //如果值是ISO 8601格式的日期字符串,则转换为Date
    if(typeof value === "string"&&
       /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$/.test(value)){
        return new Date(value);
    }
    
    //否则,返回原始值
    return value;
});

除了使用前面提到的toJSON(),JSON.stringify()也支持给它传入一个数组或函数作为第二个参数来自定义其输出字符串。

如果第二个参数传入的是一个字符串数组(或者数值数组,其中的数值会转换为字符串),那么这些字符串会被当作对象属性(或数组元素)的名字。任何名字不在这个数组之列的属性会被字符串化过程忽略。而且,返回字符串中包含的属性的顺序也会与它们在数组中的顺序相同(这在编写测试时非常有用)。

如果给JSON.stringify()的第二个参数传入一个函数,则该函数就是一个替代函数(作用与传给JSON.parse()的可选的复活函数恰好相反)。这个替代函数的第一个参数是对象属性名或值在对象中的数组索引,第二个参数是值本身。这个替代函数会作为包含要被字符串化的值的对象或数组的方法调用。替代函数的返回值会替换原始值。如果替代函数返回undefined或什么也不返回,则该值(及其数组元素或对象属性)将在字符串过程中被忽略。

//指定要序列化的字段,以及序列化它们的顺序
let text=JSON.stringify(address,["city","state","country"]);

//指定替代函数,忽略值为RegExp的属性
let json=JSON.stringify(o,(k,v)=>v instanceof RegExp?undefind: v);

这里对JSON.stringify()的两次调用友好地使用了第二个参数,即产生地序列化输出在反序列化时不需要特殊的复活函数。但一般来说,如果为某个类型定义了toJSON()方法,或者使用替代函数将无法序列化的值变成了可序列化的值,应该都要写一个自定义的复活函数让JSON.parse()能够复原最初的数据结构。如果真的这样做了,那你应该知道自己是在自定义数据格式,因而也牺牲了可移植性以及庞大JSON工具、语言生态的兼容性。

11.7 国际化API

11.7.1 格式化数值

Intl.NumberForMat类定义了一个format()方法,考虑到上述所有格式化的可能性。这个构造函数接收两个参数,第一个参数指定作为数值格式化依据的地区,第二个参数是用于指定格式化细节的对象。如果第一个参数被忽略,或者传入的是undefined,则使用系统设置中的地区(假设该地区为用户偏好地区)。如果第一个参数是字符串,那它指定就是期望地区,例如“en-US”(“美国英语”)和“fr”(法语)。第一个参数也可以是一个地区字符串数组,此时Intl.NumberFormat会选择支持最好的一个。

如果指定Intl.NumberFormat()构造函数的第二个参数,则该函数应该是一个对象,且包含一个或多个下列属性。

style

指定必需的数值格式类型。默认为“decimal”,如果指定“percent”则按百分比格式化数值,指定“curreny”则表示数值为货币数量。

currency

如果style的值为“currency”,则这个属性是必需的,用于指定3个字母的ISO货币代码(如“USD”表示美元,“GBP”表示英镑)。

currencyDisplay

如果style的值为“currency”,则这个属性指定如何显示货币值。默认值为“symbol”,即如果货币有符号则使用货币符号。值“code”表示使用3个字母的ISO代码,值“name”表示以完整形式货币的名字。

useGrouping

如果不想让数值有千分位分隔符(或其他地区相关的样式),将这个属性设置为false。

minimumIntegerDigits

数值中最少显示几位整数。如果数值的位数小于这个值,则在左侧填补0.默认值是1,但最后可以设置为21。

minimunFractionDigits、maxmumFractionDigits

这两个属性控制数值小数部分的格式。如果数值的小数部分小于最小值,则在右侧填补0。如果大于最小值,则小数部分会被舍入。这两个属性的取值范围是0倒20.默认最小值为0,最大值为3,但格式化货币数量时是例外,此时小数部分的长度根据指定的货币会有所不同。

minimumSignificantDigits、maximumSignificantDigits

这两个属性控制数值中有效应的数量,比如让它们适合格式化科学依据。如果指定,这两个属性会覆盖前面列出的整数和小数属性。合法取值范围是1到21。

以期望的地区和选项创建了Intl.NumberFormat对象之后,可以把要格式化的数值传给这个对象的format()方法,该方法返回适当格式化之后的字符串。例如:

let euros=Intl.NumberFormat("es",{style:"currency",currency:"EUR"});
euros.format(10)
=>"10,00 €"
let pounds=Intl.NumberFormat("en",{style:"currency",currency:"GBP"})
=>pounds.format(1000)
"£1,000.00"

Intl.NumberFormat(及其它Intl类)有一个很有用的特性,即它的format()方法会绑定到自己所属的NumberFormat对象。因此,不需要定义变量引用这个格式化对象,然后再在上面调用format()方法,而是可以直接把这个format()方法赋值给一个变量,然后就像使用独立的函数一样使用它,比如:

let data=[0.05,.75,1];
let formatData=Intl.NumberFormat(undefined,{
    style:"percent",
    minimumFractionDigits:1,
    maximumFractionDigits:1
}).format;
data.map(formatData)
=>(3) ["5.0%", "75.0%", "100.0%"]

某些语言,比如阿拉伯语,使用自己的文字表示十进制数字:
在这里插入图片描述
其他语言,如印地语(北印度语)使用有自己数字符号的文字,但倾向于默认使用ASCII数字0~9。如果想覆盖这种用于数字的默认文字,可以在地区中加上-u-nu,后面跟上简写形式的文字名。比如,可以像下面这样使用印度风格的分组和梵文字母来格式化数值:

let hindi=Intl.NumberFormat("hi-IN-u-nu-deva").format;
hindi(1234567890)
=>"१,२३,४५,६७,८९०"

-u-在地区中表示后面是一个Unicode扩展。nu是记数制扩展的名字,deva则是梵文Devanagari的简写。Intl API标准也为其他一些计数制定义了名字,大多数这对南亚和东南亚的印欧语系。

11.7.2 格式化日期和时间

Intl.DateTimeFormat类与Intl.NumberFormat类很相似。Intl.DateTimeFormat()构造函数与Intl.DateTimeFormat()接收相同的两个参数:一个地区数组,另一个是格式化选项的对象。使用Intl.DateTimeFormat实例的方式也是调用其format()方法,将Date对象转换为字符串。

选项对象中的属性如下所示。只需为你像在格式化输出中看到的日期和时间字段指定属性即可。

year

年,使用“numeric”表示完整的4位数年份,或使用“2-digit”表示两位数简写形式。

month

月,使用“numeric”表示肯比较短的数字,如“1”,或使用“2-digit”表示始终使用2位数字,如“01”。使用“long”表示全名,如“January”,使用“short”表示简称,如“Jan”,而使用“narrow”表示高度简写的名字,如“J”,但不保证唯一。

day

日,使用“numeric”表示一位或2位数字,或使用“2-digit”表示两位数字。

weekday

周,使用“long”表示全名,如“Monday”,或使用“short”表示简称,如“Mon”,或使用“narrow”表示高度简写的名字,如“M”,但不保证唯一。

era

这个属性指定日期在格式化时是否考虑纪元,例如CE或BCE。这个属性在格式化很久以前的日期或者使用日文日历时有用。合法值为“short”“short”和“narrow”。

hour、minute、second

这几个属性指定如何显示时间。使用“numeric”表示一位或2位数字,使用“2-digit”表示强制一位数值在左侧填补0。

timeZone

这个属性指定格式化日期使用的时区。如果省略,则使用本地时区。实现可能始终以UTC时区为准,也可能以IANA(Internet Assigned Numbers Authority,因特网地址分配机构)的时区(如“American/Los_Angeles”)为准。

timeZoneName

这个属性指定在格式化的日期和时间中如何显示时区。使用"long"表示时区全称, 而"short"表示简写或数值形式的时区。

hour12

这是个布尔值属性,指定是否使用12小时制。默认值取决于地区设置,但可以使用这个属性来覆盖。

hourCycle

这个属性允许指定半夜12点是写为0时、12时还是24时。默认值取决于地区设备, 但可以使用这个属性来覆盖。注意hour12相比这个属性具有更高的优先级。使用 "h11"指定半夜12点是0时,而此前1小时是晚上11点。使用"hl2"指定半夜是12点。使用"h23”指定半夜是0时,而此前1小时是23时。最后,使用"h24”将 半夜指定为24时

下面是几个例子:

et d=new Date("2020-01-01T13:14:15Z");

//没有选项对象,就是基本的数值式日期格式
Intl.DateTimeFormat("en-US").format(d)
=>"1/1/2020"
Intl.DateTimeFormat("fr-FR").format(d)
=>"01/01/2020"

//周和月使用名字
let opts={weekday:"long",month:"long",year:"numeric",day:"numeric"};
Intl.DateTimeFormat("en-US",opts).format(d)
=>"Wednesday, January 1, 2020"
Intl.DateTimeFormat("es-ES",opts).format(d)
=>"miércoles, 1 de enero de 2020"

//纽约时间,但适合讲法语的加拿大人
opts={hour:"numeric",minute:"2-digit",timeZone:"America/New_York"};
{hour: "numeric", minute: "2-digit", timeZone: "America/New_York"}
Intl.DateTimeFormat("fr-CA",opts).format(d)
=>"8 h 14"

Intl.DateTimeFormat默认使用儒略历,但也看哟使用其他日历。虽然有些地区默认可能使用非儒略历,但可以在地区中添加-u-ca-后跟日期来明确指定要使用什么日历。可以使用的日历名包括“buddhist”“chinese”“coptic”“ethiopic”“gregory”“hebrew”“indian”“islamic”“iso8601”“japanese”和“persian”。继续前面的例子,可以使用各种非公立来确立年份。
JavaScript权威指南 第11章JavaScript标准库_第14张图片

11.7.3 比较字符串

按字母顺序对字符串排序(或者更通用的说法是对非字母文字“整理排序”)是一个经常会超出英语人士预想的问题。英语的字母表相对较少,没有重音字母,而且有字符编码的优势(ASCII,已经整合到Unicode中),其中数字值完全匹配英语标准的字符串排序习惯。对其他语言来说就没有那么简单了。比如,西班牙语就将n看成位于n后面、o前面的字母。立陶宛语字母的Y位于J前面,威尔士语将二合字母CH和DD当成一个字母,但CH在C后面,DD则在D后面。

如果想以自然的方式向用户显示字符串,只使用字符串数组的sort()方法是不够的。但如果创建一个Intl.Collator对象,可以将这个对象的compare()方法传给sort()方法,以执行适合当地的字符串排序。Intl.Collator对象可以配置让compare()方法执行不匹配大小写的比较,甚至只考虑本字母且忽略重音和其他变音符号的比较。

与Intl.NumberFormat()和Intl.DateTimeFormat()类似,Intl.Collator()构造函数也接收两个参数。第一个参数指定地区或地区数组,第二个参数是一个可选的对象,其属性指定具体执行哪种比较。以下是选项对象参数支持的属性。

usage

这个属性指定如何使用整理器(collator)对象,默认值为“sort”,但也可以指定为“search”。背后的思想是在排序字符串时,我们通常希望整理器区分尽可能多的字符串以产生可靠的排序。但在比较两个字符串时,某些地区可能想进行不那么严格大的比较,比如忽略重音。

sensitivity

这个属性指定整理器在比较字符串时是否区分字母大小写和重音。值“base”意味着比较时忽略大小写和重音,只考虑每个字符的基本字母(不过要注意,某些语言认为有的重读字符不同于基本字母)。“accent”在比较时考虑重音但忽略大小写。“case”考虑大小写但忽略重音。而“variant”执行严格的比较,既区分大小写也考虑重音。这个属性的默认值在usage是“sort”时是是“varient”。如果usage是“search”,则默认的大小写规则取决于地区。

ignorePunctuation

将这个属性设置为true以便在比较字符串时忽略空格和标点符号。比如,将这个属性设置为true时,字符串“any one”和“anyone”会被认为相等。

numeric

如果比较的内容是整数或包含整数,而你希望按照数值顺序而非字母顺序对它们进行排序,要将这个属性设置为true。设置这个选项后,字符串“Version9”会排在“Version10”前面。

caseFirst

这个属性指定的是大写字母还是小写字母应该排在前面。如果指定“upper”,则“A”会排在“a”前面。如果指定“lower”,则“a”会排在“A”前面。无论哪种形式优先,同一字母的大写变体和小写变体都会紧挨在一起,而不同于所有ASCII大写字母会位于ASCII小写字母之前的Unicode字典顺序(即Array的sort()方法的默认行为)。这个属性的默认值因地区而异,实现可能会忽略这个属性,不允许我们覆盖大小写排列的顺序。

在通过选项为目标地区创建Intl.Collator对象之后,可以使用它的compare()方法比较两个字符串。这个方法返回一个数值。如果返回的值小于0,则第一个字符串位于第二个字符串前面。如果返回的值大于0,则第一个字符串位于第二个字符串的后面。如果compa在这里插入代码片re()返回0,则说明整理器认为两个字符串相等。

compare()方法接收两个字符串参数,返回一个小于、等于或大于0的数值,这跟Array的sort()方法期待的可选参数和返回值特点完全一致。同样,Intl.Collator也会自动将compare()方法绑定到它的实例,因此可以直接把这个方法传给sort()而无须写包装函数再通过整理器调用它。

eg:

//按照用户地区排序的简单整理器
//千万不要像这个例子这样什么也不传就对人类可读的字符串进行排序
const collator=new Intl.Collator().compare;
["a","z","A","Z"].sort(collator);
=>(4) ["a", "A", "z", "Z"]

//文件名经常包含数值,因此需要进行特殊排序
const filenameOrder=new Intl.Collator(undefined,
                                      {numeric:true}).compare;
["page10","page9"].sort(filenameOrder)
=>(2) ["page9", "page10"]

//查找大致匹配目标字符串的所有字符
const fuzzyMatcher=new Intl.Collator(undefined,{
    sensitivity:"base",
    ignorePunctuation:true
}).compare;

let strings=["food","fool","Foo Bar"];
strings.findIndex(s=>fuzzyMatcher(s,"foobar")===0)
=>2

有些地区可能存在多种整理顺序。比如在德国,电话号码簿使用与早点顺序稍微不一样的字母发音排序。1994年以前在西班牙,“ch”和“ll”被当成两个字母,因此该国目前有一个现代排序和一个传统排序。而在中国,整理顺序可以基于字符的编码、字符的笔画或字符的拼音。这些不同的整理方式无法通过Intl.Collator的选项对象来指定,但可以通过给地区字符串添加-u-co-及期待的变体名字来指定。比如,在德国可以使用“de-DE-u-co-phonebk”来指定按字母发音排序。

const modernSpanish=Intl.Collator("en-ES").compare;
const traditionalSpanish=Intl.Collator("es-ES-u-co-trad").compare;
let palabras=["luz","llama","como","chico"];
undefined
palabras.sort(modernSpanish)
(4) ["chico", "como", "llama", "luz"]
palabras.sort(traditionalSpanish)
(4) ["como", "chico", "luz", "llama"]

11.8 控制台API

本书几乎随处可见console.log()函数。在浏览器中,console.log()会在开发者工具面板的“控制台”标签页中打印字符串。在Node中,console.log()是通用的输出函数,可以将其参数打印到进程的标准输出流,通常会作为程序输出显示在用户的终端窗口中。

除了console.log()之外,控制台API还定义了几个其他非常有用的函数。这个API并不是ECMAScript标准,但已经被浏览器和Node支持,并已经正式写入标准并通过WHATWG标准化。

控制台API定义了如下函数。

console.log()

这是最常用的控制太函数。它将参数转换为字符串并输出到控制台。它会在参数之间输出空格,并在输出所有参数后重新开始一行。

console.debug()、console.warn()、console.error()

这几个函数与console.log()几乎相同。在Node中,console.error()将其输出发生到标准错误流,而不是标准输出流。除此之外的其他函数都是console.log()的别名。在浏览器中你,这几个函数生成的输出信息前面可能会带一个图标,表示级别或严重程度。开发者控制台可能页支持开发中按照级别筛选控制台消息。

console.assert()

如果这个函数的第一个参数是真值(也就是断言通过),则这个函数什么也不做。但如果第一个参数是false或其他假值,则剩余参数被传给console.error()一样打印出来,且前面带一个“Assertion failed”前缀。注意,与典型的assert()函数不同,console.assert()不会在断言失败时抛出异常。

console.clear()

这个函数在可能的情况下清空控制台。在浏览器及Node中通过终端显示输出时,这个函数是有效的。如果Node的输出被重定向到文件或管道,则调用这个函数没有任何效果。

console.table()

这个函数有一个及其强大却鲜为人知的特性,即可以生成表列数据输出,这对于需要产生摘要数据的Node程序尤其有用。console.table()尝试以表列形式显示其参数(如果无法实现,则使用常规的console.log()格式)。如果参数是相对比较短的对象数组,而数组中的所有对象具有(不那么多的)相同属性时,使用这个函数效果最好。在这种情况下,数组中的每个对象的消息都会显示在表格的一行中,对象的每个属性就是表格的一列。也可以传入一个属性数组作为可选的第二个参数,已指定需要显示的列。
如果传入的是对象而非对象的数组,那么输出会用一列属性属性名,一列显示属性值。如果属性值本身也是对象,则它们的属性名会变成表格的列。

console.trace()

这个函数会像console.log()一样打印它的参数,此外在输出之后还会打印栈跟踪信息。在Node中,这个函数的输出会进入标准错误而不是标准输出。

console.count()

这个函数接收一个字符串参数,并打印该字符串,后面跟着已经通过该字符串调用 的次数。在调试事件处理程序时,如果需要知道事件处理程序被触发的次数,可以 使用这个函数。

console.countReset()

这个函数接收一个字符串参数,并重置针对该字符串的计数器。

console.group()

这个函数将它的参数像传给console.log()一样打印到控制台,然后设置控制台的内部状态,让所有后续的控制台消息(在一个次调用console.groupEnd()之前)相对刚刚打印的消息缩进。这样可以通过缩进从视觉上把相关消息分为一组。在浏览器中,开发者控制台通常支持分组后消息以组为单位折叠和扩展。console.group()的参数通常用于为分组提供解释性的名字。

console.groupCollapsed()

这个函数与console.group()类似,但在浏览器中分组会被“折叠”,因而其中包含的消息会被隐藏,除非用户点击扩展分组。在Node中,这个函数与console.group()是同义函数。

console.groupEnd()

这个函数没有参数,本身也没有输出,只用于结束由最近一次调用console.group()或console.groupCollapsed导致的缩进和分组。

console.time()

这个函数接收一个字符串参数,并记录该字符串调用自身时的时间,没有输出。

console.timelog()

这个函数接收一个字符串作为第一个参数。如果这个字符串之前传过给console.time(),那么它会打印该字符串及自上次调用console.time()之后经过的时间。如果还有额外的参数传给console.timeLog(),则这些参数会被传给console.log()一样打印出来。

console.timeEnd()

这个函数接收一个字符串参数。如果该参数之前传给过console.time(),则它打印该参数及经过的时间。在调用console.timeEnd()之后,如果不再调用console.time(),则调用console.timeLog()将是不合法的。

11.8.1 通过控制台格式化输出

像console.log()这样打印自己参数的控制台函数都有一个不太为人所知的特性:如果第一个参数是包含%s、%i、%d、%f、%o、%0或%c的字符串,则这个参数会被当成格式字符串,后续参数的值会被带入这个字符串,以取代这些两个字符的%序列。

这些序列的含义如下。

%s

这个参数会被转换为字符串。

%i和%d

这个参数会被转换为数值,然后截断为整数。

%f

这个参数会被转换为数值。

%o和%0

这个参数会被转换为对象,对象的属性名和值会显示出来(在浏览器中,显示结果通常是可以交互的,用户可以扩展和折叠属性以查看嵌套的数据结构)。%o和%0都会显示对象细节。但大写的变体使用实现决定的输出格式,即由实现决定什么格式对软件开发者最有用。

%c

在浏览器中,这个参数会被解释为CSS样式字符串,用于给后面的文本添加样式(直到下一个%c序列或字符串结束)。在Node中,%c序列及其对应的参数会被忽略。

注意,在使用控制台函数时,通常并不需要格式字符串。一般来说,只要把一个或多个值(包括对象)传给这些函数,由手写决定如何以有用的方式显示它们就可以了。比如,给console.log()传入一个Error对象,它会自动在打印输出中包含栈跟踪信息。

11.9 URL API

由于JavaScript多用于浏览器和服务器,因此JavaScript代码经常需要操作URL。URL类可以解析URL,同时允许修改已有的URL(如添加搜索参数或修改路径),还可以正确处理对不同URL组件的转义和反转义。

URL类并不是ECMAScript标准定义的,但Node和所有浏览器(除Internet Explorer)之外都实现了它。这个类是在WHATWG中标准化的。

使用URL()构造函数创建URL对象时,要传入一个绝对URL作为参数。也可以将一个相对URL作为第一个参数,将其相对的绝对URL作为第二个参数。创建了URL对象后,可以通过它的各种属性查询URL不同部分的非转义值:

let url=new URL("https://example.com:8000/path/name?q=term#fragment");
url.href
=>"https://example.com:8000/path/name?q=term#fragment"
url,origin
=>"https://blog.csdn.net"
url.protocol
=>"https:"
url.host
=>"example.com:8000"
url.port
=>"8000"
url.pathname
=>"/path/name"
url.search
=>"?q=term"
url.hash
=>"#fragment"

尽管并不常用,但URL可以包含用户名或者用户和密码,URL类也可以解析这些URL组件:

let url=new URL("ftp://admin:[email protected]/");
url.href
=>"ftp://admin:[email protected]/"
url.origin
=>"ftp://ftp.example.com"
url.username
=>"admin"
url.password
=>"1337!"

这里的origin属性就是URL协议和主机的组合(如果提供了端口,则也会包含在内),而且它是个只读属性。但前面例子中展示的其他属性是可读写属性,即可以通过设置这些属性来设置URL中对应的部分:

let url=new URL("https://example.com")   //创建服务
url.pathname="api/search";               //为这个API添加路径
url.search="q=test";                     //添加查询参数
url.toString()
=>"https://example.com/api/search?q=test"

URL类有一个重要特性,即它会在需要时正确地在URL中添加标点符号及转义特殊字符:

let url=new URL("https://example.com");
url.pathname="path with spaces";
url.search="q=foo#bar"
url.pathname
=>"/path%20with%20spaces"
url.search
=>"?q=foo%23bar"
url.href
=>"https://example.com/path%20with%20spaces?q=foo%23bar"

以上例子中地href属性比较特殊,读取href属性相当于调用toString(),即将URL的所有组合成一个字符串形式的正式URL。将href设置为一个新字符串会返回新字符串的URL解析器,就好像再次调用了URL()构造函数一样。

在前面的例子中,我们使用search属性引用URL中的整个查询部分。查询部分从一个问号开头到URL末尾或第一个#字符结束。有时候,把这个部分作为一个URL属性就足够了。但是,HTTP请求经常使用application/x-www-form-urlencoded格式将多个表单字段的值或多个API参数编码为URL的查询部分。在这个格式中,URL的查询部分以问号开头,然后是一个或多个由和号(&)分隔的名/值对。可以有多个相同的名字,此时该搜索参数就有多个值。

如果要把这种名/值对编码为URL的查询部分,那么searchParams属性比search属性更有用。search属性是一个可读写的字符串,通过它可以获取或设置URL的查询部分。searchParams属性则是一个对URLSearchParams对象的只读索引,而URLSearchParams对象具有获取、设置、添加、删除和排序参数(该参数编码为URL的查询部分)的API:

let url=new URL("https://example.com/search");
url.search
=>""
url.searchParams.append("q","term")
url.search
=>"?q=term"
url.searchParams.set("q","x")
url.search
=>"?q=x"
url.searchParams.get("q")
=>"x"
url.searchParams.has("q")
=?true
url.searchParams.has("p")
=>false
url.searchParams.append("opts","1")
url.search
=>"?q=x&opts=1"
url.searchParams.append("opts","&")
url.search
=>"?q=x&opts=1&opts=%26"
url.searchParams.get("opts")
=>"1"
url.searchParams.getAll("opts")
=>(2) ["1", "&"]
url.searchParams.sort();
url.search
=>"?opts=1&opts=%26&q=x"
url.searchParams.set("opts","y")
url.search
=>"?opts=y&q=x"
[...url.searchParams]
=>(2) [Array(2), Array(2)]
url.searchParams.delete("opts");
url.search
=>"?q=x"
url.href
=>"https://example.com/search?q=x"

11.9.1 遗留URL函数

在前面介绍的URL API标准化之前,JavaScript语言页曾多次尝试支持对URL的转义和反转义。第一次尝试定义全局的escape()和unescape()函数,这两个函数如今已经废弃,但仍然被广泛实现了。不应该再使用这两个函数了。

在废弃escape()和unescape()的同时,ECMAScript增加了两对替代性的全局函数。

encodeURI()和decodeURI()

encodeURI()接收一个字符串参数,返回一个新字符串,新字符串中非ASCII字符及某些ASCII字符(如空格)会被转义。decodeURI()正好正好相反。需要转义的字符首先会被转换为它们的UTF-8编码,然后再将该编码的每个字节替换为%xx转义序列,其中xx是两个十六进制数字。因为encodeURL()是要编码整个URL,所以不会转义URL分隔符(如/、?和#)。但这意味着encodeURL()不能正确地处理其组件中包含这些字符的URL。

encodeUEIComponent()和decodeURIComponent()

这对函数与encodeURI()和decodeURI()类似,只不过它们专门用于转义URL的单个组件,因此它们也会转义用于分隔URL组件的/、?和#字符。这两个函数是最有用的遗留URL函数,但要注意encodeURIComponet()也会转义路径名中的/字符,而这可能并不是我们想要的。另外它也会把查询参数中的空格转换为%20,而实际上查询参数中的空格应该被转义为+。

这些遗留函数的根本问题用于它们都在寻求把一种编码模式应用给URL的所有部分,而事实却是URL的不同部分使用的是不同的编码方案。如果想正确地格式化和编码URL,最简单的办法就是使用URL类完成所有URL相关的操作。

11.10 计时器

从JavaScript问世开始,浏览器就定义了两个函数:setTimeout()和setInterval()。利用这两个函数,程序可以让浏览器在指定的时间过后调用一个函数,或者每经过一定时间就重复调用一次某个函数。这两个函数至今没有被写进核心语言标准,但所有浏览器和Node都支持,属于JavaScript标准库的事实标准。

在这里插入图片描述

注意,setTimeout()并不会等到指定时间之后再返回。前面这3行代码都会立即运行并返回,只是未到1000毫秒时什么也不会发生。

如果省略传给setTimeout()的第二个参数,则该值默认为0.但这并不意味着你的函数会被立即调用,只意味着这个函数会被注册到某个地方,将被“尽可能快地”调用。如果浏览器由于处理用户输入或其他时间而没有空闲,那么调用这个函数的时机可能再10毫秒甚至更长时间以后。

setTimout()造成的函数只会被调用一次。有时候,这个函数本身会再次调用setTimeout(),以便将来某个时刻会再有一次调用。不过,要想重复调用某个函数,通常更简单的方式是使用setInterval()。setInterval()接收的参数与setTimeout()相同,但会导致每隔指定时间(同样是个近似的毫秒值)就调用一次指定函数。

setTimeout()和setInterval()都返回一个值。如果把这个值保存再变量中,之后可以把它传给clearTimeout()或clearInterval()以取消对函数的调用。在浏览器中,这个返回值通常是一个数值,而在Node中则是一个对象。具体什么类型不重要,只要把它当成一个不透明的值就行了。这个值的唯一作用就是可以把它传给claerTimeout()以取消使用setTimout()注册的函数调用(假设函数尚未调用),或者传给clearInterval()以取消对通过setInterval()注册的函数的重复调用。

JavaScript权威指南 第11章JavaScript标准库_第15张图片

你可能感兴趣的:(JavaScript,前端基础,JavaScript标准库,javascript)