1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00
es6tutorial/docs/string.md
2015-03-28 07:36:36 +08:00

594 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 字符串的扩展
ES6加强了对Unicode的支持并且扩展了字符串对象。
## codePointAt()
JavaScript内部字符以UTF-16的格式储存每个字符固定为2个字节。对于那些需要4个字节储存的字符Unicode码点大于0xFFFF的字符JavaScript会认为它们是两个字符。
```javascript
var s = "𠮷";
s.length // 2
s.charAt(0) // ''
s.charAt(1) // ''
s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271
```
上面代码中汉字“𠮷”的码点是0x20BB7UTF-16编码为0xD842 0xDFB7十进制为55362 57271需要4个字节储存。对于这种4个字节的字符JavaScript不能正确处理字符串长度会误判为2而且charAt方法无法读取字符charCodeAt方法只能分别返回前两个字节和后两个字节的值。
ES6提供了codePointAt方法能够正确处理4个字节储存的字符返回一个字符的码点。
```javascript
var s = "𠮷a";
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.charCodeAt(2) // 97
```
codePointAt方法的参数是字符在字符串中的位置从0开始。上面代码中JavaScript将“𠮷a”视为三个字符codePointAt方法在第一个字符上正确地识别了“𠮷”返回了它的十进制码点134071即十六进制的20BB7。在第二个字符即“𠮷”的后两个字节和第三个字符“a”上codePointAt方法的结果与charCodeAt方法相同。
总之codePointAt方法会正确返回四字节的UTF-16字符的码点。对于那些两个字节储存的常规字符它的返回结果与charCodeAt方法相同。
codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
```javascript
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit("𠮷") // true
is32Bit("a") // false
```
## String.fromCodePoint()
ES5提供String.fromCharCode方法用于从码点返回对应字符但是这个方法不能识别辅助平面的字符编号大于0xFFFF
```javascript
String.fromCharCode(0x20BB7)
// "ஷ"
```
上面代码中最后返回码点U+0BB7对应的字符而不是码点U+20BB7对应的字符。
ES6提供了String.fromCodePoint方法可以识别0xFFFF的字符弥补了String.fromCharCode方法的不足。在作用上正好与codePointAt方法相反。
```javascript
String.fromCodePoint(0x20BB7)
// "𠮷"
```
注意fromCodePoint方法定义在String对象上而codePointAt方法定义在字符串的实例对象上。
## at()
ES5提供String.prototype.charAt方法返回字符串给定位置的字符。该方法不能识别码点大于0xFFFF的字符。
```javascript
'𠮷'.charAt(0)
// '\uD842'
```
上面代码中charAt方法返回的是UTF-16编码的第一个字节实际上是无法显示的。
ES7提供了at方法可以识别Unicode编号大于0xFFFF的字符返回正确的字符。
```javascript
'𠮷'.at(0)
// '𠮷'
```
## 字符的Unicode表示法
JavaScript允许采用“\uxxxx”形式表示一个字符其中“xxxx”表示字符的码点。
```javascript
"\u0061"
// "a"
```
但是,这种表示法只限于\u0000——\uFFFF之间的字符。超出这个范围的字符必须用两个双字节的形式表达。
```javascript
"\uD842\uDFB7"
// "𠮷"
"\u20BB7"
// " 7"
```
上面代码表示,如果直接在“\u”后面跟上超过0xFFFF的数值比如\u20BB7JavaScript会理解成“\u20BB+7”。由于\u20BB是一个不可打印字符所以只会显示一个空格后面跟着一个7。
ES6对这一点做出了改进只要将码点放入大括号就能正确解读该字符。
```javascript
"\u{20BB7}"
// "𠮷"
"\u{41}\u{42}\u{43}"
// "ABC"
```
## 正则表达式的u修饰符
ES6对正则表达式添加了u修饰符用来正确处理大于\uFFFF的Unicode字符。
**1点字符**
点(.字符在正则表达式中解释为除了换行以外的任意单个字符。对于码点大于0xFFFF的Unicode字符点字符不能识别必须加上u修饰符。
```javascript
var s = "𠮷";
/^.$/.test(s) // false
/^.$/u.test(s) // true
```
上面代码表示如果不添加u修饰符正则表达式就会认为字符串为两个字符从而匹配失败。
**2Unicode字符表示法**
ES6新增了使用大括号表示Unicode字符这种表示法在正则表达式中必须加上u修饰符才能识别。
```javascript
/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true
```
上面代码表示如果不加u修饰符正则表达式无法识别`\u{61}`这种表示法只会认为这匹配61个连续的u。
**3量词**
使用u修饰符后所有量词都会正确识别大于码点大于0xFFFF的Unicode字符。
```javascript
/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true
```
**4预定义模式**
u修饰符也影响到预定义模式能否正确识别码点大于0xFFFF的Unicode字符。
```javascript
/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷')
```
上面代码的`\S`是预定义模式匹配所有不是空格的字符。只有加了u修饰符它才能正确匹配码点大于0xFFFF的Unicode字符。
利用这一点,可以写出一个正确返回字符串长度的函数。
```javascript
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
var s = "𠮷𠮷";
s.length // 4
codePointLength(s) // 2
```
**5i修饰符**
有些Unicode字符的编码不同但是字型很相近比如\u004B与\u212A都是大写的K。
```javascript
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
```
上面代码中不加u修饰符就无法识别非规范的K字符。
## normalize()
为了表示语调和重音符号Unicode提供了两种方法。一种是直接提供带重音符号的字符比如Ǒ\u01D1。另一种是提供合成符号combining character即原字符与重音符号的合成两个字符合成一个字符比如O\u004F和ˇ\u030C合成Ǒ\u004F\u030C
这两种表示方法在视觉和语义上都等价但是JavaScript不能识别。
```javascript
'\u01D1'==='\u004F\u030C' //false
'\u01D1'.length // 1
'\u004F\u030C'.length // 2
```
上面代码表示JavaScript将合成字符视为两个字符导致两种表示方法不相等。
ES6提供String.prototype.normalize()方法用来将字符的不同表示方法统一为同样的形式这称为Unicode正规化。
```javascript
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true
```
normalize方法可以接受四个参数。
- NFC默认参数表示“标准等价合成”Normalization Form Canonical Composition返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
- NFD表示“标准等价分解”Normalization Form Canonical Decomposition即在标准等价的前提下返回合成字符分解的多个简单字符。
- NFKC表示“兼容等价合成”Normalization Form Compatibility Composition返回合成字符。所谓“兼容等价”指的是语义上存在等价但视觉上不等价比如“囍”和“喜喜”。
- NFKD表示“兼容等价分解”Normalization Form Compatibility Decomposition即在兼容等价的前提下返回合成字符分解的多个简单字符。
```javascript
'\u004F\u030C'.normalize('NFC').length // 1
'\u004F\u030C'.normalize('NFD').length // 2
```
上面代码表示NFC参数返回字符的合成形式NFD参数返回字符的分解形式。
不过normalize方法目前不能识别三个或三个以上字符的合成。这种情况下还是只能使用正则表达式通过Unicode编号区间判断。
## includes(), startsWith(), endsWith()
传统上JavaScript只有indexOf方法可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。
- **includes()**:返回布尔值,表示是否找到了参数字符串。
- **startsWith()**:返回布尔值,表示参数字符串是否在源字符串的头部。
- **endsWith()**:返回布尔值,表示参数字符串是否在源字符串的尾部。
```javascript
var s = "Hello world!";
s.startsWith("Hello") // true
s.endsWith("!") // true
s.includes("o") // true
```
这三个方法都支持第二个参数,表示开始搜索的位置。
```javascript
var s = "Hello world!";
s.startsWith("world", 6) // true
s.endsWith("Hello", 5) // true
s.includes("Hello", 6) // false
```
上面代码表示使用第二个参数n时endsWith的行为与其他两个方法有所不同。它针对前n个字符而其他两个方法针对从第n个位置直到字符串结束。
## repeat()
repeat()返回一个新字符串表示将原字符串重复n次。
```javascript
"x".repeat(3) // "xxx"
"hello".repeat(2) // "hellohello"
```
## 正则表达式的y修饰符
除了u修饰符ES6还为正则表达式添加了y修饰符叫做“粘连”sticky修饰符。它的作用与g修饰符类似也是全局匹配后一次匹配都从上一次匹配成功的下一个位置开始不同之处在于g修饰符只确保剩余位置中存在匹配而y修饰符确保匹配必须从剩余的第一个位置开始这也就是“粘连”的涵义。
```javascript
var s = "aaa_aa_a";
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
```
上面代码有两个正则表达式一个使用g修饰符另一个使用y修饰符。这两个正则表达式各执行了两次第一次执行的时候两者行为相同剩余字符串都是“_aa_a”。由于g修饰没有位置要求所以第二次执行会返回结果而y修饰符要求匹配必须从头部开始所以返回null。
如果改一下正则表达式保证每次都能头部匹配y修饰符就会返回结果了。
```javascript
var s = "aaa_aa_a";
var r = /a+_/y;
r.exec(s) // ["aaa_"]
r.exec(s) // ["aa_"]
```
上面代码每次匹配,都是从剩余字符串的头部开始。
进一步说y修饰符号隐含了头部匹配的标志ˆ
```javascript
/b/y.exec("aba")
// null
```
上面代码由于不能保证头部匹配所以返回null。y修饰符的设计本意就是让头部匹配的标志ˆ在全局匹配中都有效。
与y修饰符相匹配ES6的正则对象多了sticky属性表示是否设置了y修饰符。
```javascript
var r = /hello\d/y;
r.sticky // true
```
## 模板字符串
在ES6输出模板通常是这样写的。
```js
$("#result").append(
"There are <b>" + basket.count + "</b> " +
"items in your basket, " +
"<em>" + basket.onSale +
"</em> are on sale!"
);
```
上面这种写法相当繁琐不方便ES6引入了模板字符串解决这个问题。
```js
$("#result").append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
```
模板字符串template string是增强版的字符串用反引号`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
```javascript
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
```
上面代码中的字符串,都是用反引号表示。如果在模板字符串中嵌入变量,需要将变量名写在`${}`之中。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
```javascript
var greeting = `\`Yo\` World!`;
```
大括号内部可以进行运算,以及引用对象属性。
```javascript
var x = 1;
var y = 2;
console.log(`${x} + ${y} = ${x+y}`)
// "1 + 2 = 3"
console.log(`${x} + ${y*2} = ${x+y*2}`)
// "1 + 4 = 5"
var obj = {x: 1, y: 2};
console.log(`${obj.x + obj.y}`)
// 3
```
模板字符串之中还能调用函数。
```javascript
function fn() {
return "Hello World";
}
console.log(`foo ${fn()} bar`);
// foo Hello World bar
```
如果模板字符串中的变量没有声明,将报错。
```javascript
var msg = `Hello, ${place}`;
// throws error
```
模板字符串使得字符串与变量的结合,变得容易。下面是一个例子。
```javascript
if (x > MAX) {
throw new Error(`Most ${MAX} allowed: ${x}!`);
// 传统写法为'Most '+MAX+' allowed: '+x+'!'
}
```
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。
```javascript
var a = 5;
var b = 10;
tag`Hello ${ a + b } world ${ a * b}`;
```
上面代码中模板字符串前面有一个函数tag整个表达式将返回tag处理模板字符串后的返回值。
函数tag依次接受三个参数。第一个参数是一个数组该数组的成员是模板字符串中那些没有变量替换的部分也就是说变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间以此类推。第一个参数之后的参数都是模板字符串各个变量被替换后的值。
- 第一个参数:['Hello ', ' world ']
- 第二个参数: 15
- 第三个参数50
也就是说tag函数实际的参数如下。
```javascript
tag(['Hello ', ' world '], 15, 50)
```
下面是tag函数的代码以及运行结果。
```javascript
var a = 5;
var b = 10;
function tag(s, v1, v2) {
console.log(s[0]);
console.log(s[1]);
console.log(v1);
console.log(v2);
return "OK";
}
tag`Hello ${ a + b } world ${ a * b}`;
// "Hello "
// " world "
// 15
// 50
// "OK"
```
下面是一个更复杂的例子。
```javascript
var total = 30;
var msg = passthru`The total is ${total} (${total*1.05} with tax)`;
function passthru(literals) {
var result = "";
var i = 0;
while (i < literals.length) {
result += literals[i++];
if (i < arguments.length) {
result += arguments[i];
}
}
return result;
}
msg
// "The total is 30 (31.5 with tax)"
```
上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。
处理函数的第一个参数还有一个raw属性。它也是一个数组成员与处理函数的第一个参数完全一致唯一的区别是字符串被转义前的原始格式这是为了模板函数处理的方便而提供的。
```javascript
tag`First line\nSecond line`
```
上面代码中tag函数的第一个参数是一个数组`["First line\nSecond line"]`这个数组有一个raw属性等于`["First line\\nSecond line"]`,两者唯一的区别就是斜杠被转义了。
```javascript
function tag(strings) {
console.log(strings.raw[0]);
// "First line\\nSecond line"
}
```
## String.raw()
String.raw方法往往用来充当模板字符串的处理函数返回字符串被转义前的原始格式。
```javascript
String.raw`Hi\n${2+3}!`;
// "Hi\\n5!"
String.raw`Hi\u000A!`;
// 'Hi\\u000A!'
```
String.raw方法也可以正常的函数形式使用。这时它的第一个参数应该是一个具有raw属性的对象且raw属性的值应该是一个数组。
```javascript
String.raw({ raw: 'test' }, 0, 1, 2);
// 't0e1s2t'
// 等同于
String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
```