diff --git a/9.md b/9.md index 5289bf8..e986801 100644 --- a/9.md +++ b/9.md @@ -218,7 +218,7 @@ JavaScript中约定是:使用从0开始的数字表示月份(因此使用11 构造函数的后四个参数(小时、分钟、秒、毫秒)是可选的,如果用户没有指定这些参数,则参数的值默认为0。 -时间戳存储为 UTC 时区中 1970 年以来的毫秒数。 这遵循一个由“Unix 时间”设定的约定,该约定是在那个时候发明的。 您可以对 1970 年以前的时间使用负数。 日期对象上的`getTime`方法返回这个数字。 你可以想象它会很大。 +时间戳存储为 UTC 时区中 1970 年以来的毫秒数。 这遵循一个由“Unix 时间”设定的约定,该约定是在那个时候发明的。 你可以对 1970 年以前的时间使用负数。 日期对象上的`getTime`方法返回这个数字。 你可以想象它会很大。 ``` console.log(new Date(2013, 11, 19).getTime()); @@ -504,7 +504,7 @@ console.log(sticky.exec("xyz abc")); // → null ``` -对多个`exec`调用使用共享的正则表达式值时,这些`lastIndex`属性的自动更新可能会导致问题。 您的正则表达式可能意外地在之前的调用留下的索引处开始。 +对多个`exec`调用使用共享的正则表达式值时,这些`lastIndex`属性的自动更新可能会导致问题。 你的正则表达式可能意外地在之前的调用留下的索引处开始。 ``` let digit = /\d/g; @@ -572,7 +572,7 @@ outputdir=/home/marijn/enemies/davaeorn + 其他的格式都是无效的。 -我们的任务是将这样的字符串转换成对象的数组,数组中每个元素包含一个name属性和一个选项数组。我们需要使用一个对象表示节,也需要使用一个对象表示全局选项。 +我们的任务是将这样的字符串转换为一个对象,该对象的属性包含没有段的设置的字符串,和段的子对象的字符串,段的子对象也包含段的设置。 由于我们需要逐行处理这种格式的文件,因此预处理时最好将文件分割成一行行文本。我们使用第6章中的string.split(“\n”)来分割文件内容。但是一些操作系统并非使用换行符来分隔行,而是使用回车符加换行符(“\r\n”)。考虑到这点,我们也可以使用正则表达式作为split方法的参数,我们使用类似于/\r?\n/的正则表达式,这样可以同时支持“\n”和“\r\n”两种分隔符。 @@ -582,68 +582,91 @@ function parseINI(string) { let currentSection = {name: null, fields: []}; let categories = [currentSection]; - string.split(/\r?\n/).forEach(function(line) { + string.split(/\r?\n/).forEach(line => { let match; - if (/^\s*(;.*)?$/.test(line)) { - return; - } else if (match = line.match(/^\[(.*)\]$/)) { - currentSection = {name: match[1], fields: []}; - categories.push(currentSection); - } else if (match = line.match(/^(\w+)=(.*)$/)) { - currentSection.fields.push({name: match[1], - value: match[2]}); - } else { - throw new Error("Line '" + line + "' is invalid."); + if (match = line.match(/^(\w+)=(.*)$/)) { + section[match[1]] = match[2]; + section = result[match[1]] = {}; + } else if (!/^\s*(;.*)?$/.test(line)) { + throw new Error("Line '" + line + "' is not valid."); } }); - return categories; + return result; } + +console.log(parseINI(` +name=Vasilis +[address] +city=Tessaloniki`)); +// → {name: "Vasilis", address: {city: "Tessaloniki"}} ``` -上面这段代码会遍历文件中的每一行,并更新当前节对应的对象。首先,使用表达式/^\s*(;.*)?$/检查是否应该忽略该行。你能看出这段表达式是如何工作的吗?括号中的表达式负责匹配注释,而?则匹配全为空白字符的行。 +代码遍历文件的行并构建一个对象。 顶部的属性直接存储在该对象中,而在段中找到的属性存储在单独的段对象中。 `section`绑定指向当前段的对象。 -如果这行文本不是注释,代码会检查这一行是否是一个新节的起始位置。如果是的话,则创建一个新的节对象,并将后续选项都添加到这个对象中。 - -这行文本还可能是一个普通选项,这种情况下代码会将选项添加到当前节对象中。 - -如果某行文本无法匹配任何模式,那么函数会抛出一个错误。 +有两种重要的行 - 段标题或属性行。 当一行是常规属性时,它将存储在当前段中。 当它是一个段标题时,创建一个新的段对象,并设置`section`来指向它。 这里需要注意,我们反复使用^和$确保表达式匹配整行,而非一行中的一部分。如果不使用这两个符号,大多数情况下程序也可以正常工作,但在处理特定输入时,程序就会出现不合理的行为,我们一般很难发现这个缺陷的问题所在。 -类似于在while循环中使用赋值表达式,if(match=string.match(...))这种形式的表达式也是一种技巧。一般我们无法确定match是否执行成功,因此一般只需要在确实存在匹配结果时,我们才在if语句中访问match的匹配结果。为了不打破if这种优雅的链形式,我们需要将匹配结果赋值给一个变量,并使用赋值表达式的值来作为if中的判断条件。 +`if (match = string.match(...))`类似于使用赋值作为`while`的条件的技巧。你通常不确定你对`match`的调用是否成功,所以你只能在测试它的`if`语句中访问结果对象。 为了不打破`else if`形式的令人愉快的链条,我们将匹配结果赋给一个绑定,并立即使用该赋值作为`if`语句的测试。 -### 9.18 国际化字符 +### 国际化字符 -由于JavaScript最初的实现非常简单,而且这种简单的处理方式后来也成了标准,因此JavaScript正则表达式处理非英语字符时非常无力。例如,就JavaScript的正则表达式而言,“单词字符”只是26个拉丁字母(大写和小写),而且由于某些原因还包括下划线字符。 - -像a`或或β这种明显的单词字符,则无法匹配\w(会匹配大写的\W,因为它们属于非单词字符)。 +由于JavaScript最初的实现非常简单,而且这种简单的处理方式后来也成了标准,因此JavaScript正则表达式处理非英语字符时非常无力。例如,就JavaScript的正则表达式而言,“单词字符”只是26个拉丁字母(大写和小写)和数字,而且由于某些原因还包括下划线字符。像α或或β这种明显的单词字符,则无法匹配\w(会匹配大写的\W,因为它们属于非单词字符)。 由于奇怪的历史性意外,\s(空白字符)则没有这种问题,会匹配所有Unicode 标准中规定的空白字符,包括不间断空格和蒙古文元音分隔符。 -一些程序设计语言中的正则表达式语法实现可以匹配特定Unicode 字符集,比如所有大写字母,所有标点符号或控制字符JavaScript 中也计划加入对这些字符集的支持,但遗憾的是在不远的将来这貌似是无法实现的。 +另一个问题是,默认情况下,正则表达式使用代码单元,而不是实际的字符,正如第 5 章中所讨论的那样。 这意味着由两个代码单元组成的字符表现很奇怪。 -### 9.19 本章小结 +``` +console.log(/\ud83c\udf4e{3}/.test("\ud83c\udf4e\ud83c\udf4e\ud83c\udf4e")); +// → false +console.log(/<.>/.test("<\ud83c\udf39>")); +// → false +console.log(/<.>/u.test("<\ud83c\udf39>")); +// → true +``` -正则表达式是表示字符串模式的对象,使用自己的语法来表达这些模式: +问题是第一行中的`"\ud83c\udf4e"`(emoji 苹果)被视为两个代码单元,而`{3}`部分仅适用于第二个。 与之类似,点匹配单个代码单元,而不是组成玫瑰 emoji 符号的两个代码单元。 + +你必须在正则表达式中添加一个`u`选项(表示 Unicode),才能正确处理这些字符。 不幸的是,错误的行为仍然是默认行为,因为改变它可能会导致依赖于它的现有代码出现问题。 + +尽管这是刚刚标准化的,在撰写本文时尚未得到广泛支持,但可以在正则表达式中使用'\p'(必须启用 Unicode 选项)以匹配 Unicode 标准分配了给定属性的所有字符。 + +``` +console.log(/\p{Script=Greek}/u.test("α")); +// → true +console.log(/\p{Script=Arabic}/u.test("α")); +// → false +console.log(/\p{Alphabetic}/u.test("α")); +// → true +console.log(/\p{Alphabetic}/u.test("!")); +// → false +``` + +Unicode定义了许多有用的属性,尽管找到你需要的属性可能并不总是没有意义。 你可以使用'\p{Property=Value}'符号来匹配任何具有该属性的给定值的字符。 如果属性名称保持不变,如`\p{Name}`中那样,名称被假定为二元属性,如`Alphabetic`,或者类别,如`Number`。 + +### 本章小结 + +正则表达式是表示字符串模式的对象,使用自己的语言来表达这些模式: + /abc/:字符序列 + /[abc]/:字符集中的任何字符 -+ /[^abc]/:任何不在字符集中的字符 ++ /[^abc]/:不在字符集中的任何字符 -+ /[0-9]/:任何在字符范围内的字符 ++ /[0-9]/:字符范围内的任何字符 -+ /x+/:出现模式x一次或多次 ++ /x+/:出现一次或多次 -+ /x+?/:出现模式x一次或多次,非贪婪模式 ++ /x+?/:出现一次或多次,非贪婪模式 -+ /x*/:出现模式次或多次 ++ /x*/:出现零次或多次 -+ /x?/:出现模式零次或多次,非贪婪模式 ++ /x?/:出现零次或多次,非贪婪模式 -+ /x{2,4}/:出现次数在两个数字范围之间 ++ /x{2,4}/:出现两次到四次 + /(abc)/:元组 @@ -663,21 +686,19 @@ function parseINI(string) { + /$/:输入结束位置 -正则表达式有test方法,用于测试给定的字符串是否匹配模式。还有一个exec方法,当匹配模式后,返回包含所有匹配元组的数组。这类数组有一个index属性,用于指出匹配的起始位置。 +正则表达式有一个`test`方法来测试给定的字符串是否匹配它。 它还有一个`exec`方法,当找到匹配项时,返回一个包含所有匹配组的数组。 这样的数组有一个`index`属性,用于表明匹配开始的位置。 -字符串有一个match方法,可以使用正则表达式进行匹配,还有一个search方法,可以搜索一个匹配项,返回匹配的起始位置。字符串的replace方法可以使用匹配字符串替换原字符串中匹配模式的文本。你还可以向replace传递一个函数,根据匹配文本和匹配元组创建匹配字符串。 +字符串有一个`match`方法来对正确表达式匹配它们,以及`search`方法来搜索字符串,只返回匹配的起始位置。 他们的`replace`方法可以用替换字符串或函数替换模式匹配。 -正则表达式可以在结尾的斜杠后添加选项。选项i使得匹配不区分大小写,选项g使得表达式变成全局匹配,这种情况下,replace方法会替换所有匹配项,而非第一项。 +正则表达式拥有选项,这些选项写在闭合斜线后面。 `i`选项使匹配不区分大小写。 `g`选项使表达式成为全聚德,除此之外,它使`replace`方法替换所有实例,而不是第一个。 `y`选项使它变为粘性,这意味着它在搜索匹配时不会向前搜索并跳过部分字符串。 `u`选项开启 Unicode 模式,该模式解决了处理占用两个代码单元的字符时的一些问题。 -RegExp构造函数可以用于通过字符串创建正则表达式。 +正则表达式是难以驾驭的强力工具。它可以简化一些任务,但用到一些复杂问题上时也会难以控制管理。想要学会使用正则表达式的重要一点是:不要将其用到无法干净地表达为正则表达式的问题。 -正则表达式是难以驾驭的强力工具。它可以简化一些任务,但用到一些复杂问题上时也会难以控制管理。想要学会使用正则表达式的重要一点是:不要将其用到无法使用正则表达式描述的问题中。 - -### 9.20 习题 +### 习题 在做本章习题时,读者不可避免地会对一些正则表达式的莫名其妙的行为感到困惑,因而备受挫折。读者可以使用类似于[http://debuggex.com/](http://debuggex.com/)这样的在线学习工具,将你想编写的正则表达式可视化,并试验其对不同输入字符串的响应。 -#### 9.20.1 RegexpGolf +#### RegexpGolf Code Golf是一种游戏,尝试尽量用最少的字符来描述特定程序。类似的,Regexp Golf这种活动是编写尽量短小的正则表达式,来匹配给定模式(而且只能匹配给定模式)。 @@ -691,11 +712,11 @@ Code Golf是一种游戏,尝试尽量用最少的字符来描述特定程序 4.以ious结尾的单词 -5.空白字符后面紧跟着句号、冒号、分号 +5.句号、冒号、分号之前的空白字符 6.多于六个字母的单词 -7.不包含e的单词 +7.不包含e(或者E)的单词 需要帮助时,请参考本章总结中的表格。使用少量测试字符串来测试每个解决方案。 @@ -708,7 +729,7 @@ verify(/.../, verify(/.../, ["pop culture", "mad props"], - ["plop"]); + ["plop", "prrrop"]]); verify(/.../, ["ferret", "ferry", "ferrari"], @@ -720,7 +741,7 @@ verify(/.../, verify(/.../, ["bad punctuation ."], - ["escape the dot"]); + ["escape the period"]); verify(/.../, ["hottentottententen"], @@ -728,31 +749,53 @@ verify(/.../, verify(/.../, ["red platypus", "wobbling nest"], - ["earth bed", "learning ape"]); + ["earth bed", "learning ape", "BEET"]); function verify(regexp, yes, no) { // Ignore unfinished exercises if (regexp.source == "...") return; - yes.forEach(function(s) { - if (!regexp.test(s)) - console.log("Failure to match '" + s + "'"); - }); - no.forEach(function(s) { - if (regexp.test(s)) - console.log("Unexpected match for '" + s + "'"); - }); + for (let str of yes) if (!regexp.test(str)) { + console.log(`Failure to match '${str}'`); + } + for (let str of no) if (regexp.test(str)) { + console.log(`Unexpected match for '${str}'`); + } } ``` -#### 9.20.2 QuotingStyle +#### QuotingStyle 想象一下,你编写了一个故事,自始至终都使用单引号来标记对话。现在你想要将对话的引号替换成双引号,但不能替换在缩略形式中使用的单引号。 思考一下可以区分这两种引号用法的模式,并手动调用replace方法进行正确替换。 -#### 9.20.3 NumbersAgain +``` +let text = "'I'm the cook,' he said, 'it's my job.'"; +// Change this call. +console.log(text.replace(/A/g, "B")); +// → "I'm the cook," he said, "it's my job." +``` -一个数字序列可以使用简单的正则表达式/\d+/匹配。 +#### NumbersAgain 编写一个表达式,只匹配JavaScript风格的数字。支持数字前可选的正号与负号、十进制小数点、指数计数法(5e-3或1E10,指数前也需要支持可选的符号)。也请注意小数点前或小数点后的数字也是不必要的,但数字不能只有小数点。例如.5和5.都是合法的JavaScript数字,但单个点则不是。 + +``` +// Fill in this regular expression. +let number = /^...$/; + +// Tests: +for (let str of ["1", "-1", "+15", "1.55", ".5", "5.", + "1.3e2", "1E-4", "1e+12"]) { + if (!number.test(str)) { + console.log(`Failed to match '${str}'`); + } +} +for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5", + ".5.", "1f5", "."]) { + if (number.test(str)) { + console.log(`Incorrectly accepted '${str}'`); + } +} +```