基本引用类型
笔记
引用类型与类是不等同的概念。
引用值(或者对象)是某个特定引用类型的实例。在ECMAScript 中,引用类型是把数据和功能组织到一起的结构,经常被人错误地称作“类”。虽然从技术上讲JavaScript 是一门面向对象语言,但ECMAScript 缺少传统的面向对象编程语言所具备的某些基本结构,包括类和接口。引用类型有时候也被称为对象定义,因为它们描述了自己的对象应有的属性和方法。
引用类型虽然有点像类,但跟类并不是一个概念。为避免混淆,本章后面不会使用术语“类”。
函数也是一种引用类型。
# Date
# Date.UTC() 和 Date.parse()
Date.parse()方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。
Date.UTC 方法返回自 1970 年 1 月 1 日 00:00:00 UTC(协调世界时)以来的毫秒数。这个方法接受年、月、日、小时、分钟、秒和毫秒作为参数,并返回一个表示该日期和时间的毫秒数。
ECMAScript 还提供了Date.now()方法,返回表示方法执行时日期和时间的毫秒数。这个方法可以方便地用在代码分析中。
提示
Date 引用类型的 toLocaleString() toString() ValueOf()在不同的浏览器表现不同,需要注意。
# RegExp
正则表达式使用类似 Perl 的简洁语法进行创建
let expression = /pattern/flags
这个正则表达式的pattern(模式)可以是任何简单或复杂的正则表达式,包括字符类、限定符、分组、向前查找和反向引用。每个正则表达式可以带零个或多个flags(标记),用于控制正则表达式的行为。
# 表示匹配模式的标记
- g:全局模式,表示查找字符串的全部内容,而不是在找到第一个匹配项时停止。
- i:不区分大小写,表示在查找匹配项时忽略pattern和字符串的大小写。
- m:多行模式,表示查找到一行文本末尾时会继续查找下一行。
- y: 粘附模式,表示只查找从 lastIndex 开始及之后的字符串
- u: Unicode 模式,表示使用 Unicode 来匹配字符
- s: dotAll 模式,表示元字符.匹配任何字符,包括\n和\r
# 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"
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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.index); // 0
console.log(matches.input); // "mom and dad and baby"
console.log(matches[0]); // "mom and dad and baby"
console.log(matches[1]); // " and dad and baby"
console.log(matches[2]); // " and baby"
2
3
4
5
6
7
8
在这个例子中,模式包含两个捕获组:最内部的匹配项" and baby",以及外部的匹配项" and dad"或" and dad and baby"。调用exec()后找到了一个匹配项。因为整个字符串匹配模式,所以matchs数组的index 属性就是0。数组的第一个元素是匹配的整个字符串,第二个元素是匹配第一个捕获组的字符串,第三个元素是匹配第二个捕获组的字符串。
如果模式设置了全局标记,则每次调用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
2
3
4
5
6
7
8
9
10
这次模式设置了全局标记,因此每次调用exec()都会返回字符串中的下一个匹配项,直到搜索到字符串末尾。注意模式的lastIndex 属性每次都会变化。在全局匹配模式下,每次调用exec()都会更新lastIndex 值,以反映上次匹配的最后一个字符的索引。
如果模式设置了粘附标记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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
正则表达式的另一个方法是test(),接收一个字符串参数。如果输入的文本与模式匹配,则参数 返回true,否则返回false。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。 test()经常用在if 语句中。
# 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 summer
console.log(RegExp.leftContext); // this has been a
console.log(RegExp.rightContext); // summer
console.log(RegExp.lastMatch); // short
console.log(RegExp.lastParen); // s
console.log(RegExp.$_); // this has been a short summer
console.log(RegExp["$`"]); // this has been a
console.log(RegExp["$'"]); // summer
console.log(RegExp["$&"]); // short
console.log(RegExp["$+"]); // s
}
2
3
4
5
6
7
8
9
10
11
12
13
14
RegExp 还有其他几个构造函数属性,可以存储最多9 个捕获组的匹配项。这些属性通过RegExp.$1~RegExp.$9 来访问,分别包含第1~9 个捕获组的匹配项。在调用exec()或test()时,这些属性就会被填充,然后就可以像下面这样使用它们:
let text = "this has been a short summer";
let pattern = /(..)or(.)/g;
if (pattern.test(text)) {
console.log(RegExp.$1); // sh
console.log(RegExp.$2); // t
}
2
3
4
5
6
# 模式局限
笔记
虽然ECMAScript 对正则表达式的支持有了长足的进步,但仍然缺少Perl 语言中的一些高级特性。下列特性目前还没有得到ECMAScript 的支持(想要了解更多信息,可以参考Regular-Expressions.info网站)
- \A 和\Z 锚(分别匹配字符串的开始和末尾)
- 联合及交叉类
- 原子组
- x(忽略空格)匹配模式
- 条件式匹配
- 正则表达式注释 虽然还有这些局限,但ECMAScript 的正则表达式已经非常强大,可以用于大多数模式匹配任务。
# 原始值包装类型
# String
对于U+0000~U+FFFF 范围内的字符,length、charAt()、charCodeAt()和fromCharCode()返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用16 位表示的,而这几个方法也都基于16 位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。
这个对应关系在扩展到Unicode 增补字符平面时就不成立了。问题很简单,即16 位只能唯一表示65 536 个字符。这对于大多数语言字符集是足够了,在Unicode 中称为基本多语言平面(BMP)。**为了表示更多的字符,Unicode 采用了一个策略,即每个字符使用另外16 位去选择一个增补平面。**这种每个字符使用两个16 位码元的策略称为代理对。
# normalize() 方法
某些Unicode 字符可以有多种编码方式。有的字符既可以通过一个BMP 字符表示,也可以通过一个代理对表示。比如:
// 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)); // Å
2
3
4
5
6
7
比较操作符不在乎字符看起来是什么样的,因此这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
2
3
4
5
6
7
为解决这个问题,Unicode 提供了4 种规范化形式,可以将类似上面的字符规范化为一致的格式,无论底层字符的代码是什么。这4 种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、NFKD(Normalization Form KD)和NFKC(Normalization Form KC)。可以使用normalize()方法对字符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:"NFD"、"NFC"、"NFKD"或"NFKC"。
提示
这4 种规范化形式的具体细节超出了本书范围,有兴趣的读者可以自行参考UAX15#: Unicode Normalization Forms 中的1.2 节“Normalization Forms”。
通过比较字符串与其调用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
选择同一种规范化形式可以让比较操作符返回正确的结果:
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
2
3
4
5
6
# 字符串迭代与解构
字符串的原型上暴露了一个@@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}
2
3
4
5
6
在for-of 循环中可以通过这个迭代器按序访问每个字符:
for (const c of "abcde") {
console.log(c);
}
// a
// b
// c
// d
// e
2
3
4
5
6
7
8
# 字符串模式匹配方法
为简化子字符串替换操作,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"
2
3
4
5
字符序列 替换文本
$$
😒
$&
:匹配整个模式的子字符串。与RegExp.lastMatch 相同
$'
:匹配的子字符串之前的字符串。与RegExp.rightContext 相同
$\``:匹配的子字符串之后的字符串。与RegExp.leftContext 相同
$n:匹配第n 个捕获组的字符串,其中n 是0~9。比如,$1 是匹配第一个捕获组的字符串,$2 是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串
$nn`:匹配第nn 个捕获组字符串,其中nn 是01~99。比如,$01 是匹配第一个捕获组的字符串,$02 是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串
使用这些特殊的序列,可以在替换文本中使用之前匹配的内容,如下面的例子所示:
let text = "cat, bat, sat, fat";
result = text.replace(/(.at)/g, "word ($1)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)
2
3
这里,每个以"at"结尾的词都会被替换成"word"后跟一对小括号,其中包含捕获组匹配的内容$1。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>"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# localeCompare
方法
localeCompare()的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字符串。在美国,英语是ECMAScript 实现的标准语言,localeCompare()区分大小写,大写字母排在小写字母前面。但其他地区未必是这种情况。
# 单例内置对象
# Global 对象
Global 对象是ECMAScript 中最特别的对象,因为代码不会显式地访问它。ECMA-262 规定Global对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成Global 对象的属性 。本书前面介绍的函数,包括isNaN()、isFinite()、parseInt()和parseFloat(),实际上都是Global 对象的方法。除了这些,Global 对象上还有另外一些方法。
# URL 编码方法
encodeURI()和encodeURIComponent()方法用于编码统一资源标识符(URI),以便传给浏览器。有效的URI 不能包含某些字符,比如空格。使用URI 编码方法来编码URI 可以让浏览器能够理解它们,同时又以特殊的UTF-8 编码替换掉所有无效字符。
这两个方法的主要区别是,encodeURI()不会编码属于URL 组件的特殊字符,比如冒号、斜杠、问号、井号,而encodeURIComponent()会编码它发现的所有非标准字符。
与encodeURI()和encodeURIComponent()相对的是decodeURI()和decodeURIComponent()。decodeURI()只对使用encodeURI()编码过的字符解码。例如,%20 会被替换为空格,但%23 不会被替换为井号(#),因为井号不是由encodeURI()替换的。类似地,decodeURIComponent()解码所有被encodeURIComponent()编码的字符,基本上就是解码所有特殊值。
# eval
eval方法就是一个完整的ECMAScript 解释器,它接收一个参数,即一个要执行的ECMAScript(JavaScript)字符串。
eval("console.log('hi')");
等价
console.log("hi");
注意
注意 解释代码字符串的能力是非常强大的,但也非常危险。在使用eval()的时候必须极为慎重,特别是在解释用户输入的内容时。因为这个方法会对XSS 利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。