基本引用类型

前文有讲过,引用值是某个特定引用类型的实例。引用类型虽然有点像类,但跟类并不是一个概念。

对象被认为是某个特定引用类型的实例。新对象通过使用 new 操作符后跟一个构造函数来创建。

let now = new Date()

Date

ECMAScript 的 Date 类参考了 Java 的实现方式。使用 UTC 时间,从 1970-01-01 开始至今经过的毫秒数。

创建日期对象:let now = new Date() 在不传参的情况下,创建的对象将保存当前日期和时间。

要基于特定日期和时间创建日期对象,则需要传入其毫秒数表示。

ECMAScript 提供了两个辅助方法:Date.parse() 和 Date.UTC()。

Date.parse() 接受一个表示日期的字符串,尝试将这个字符串转换为表示该日期的毫秒数。

  • “月/日/年”
  • ”月名 日, 年“
  • “周几 月名 日 年 时:分:秒 时区”
  • “YYYY-MM-DDTHH:mm:ss.sssZ”
let someDate = new Date(Date.parse("May 31, 2021"))

直接使用字符串传给 Date 构造函数,那么 Date 会在后台调用 Date.parse()

let someDate = new Date("May 31, 2021")

以上两种方式得到的日期对象相同。

Date.UTC() 方法也返回日期的毫秒数表示,但接收的参数是 年、零起点约束、日、时、分、秒、毫秒。其中,年、月是必须的。

let y2k = new Date(Date.UTC(2021, 4))
// Sat May 01 2021 08:00:00 GMT+0800 (中国标准时间)

let y2k_1 = new Date(2021, 4)
// Sat May 01 2021 00:00:00 GMT+0800 (中国标准时间)

Date.UTC() 隐式调用时,创建的是本地日期,不是GMT日期。

Date.now() 返回执行时日期和时间的毫秒数。

继承的方法

Date 类型重写了 toLocaleString()、toString()、valueOf() 方法。

toLocaleString() 方法返回与浏览器运行的本地环境一致的日期和时间。包含时间的 AM 或 PM,但不包含时区信息。

toString() 方法通常返回带时区信息的日期和时间,24小时制。

valueOf() 方法根本就不返回字符串,返回的是日期的毫秒数表示。因此可以直接使用它返回的值。

let date1 = new Date(2021, 0, 1)
let date2 = new Date(2021, 1, 1)

console.log(date1 < date2) // true
console.log(date1 > date2) // false

日期格式化方法

Date 类型有几个专门用于格式化日期的方法,都返回字符串

  • toDateString() 周几、月、日、年
  • toTimeString() 时、分、秒和时区
  • toLocaleDateString() 周几、月、日、年
  • toLocaleTimeString() 时、分、秒
  • toUTCString() 完整的 UTC 日期

日期/时间组件方法

方法 说明
getTime() 返回日期的毫秒数表示,与 valueOf() 相同
getFullYear() 返回4位数年
getMonth() 返回日期的月
getDate() 返回日期中的日
getDay() 返回周几的数值
getHours() 返回日期中的时
getMinutes() 返回日期中的分
getSeconds() 返回日期中的秒
getMilliseconds() 返回日期中的毫秒
getTimezoneOffset() 返回以分钟计的 UTC 与本地时区的偏移量。

RegExp

ECMAScript 通过 RegExp 类型支持正则表达式。

let expression = /pattern/flags

pattern - 模式,可以是任何简单或复杂的正则表达式,包括字符类、限定符、分组、向前查找和反向引用。

flags - 标记,用于控制正则表达式的行为。

  • g:全局模式,查找符合的全部内容
  • i:不区分大小写
  • m:多行模式,查找到一行文本末尾时会继续查找
  • y:粘附模式,只查找从 lastIndex 开始及之后的字符串
  • u:Unicode 模式,启用 Unicode 匹配
  • s:dotAll 模式,表示元字符.匹配任何字符(包括\n或\r)

RegExp 实例属性

  • global:布尔值,表示是否设置了 g 标记
  • ignoreCase:布尔值,表示是否设置了 i 标记
  • unicode:布尔值,表示是否设置了 u 标记
  • sticky:布尔值,表示是否设置了 y 标记
  • multiline:布尔值,表示是否设置了 m 标记
  • dotAll:布尔值,表示是否设置了 s 标记
  • lastIndex:整数,表示在源字符串钟下一次搜索的开始位置,始终从0开始
  • source:正则表达式的字面量字符串,没有开头和结尾的斜杠
  • flags:正则表达式的标记字符串。始终以字面量而非传入构造函数的字符串模式形式返回。

