【ES6系列】字符串和正则表达式

386 阅读10分钟

更好地Unicode支持

在ES6以前,JavaScript字符串一直基于16位字符编码(UTF-16)进行构建,每16位的序列是一个编码单元(code unit),在过去,16位字符编码足以包含任何字符,直到Unicode引入扩展字符集,编码规则已经不满足需求,不得不进行变更。

  1. UTF-16码位

    • Unicode的目标是为全世界每一个字符提供全球唯一的标识符(码位),而如果编码单元的长度限制在16位,码位数量不足以表示如此多的字符,码位是从0开始的数值,表示字符的这些数值或码位,又称之为字符编码,字符编码必须将码位编码为内部一致的编码单元

    • 在UTF-16中,前216个码位均以16位的编码单元表示,这个范围被称作基本多文种平面,超出这个范围的码位则要归属于某个辅助平面,,其中的码位使用16位就无法表示了,为了UTF-16引入了代理对,其规定用两个16位编码单元表示一个码位

    • 也就是说,字符串里的字符有两种,一种是由一个编码单元16位表示的基本多文种平面,一种是由两个编码单元32位表示的辅助平面字符

    • ES5中,所有字符串的操作都是基于16位编码单元的,如果采用同样的方法处理包含代理对的UTF-16编码字符,得到的结果可能与预期不符合

    • length,charAt()等字符串属性和方法都是基于16位编码单元构造的

    • 举个例子

      '𠮷'.length   //2
      '李'.length   //1
      

      '𠮷'这个字符就是通过代理对表示的,因此length会认为其长度为2,造成的影响有:

      1. 长度判断失败
      2. 匹配单一字符的正则表达式失败
      3. 前后两个16位的编码单元都不表示任何可打印的字符,charAt()不会返回合法的字符串
      4. charCodeAt()方法不能正确识别字符,他会返回每个16位编码单元对应的数值
    • ES6强制使用UTF-16字符串编码来解决上述问题,并按照这种字符编码来标准化字符串操作,在JS中增加了专门针对代理对的功能

  2. codePointAt()方法

    • ES6新增加了完全支持UTF-16的codePointAt()方法,这个方法接受编码单元的位置而非字符位置作为参数,返回与字符串中给定位置对应的码位,即一个整数值

    • MDN源码

      /*! http://mths.be/codepointat v0.1.0 by @mathias */
      if (!String.prototype.codePointAt) {
        (function() {
          'use strict'; // 严格模式,needed to support `apply`/`call` with `undefined`/`null`
          var codePointAt = function(position) {
            if (this == null) {
              throw TypeError();
            }
            var string = String(this);
            var size = string.length;
            // 变成整数
            var index = position ? Number(position) : 0;
            if (index != index) { // better `isNaN`
              index = 0;
            }
            // 边界
            if (index < 0 || index >= size) {
              return undefined;
            }
            // 第一个编码单元
            var first = string.charCodeAt(index);
            var second;
            if ( // 检查是否开始 surrogate pair
              first >= 0xD800 && first <= 0xDBFF && // high surrogate
              size > index + 1 // 下一个编码单元
            ) {
              second = string.charCodeAt(index + 1);
              if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
                // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
                return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
              }
            }
            return first;
          };
          if (Object.defineProperty) {
            Object.defineProperty(String.prototype, 'codePointAt', {
              'value': codePointAt,
              'configurable': true,
              'writable': true
            });
          } else {
            String.prototype.codePointAt = codePointAt;
          }
        }());
      }
      
    • 对于BMP字符集中的字符,codePointAt()方法的返回值与charCodeAt()方法的返回值相同

    • 对于非BMP字符集中的字符来说返回值不同,举例来说

      '𠮷a'.length   //3
      charCodeAt(0)//返回的只是位置0处的第一个编码单元
      codePointAt(0)//返回的是完整的码位,即使这个码位包含多个编码单元
      
    • 要检测一个字符占用的编码单元数量,可以通过codePointAt()方法检测

      function is32Bit(c){
        return c.codePointAt(0) > 0xFFFF;
      }
      

      原理:用16位表示的字符集上界为十六进制FFFF,所有超过这个上界的码位一定是有两个编码单元来表示的

  3. String.fromCodePoint()方法

    • ES通常会面向同一个操作提供正反两种方法:

      1. codePointAt()方法:在字符串中检索一个字符的码位
      2. String.fromCodePoint()方法:根据指定的码位生成一个字符;String.fromCharCode()完整版,可以识别非BMP字符
  4. normalie()方法

    • 在Unicode中,如果我们对两个不同的字符进行排序或者比较操作,会存在一种可能,他们是等效的

    • 定义等效的两种方式:

      1. 二者完全相同,不论是码位还是表现

      2. 兼容性,两个相互兼容的码位序列看起来不同,但是在特定情况下,可以被互相交换使用。举个例子:

        // '\u004F\u030C'~79 780
        // '\u01D1'~465
        String.fromCharCode(79)//"O"
        String.fromCharCode(780)//"̌"
        String.fromCharCode(465)//"Ǒ"
        String.fromCharCode(79,780)//"Ǒ"
        

        码位('\u004F\u030C')和码位('\u01D1')在某种情况下就是可以交换使用的

        但是如果使用正常情况下的“==”或者“===”进行判断,二者是永远不会相等的,永远都是false

        '\u01D1'==='\u004F\u030C' //false
         
        '\u01D1'.length // 1
        '\u004F\u030C'.length // 2
        
    • 针对上面的兼容性的情况,ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

      '\u01D1'.normalize() === '\u004F\u030C'.normalize()  // true
      
    • normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。

      NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。

      NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。

      NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)

      NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。

    • NFC参数返回字符的合成形式,NFD参数返回字符的分解形式。

      '\u004F\u030C'.normalize('NFC').length // 1
      '\u004F\u030C'.normalize('NFD').length // 2
      
    • normalize方法目前不能识别三个或三个以上字符的合成

  5. 正则表达式u修饰符

    • 正则表达式可以完成简单的字符串操作,但是默认将字符串中的没一个字符按照16位编码单元处理,为了解决这个问题,ES6给正则表达式定义了一个支持Unicode的u修饰符

    • 当正则表达式添加u修饰符,他就从编码单元操作模式切换为字符模式,这样正则表达式就不会视代理对为两个字符

    • ES6不支持字符串码位数量的检测,但是通过u修饰符,可以通过正则表达式计算数量

      function codePointLength(text){
        let res = text.match(/[\s\S]/gu)
        return res?res.length:0
      }
      codePointLength('abc') //3
      codePointLength('𠮷a') //3
      

      但是这个方法的运行效率很低

    • 检测u修饰符的支持,u修饰符ES5并不支持,使用之前需要注意

      function hasRegExpU(){
        try{
          let pattern = new RegExp('.','u')
          return true
        }catch(error){
          return false
        }
      }
      

