diff --git a/docs/iterator.md b/docs/iterator.md index b87c1f9..46e2ca7 100644 --- a/docs/iterator.md +++ b/docs/iterator.md @@ -1,14 +1,14 @@ -# Iterator和for...of循环 +# Iterator 和 for...of 循环 ## Iterator(遍历器)的概念 -JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。 +JavaScript 原有的表示“集合”的数据结构,主要是数组(`Array`)和对象(`Object`),ES6 又添加了`Map`和`Set`。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是`Map`,`Map`的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 -Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令`for...of`循环,Iterator接口主要供`for...of`消费。 +Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令`for...of`循环,Iterator接口主要供`for...of`消费。 -Iterator的遍历过程是这样的。 +Iterator 的遍历过程是这样的。 (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。 @@ -64,7 +64,7 @@ function makeIterator(array) { } ``` -由于Iterator只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。下面是一个无限运行的遍历器对象的例子。 +由于 Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。下面是一个无限运行的遍历器对象的例子。 ```javascript var it = idMaker(); @@ -87,9 +87,7 @@ function idMaker() { 上面的例子中,遍历器生成函数`idMaker`,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。 -在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被`for...of`循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。 - -如果使用TypeScript的写法,遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。 +如果使用 TypeScript 的写法,遍历器接口(Iterable)、指针对象(Iterator)和`next`方法返回值的规格可以描述如下。 ```javascript interface Iterable { @@ -106,13 +104,13 @@ interface IterationResult { } ``` -## 数据结构的默认Iterator接口 +## 默认 Iterator 接口 -Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即`for...of`循环(详见下文)。当使用`for...of`循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。 +Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即`for...of`循环(详见下文)。当使用`for...of`循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。 -一种数据结构只要部署了Iterator接口,我们就称这种数据结构是”可遍历的“(iterable)。 +一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是”可遍历的“(iterable)。 -ES6规定,默认的Iterator接口部署在数据结构的`Symbol.iterator`属性,或者说,一个数据结构只要具有`Symbol.iterator`属性,就可以认为是“可遍历的”(iterable)。`Symbol.iterator`属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名`Symbol.iterator`,它是一个表达式,返回`Symbol`对象的`iterator`属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内。(参见Symbol一章)。 +ES6 规定,默认的 Iterator 接口部署在数据结构的`Symbol.iterator`属性,或者说,一个数据结构只要具有`Symbol.iterator`属性,就可以认为是“可遍历的”(iterable)。`Symbol.iterator`属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名`Symbol.iterator`,它是一个表达式,返回`Symbol`对象的`iterator`属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见 Symbol 一章)。 ```javascript const obj = { @@ -131,7 +129,18 @@ const obj = { 上面代码中,对象`obj`是可遍历的(iterable),因为具有`Symbol.iterator`属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有`next`方法。每次调用`next`方法,都会返回一个代表当前成员的信息对象,具有`value`和`done`两个属性。 -在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。 +ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被`for...of`循环遍历。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。 + +原生具备 Iterator 接口的数据结构如下。 + +- Array +- Map +- Set +- String +- TypedArray +- 函数的 arguments 对象 + +下面的例子是数组的`Symbol.iterator`属性。 ```javascript let arr = ['a', 'b', 'c']; @@ -145,11 +154,11 @@ iter.next() // { value: undefined, done: true } 上面代码中,变量`arr`是一个数组,原生就具有遍历器接口,部署在`arr`的`Symbol.iterator`属性上面。所以,调用这个属性,就得到遍历器对象。 -上面提到,原生就部署Iterator接口的数据结构有三类,对于这三类数据结构,不用自己写遍历器生成函数,`for...of`循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在`Symbol.iterator`属性上面部署,这样才会被`for...of`循环遍历。 +对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,`for...of`循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在`Symbol.iterator`属性上面部署,这样才会被`for...of`循环遍历。 -对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5没有Map结构,而ES6原生提供了。 +对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。 -一个对象如果要有可被`for...of`循环调用的Iterator接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。 +一个对象如果要具备可被`for...of`循环调用的 Iterator 接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。 ```javascript class RangeIterator { @@ -175,11 +184,11 @@ function range(start, stop) { } for (var value of range(0, 3)) { - console.log(value); + console.log(value); // 0, 1, 2 } ``` -上面代码是一个类部署Iterator接口的写法。`Symbol.iterator`属性对应一个函数,执行后返回当前对象的遍历器对象。 +上面代码是一个类部署 Iterator 接口的写法。`Symbol.iterator`属性对应一个函数,执行后返回当前对象的遍历器对象。 下面是通过遍历器实现指针结构的例子。 @@ -190,9 +199,7 @@ function Obj(value) { } Obj.prototype[Symbol.iterator] = function() { - var iterator = { - next: next - }; + var iterator = { next: next }; var current = this; @@ -200,14 +207,9 @@ Obj.prototype[Symbol.iterator] = function() { if (current) { var value = current.value; current = current.next; - return { - done: false, - value: value - }; + return { done: false, value: value }; } else { - return { - done: true - }; + return { done: true }; } } return iterator; @@ -221,16 +223,13 @@ one.next = two; two.next = three; for (var i of one){ - console.log(i); + console.log(i); // 1, 2, 3 } -// 1 -// 2 -// 3 ``` 上面代码首先在构造函数的原型链上部署`Symbol.iterator`方法,调用该方法会返回遍历器对象`iterator`,调用该对象的`next`方法,在返回一个值的同时,自动将内部指针移到下一个实例。 -下面是另一个为对象添加Iterator接口的例子。 +下面是另一个为对象添加 Iterator 接口的例子。 ```javascript let obj = { @@ -254,7 +253,7 @@ let obj = { }; ``` -对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数组的Iterator接口。 +对于类似数组的对象(存在数值键名和`length`属性),部署 Iterator 接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数组的 Iterator 接口。 ```javascript NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; @@ -304,7 +303,7 @@ obj[Symbol.iterator] = () => 1; [...obj] // TypeError: [] is not a function ``` -上面代码中,变量obj的Symbol.iterator方法对应的不是遍历器生成函数,因此报错。 +上面代码中,变量`obj`的`Symbol.iterator`方法对应的不是遍历器生成函数,因此报错。 有了遍历器接口,数据结构就可以用`for...of`循环遍历(详见下文),也可以使用`while`循环遍历。 @@ -320,13 +319,13 @@ while (!$result.done) { 上面代码中,`ITERABLE`代表某种可遍历的数据结构,`$iterator`是它的遍历器对象。遍历器对象每次移动指针(`next`方法),都检查一下返回值的`done`属性,如果遍历还没结束,就移动遍历器对象的指针到下一步(`next`方法),不断循环。 -## 调用Iterator接口的场合 +## 调用 Iterator 接口的场合 -有一些场合会默认调用Iterator接口(即`Symbol.iterator`方法),除了下文会介绍的`for...of`循环,还有几个别的场合。 +有一些场合会默认调用 Iterator 接口(即`Symbol.iterator`方法),除了下文会介绍的`for...of`循环,还有几个别的场合。 **(1)解构赋值** -对数组和Set结构进行解构赋值时,会默认调用`Symbol.iterator`方法。 +对数组和 Set 结构进行解构赋值时,会默认调用`Symbol.iterator`方法。 ```javascript let set = new Set().add('a').add('b').add('c'); @@ -340,7 +339,7 @@ let [first, ...rest] = set; **(2)扩展运算符** -扩展运算符(...)也会调用默认的iterator接口。 +扩展运算符(...)也会调用默认的 Iterator 接口。 ```javascript // 例一 @@ -353,9 +352,9 @@ let arr = ['b', 'c']; // ['a', 'b', 'c', 'd'] ``` -上面代码的扩展运算符内部就调用Iterator接口。 +上面代码的扩展运算符内部就调用 Iterator 接口。 -实际上,这提供了一种简便机制,可以将任何部署了Iterator接口的数据结构,转为数组。也就是说,只要某个数据结构部署了Iterator接口,就可以对它使用扩展运算符,将其转为数组。 +实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。 ```javascript let arr = [...iterable]; @@ -363,7 +362,7 @@ let arr = [...iterable]; **(3)yield* ** -yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。 +`yield*`后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。 ```javascript let generator = function* () { @@ -392,9 +391,9 @@ iterator.next() // { value: undefined, done: true } - Promise.all() - Promise.race() -## 字符串的Iterator接口 +## 字符串的 Iterator 接口 -字符串是一个类似数组的对象,也原生具有Iterator接口。 +字符串是一个类似数组的对象,也原生具有 Iterator 接口。 ```javascript var someString = "hi"; diff --git a/docs/reference.md b/docs/reference.md index 5c2aa64..a4845a2 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -37,7 +37,7 @@ - kangax, [Javascript quiz. ES6 edition](http://perfectionkills.com/javascript-quiz-es6/): ES6小测试 - Jeremy Fairbank, [HTML5DevConf ES7 and Beyond!](https://speakerdeck.com/jfairbank/html5devconf-es7-and-beyond): ES7新增语法点介绍 -## let和const +## let 和 const - Kyle Simpson, [For and against let](http://davidwalsh.name/for-and-against-let): 讨论let命令的作用域 - kangax, [Why typeof is no longer “safe”](http://es-discourse.com/t/why-typeof-is-no-longer-safe/15): 讨论在块级作用域内,let命令的变量声明和赋值的行为 @@ -65,6 +65,7 @@ - Mathias Bynens, [Unicode-aware regular expressions in ES6](https://mathiasbynens.be/notes/es6-unicode-regex): 详细介绍正则表达式的u修饰符 - Axel Rauschmayer, [New regular expression features in ECMAScript 6](http://www.2ality.com/2015/07/regexp-es6.html):ES6正则特性的详细介绍 - Yang Guo, [RegExp lookbehind assertions](http://v8project.blogspot.jp/2016/02/regexp-lookbehind-assertions.html):介绍后行断言 +- Axel Rauschmayer, [ES proposal: RegExp named capture groups](http://2ality.com/2017/05/regexp-named-capture-groups.html): 具名组匹配的介绍 ## 数值 diff --git a/docs/regex.md b/docs/regex.md index 389c357..dba9010 100644 --- a/docs/regex.md +++ b/docs/regex.md @@ -1,8 +1,8 @@ # 正则的扩展 -## RegExp构造函数 +## RegExp 构造函数 -在ES5中,RegExp构造函数的参数有两种情况。 +在 ES5 中,`RegExp`构造函数的参数有两种情况。 第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。 @@ -20,14 +20,14 @@ var regex = new RegExp(/xyz/i); var regex = /xyz/i; ``` -但是,ES5不允许此时使用第二个参数,添加修饰符,否则会报错。 +但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。 ```javascript var regex = new RegExp(/xyz/, 'i'); // Uncaught TypeError: Cannot supply flags when constructing one RegExp from another ``` -ES6改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。 +ES6 改变了这种行为。如果`RegExp`构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。 ```javascript new RegExp(/abc/ig, 'i').flags @@ -40,31 +40,29 @@ new RegExp(/abc/ig, 'i').flags 字符串对象共有4个方法,可以使用正则表达式:`match()`、`replace()`、`search()`和`split()`。 -ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。 +ES6 将这4个方法,在语言内部全部调用`RegExp`的实例方法,从而做到所有与正则相关的方法,全都定义在`RegExp`对象上。 - `String.prototype.match` 调用 `RegExp.prototype[Symbol.match]` - `String.prototype.replace` 调用 `RegExp.prototype[Symbol.replace]` - `String.prototype.search` 调用 `RegExp.prototype[Symbol.search]` - `String.prototype.split` 调用 `RegExp.prototype[Symbol.split]` -## u修饰符 +## u 修饰符 -ES6对正则表达式添加了`u`修饰符,含义为“Unicode模式”,用来正确处理大于`\uFFFF`的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。 +ES6 对正则表达式添加了`u`修饰符,含义为“Unicode模式”,用来正确处理大于`\uFFFF`的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。 ```javascript -/^\uD83D/u.test('\uD83D\uDC2A') -// false -/^\uD83D/.test('\uD83D\uDC2A') -// true +/^\uD83D/u.test('\uD83D\uDC2A') // false +/^\uD83D/.test('\uD83D\uDC2A') // true ``` -上面代码中,`\uD83D\uDC2A`是一个四个字节的UTF-16编码,代表一个字符。但是,ES5不支持四个字节的UTF-16编码,会将其识别为两个字符,导致第二行代码结果为`true`。加了`u`修饰符以后,ES6就会识别其为一个字符,所以第一行代码结果为`false`。 +上面代码中,`\uD83D\uDC2A`是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为`true`。加了`u`修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为`false`。 一旦加上`u`修饰符号,就会修改下面这些正则表达式的行为。 **(1)点字符** -点(`.`)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于`0xFFFF`的Unicode字符,点字符不能识别,必须加上`u`修饰符。 +点(`.`)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于`0xFFFF`的 Unicode 字符,点字符不能识别,必须加上`u`修饰符。 ```javascript var s = '𠮷'; @@ -75,9 +73,9 @@ var s = '𠮷'; 上面代码表示,如果不添加`u`修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。 -**(2)Unicode字符表示法** +**(2)Unicode 字符表示法** -ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别。 +ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别当中的大括号,否则会被解读为量词。 ```javascript /\u{61}/.test('a') // false @@ -89,7 +87,7 @@ ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达 **(3)量词** -使用`u`修饰符后,所有量词都会正确识别码点大于`0xFFFF`的Unicode字符。 +使用`u`修饰符后,所有量词都会正确识别码点大于`0xFFFF`的 Unicode 字符。 ```javascript /a{2}/.test('aa') // true @@ -98,24 +96,16 @@ ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达 /𠮷{2}/u.test('𠮷𠮷') // true ``` -另外,只有在使用`u`修饰符的情况下,Unicode表达式当中的大括号才会被正确解读,否则会被解读为量词。 - -```javascript -/^\u{3}$/.test('uuu') // true -``` - -上面代码中,由于正则表达式没有`u`修饰符,所以大括号被解读为量词。加上`u`修饰符,就会被解读为Unicode表达式。 - **(4)预定义模式** -`u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的Unicode字符。 +`u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的 Unicode 字符。 ```javascript /^\S$/.test('𠮷') // false /^\S$/u.test('𠮷') // true ``` -上面代码的`\S`是预定义模式,匹配所有不是空格的字符。只有加了`u`修饰符,它才能正确匹配码点大于`0xFFFF`的Unicode字符。 +上面代码的`\S`是预定义模式,匹配所有不是空格的字符。只有加了`u`修饰符,它才能正确匹配码点大于`0xFFFF`的 Unicode 字符。 利用这一点,可以写出一个正确返回字符串长度的函数。 @@ -131,20 +121,20 @@ s.length // 4 codePointLength(s) // 2 ``` -**(5)i修饰符** +**(5)i 修饰符** -有些Unicode字符的编码不同,但是字型很相近,比如,`\u004B`与`\u212A`都是大写的`K`。 +有些 Unicode 字符的编码不同,但是字型很相近,比如,`\u004B`与`\u212A`都是大写的`K`。 ```javascript /[a-z]/i.test('\u212A') // false /[a-z]/iu.test('\u212A') // true ``` -上面代码中,不加`u`修饰符,就无法识别非规范的K字符。 +上面代码中,不加`u`修饰符,就无法识别非规范的`K`字符。 ## y 修饰符 -除了`u`修饰符,ES6还为正则表达式添加了`y`修饰符,叫做“粘连”(sticky)修饰符。 +除了`u`修饰符,ES6 还为正则表达式添加了`y`修饰符,叫做“粘连”(sticky)修饰符。 `y`修饰符的作用与`g`修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,`g`修饰符只要剩余位置中存在匹配就可,而`y`修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。 @@ -217,7 +207,7 @@ match.index // 3 REGEX.lastIndex // 4 ``` -进一步说,`y`修饰符号隐含了头部匹配的标志`^`。 +实际上,`y`修饰符号隐含了头部匹配的标志`^`。 ```javascript /b/y.exec('aba') @@ -255,7 +245,7 @@ const REGEX = /a/gy; 'aaxa'.replace(REGEX, '-') // '--xa' ``` -上面代码中,最后一个`a`因为不是出现下一次匹配的头部,所以不会被替换。 +上面代码中,最后一个`a`因为不是出现在下一次匹配的头部,所以不会被替换。 单单一个`y`修饰符对`match`方法,只能返回第一个匹配,必须与`g`修饰符联用,才能返回所有匹配。 @@ -264,7 +254,7 @@ const REGEX = /a/gy; 'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"] ``` -`y`修饰符的一个应用,是从字符串提取token(词元),`y`修饰符确保了匹配之间不会有漏掉的字符。 +`y`修饰符的一个应用,是从字符串提取 token(词元),`y`修饰符确保了匹配之间不会有漏掉的字符。 ```javascript const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y; @@ -296,76 +286,31 @@ tokenize(TOKEN_G, '3x + 4') 上面代码中,`g`修饰符会忽略非法字符,而`y`修饰符不会,这样就很容易发现错误。 -## sticky属性 +## sticky 属性 -与`y`修饰符相匹配,ES6的正则对象多了`sticky`属性,表示是否设置了`y`修饰符。 +与`y`修饰符相匹配,ES6 的正则对象多了`sticky`属性,表示是否设置了`y`修饰符。 ```javascript var r = /hello\d/y; r.sticky // true ``` -## flags属性 +## flags 属性 -ES6为正则表达式新增了`flags`属性,会返回正则表达式的修饰符。 +ES6 为正则表达式新增了`flags`属性,会返回正则表达式的修饰符。 ```javascript -// ES5的source属性 +// ES5 的 source 属性 // 返回正则表达式的正文 /abc/ig.source // "abc" -// ES6的flags属性 +// ES6 的 flags 属性 // 返回正则表达式的修饰符 /abc/ig.flags // 'gi' ``` -## RegExp.escape() - -字符串必须转义,才能作为正则模式。 - -```javascript -function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); -} - -let str = '/path/to/resource.html?search=query'; -escapeRegExp(str) -// "\/path\/to\/resource\.html\?search=query" -``` - -上面代码中,`str`是一个正常字符串,必须使用反斜杠对其中的特殊字符转义,才能用来作为一个正则匹配的模式。 - -已经有[提议](https://esdiscuss.org/topic/regexp-escape)将这个需求标准化,作为RegExp对象的静态方法[RegExp.escape()](https://github.com/benjamingr/RexExp.escape),放入ES7。2015年7月31日,TC39认为,这个方法有安全风险,又不愿这个方法变得过于复杂,没有同意将其列入ES7,但这不失为一个真实的需求。 - -```javascript -RegExp.escape('The Quick Brown Fox'); -// "The Quick Brown Fox" - -RegExp.escape('Buy it. use it. break it. fix it.'); -// "Buy it\. use it\. break it\. fix it\." - -RegExp.escape('(*.*)'); -// "\(\*\.\*\)" -``` - -字符串转义以后,可以使用RegExp构造函数生成正则模式。 - -```javascript -var str = 'hello. how are you?'; -var regex = new RegExp(RegExp.escape(str), 'g'); -assert.equal(String(regex), '/hello\. how are you\?/g'); -``` - -目前,该方法可以用上文的`escapeRegExp`函数或者垫片模块[regexp.escape](https://github.com/ljharb/regexp.escape)实现。 - -```javascript -var escape = require('regexp.escape'); -escape('hi. how are you?'); -// "hi\\. how are you\\?" -``` - ## s 修饰符:dotAll 模式 正则表达式中,点(`.`)是一个特殊字符,代表任意的单个字符,但是行终止符(line terminator character)除外。 @@ -413,9 +358,7 @@ re.flags // 's' ## 后行断言 -JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。 - -目前,有一个[提案](https://github.com/goyakin/es-regexp-lookbehind),引入后行断言。V8 引擎4.9版已经支持,Chrome 浏览器49版打开”experimental JavaScript features“开关(地址栏键入`about:flags`),就可以使用这项功能。 +JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。目前,有一个[提案](https://github.com/goyakin/es-regexp-lookbehind),引入后行断言,V8 引擎4.9版已经支持。 ”先行断言“指的是,`x`只有在`y`前面才匹配,必须写成`/x(?=y)/`。比如,只匹配百分号之前的数字,要写成`/\d+(?=%)/`。”先行否定断言“指的是,`x`只有不在`y`前面才匹配,必须写成`/x(?!y)/`。比如,只匹配不在百分号之前的数字,要写成`/\d+(?!%)/`。 @@ -435,6 +378,16 @@ JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先 上面的例子中,“后行断言”的括号之中的部分(`(?<=\$)`),也是不计入返回结果。 +下面的例子是使用后行断言进行字符串替换。 + +```javascript +const RE_DOLLAR_PREFIX = /(?<=\$)foo/g; +'$foo %foo foo'.replace(RE_DOLLAR_PREFIX, 'bar'); +// '$bar %foo foo' +``` + +上面代码中,只有在美元符号后面的`foo`才会被替换。 + “后行断言”的实现,需要先匹配`/(?<=y)x/`的`x`,然后再回到左边,匹配`y`的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。 首先,”后行断言“的组匹配,与正常情况下结果是不一样的。 @@ -453,20 +406,20 @@ JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先 /(?<=\1d(o))r/.exec('hodor') // ["r", "o"] ``` -上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。 +上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。 -## Unicode属性类 +## Unicode 属性类 -目前,有一个[提案](https://github.com/mathiasbynens/es-regexp-unicode-property-escapes),引入了一种新的类的写法`\p{...}`和`\P{...}`,允许正则表达式匹配符合Unicode某种属性的所有字符。 +目前,有一个[提案](https://github.com/mathiasbynens/es-regexp-unicode-property-escapes),引入了一种新的类的写法`\p{...}`和`\P{...}`,允许正则表达式匹配符合 Unicode 某种属性的所有字符。 ```javascript const regexGreekSymbol = /\p{Script=Greek}/u; -regexGreekSymbol.test('π') // u +regexGreekSymbol.test('π') // true ``` 上面代码中,`\p{Script=Greek}`指定匹配一个希腊文字母,所以匹配`π`成功。 -Unicode属性类要指定属性名和属性值。 +Unicode 属性类要指定属性名和属性值。 ```javascript \p{UnicodePropertyName=UnicodePropertyValue} @@ -480,9 +433,9 @@ Unicode属性类要指定属性名和属性值。 `\P{…}`是`\p{…}`的反向匹配,即匹配不满足条件的字符。 -注意,这两种类只对Unicode有效,所以使用的时候一定要加上`u`修饰符。如果不加`u`修饰符,正则表达式使用`\p`和`\P`会报错,ECMAScript预留了这两个类。 +注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上`u`修饰符。如果不加`u`修饰符,正则表达式使用`\p`和`\P`会报错,ECMAScript 预留了这两个类。 -由于Unicode的各种属性非常多,所以这种新的类的表达能力非常强。 +由于 Unicode 的各种属性非常多,所以这种新的类的表达能力非常强。 ```javascript const regex = /^\p{Decimal_Number}+$/u; @@ -504,10 +457,10 @@ regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true 下面是其他一些例子。 ```javascript -// 匹配各种文字的所有字母,等同于Unicode版的\w +// 匹配各种文字的所有字母,等同于 Unicode 版的 \w [\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}] -// 匹配各种文字的所有非字母的字符,等同于Unicode版的\W +// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W [^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}] // 匹配所有的箭头字符 @@ -515,3 +468,117 @@ const regexArrows = /^\p{Block=Arrows}+$/u; regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true ``` +## 具名组匹配 + +### 简介 + +正则表达式使用圆括号进行组匹配。 + +```javascript +const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/; +``` + +上面代码中,正则表达式里面有三组圆括号。使用`exec`方法,就可以将这三组匹配结果提取出来。 + +```javascript +const matchObj = RE_DATE.exec('1999-12-31'); +const year = matchObj[1]; // 1999 +const month = matchObj[2]; // 12 +const day = matchObj[3]; // 31 +``` + +组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号引用,要是组的顺序变了,引用的时候就必须修改序号。 + +现在有一个“具名组匹配”(Named Capture Groups)的[提案](https://github.com/tc39/proposal-regexp-named-groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。 + +```javascript +const RE_DATE = /(?\d{4})-(?\d{2})-(?\d{2})/; + +const matchObj = RE_DATE.exec('1999-12-31'); +const year = matchObj.groups.year; // 1999 +const month = matchObj.groups.month; // 12 +const day = matchObj.groups.day; // 31 +``` + +上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(`?`),然后就可以在`exec`方法返回结果的`groups`属性上引用该组名。同时,数字序号(` matchObj[1]`)依然有效。 + +具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。 + +如果具名组没有匹配,那么对应的`groups`对象属性会是`undefined`。 + +```javascript +const RE_OPT_A = /^(?a+)?$/; +const matchObj = RE_OPT_A.exec(''); + +matchObj.groups.as // undefined +'as' in matchObj.groups // true +``` + +上面代码中,具名组`as`没有找到匹配,那么`matchObj.groups.as`属性值就是`undefined`,并且`as`这个键名在`groups`是始终存在的。 + +### 解构赋值和替换 + +有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。 + +```javascript +let {groups: {one, two}} = /^(?.*):(?.*)$/u.exec('foo:bar'); +one // foo +two // bar +``` + +字符串替换时,使用`$<组名>`引用具名组。 + +```javascript +let re = /(?\d{4})-(?\d{2})-(?\d{2})/u; + +'2015-01-02'.replace(re, '$/$/$') +// '02/01/2015' +``` + +上面代码中,`replace`方法的第二个参数是一个字符串,而不是正则表达式。 + +`replace`方法的第二个参数也可以是函数,该函数的参数序列如下。 + +```javascript +'2015-01-02'.replace(re, ( + matched, // 整个匹配结果 2015-01-02 + capture1, // 第一个组匹配 2015 + capture2, // 第二个组匹配 01 + capture3, // 第三个组匹配 02 + position, // 匹配开始的位置 0 + S, // 原字符串 2015-01-05 + groups // 具名组构成的一个对象 {year, month, day} + ) => { + let {day, month, year} = args[args.length - 1]; + return `${day}/${month}/${year}`; +}); +``` + +具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。 + +### 引用 + +如果要在正则表达式内部引用某个“具名组匹配”,可以使用`\k<组名>`的写法。 + +```javascript +const RE_TWICE = /^(?[a-z]+)!\k$/; +RE_TWICE.test('abc!abc') // true +RE_TWICE.test('abc!ab') // false +``` + +数字引用(`\1`)依然有效。 + +```javascript +const RE_TWICE = /^(?[a-z]+)!\1$/; +RE_TWICE.test('abc!abc') // true +RE_TWICE.test('abc!ab') // false +``` + +这两种引用语法还可以同时使用。 + +```javascript +const RE_TWICE = /^(?[a-z]+)!\k!\1$/; +RE_TWICE.test('abc!abc!abc') // true +RE_TWICE.test('abc!abc!ab') // false +``` + diff --git a/docs/set-map.md b/docs/set-map.md index 29f4157..c1bcb48 100644 --- a/docs/set-map.md +++ b/docs/set-map.md @@ -21,7 +21,7 @@ for (let i of s) { 上面代码通过`add`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。 -Set 函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。 +Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。 ```javascript // 例一