第 5 章 基本引用类型
5.1 Date
let now = new Date()
在不给 Date 构造函数传参数的情况下,创建的对象将保存当前日期和时间。要基于其他日期和时间创建日期对象,必须传入其毫秒表示(UNIX 纪元 1970 年 1 月 1 日午夜之后的毫秒数)。ECMAScript 为此提供了两个辅助方法:Date.parse()
和 Date.UTC()
。
Date.parse()
方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。
❑ 月/日/年”,如"5/23/2019";
❑ “月名日,年”,如"May 23, 2019";
❑ “周几 月名 日 年 时:分:秒 时区”,如"Tue May 232019 00:00:00 GMT-0700";
❑ ISO 8601 扩展格式“YYYY-MM-DDTHH:mm:ss.sssZ”,如 2019-05-23T00:00:00(只适用于兼容 ES5 的实现)。
如果传给 Date.parse()的字符串并不表示日期,则该方法会返回 NaN。如果直接把表示日期的字符串传给 Date 构造函数,那么 Date 会在后台调用 Date.parse()
。
Date.UTC()
方法也返回日期的毫秒表示,但使用的是跟 Date.parse()不同的信息来生成这个值。传给 Date.UTC()
的参数是年、零起点月数(1 月是 0,2 月是 1,以此类推)、日(131)、时(023)、分、秒和毫秒。这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为 1 日。其他参数的默认值都是 0。下面是使用 Date.UTC()
的两个例子:
// GMT时间2000年1月1日零点
let y2k = new Date(Date.UTC(2000, 0))
// GMT时间2005年5月5日下午5 点55 分55 秒
let allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55))
与 Date.parse()
一样,Date.UTC()
也会被 Date 构造函数隐式调用,但有一个区别:这种情况下创建的是本地日期,不是 GMT 日期。
Date.now()
方法,返回表示方法执行时日期和时间的毫秒数。
5.1.1 继承的方法
Date 类型的 toLocaleString()
方法返回与浏览器运行的本地环境一致的日期和时间。这通常意味着格式中包含针对时间的 AM(上午)或 PM(下午),但不包含时区信息(具体格式可能因浏览器而不同)。toString()
方法通常返回带时区信息的日期和时间,而时间也是以 24 小时制(0~23)表示的
console.log(date.toLocaleString()) // '2024/12/4 10:19:44'
console.log(date.toString()) //'Wed Dec 04 2024 10:20:05 GMT+0800 (中国标准时间)'
Date 类型的 valueOf()
方法根本就不返回字符串,这个方法被重写后返回的是日期的毫秒表示。
console.log(date.valueOf()) // 1733278859535
5.1.2 日期格式化方法
Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串:
❑ toDateString()
显示日期中的周几、月、日、年(格式特定于实现);
❑ toTimeString()
显示日期中的时、分、秒和时区(格式特定于实现);
❑ toLocaleDateString()
显示日期中的周几、月、日、年(格式特定于实现和地区);
❑ toLocaleTimeString()
显示日期中的时、分、秒(格式特定于实现和地区);
❑ toUTCString()
显示完整的 UTC 日期(格式特定于实现)。
console.log(date.toDateString()) // 'Wed Dec 04 2024'
console.log(date.toTimeString()) // '10:23:48 GMT+0800 (中国标准时间)'
console.log(date.toLocaleDateString()) // '2024/12/4'
console.log(date.toLocaleTimeString()) // '10:24:06'
console.log(date.toUTCString()) // 'Wed, 04 Dec 2024 02:24:13 GMT'
5.1.3 日期/时间组件方法
const date = new Date()
// 返回日期的毫秒表示;与 valueOf() 相同
console.log(date.getTime()) // 1733279147381
// 设置日期的毫秒表示,从而修改整个日期
date.setTime(1733279147381)
console.log(date.toLocaleString()) // '2024/12/4 10:25:47'
//返回 4 位年数
console.log(date.getFullYear()) // 2024
// 返回 UTC 日期的 4 位年数
console.log(date.getUTCFullYear()) // 2024
// 设置 UTC 日期的年
date.setFullYear(2025)
console.log(date.toLocaleString()) // '2025/12/4 10:25:47'
// 返回日期的月(0 表示 1 月,11 表示 12 月)
console.log(date.getMonth()) // 11
// 返回 UTC 日期的月(0 表示 1 月,11 表示 12 月)
console.log(date.getUTCMonth()) // 11
// 设置日期的月(month 为大于等于 0 的数值,大于 11 加年)
date.setMonth(0)
console.log(date.toLocaleString()) // '2025/1/4 10:25:47'
// 返回日期中的日(1~31)
console.log(date.getDate()) // 4
// 返回 UTC 日期中的日(1~31)
console.log(date.getUTCDate()) // 4
// 设置日期中的日(如果date 大于该月的天数,则加月)
date.setDate(1)
console.log(date.toLocaleString()) // '2025/1/1 10:25:47'
// 设置 UTC 日期中的日(如果date 大于该月的天数,则加月)
date.setUTCDate(1)
console.log(date.toLocaleString()) // '2025/1/1 10:25:47'
// 返回日期中的星期几(0 表示星期日,6 表示星期六)
console.log(date.getDay()) // 3
// 返回 UTC 日期中的星期几(0 表示星期日,6 表示星期六)
console.log(date.getUTCDay()) // 3
// 返回日期中的时(0~23)
console.log(date.getHours()) // 10
// 返回 UTC 日期中的时(0~23)
console.log(date.getUTCHours()) // 2
// 设置日期中的时(如果 hours 大于 23,则加日)
date.setHours(11)
console.log(date.toLocaleString()) // '2025/1/1 11:25:47'
// 设置 UTC 日期中的时(如果 hours 大于 23,则加日)
date.setUTCHours(11)
console.log(date.toLocaleString()) // '2025/1/1 19:25:47'
// 返回日期中的分(0~59)
console.log(date.getMinutes()) // 25
// 返回 UTC 日期中的分(0~59)
console.log(date.getUTCMinutes()) // 25
// 设置日期中的分(如果 minutes 大于 59,则加时)
date.setMinutes(26)
console.log(date.toLocaleString()) // '2025/1/1 19:26:47'
// 设置 UTC 日期中的分(如果 minutes 大于 59,则加时)
date.setUTCMinutes(26)
console.log(date.toLocaleString()) // '2025/1/1 19:26:47'
// 返回日期中的秒(0~59)
console.log(date.getSeconds()) // 47
// 返回 UTC 日期中的秒(0~59)
console.log(date.getUTCSeconds()) // 47
// 设置日期中的秒(如果 seconds 大于 59,则加分)
date.setSeconds(48)
console.log(date.toLocaleString()) // '2025/1/1 19:26:48'
// 设置 UTC 日期中的秒(如果 seconds 大于 59,则加分)
date.setUTCSeconds(48)
console.log(date.toLocaleString()) // '2025/1/1 19:26:48'
// 返回日期中的毫秒(0~999)
console.log(date.getMilliseconds()) // 381
// 返回 UTC 日期中的毫秒(0~999)
console.log(date.getUTCMilliseconds()) // 381
// 设置日期中的毫秒(如果 milliseconds 大于 999,则加秒)
date.setMilliseconds(1)
console.log(date.toLocaleString()) // '2025/1/1 19:26
// 设置 UTC 日期中的毫秒(如果 milliseconds 大于 999,则加秒)
date.setUTCMilliseconds(1)
console.log(date.toLocaleString()) // '2025/1/1 19:26:48'
// 返回以分钟计的 UTC 与本地时区的偏移量
console.log(date.getTimezoneOffset()) // -480
5.2 RegExp
let expression = /pattern/flags;
// 匹配第一个"bat"或"cat",忽略大小写
let pattern1 = /[bc]at/i
// 跟pattern1 一样,只不过是用构造函数创建的
let pattern2 = new RegExp('[bc]at', 'i')
表示匹配模式的标记。
❑ g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。
❑ i:不区分大小写,表示在查找匹配时忽略 pattern 和字符串的大小写。
❑ m:多行模式,表示查找到一行文本末尾时会继续查找。
❑ y:粘附模式,表示只查找从 lastIndex 开始及之后的字符串。
❑ u: Unicode 模式,启用 Unicode 匹配。
❑ s:dotAll 模式,表示元字符.匹配任何字符(包括\n 或\r)。
所有元字符在模式中也必须转义,包括:
( [ { \ ^ $ | ) ] } ? * + .
5.2.1 RegExp 实例属性
每个 RegExp 实例都有下列属性,提供有关模式的各方面信息。
❑ global:布尔值,表示是否设置了 g 标记。
❑ ignoreCase:布尔值,表示是否设置了 i 标记。
❑ unicode:布尔值,表示是否设置了 u 标记。
❑ sticky:布尔值,表示是否设置了 y 标记。
❑ lastIndex:整数,表示在源字符串中下一次搜索的开始位置,始终从 0 开始。
❑ multiline:布尔值,表示是否设置了 m 标记。
❑ dotAll:布尔值,表示是否设置了 s 标记。
❑ source:正则表达式的字面量字符串(不是传给构造函数的模式字符串),没有开头和结尾的斜杠。
❑ flags:正则表达式的标记字符串。始终以字面量而非传入构造函数的字符串模式形式返回(没有前后斜杠)。
let pattern1 = /\[bc\]at/i
console.log(pattern1.global) // false
console.log(pattern1.ignoreCase) // true
console.log(pattern1.multiline) // false
console.log(pattern1.lastIndex) // 0
console.log(pattern1.source) // "\[bc\]at"
console.log(pattern1.flags) // "i"
let pattern2 = new RegExp('\\[bc\\]at', 'i')
console.log(pattern2.global) // false
console.log(pattern2.ignoreCase) // true
console.log(pattern2.multiline) // false
console.log(pattern2.lastIndex) // 0
console.log(pattern2.source) // "\[bc\]at"
console.log(pattern2.flags) // "i"
5.2.2 RegExp 实例方法
RegExp 实例的主要方法是 exec()
,主要用于配合捕获组使用。这个方法只接收一个参数,即要应用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回 null。返回的数组虽然是 Array 的实例,但包含两个额外的属性:index 和 input。index 是字符串中匹配模式的起始位置,input 是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串,其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素。来看下面的例子:
let text = 'mom and dad and baby'
let pattern = /mom( and dad( and baby)?)? /gi
let matches = pattern.exec(text)
console.log(matches) // [ 'mom and dad ', ' and dad', undefined, index: 0, input: 'mom and dad and baby', groups: undefined ]
console.log(matches.index) // 0
console.log(matches.input) // "mom and dad and baby"
console.log(matches[0]) // "mom and dad "
console.log(matches[1]) // " and dad"
console.log(matches[2]) // undefined
如果模式设置了全局标记,则每次调用 exec()方法会返回一个匹配的信息。如果没有设置全局标记,则无论对同一个字符串调用多少次 exec(),也只会返回第一个匹配的信息。
let text = 'cat, bat, sat, fat'
let pattern = /.at/
let matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 0
matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 0
lastIndex 在非全局模式下始终不变。
如果在这个模式上设置了 g 标记,则每次调用 exec()都会在字符串中向前搜索下一个匹配项,如下面的例子所示:
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(pattern.lastIndex) // 3
matches = pattern.exec(text)
console.log(matches.index) // 5
console.log(matches[0]) // bat
console.log(pattern.lastIndex) // 8
matches = pattern.exec(text)
console.log(matches.index) // 10
console.log(matches[0]) // sat
console.log(pattern.lastIndex) // 13
如果模式设置了粘附标记 y,则每次调用 exec()就只会在 lastIndex 的位置上寻找匹配项。粘附标记覆盖全局标记。
let text = 'cat, bat, sat, fat'
let pattern = /.at/y // 点号(.)在正则表达式中是一个特殊字符,它匹配除换行符之外的任何单个字符。
let matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 3
// 以索引3 对应的字符开头找不到匹配项,因此exec()返回null
// exec()没找到匹配项,于是将lastIndex设置为0
matches = pattern.exec(text)
console.log(matches) // null
console.log(pattern.lastIndex) // 0
// 向前设置lastIndex可以让粘附的模式通过exec()找到下一个匹配项:
pattern.lastIndex = 5
matches = pattern.exec(text)
console.log(matches.index) // 5
console.log(matches[0]) // bat
console.log(pattern.lastIndex) // 8
正则表达式的另一个方法是 test()
,接收一个字符串参数。如果输入的文本与模式匹配,则参数返回 true,否则返回 false。
let text = '000-00-0000'
let pattern = /\d{3}-\d{2}-\d{4}/
if (pattern.test(text)) {console.log('The pattern was matched.')
}
无论正则表达式是怎么创建的,继承的方法 toLocaleString()和 toString()都返回正则表达式的字面量表示。正则表达式的 valueOf()方法返回正则表达式本身。
let pattern = new RegExp('\\[bc\\]at', 'gi')
console.log(pattern.toString()) // /\[bc\]at/gi
console.log(pattern.toLocaleString()) // /\[bc\]at/gi
console.log(pattern.valueOf()) // /\[bc\]at/gi
5.2.3 RegExp 构造函数属性
RegExp 构造函数本身也有几个属性。(在其他语言中,这种属性被称为静态属性。)这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了 RegExp 构造函数的属性。
属性名 | 简写 | 描述 |
---|---|---|
input(已弃用) | $_ | 最后搜索的字符串(非标准特性) |
lastMatch(已弃用) | $& | 最后匹配的文本 |
lastParen(已弃用) | $+ | 最后匹配的捕获组(非标准特性) |
leftContext(已弃用) | $` | input 字符串中出现在 lastMatch 前面的文本 |
rightContext(已弃用) | $' | input 字符串中出现在 lastMatch 后面的文本 |
let text = 'this has been a short summer'
let pattern = /(.)hort/g
if (pattern.test(text)) {console.log(RegExp.input) // this has been a short summerconsole.log(RegExp.leftContext) // 'this has been a 'console.log(RegExp.rightContext) // ' summer'console.log(RegExp.lastMatch) // 'short'console.log(RegExp.lastParen) // 's'
}let text = 'this has been a short summer'
let pattern = /(.)hort/g
/** 注意:Opera不支持简写属性名* IE不支持多行匹配*/
if (pattern.test(text)) {console.log(RegExp.$_) // this has been a short summerconsole.log(RegExp['$`']) // 'this has been a 'console.log(RegExp["$'"]) // ' summer'console.log(RegExp['$&']) // 'short'console.log(RegExp['$+']) // 's'
}
5.2.4 模式局限
5.3 原始值包装类型
在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:
(1)创建一个 String 类型的实例;
(2)调用实例上的特定方法;
(3)销毁实例。
对布尔值和数值而言,以上 3 步也会在后台发生,只不过使用的是 Boolean 和 Number 包装类型而已。
引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法。
let s1 = 'some text'
s1.color = 'red'
console.log(s1.color) // undefined
在原始值包装类型的实例上调用 typeof 会返回"object",所有原始值包装对象都会转换为布尔值 true。
另外,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"
5.3.1 Boolean
Boolean 的实例会重写 valueOf()
方法,返回一个原始值 true 或 false。toString()
方法被调用时也会被覆盖,返回字符串"true"或"false"。
let falseObject = new Boolean(false)
let result = falseObject && true
console.log(result) // true
let falseValue = false
result = falseValue && true
console.log(result) // false
- typeof 操作符对原始值返回"boolean",但对引用值返回"object"
- Boolean 对象是 Boolean 类型的实例,在使用 instaceof 操作符时返回 true,但对原始值则返回 false
console.log(typeof falseObject) // 'object'
console.log(typeof falseValue) // 'boolean'
console.log(falseObject instanceof Boolean) // true
console.log(falseValue instanceof Boolean) // false
5.3.2 Number
Number 类型重写了 valueOf()
、toLocaleString()
和 toString()
方法。valueOf()
方法返回 Number 对象表示的原始数值,另外两个方法返回数值字符串。toString()
方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串
let num = 10
console.log(num.toString()) // "10"
console.log(num.toString(2)) // "1010"
console.log(num.toString(8)) // "12"
console.log(num.toString(10)) // "10"
console.log(num.toString(16)) // "a"
toFixed()
方法返回包含指定小数点位数的数值字符串
toExponential()
方法返回以科学记数法(也称为指数记数法)表示的数值字符串。与 toFixed()
一样,toExponential()
也接收一个参数,表示结果中小数的位数。来看下面的例子:
let num = 10
console.log(num.toExponential(1)) // "1.0e+1"
toPrecision()
方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。
let num = 99
console.log(num.toPrecision(1)) // "1e+2"
console.log(num.toPrecision(2)) // "99"
console.log(num.toPrecision(3)) // "99.0"
isInteger()方法与安全整数
ES6 新增了 Number.isInteger()
方法,用于辨别一个数值是否保存为整数。有时候,小数位的 0 可能会让人误以为数值是一个浮点值:
console.log(Number.isInteger(1)) // true
console.log(Number.isInteger(1.0)) // true
console.log(Number.isInteger(1.01)) // false
IEEE 754 数值格式有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。这个数值范围从 Number.MIN_SAFE_INTEGER
(-253+ 1)到 Number.MAX_SAFE_INTEGER
(253-1)。对超出这个范围的数值,即使尝试保存为整数,IEEE 754 编码格式也意味着二进制值可能会表示一个完全不同的数值。为了鉴别整数是否在这个范围内,可以使用 Number.isSafeInteger()
方法:
console.log(Number.isSafeInteger(-1 * 2 ** 53)) // false
console.log(Number.isSafeInteger(-1 * 2 ** 53 + 1)) // true
console.log(Number.isSafeInteger(2 ** 53)) // false
console.log(Number.isSafeInteger(2 ** 53 - 1)) // true
5.3.3 String
3 个继承的方法 valueOf()
、toLocaleString()
和 toString()
都返回对象的原始字符串值。
每个 String 对象都有一个 length 属性,表示字符串中字符的数量。即使字符串中包含双字节字符(而不是单字节的 ASCII 字符),也仍然会按单字符来计数。
1.JavaScript 字符
JavaScript 字符串由 16 位码元(code unit)组成。对多数字符来说,每 16 位码元对应一个字符。换句话说,字符串的 length 属性表示字符串包含多少 16 位码元。
charAt()
方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方法查找指定索引位置的 16 位码元,并返回该码元对应的字符:
let message = 'abcde'
console.log(message.charAt(2)) // "c"
使用 charCodeAt()
方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定。
let message = 'abcde'
// Unicode "Latin small letter C"的编码是U+0063
console.log(message.charCodeAt(2)) // 99
// 十进制99 等于十六进制63
console.log(99 === 0x63) // true
fromCharCode()
方法用于根据给定的 UTF-16 码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:
// Unicode "Latin small letter A"的编码是U+0061
// Unicode "Latin small letter B"的编码是U+0062
// Unicode "Latin small letter C"的编码是U+0063
// Unicode "Latin small letter D"的编码是U+0064
// Unicode "Latin small letter E"的编码是U+0065
console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65)) // "abcde"
// 0x0061 === 97
// 0x0062 === 98
// 0x0063 === 99
// 0x0064 === 100
// 0x0065 === 101
console.log(String.fromCharCode(97, 98, 99, 100, 101)) // "abcde"
为了表示更多的字符,Unicode 采用了一个策略,即每个字符使用另外 16 位去选择一个增补平面。这种每个字符使用两个 16 位码元的策略称为代理对。在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。
// "smiling face with smiling eyes" 表情符号的编码是U+1F60A
// 0x1F60A === 128522
let message = 'ab😊de'
console.log(message.length) // 6
console.log(message.charAt(1)) // 'b'
console.log(message.charAt(2)) // '�'
console.log(message.charAt(3)) // '�'
console.log(message.charAt(4)) // 'd'
console.log(message.charCodeAt(1)) // 98
console.log(message.charCodeAt(2)) // 55357
console.log(message.charCodeAt(3)) // 56877
console.log(message.charCodeAt(4)) // 100
console.log(String.fromCodePoint(0x1f60a)) // 😊
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)) // ab😊de
fromCharCode()方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个 Unicode 笑脸字符。
为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用 codePointAt()
来代替 charCodeAt()
。跟使用 charCodeAt()
时类似,codePointAt()
接收 16 位码元的索引并返回该索引位置上的码点(code point)。码点是 Unicode 中一个字符的完整标识。比如,"c"的码点是 0x0063,而"☺"的码点是 0x1F60A。码点可能是 16 位,也可能是 32 位,而 codePointAt()
方法可以从指定码元位置识别完整的码点。
let message = 'ab😊de'
console.log(message.codePointAt(1)) // 98
console.log(message.codePointAt(2)) // 128522
console.log(message.codePointAt(3)) //56842
console.log(message.codePointAt(4)) // 100
注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代理对的码点:
console.log([...'ab😊de']) // [ 'a', 'b', '😊', 'd', 'e' ]
与 charCodeAt()有对应的 codePointAt()一样,fromCharCode()也有一个对应的 fromCodePoint()。这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)) // 'ab😊de'
console.log(String.fromCodePoint(97, 98, 128522, 100, 101)) // 'ab😊de'
2.normalize()方法
// U+00C5:上面带圆圈的大写拉丁字母A
console.log(String.fromCharCode(0x00c5)) // Å
// U+212B:长度单位“埃”
console.log(String.fromCharCode(0x212b)) // Å
// U+004:大写拉丁字母A
// U+030A:上面加个圆圈
console.log(String.fromCharCode(0x0041, 0x030a)) // Å
比较操作符不在乎字符看起来是什么样的,因此这 3 个字符互不相等。
let a1 = String.fromCharCode(0x00c5),a2 = String.fromCharCode(0x212b),a3 = String.fromCharCode(0x0041, 0x030a)
console.log(a1, a2, a3) // Å, Å, Å
console.log(a1 === a2) // false
console.log(a1 === a3) // false
console.log(a2 === a3) // false
为解决这个问题,Unicode 提供了 4 种规范化形式,可以将类似上面的字符规范化为一致的格式,无论底层字符的代码是什么。这 4 种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、NFKD(Normalization Form KD)和 NFKC(Normalization Form KC)。可以使用 normalize()方法对字符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:“NFD”、“NFC”、“NFKD"或"NFKC”。
通过比较字符串与其调用 normalize()的返回值,就可以知道该字符串是否已经规范化了:
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
3.字符串操作方法
concat()
用于将一个或多个字符串拼接成一个新字符串,可以接收任意多个参数,原字符串不改变
let stringValue = 'hello '
let result = stringValue.concat('world', '!')
console.log(result) // "hello world!"
console.log(stringValue) // "hello"
ECMAScript 提供了 3 个从字符串中提取子字符串的方法:slice()
、substr()
和 substring()
。这 3 个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。对 slice()
和 substring()
而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。对 substr()
而言,第二个参数表示返回的子字符串数量。任何情况下,省略第二个参数都意味着提取到字符串末尾。与 concat()
方法一样,slice()
、substr()
和 substring()
也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。
substr()
已弃用
let stringValue = 'hello world'
console.log(stringValue.slice(3)) // "lo world"
console.log(stringValue.substring(3)) // "lo world"
console.log(stringValue.substr(3)) // "lo world"
console.log(stringValue.slice(3, 7)) // "lo w"
console.log(stringValue.substring(3, 7)) // "lo w"
console.log(stringValue.substr(3, 7)) // "lo worl"
当某个参数是负值时,这 3 个方法的行为又有不同。比如,slice()
方法将所有负值参数都当成字符串长度加上负参数值。而 substr()
方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为 0。substring()
方法会将所有负参数值都转换为 0。
let stringValue = 'hello world'
console.log(stringValue.slice(-3)) // "rld"
console.log(stringValue.substring(-3)) // "hello world"
console.log(stringValue.substr(-3)) // "rld"
console.log(stringValue.slice(3, -4)) // "lo w"
console.log(stringValue.substring(3, -4)) // "hel"
console.log(stringValue.substr(3, -4)) // "" (empty string)
4.字符串位置方法
有两个方法用于在字符串中定位子字符串:indexOf()
和 lastIndexOf()
。这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)。两者的区别在于,indexOf()
方法从字符串开头开始查找子字符串,而 lastIndexOf()
方法从字符串末尾开始查找子字符串。这两个方法都可以接收可选的第二个参数,表示开始搜索的位置
5.字符串包含方法
ECMAScript 6 增加了 3 个用于判断字符串中是否包含另一个字符串的方法:startsWith()
、endsWith()
和 includes()
。
startsWith()
和 includes()
方法接收可选的第二个参数,表示开始搜索的位置。
endsWith()
方法接收可选的第二个参数,表示应该当作字符串末尾的位置。
let message = 'foobarbaz'
console.log(message.startsWith('foo')) // true
console.log(message.startsWith('foo', 1)) // false
console.log(message.includes('bar')) // true
console.log(message.includes('bar', 4)) // false
console.log(message.endsWith('bar')) // false
console.log(message.endsWith('bar', 6)) // true
6.trim()方法
trim()
方法会创建字符串的一个副本(原始字符串不受影响),删除前、后所有空格符,再返回结果
trimLeft()
和 trimRight()
方法分别用于从字符串开始和末尾清理空格符。
7.repeat()方法
repeat()
方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果。
8.padStart()和 padEnd()方法
padStart()
和 padEnd()
方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格(U+0020)。
可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串。
let stringValue = 'foo'
console.log(stringValue.padStart(8, 'bar')) // "barbafoo"
console.log(stringValue.padStart(2)) // "foo"
console.log(stringValue.padEnd(8, 'bar')) // "foobarba"
console.log(stringValue.padEnd(2)) // "foo"
9.字符串迭代与解构
字符串的原型上暴露了一个@@iterator 方法,表示可以迭代字符串的每个字符。可以像下面这样手动使用迭代器:
let message = 'abc'
let stringIterator = message[Symbol.iterator]()
console.log(stringIterator.next()) // {value: "a", done: false}
console.log(stringIterator.next()) // {value: "b", done: false}
console.log(stringIterator.next()) // {value: "c", done: false}
console.log(stringIterator.next()) // {value: undefined, done: true}
在 for-of 循环中可以通过这个迭代器按序访问每个字符。有了这个迭代器之后,字符串就可以通过解构操作符来解构了。
10.字符串大小写转换
大小写转换包括 4 个方法:toLowerCase()
、toLocaleLowerCase()
、toUpperCase()
和 toLocaleUpperCase()
11.字符串模式匹配方法
String 类型专门为在字符串中实现模式匹配设计了几个方法。第一个就是 match()
方法,这个方法本质上跟 RegExp 对象的 exec()
方法相同。match()
方法接收一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象。
let text = 'cat, bat, sat, fat'
let pattern = /.at/
// 等价于pattern.exec(text)
let matches = text.match(pattern)
console.log(matches.index) // 0
console.log(matches[0]) // "cat"
console.log(pattern.lastIndex) // 0
另一个查找模式的字符串方法是 search()
。这个方法唯一的参数与 match()
方法一样:正则表达式字符串或 RegExp 对象。这个方法返回模式第一个匹配的位置索引,如果没找到则返回-1。search()
始终从字符串开头向后匹配模式。
let text = 'cat, bat, sat, fat'
let pos = text.search(/at/)
console.log(pos) // 1
为简化子字符串替换操作,ECMAScript 提供了 replace()
方法。这个方法接收两个参数,第一个参数可以是一个 RegExp 对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记,如下面的例子所示:
let text = 'cat, bat, sat, fat'
let result = text.replace('at', 'ond')
console.log(result) // "cond, bat, sat, fat"
result = text.replace(/at/g, 'ond')
console.log(result) // "cond, bond, sond, fond"
第二个参数是字符串的情况下,有几个特殊的字符序列,可以用来插入正则表达式操作的值。ECMA-262 中规定了下表中的值。
模式 | 插入值 |
---|---|
$$ | 插入一个 "$" 。 |
$& | 插入匹配的子字符串。 |
$` | 插入匹配子字符串之前的字符串片段。 |
$' | 插入匹配子字符串之后的字符串片段。 |
$n | 插入第 n (索引从 1 开始)个捕获组,其中 n 是小于 100 的正整数。 |
$ | 插入名称为 Name 的命名捕获组。 |
let text = 'cat, bat, sat, fat'
result = text.replace(/(.at)/g, 'word($1)')
console.log(result) //word(cat), word(bat), word(sat), word(fat)
replace()
的第二个参数可以是一个函数。在只有一个匹配项时,这个函数会收到 3 个参数:与整个模式匹配的字符串、匹配项在字符串中的开始位置,以及整个字符串。在有多个捕获组的情况下,每个匹配捕获组的字符串也会作为参数传给这个函数,但最后两个参数还是与整个模式匹配的开始位置和原始字符串。这个函数应该返回一个字符串,表示应该把匹配项替换成什么。
function htmlEscape(text) {return text.replace(/[<>"&]/g, function (match, pos, originalText) {switch (match) {case '<':return '< 'case '>':return '> 'case '&':return '& 'case '"':return '" '}})
}
console.log(htmlEscape('<p class="greeting">Hello world! </p>')) // "< p class=" greeting" > Hello world! </p>"
split()
方法会根据传入的分隔符将字符串拆分成数组。作为分隔符的参数可以是字符串,也可以是 RegExp 对象。(字符串分隔符不会被这个方法当成正则表达式。)还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。
let colorText = 'red, blue, green, yellow'
console.log(colorText.split(', ')) // [ 'red', 'blue', 'green', 'yellow' ]
console.log(colorText.split(', ', 2)) // [ 'red', 'blue' ]
console.log(colorText.split(/[^, ]+/)) // [ '', ', ', ', ', ', ', '' ]
12.localeCompare()方法
localeCompare()
方法比较两个字符串,返回如下 3 个值中的一个。
❑ 如果按照字母表顺序,字符串应该排在字符串参数前头,则返回负值。(通常是-1,具体还要看与实际值相关的实现。)
❑ 如果字符串与字符串参数相等,则返回 0。
❑ 如果按照字母表顺序,字符串应该排在字符串参数后头,则返回正值。(通常是 1,具体还要看与实际值相关的实现。)
let stringValue = 'yellow'
console.log(stringValue.localeCompare('brick')) // 1
console.log(stringValue.localeCompare('yellow')) // 0
console.log(stringValue.localeCompare('zoo')) // -1
13.HTML 方法
5.4 单例内置对象
5.4.1 Global
在全局作用域中定义的变量和函数都会变成 Global 对象的属性。
1.URL 编码方法
encodeURI()
和 encodeURIComponent()
方法用于编码统一资源标识符(URI),以便传给浏览器。有效的 URI 不能包含某些字符,比如空格。使用 URI 编码方法来编码 URI 可以让浏览器能够理解它们,同时又以特殊的 UTF-8 编码替换掉所有无效字符。
ecnodeURI()
方法用于对整个 URI 进行编码,比如"www.wrox.com/illegal value.js"。而 encodeURIComponent()
方法用于编码 URI 中单独的组件,比如前面 URL 中的"illegal value.js"。这两个方法的主要区别是,encodeURI()
不会编码属于 URL 组件的特殊字符,比如冒号、斜杠、问号、井号,而 encodeURIComponent()
会编码它发现的所有非标准字符。来看下面的例子:
let uri = 'http://www.wrox.com/illegal value.js#start'
console.log(encodeURI(uri)) // 'http://www.wrox.com/illegal%20value.js#start'
console.log(encodeURIComponent(uri)) // 'http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start'
与 encodeURI()
和 encodeURIComponent()
相对的是 decodeURI()
和 decodeURIComponent()
。
2.eval()方法
当解释器发现 eval()
调用时,会将参数解释为实际的 ECMAScript 语句,然后将其插入到该位置。通过 eval()
执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意味着定义在包含上下文中的变量可以在 eval()调用内部被引用,比如下面这个例子:
let msg = 'hello world'
eval('console.log(msg)') // "hello world"
通过 eval()
定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在 eval()
执行的时候才会被创建。
在严格模式下,在 eval()
内部创建的变量和函数无法被外部访问。同样,在严格模式下,赋值给 eval 也会导致错误。
3.Global 对象属性
4.window 对象
虽然 ECMA-262 没有规定直接访问 Global 对象的方式,但浏览器将 window 对象实现为 Global 对象的代理。因此,所有全局作用域中声明的变量和函数都变成了 window 的属性。
var color = 'red'
function sayColor() {console.log(window.color)
}
window.sayColor() // "red"
另一种获取 Global 对象的方式是使用如下的代码:
let global = (function () {return this
})()
5.4.2 Math
1.Math 对象属性
Math 对象有一些属性,主要用于保存数学中的一些特殊值。
属性 | 说明 |
---|---|
Math.E | 欧拉数,即自然对数的底数,约等于 2.718 |
Math.LN2 | 2 的自然对数,约等于 0.693。 |
Math.LN10 | 10 的自然对数,约等于 2.303。 |
Math.LOG2E | 以 2 为底的 E 的对数,约等于 1.443。 |
Math.LOG10E | 以 10 为底的 E 的对数,约等于 0.434。 |
Math.PI | 圆周率,一个圆的周长和直径之比,约等于 3.14159。 |
Math.SQRT1_2 | 二分之一 ½ 的平方根,同时也是 2 的平方根的倒数,约等于 0.707。 |
Math.SQRT2 | 2 的平方根,约等于 1.414。 |
2.min()和 max()方法
min()
和 max()
方法用于确定一组数值中的最小值和最大值
let max = Math.max(3, 54, 32, 16)
console.log(max) // 54
let min = Math.min(3, 54, 32, 16)
console.log(min) // 3
3.舍入方法
❑ Math.ceil()
方法始终向上舍入为最接近的整数。
❑ Math.floor()
方法始终向下舍入为最接近的整数。
❑ Math.round()
方法执行四舍五入。
❑ Math.fround()
方法返回数值最接近的单精度(32 位)浮点值表示。
4.random()方法
Math.random()
方法返回一个 0~1 范围内的随机数,其中包含 0 但不包含 1
Math.random()
方法在这里出于演示目的是没有问题的。如果是为了加密而需要生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用window.crypto.getRandomValues()
。
5.其他方法
方法 | 说明 |
---|---|
Math.abs(x) | 返回一个数的绝对值。 |
Math.acos(x) | 返回一个数的反余弦值。 |
Math.acosh(x) | 返回一个数的反双曲余弦值。 |
Math.asin(x) | 返回一个数的反正弦值。 |
Math.asinh(x) | 返回一个数的反双曲正弦值。 |
Math.atan(x) | 返回一个数的反正切值。 |
Math.atanh(x) | 返回一个数的反双曲正切值。 |
Math.atan2(y, x) | 返回 y/x 的反正切值。 |
Math.cbrt(x) | 返回一个数的立方根。 |
Math.clz32(x) | 返回一个 32 位整数的前导零的数量。 |
Math.cos(x) | 返回一个数的余弦值。 |
Math.cosh(x) | 返回一个数的双曲余弦值。 |
Math.exp(x) | 返回欧拉常数的参数次方,E^x,其中 x 为参数,E 是欧拉常数(2.718…,自然对数的底数)。 |
Math.expm1(x) | 返回 exp(x) - 1 的值。 |
Math.hypot([x[, y[, …]]]) | 返回其所有参数平方和的平方根。 |
Math.imul(x, y) | 返回 32 位整数乘法的结果。 |
Math.log(x) | 返回一个数的自然对数(㏒e,即 ㏑)。 |
Math.log1p(x) | 返回一个数加 1 的和的自然对数(㏒e,即 ㏑)。 |
Math.log10(x) | 返回一个数以 10 为底数的对数。 |
Math.log2(x) | 返回一个数以 2 为底数的对数。 |
Math.pow(x, y) | 返回一个数的 y 次幂 |
Math.round(x) | 返回四舍五入后的整数。 |
Math.sign(x) | 返回一个数的符号,得知一个数是正数、负数还是 0。 |
Math.sin(x) | 返回一个数的正弦值。 |
Math.sinh(x) | 返回一个数的双曲正弦值。 |
Math.sqrt(x) | 返回一个数的平方根。 |
Math.tan(x) | 返回一个数的正切值。 |
Math.tanh(x) | 返回一个数的双曲正切值。 |
Math.toSource() | 返回字符串 “Math”。 |
Math.trunc(x) | 返回一个数的整数部分,直接去除其小数点及之后的部分。 |