其他字符串变更

  1. 字符串中的子串识别

    • includes()方法,如果在字符串中检测到指定文本,返回true,否则返回false
    • startWith()方法,如果在字符串的起始部位检测到指定文本,返回true,否则返回false
    • endWith()方法,如果在字符串的结束部分检测到指定文本,返回true,否则返回false

    以上的3个方法都接受第二个可选参数,指定一个开始搜索的索引值,指定第二个参数会大大减少字符串被搜索的范围

  2. repeat()语法

    • 该方法接受一个number类型参数,表示该字符串的重复次数,返回值是当前字符串重复一定次数后的新字符串

      'x'.repeat(3) //xxx
      

其他正则表达式语法变更

  1. 正则表达式y修饰符

    • 叫做 “粘连”(sticky)修饰符。用来正确处理匹配粘连的字符串

    • 和g修饰符很像,也是全局匹配,不同之处是y修饰符每次只匹配剩余字符串的头部,如果匹配不到就退出匹配,举个例子:

      let str = "aaa_aa_aaaa"
      let reg_g = /a+/g
      let reg_y = /a+/y
      ​
      reg_g.exec(str)
      // aaa
      reg_y.exec(str)
      // aaa
      ​
      reg_g.exec(str)
      // aa
      reg_y.exec(str)
      // null
      
    • 依据上面的例子,需要补充说明一点:关于lastIndex

      lastIndex就是匹配开始的位置,接受指定

      举例说明:

      let str = "_aaa_aa_aaaa"
      let reg_y = /a+/y
      reg_y.lastIndex = 1
      reg_y.exec(str)
      // aaa
      reg_y.lastIndex = 5
      reg_y.exec(str)
      // aa
      reg_y.lastIndex = 8
      reg_y.exec(str)
      // aaaaa
      

      只有调用exec()和test()这些正则表达式对象的方法,才会涉及到lastIndex属性,调用字符串的方法,例如match,则不会触发粘滞行为

    • 当执行操作时,y修饰符会把上次匹配后面一个字符的索引保存在lastIndex中,如果匹配失败,lastIndex置为0。没有修饰符的正则表达式匹配时则忽略lastIndex的作用

    • 检测y修饰符是否可用

      function hasRegExpU(){
        try{
          let pattern = new RegExp('.','y')
          return true
        }catch(error){
          return false
        }
      }
      
  2. 正则表达式的复制

    • 在ES5中,可以通过给RegExp构造函数传递一个正则表达式参数,来复制这个正则表达式,

      let re1 = /ab/i,
          re2 = new RegExp(re1)
      
    • 在ES5中,在复制正则表达式的时候,不可以为re2添加修饰符,ES6修改了这个问题,即便是复制,也可以在复制的基础上进行修改

      let re1 = /ab/i,
          re2 = new RegExp(re1,'g')//ES5报错,ES6可行
      
  3. flags属性

    • source属性获取正则表达式的文本

    • flags属性获取正则表达式的修饰符

      let re1 = new RegExp(/ab/i)
      let re2 = new RegExp(re1,'g')//re2此时为:/ab/g
      re2.source  //'ab'
      re2.flags   // 'g'
      