RegExp 实例方法

主要方法:exec(),主要用于配合捕获组使用。

exec():接受一个参数,即要应用模式的字符串。返回包含第一个匹配信息的数组 或 null。返回的数组信息虽然是 Array 的实例,但包含两个额外属性:index 和 input。

index 是字符串中匹配模式的起始位置,input 是要查找的字符串。

非全局模式下,lastIndex 始终不变。全局模式下,每次调用 exec() 都会在字符串中向前搜索下一个匹配项,直到搜索到字符串末尾。全局匹配模式下,每次调用 exec() 都会更新 lastIndex 值,以反映上次匹配的最后一个字符的索引。

let text = "cat, bat, sat, fat"
let pattern = /.at/g

let matches = pattern.exec(text)
console.log(matches.index)      // 0
console.log(matches[0])         // cat
console.log(matches.lastIndex)  // 3

matches = pattern.exec(text)
console.log(matches.index)      // 5
console.log(matches[0])         // bat
console.log(matches.lastIndex)  // 8

matches = pattern.exec(text)
console.log(matches.index)      // 10
console.log(matches[0])         // sat
console.log(matches.lastIndex)  // 13

如果设置了粘附标记 y,则每次调用 exec() 就只会在 lastIndex 的位置上寻找匹配项。粘附标记会覆盖全局标记。

test() 方法,接受一个字符串参数,如果输入的文本与模式匹配,则参数返回 true,否则返回 false。这个方法适用于只想测试模式是否匹配,不需要实际匹配内容得情况。

继承的方法 toLocaleString() 和 toString() 返回的都是其字面量的形式。valueOf() 返回正则表达式本身。

let pattern = new RegExp("\\[bc\\]at", "gi")
pattern.valueOf()
// /\[bc\]at/gi
pattern.toString()
// "/\\[bc\\]at/gi"
pattern.toLocaleString()
// "/\\[bc\\]at/gi"

RegExp 构造函数属性

RegExp 构造函数本身具有几个属性。

