Osheep

时光不回头,当下最重要。

ECMAScript字符串扩展

字符的Unicode表示法

  • JavaScript允许采用 \uxxxx 形式表示一个字符,其中“xxxx”表示字符的码点

    《ECMAScript字符串扩展》

    示例
  • 但是,这种表示法只限于 \u0000 —— \uFFFF 之间的字符。超出这个范围的字符,必须用两个双字节的形式表达

    《ECMAScript字符串扩展》

    示例

如果直接在“\u”后面跟上超过 0xFFFF 的数值(比如 \u20BB7 ),JavaScript会理解成“\u20BB+7”。由
于 \u20BB 是一个不可打印字符,所以只会显示一个空格,后面跟着一个7

ES6对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符

《ECMAScript字符串扩展》

示例

JavaScript表示一个字符的6种方法

'\z' === 'z' // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

JavaScript内部,字符以UTF-16的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode码点大于
0xFFFF的字符),JavaScript会认为它们是两个字符。

《ECMAScript字符串扩展》

示例

汉字的UTF-16编码为 0xD842 0xDFB7 (十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript不能正确处理,字符串长度会误判为2,而且 charAt 方法无法读取整个字
符, charCodeAt 方法只能分别返回前两个字节和后两个字节的值
ES6中 codePointAt 方法,能够正确处理4个字节储存的字符,返回一个字符的码点

《ECMAScript字符串扩展》

示例

codePointAt 方法的参数,是字符在字符串中的位置(从0开始)codePointAt方法在第一个字符上正确地识别并返回了它的十进制码点134071(即十六进制的 20BB7 )。在第二个字符(汉字的后一个字节)和第三个字符“a”上, codePointAt 方法的结果与 charCodeAt 方法相同

  • codePointAt 方法会正确返回32位的UTF-16字符的码点。对于那些两个字节储存的常规字符,它的返回结果与 charCodeAt 方法相同。
  • codePointAt 方法返回的是码点的十进制值,如果想要十六进制的值,可以使用 toString 方法转换一下

    《ECMAScript字符串扩展》

    示例

字符 a 在字符串 s 的正确位置序号应该是1,但是必须向 charCodeAt 方法传入2。解决这个问题的一个办法是使用 for…of 循环,因为它会正确识别32位的UTF-16字符

《ECMAScript字符串扩展》

示例

codePointAt 方法可以测试一个字符由两个字节还是由四个字节组成。

《ECMAScript字符串扩展》

示例

String.fromCodePoint()

ES5中 String.fromCharCode 方法,用于从码点返回对应字符,但是这个方法不能识别32位的UTF-16字符(Unicode编号大于 0xFFFF )

《ECMAScript字符串扩展》

示例

String.fromCharCode 不能识别大于 0xFFFF 的码点,所以 0x20BB7 就发生了溢出,最高位 2 被舍弃了,最后返回码点 U+0BB7 对应的字符,而不是码点 U+20BB7 对应的字符

ES6提供了 String.fromCodePoint 方法,可以识别 0xFFFF 的字符,弥补了 String.fromCharCode 方法的不足。在作用上,正好与 codePointAt 方法相反。

《ECMAScript字符串扩展》

示例

如果String.fromCharCode 方法有多个参数,则它们会被合并成一个字符串返回。注意, fromCodePoint 方法定义在 String 对象上,而 codePointAt 方法定义在字符串的实例对象上。

字符串的遍历器接口

for (let codePoint of 'foo') {
   console.log(codePoint)
}
// "f"
// "o"
// "o"

这个遍历器最大的优点是可以识别大于 0xFFFF 的码点,传统的 for 循环无法识别这样的码点

《ECMAScript字符串扩展》

示例

字符串 text 只有一个字符,但是 for 循环会认为它包含两个字符(都不可打印),而 for…of 循环会正确识别出这一个字符

at()

ES5对字符串对象提供 charAt 方法,返回字符串给定位置的字符。该方法不能识别码点大于 0xFFFF 的字符

《ECMAScript字符串扩展》

示例

字符串实例的 at 方法,可以识别Unicode编号大于 0xFFFF 的字符,返回正确的字符

《ECMAScript字符串扩展》

示例

normalize()

为了表示语调和重音符号,Unicode提供了两种方法。一种是直接提供带重音符号的字符,比如 Ǒ (\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如 O (\u004F)和(\u030C)合成 Ǒ (\u004F\u030C)。

这两种表示方法,在视觉和语义上都等价,但是JavaScript不能识别。

'\u01D1'==='\u004F\u030C' //false
'\u01D1'.length // 1
'\u004F\u030C'.length // 2

JavaScript将合成字符视为两个字符,导致两种表示方法不相等
ES6提供字符串实例的 normalize() 方法,用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化

'\u01D1'.normalize()=== '\u004F\u030C'.normalize()
// true

normalize 方法可以接受四个参数

  • NFC ,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
  • NFD ,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。
  • NFKC ,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例, normalize 方法不能识别中文。)
  • NFKD ,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符
'\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():返回布尔值,表示参数字符串是否在源字符串的尾
var s = Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置

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 次

'x'.repeat(3) // "xxx"
'na'.repeat(0) // ""

参数如果是小数,会被取整。

'na'.repeat(2.9) // "nan

如果 repeat 的参数是负数或者 Infinity ,会报错

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

参数 NaN 等同于0;如果 repeat 的参数是字符串,则会先转换成

'na'repeat(NaN) // ""
'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"

padStart(),padEnd()

  • 如果某个字符串不够指定长度,会在头部或尾部补全。 padStart 用于头部补全, padEnd 用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
  • 如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串
  • 如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串
  • 如果省略第二个参数,则会用空格补全长度
    用于提示字符串格式。
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-1

模板字符串

ES6引入了模板字符串

$("#result").append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量

// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

字符串都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义

var greeting = `\`Yo\` World!`;
  • 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中
  • 字符串中嵌入变量,需要将变量名写在 ${} 之中
function authorize(user, action) {
   if (!user.hasPrivilege(action)) {
      throw new Error(
        // 传统写法为
        // 'User '
        // + user.name
        // + ' is not authorized to do '
        // + action
        // + '.'
        `User ${user.name} is not authorized to do ${action}.`);
      }
}
  • 大括号内部可以放入任意的JavaScript表达式
var x 1;
var y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
var obj = {x: 1, y: 2};
`${obj.x + obj.y}`
//3
  • 模板字符串之中还能调用
function fn() {
   return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar

如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的 toString 方法。

  • 如果模板字符串中的变量没有声明,将报错
// 变量place没有声明
var msg = `Hello, ${place}`;
// 报错
  • 如果大括号内部是一个字符串,将会原样输出
`Hello ${'World'}`
// "Hello World"

模板字符串还能嵌套

const tmpl = addrs => `
    <table>
   ${addrs.map(addr => `
      <tr><td>${addr.first}</td></tr>
      <tr><td>${addr.last}</td></tr>
   `).join('')}
   </table>
`;
const data = [
   { first: '<Jane>', last: 'Bond' },
   { first: 'Lars', last: '<Croft>' },
];
console.log(tmpl(data));
// <table>
//    <tr><td><Jane></td></tr>
//    <tr><td>Bond</td></tr>
//    <tr><td>Lars</td></tr>
//    <tr><td><Croft></td></tr>
// </table>

需要引用模板字符串本身,在需要时执行如下

// 写法一
let str = 'return ' + '`Hello ${name}!`';
let func = new Function('name', str);
func('Jack') // "Hello Jack!"
// 写法二
let str = '(name) => `Hello ${name}!`';
let func = eval.call(null, str);
func('Jack') // "Hello Jack

实例:模板编译

个通过模板字符串,生成正式模板

var template = `
<ul>
   <% for(var i=0; i < data.supplies.length; i++) { %>
   <li><%= data.supplies[i] %></li>
   <% } %>
</ul>
`;

上面代码在模板字符串中使用 <%…%> 放置JavaScript代码,使用 <%= … %> 输出JavaScript表达式。

如何编译这个模板字符串呢?

  • 是将其转换为JavaScript表达式字符串。
echo('<ul>');
for(var i=0; i < data.supplies.length; i++) {
   echo('<li>');
   echo(data.supplies[i]);
   echo('</li>');
};
echo('</ul>');
  • 这个转换使用正则表达式就行了
var evalExpr = /<%=(.+?)%>/g;
var expr = /<%([\s\S]+?)%>/g;
template = template
   .replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
   .replace(expr, '`); \n $1 \n echo(`');
template = 'echo(`' + template + '`);';
  • 然后,将 template 封装在一个函数里面返回
var script =
`(function parse(data){
   var output = "";
   function echo(html){
      output += html;
   }
   ${ template }
   return output;
})`;
return script;
  • 将上面的内容拼装成一个模板编译函数 compile
function compile(template){
   var evalExpr = /<%=(.+?)%>/g;
   var expr = /<%([\s\S]+?)%>/g;
   template = template
      .replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
      .replace(expr, '`); \n $1 \n echo(`');
   template = 'echo(`' + template + '`);';
   var script =
   `(function parse(data){
      var output = "";
      function echo(html){
         output += html
      }
      ${ template}
         return output;
   })`;
   return script
}
  • compile 函数的用法
var parse = eval(compile(template));
div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>

标签模板

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数

var a = 5;
var b = 10;
tag`Hello ${ a + b } world ${ a * b }`;

上面代码中,模板字符串前面有一个标识名 tag ,它是一个函数。整个表达式的返回值,就是 tag 函数处理模板字符串后的返回值。
函数 tag 依次会接收到多个参数

function tag(stringArr, value1, value2){
// ...
}
// 等同于
function tag(stringArr, ...values){
// ...
}

tag 函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。
tag 函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此 tag 会接受到 value1 和 value2 两个参数。
tag 函数所有参数的实际值如下

  • 第一个参数: [‘Hello ‘, ‘ world ‘, ”]
  • 第二个参数: 15
  • 第三个参数:50
    tag 函数实际上以下面的形式调用
tag(['Hello ', ' world ', ''], 15, 50)

下面是 tag 函数的一种写法,以及运行结果

var a = 5;
var b = 10;
function tag(s, v1, v2) {
console.log(s[0]);
console.log(s[1]);
console.log(s[2]);
console.log(v1);
console.log(v2);
return "OK";
}
tag`Hello ${ a + b } world ${ a * b}`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK
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)"

上面展示了,如何将各个参数按照原来的位置拼合回去
passthru 函数采用rest参数的写法如下

function passthru(literals, ...values) {
   var output = "";
   for (var index = 0; index < values.length; index++) {
      output += literals[index] + values[index];
   }
   output += literals[index]
   return output;
}

“标签模板”的一个重要应用,就是过滤HTML字符串,防止用户输入恶意内容

var message =
   SaferHTML`<p>${sender} has sent you a message.</p>`;
function SaferHTML(templateData) {
   var s = templateData[0];
   for (var i = 1; i < arguments.length; i++) {
      var arg = String(arguments[i]);
      // Escape special characters in the substitution.
      s += arg.replace(/&/g, "&amp;")
                   .replace(/</g, "&lt;")
                   .replace(/>/g, "&gt;");
      // Don't escape special characters in the template.
      s += templateData[i];
   }
   return s;
}

经过 SaferHTML 函数处理,HTML字符串的特殊字符都会被转义

String.raw()

ES6为原生的String对象,提供了一个 raw 方法。
String.raw 方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。

String.raw`Hi\n${2+3}!`;
// "Hi\\n5!"
String.raw`Hi\u000A!`;
// 'Hi\\u000A!'

如果原字符串的斜杠已经转义,那么 String.raw 不会做任何处理

String.raw 的代码基本如下

String.raw = function (strings, ...values) {
var output = "";
for (var index = 0; index < values.length; index++) {
output += strings.raw[index] + values[index];
}
output += strings.raw[index]
return output;
}

String.raw 方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串
来使用。
String.raw 方法也可以作为正常的函数使用。这时,它的第一个参数,应该是一个具有 raw 属性的对象,且 raw 属性的
值应该是一个数组

String.raw({ raw: 'test' }, 0, 1, 2);
// 't0e1s2t'
// 等同于
String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2
点赞