模板字面量

  • ES6尝试跳出JavaScript已有的字符串体系对ES5进行填补,通过=模板字面量的方式填补一部分字符串方面的空白:

    1. 多行字符串
    2. 基本的字符串格式化:将变量的值嵌入字符串的能力
    3. HTML转义:向HTML插入经过安全转换后的字符串的能力
  1. 基础语法

    • 反引号“ ` ”代替单双引号,如果在字符串中要使用反引号的话,可以通过转义,在模板字面量中不需要转义单双引号
  2. 多行字符串

    • 在反引号中的所有空白和换行都是有意义的,都属于字符串的一部分
  3. 字符串占位符

    • 占位符由一个$和一对{}组成,在大括号中间可以插入任意的JavaScript表达式

    • 可以通过占位符进行模板字面量的嵌套

      `hello,${`world`}`
      
  4. 标签模板

    • 每个模板标签都可以执行模板字面量上的转换并返回最终的字符串值

    • 模板标签就是一个对模板字面量进行处理的函数(行为上有点像数组的map、reduce函数)

      let tag = function(literals,...substitutions){
        //返回一个字符串
      }
      let message = tag`hello`   //message的值等于tag函数中最后返回的字符串
    • 进一步理解标签函数

      let tag = function(literals,...substitutions){
        //返回一个字符串
      }
      ​
      let count = 10,
          price = 0.5,
          message = tag`${count} items cost $${(count*price).toFixed(2)}.`
      
      1. 标签函数一般使用不定参数特性来定义占位符...substitutions

      2. 此时tag接收到的参数为:

        literals数组:包含元素:['',' items cost $','.'],即以占位符为分隔符,将模板字面量拆开

        substitutions数组:包含占位符所有解析过的值[10,0.50]

      3. literals数组里的第一个元素是空字符串,确保literals[0]是字符串的开端

      4. literals数组的长度总比substitutions的长度多1

    • 在模板字面量中使用原始值

      通过String.raw(),模板标签同样可以访问原生字符串信息,也是就是说通过模板标签可以访问到字符转义被转换成等价字符前的原生字符串

      在标签函数中,literals[0]总有一个等价的literals.raw[0],包含着它的原生字符串信息