全名 简写 说明
input $_ 最后搜索的字符串
lastMatch $& 最后匹配的文本
lastParen $+ 最后匹配的捕获组
leftContext $` input 字符串中出现 lastMatch 前面的文本
rightContext $' input 字符串出现在 lastMatch 后面的文本

RegExp 构造函数的属性不要在生产环境中使用。

原始值包装类型

ECMAScript 提供了三种特殊的应用类型:Boolean、Number和String。

每当调用原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。

let s1 = "some text"
let s2 = s1.substring(2)

第二行访问 s1 时,是以读模式访问的,以读模式访问字符串值的任何时候,后台都会执行如下3步:

  1. 创建一个String类型的实例
  2. 调用实例上的方法
  3. 销毁实例

即,第二行代码可以想象的执行了如下三行 ECMAScript 代码:

let s1 = new String("some text")
let s2 = s1.substring(2)
s1 = null

对于布尔值和数值而言,以上3步也会发生在后台,不过使用的分别是 Boolean 和 Number 包装类型而已。

引用类型和原始值包装类型的主要区别在于生命周期。通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而主动创建的原始值包装类型则只存在于访问它的那行代码执行期间。所以不能给原始值包装类型添加属性和方法。因为它会在执行添加后,自动销毁。

let s1 = "some text"
s1.color = 'red'
console.log(s1.color) // undefined

原始值包装类型调用 typeof 会返回 ‘object’。

Object 构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。

let obj = new Object("some text")
console.log(obj instanceof String) // true

注意,使用 new 调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。

let value = '25'
let number = Number(value) // 转型函数
console.log(typeof number) // 'number'
let obj = new Number(value) // 构造函数
console.log(typeof obj)    // 'object'

Boolean

Boolean 是对应布尔值的引用类型。要创建一个 Boolean 对象,就是用 Boolean 构造函数并传入 true 或 false。

let booleanObject = new Boolean(true)

Boolean 实例会重写 valueOf() 方法,返回一个原始值 true 或 false。toString() 方法被调用时也会被覆盖,返回字符串 “true” 或 “false”。

Boolean 对象 在 ECMAScript 中使用时,容易引起误会。

let falseObject = new Boolean(false)
let result = falseObject && true
console.log(result) // true

let falseValue = false
result = falseValue && true
console.log(result) // false

原因,所有的对象在布尔表达式中都表示为 true 值。

理解原始布尔值和Boolean对象之间的区别,强烈建议永远不要使用后者。

Number

Number 类型重写了 valueOf()、toLocaleString()、toString() 方法。

valueOf() 返回Number对象表示的原始数值,另外两个方法返回字符串。

toString() 还可以接受一个可选的参数,表示基数,并返回相应基数形式的数值字符串。

toFixed() 返回包含指定小数点位数的数值字符串。通常支持0~20个小数位。

toExponential() 返回科学计数法。接受一个参数,表示结果中小数的位数。

toPrecision() 根据情况返回最合理的输出结果。接受一个参数,表示结果中数字的总位数。通常支持1~21个小数位。

isinteger() 方法,用于辨别一个数值是否保存为整数。

Number.isInteger(1)    // true
Number.isInteger(1.00) // true
Number.isInteger(1.01) // false

IEEE 754 数据格式 有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。Number.MIN_SAFE_INTEGER($\ce{-2^53 + 1}$)到 Number.MAX_SAFE_INTEGER($\ce{2^53 - 1}$)范围内的数据,可以使用 Number.isSafeInteger() 方法鉴别。

String

String 是对应字符串的引用类型。集成的三个方法 valueOf()、toLocaleString()、toString() 都返回对象的原始字符串值。

length 属性,表示字符串中字符的数量。

JavaScript 字符串是由 16 位码元组成。对多数字符来说,每 16 位码元对应一个字符。换句话说,length 属性表示字符串包含多少16位码元。

charAt() 方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,查找指定索引位置的 16 位码元,并返回该码元对应的字符。

JavaScript 字符串使用了两种 Unicode 编码混合的策略: UCS-2 和 UTF-16。

charCodeAt() 方法可以查看指定码元的字符编码。返回指定索引位置的码元值,索引以整数指定。

fromCharCode() 方法根据给定的 UTF-16 码元创建字符串中的字符。接受任意多个数值,并返回所有数值对应的字符拼接起来的字符串。

console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65))
// "abcde"

对于 U+0000~U+FFFF 范围内的字符,length、charAt()、charCodeAt()、fromCharCode() 返回的结果都是跟预期一样的。但是在扩展到 Unicode 增补字符平面时就不成立了。16位只能表示 65536 个字符,为了表示更多字符,Unicode 采用了一个策略,即每个字符使用另外16位去选择一个增补平面。这种每个字符使用两个16位码元的策略称为代理对。

涉及增补平面的字符时,之前讨论的方法就会出现问题。

为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用 codePointAt() 来代替 charCodeAt()。

可以使用遍历的方式,来迭代字符串,识别对应的码点。

console.log([..."ab☺de"])
// ['a', 'b', '☺', 'c', 'd']

可以使用 fromCodePoint() 来代替 fromCharCode()。

某些 Unicode 字符有多种编码方式。有的字符可以通过一个 BMP 字符表示,也可以使用一个代理对表示。但是比较操作符会认为他们各不相等。

为了解决这个问题,Unicode 提供了4种规范化形式:

  • NFD
  • NFC
  • NFKD
  • NFKC

可以使用 normalize() 方法对字符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:“NFD”、“NFC”、“NFKD”、”NFDC“。

let a1 = String.fromCharCode(0x00C5),
    a2 = String.fromCharCode(0x212B),
    a3 = String.fromCharCode(0x0041, 0x030A);
// U+00C5 是对 0+212B 进行 NFC/NFKC 规范化之后的结果
console.log(a1 === a1.normalize("NFD"))
// false
console.log(a1 === a1.normalize("NFC"))
// true
console.log(a1 === a1.normalize("NFKD"))
// false
console.log(a1 === a1.normalize("NFKC"))
// true

// U+212B 是未规范化的
console.log(a2 === a2.normalize("NFD"))
// false
console.log(a2 === a2.normalize("NFC"))
// false
console.log(a2 === a2.normalize("NFKD"))
// false
console.log(a2 === a2.normalize("NFKC"))
// false

// U+0041/U+030A 是对 0+212B 进行 NFD/NFKD 规范化之后的结果
console.log(a3 === a3.normalize("NFD"))
// true
console.log(a3 === a3.normalize("NFC"))
// false
console.log(a3 === a3.normalize("NFKD"))
// true
console.log(a3 === a3.normalize("NFKC"))
// false

选择同一种规范化形式可以让比较操作符返回正确的结果:

let a1 = String.fromCharCode(0x00C5),
    a2 = String.fromCharCode(0x212B),
    a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1.normalize("NFD") === a2.normalize("NFD"))
// true
console.log(a2.normalize("NFKC") === a3.normalize("NFKC"))
// true
console.log(a1.normalize("NFC") === a3.normalize("NFC"))
// true