美文网首页
ES6-02 字符串与正则表达式

ES6-02 字符串与正则表达式

作者: 小线亮亮 | 来源:发表于2018-06-21 10:34 被阅读0次

    ES6学习笔记-字符串与正则表达式

    • JS字符串编码

    在ES6之前,JS的字符串以16位字符编码(UCS-2)为基础。

    每个 16 位序列都是一个码元( code unit ),用于表示一个字符。

    字符串所有的属性与方法都是基于 16 位的码元。

    原本16 位足以容纳任何字符,由于Unicode 引入了扩展字符集,那么就不够用了。

    UTF-16 是变长的字符编码方式,有 16 位与 32 位两种情况。 JS 原先使用的则是固定 16 位(双字节)的字符编码方式,即 UCS-2 。

    Unicode目标是给世界上所有的字符提供全局唯一标识符,而 16 位的字符长度限制已不能满足这种需求。
    这些全球唯一标识符被称为代码点( code points ),是从 0 开始的简单数字。
    代码点是如你想象的字符代码那样,用一个数字来代表一个字符
    字符编码要求将代码点转换为内部一致的码元,而对于 UTF-16 来说,代码点可以由多个码元组成

    在 UTF-16 中的第一个 2^16 代码点表示单个 16 位码元,这个范围被称为多语言基本平面(
    Basic Multilingual Plane , BMP )。
    任何超出该范围的代码点都不能用单个 16 位码元表示,而是会落在扩展平面( supplementary planes )内。
    UTF-16 引入了代理对(surrogate pairs )来解决这个问题,允许使用两个 16 位码元来表示单个代码点。
    这意味着字符串内的任意单个字符都可以用一个码元(共 16 位)或两个码元(共 32 位)来表示,
    前者对应基本平面字符,而后者对应扩展平面字符。

    在ES5中,所有字符串操作都基于16位码元,那么在处理包含代理对的 UTF-16 字符时会出现预期外的结果。

    var text = "𠮷" ;// 使用了代理对,“𠮷”当作两个16位字符对待。
    console.log(text.length); // 2,而不是1
    console.log(/^.$/.test(text)); // false,匹配失败,这里有两个字符。 
    console.log(text.charAt(0)); // "" ,16位代码点都不是可打印字符。
    console.log(text.charAt(1)); // ""
    console.log(text.charCodeAt(0)); // 55362 // 返回每个码元的16位数字。
    console.log(text.charCodeAt(1)); // 57271
    
    • codePointAt() 方法

    ES6为全面支持 UTF-16 而新增的方法,可以在给定字符串中按位置提取 Unicode 代码点。
    该方法接受的是码元位置而非字符位置,并返回一个整数值。

    var text = "𠮷a";
    console.log(text.charCodeAt(0)); // 55362
    console.log(text.charCodeAt(1)); // 57271
    console.log(text.charCodeAt(2)); // 97
    console.log(text.codePointAt(0)); // 134071
    console.log(text.codePointAt(1)); // 57271
    console.log(text.codePointAt(2)); // 97
    

    操作对象是 BMP 字符,则codePointAt()与charCodeAt()返回值相同。
    text 字符串的第一个字符不是 BMP 字符,因此它占用了两个码元,字符串的length 属性是 3 而不是 2 。
    charCodeAt() 方法只返回了位置 0 的第一个码元;而codePointAt() 返回的是完整的代码点,即使它占用了多个码元。

    判断字符包含了一个还是两个码元

    function is32Bit(c) {
        return c.codePointAt(0) > 0xFFFF;
    }
    console.log(is32Bit("")); // true
    console.log(is32Bit("a")); // false
    

    16 位字符的上边界用十六进制表示就是 FFFF ,因此任何大于该数字的代码点必须用两个码元(共 32 位)来表示。

    • String.fromCodePoint() 方法

    codePointAt()来提取字符串内中某个字符的代码点,String.fromCodePoint()用给定的代码点来产生包含单个字符的字符串。

    console.log(String.fromCodePoint(134071)); // "𠮷"
    

    可以将 String.fromCodePoint() 视为 String.fromCharCode() 的完善版本。两者处理 BMP 字
    符时会返回相同结果,只有处理 BMP 范围之外的字符时才会有差异。

    • normalize() 方法

    ES6 给字符串提供了 normalize() 方法,以支持 Unicode 标准形式。

    Unicode 标准形式:
    Normalization Form Canonical Composition ( "NFC" ),这是默认值;
    Normalization Form Canonical Decomposition ( "NFD" );
    Normalization Form Compatibility Composition ( "NFKC" );
    Normalization Form Compatibility Decomposition ( "NFKD" )。

    当比较字符串时,它们必须被标准化为同一种形式。

    // 将values数组中的字符串转换为一种标准形式,以便让转换后的数组可以被正确排序。
    var normalized = values.map(function (text) {
        return text.normalize();
    });
    normalized.sort(function (first, second) {
        if (first < second) {
            return -1;
        } else if (first === second) {
            return 0;
        } else {
            return 1;
        }
    });
    
    // 比较过程中调用 normalize() 来对原始数组进行排序。    
    values.sort(function (first, second) {
        var firstNormalized = first.normalize(),
            secondNormalized = second.normalize();
        if (firstNormalized < secondNormalized) {
            return -1;
        } else if (firstNormalized === secondNormalized) {
            return 0;
        } else {
            return 1;
        }
    });
    
    

    first 与 second 再一次使用同一方式被标准化了。

    • 正则表达式 u 标志

    正则表达式假定单个字符使用一个 16 位的码元来表示。
    为了解决这个问题,ES6为正则表达式定义了用于处理Unicode的u标志。

    u 标志如何运作:

    当一个正则表达式设置了u标志时,它的工作模式将切换到针对字符,而不是针对码元。
    这意味着正则表达式将不会被字符串中的代理对所混淆,而是会如预期那样工作。

    var text = "𠮷";
    console.log(text.length); // 2
    console.log(/^.$/.test(text)); // false,不使用 u 标志时,该正则表达式只匹配码元。
    console.log(/^.$/u.test(text)); // true,启用 u 标志后,正则表达式就会比较字符而不是码元。
    

    计算代码点数量

    ES6 并没有添加方法用于判断一个字符串包含多少个代码点。

    可借助 u 标志,使用正则表达式来进行计算。

    function codePointLength(text) {
        var result = text.match(/[\s\S]/gu);
        return result ? result.length : 0;
    }
    console.log(codePointLength("abc")); // 3
    console.log(codePointLength("𠮷bc")); // 3
    /*此例调用了 match() 方法来检查 text 中的空白字符与非空白字符(使用 [\s\S] 以确保
    该模式能匹配换行符),所用的正则表达式启用了全局与 Unicode 特性。在匹配至少成功一
    次的情况下, result 变量会是包含匹配结果的数组,因此该数组的长度就是字符串中代码
    点的数量。在 Unicode 中,字符串 "abc" 与 "bc" 同样包含三个字符,所以数组长度为 3
    。
    虽然这种方法可用,但它并不快,尤其在操作长字符串时。你也可以使用字符串的迭代
    器来达到相同目的。一般来说,只要有可能就应尽量减少对代码点数量的计算。
    */
    

    判断是否支持 u 标志

    使用一个函数来判断是否支持 u 标志是最安全的方式

    function hasRegExpU() {
        try {
            var pattern = new RegExp(".", "u");
            return true;
        } catch (ex) {
            return false;
        }
    }
    /*
    若你的代码仍然需要在旧版 JS 引擎中工作,那么在使用 u 标志时应当始终使用
    RegExp 构造器。这会防止语法错误,并允许你有选择地检测并使用 u 标志,而不会导
    致执行被中断。
    */
    
    • 识别子字符串的方法

    includes()方法,在给定文本存在于字符串中的任意位置时会返回true,否则返回false;
    startsWith()方法,在给定文本出现在字符串起始处时返回true,否则返回false;
    endsWith()方法,在给定文本出现在字符串结尾处时返回true ,否则返回false。

    每个方法都接受两个参数:需要搜索的文本,以及可选的搜索起始位置索引。
    当提供了第二个参数时, includes() 与 startsWith() 方法会从该索引位置开始尝试匹配;
    而endsWith() 方法会将字符串长度减去该参数,以此为起点开始尝试匹配。
    当第二个参数未提供时, includes() 与 startsWith() 方法会从字符串起始处开始查找,
    而 endsWith() 方法则从尾部开始。实际上,第二个参数减少了搜索字符串的次数。

    var msg = "Hello world!";
    console.log(msg.startsWith("Hello")); // true
    console.log(msg.endsWith("!")); // true
    console.log(msg.includes("o")); // true
    console.log(msg.startsWith("o")); // false
    console.log(msg.endsWith("world!")); // true
    console.log(msg.includes("x")); // false
    console.log(msg.startsWith("o", 4)); // true
    console.log(msg.endsWith("o", 8)); // true
    console.log(msg.includes("o", 8)); // false
    

    若要找到它们在另一个字符串中的确切位置,则需要使用 indexOf() 和 lastIndexOf()。

    如果向 startsWith() 、 endsWith() 或 includes() 方法传入了正则表达式而不是字符
    串,会抛出错误。这与 indexOf() 以及 lastIndexOf() 方法的表现形成了反差,它们会
    将正则表达式转换为字符串并搜索它。

    • repeat() 方法

    repeat() 方法,接受一个参数作为字符串的重复次数,返回一个将初始字符串重复指定次数的新字符串。

    console.log("x".repeat(3)); // "xxx"
    console.log("hello".repeat(2)); // "hellohello"
    console.log("abc".repeat(4)); // "abcabcabcabc"
    
    • 正则表达式 y 标志

    在Firefox实现了对正则表达式y标志的专有扩展之后,ES6 将该实现标准化。
    y标志影响正则表达式搜索时的粘连( sticky )属性,它表示从正则表达式的 lastIndex 属性值的
    位置开始检索字符串中的匹配字符。如果在该位置没有匹配成功,那么正则表达式将停止检
    索。

    var text = "hello1 hello2 hello3",
        pattern = /hello\d\s?/,
        result = pattern.exec(text),
        globalPattern = /hello\d\s?/g,
        globalResult = globalPattern.exec(text),
        stickyPattern = /hello\d\s?/y,
        stickyResult = stickyPattern.exec(text);
    console.log(result[0]); // "hello1 "
    console.log(globalResult[0]); // "hello1 "
    console.log(stickyResult[0]); // "hello1 "
    
    // 表示三个模式的正则表达式都应当从第二个字符开始尝试匹配。
    pattern.lastIndex = 1;
    globalPattern.lastIndex = 1;
    stickyPattern.lastIndex = 1;
    
    result = pattern.exec(text);//不使用任何标志的正则表达式完全忽略了对于lastIndex的更改
    globalResult = globalPattern.exec(text);//而使用g标志的正则表达式继续匹配了 "hello2 "
    stickyResult = stickyPattern.exec(text);//粘连的正则表达式则在第二个字符处没有匹配成功
    console.log(result[0]); // "hello1 "
    console.log(globalResult[0]); // "hello2 "
    console.log(stickyResult[0]); // Error! stickyResult is null
    

    一旦匹配操作成功,粘连标志就会将匹配结果之后的那个字符的索引值保存在 lastIndex
    中;若匹配未成功,那么 lastIndex 的值将重置为 0 。全局标志的行为与其相同。

    var text = "hello1 hello2 hello3",
        pattern = /hello\d\s?/,
        result = pattern.exec(text),
        globalPattern = /hello\d\s?/g,
        globalResult = globalPattern.exec(text),
        stickyPattern = /hello\d\s?/y,
        stickyResult = stickyPattern.exec(text);
    console.log(result[0]); // "hello1 "
    console.log(globalResult[0]); // "hello1 "
    console.log(stickyResult[0]); // "hello1 "
    console.log(pattern.lastIndex); // 0
    console.log(globalPattern.lastIndex); // 7
    console.log(stickyPattern.lastIndex); // 7
    result = pattern.exec(text);
    globalResult = globalPattern.exec(text);
    stickyResult = stickyPattern.exec(text);
    console.log(result[0]); // "hello1 "
    console.log(globalResult[0]); // "hello2 "
    console.log(stickyResult[0]); // "hello2 "
    console.log(pattern.lastIndex); // 0
    console.log(globalPattern.lastIndex); // 14
    console.log(stickyPattern.lastIndex); // 14
    

    对于 stickyPattern 和 globalPattern 模式变量来说,第一次调用之后 lastIndex 的值均
    被更改为 7 ,而第二次则均被改为 14。

    1. 只有调用正则表达式对象上的方法(例如 exec() 与 test() 方法), lastIndex 属性
      才会生效。而将正则表达式作为参数传递给字符串上的方法(例如 match() ),并不会
      体现粘连特性。
    2. 当使用 ^ 字符来匹配字符串的起始处时,粘连的正则表达式只会匹配字符串的起始处
      (或者在多行模式下匹配行首)。当 lastIndex 为 0 时, ^ 不会让粘连的正则表达式
      与非粘连的有任何区别;而当 lastIndex 在单行模式下不对应整个字符串起始处,或者
      当它在多行模式下不对应行首时,粘连的正则表达式永远不会匹配成功。

    根据sticky属性来检测 y 标志是否存在

    var pattern = /hello\d/y;
    console.log(pattern.sticky);
    // sticky属性由y标志存在与否决定,是只读的,它的值不能在代码中修改。
    

    检测JS 引擎是否支持y标志

    function hasRegExpY() {
        try {
            var pattern = new RegExp(".", "y");
            return true;
        } catch (ex) {
            return false;
        }
    }
    /*
    在旧版JS引擎中运行的代码中使用y标志,请确保使用 RegExp 构造器来定义正则表达式,以避免语法错误。
    */
    
    • 复制正则表达式

    ES5中可以将正则表达式传递给RegExp构造器来复制它

    var re1 = /ab/i,
    re2 = new RegExp(re1);
    // re2 变量只是 re1 的一个副本
    // 若向RegExp构造器传递了第二个参数,即正则表达式的标志,那么该代码就无法工作
    

    ES6允许使用第二个参数,并且让它覆盖第一个参数中的标志

    var re1 = /ab/i,
    // ES5 中会抛出错误, ES6 中可用
    re2 = new RegExp(re1, "g");
    console.log(re1.toString()); // "/ab/i"
    console.log(re2.toString()); // "/ab/g"
    console.log(re1.test("ab")); // true
    console.log(re2.test("ab")); // true
    console.log(re1.test("AB")); // true
    console.log(re2.test("AB")); // false
    
    • flags属性

    es6新增了与标志关联的属性。

    在ES5中,使用source属性来获取正则表达式的文本。
    若想获取标志字符串,必须解析 toString() 方法的输出。

    function getFlags(re) {
        var text = re.toString();
        return text.substring(text.lastIndexOf("/") + 1, text.length);
    }
    // toString() 的输出为 "/ab/g"
    var re = /ab/g;
    console.log(getFlags(re)); // "g"
    // 将正则表达式转换为一个字符串,并返回了最后一个 / 之后的字符,即标志。
    

    ES6 新增了flags 属性用于配合 source 属性,让标志的获取变得更容易。
    这两个属性均为只有 getter 的原型访问器属性,因此都是只读的。
    flags 属性使得检查正则表达式更容易,有助于调试与继承。

    var re = /ab/g;
    console.log(re.source); // "ab"
    console.log(re.flags); // "g"
    

    使用 source 和 flags 允许你直接提取正则表达式的组成部分,而不必将正则表达式转换为字符串。

    • 模板字面量

    ES6的模板字面量( template literal )提供了创建领域专用语言( domain-specific language ,
    DSL )的语法,与 ES5 及更早版本的解决方案相比,处理内容可以更安全。
    本方案通过语法糖扩展了 ECMAScript 的语法,允许语言库提供 DSL 以便制作、查询并
    操纵来自于其它语言的内容,并且对注入攻击( 如 XSS 、 SQL 注入,等等 )能够免疫
    或具有抗性。
    模板字面量是 ES6 针对 JS 直到 ES5 依然完全缺失的如下功能的回应:
    多行字符串:针对多行字符串的形式概念;
    基本的字符串格式化:将字符串部分替换为已存在的变量值的能力;
    HTML 转义:能转换字符串以便将其安全插入到 HTML 中的能力。
    模板字面量以一种新的方式解决了这些问题,而并未给 JS 已有的字符串添加额外功能。

    基本语法

    模板字面量的最简单语法,是使用反引号( ` )来包裹普通字符串,而不是用双引号或单引号。

    let message = `Hello world!`;
    console.log(message); // "Hello world!"
    console.log(typeof message); // "string"
    console.log(message.length); // 12
    
    // 在字符串中包含反引号,只需使用反斜杠( \ )转义即可。
    let message = `\`Hello\` world!`;
    console.log(message); // "`Hello` world!"
    console.log(typeof message); // "string"
    console.log(message.length); // 14
    

    在模板字面量中无需对双引号或单引号进行转义

    多行字符串

    使用双引号或单引号时,整个字符串只能放在单独一行。

    ES6 之前创建多行字符串的尝试,一般都基于数组或字符串的拼接。

    var message = [
        "Multiline ",
        "string"
    ].join("\n");
    let message = "Multiline \n" +
        "string";
    

    多行字符串的简单解决方法,ES6 的模板字面量使多行字符串更易创建,因为它不需要特殊的语法。只需在想要的位置包含换行即可,而且它会显示在结果中。

    let message = `Multiline
    string`;
    console.log(message); // "Multiline
    // string"
    console.log(message.length); // 16
    // 反引号之内的所有空白符都是字符串的一部分,因此需要留意缩进。
    let html = `
    <div>
    <h1>Title</h1>
    </div>`.trim();
    
    //如果你喜欢的话,也可以在模板字面量中使用 \n 来指示换行的插入位置:
    let message = `Multiline\nstring`;
    console.log(message); // "Multiline
    // string"
    console.log(message.length); // 16
    
    • 制造替换位

    模板字面量与普通 JS 字符串的区别在于模板字面量的“替换位”。

    替换位允许你将任何有效的 JS 表达式嵌入到模板字面量中,并将其结果输出为字符串的一部分。

    替换位由起始的 ${ 与结束的 } 来界定,之间允许放入任意的 JS 表达式。

    let name = "Nicholas",
    message = `Hello, ${name}.`;
    console.log(message); // "Hello, Nicholas."
    // 替换位 ${name} 会访问本地变量 name ,并将其值插入到 message 字符串中。 
    // message 变量会立即保留该替换位的结果。
    

    模板字面量能访问到作用域中任意的可访问变量。试图使用未定义的变量会抛出错误,无论是严格模式还是非严格模式。

    既然替换位是 JS 表达式,那么可替换的就不仅仅是简单的变量名。可以嵌入计算、函数调用等等。

    let count = 10,
        price = 0.25,
        message = `${count} items cost $${(count * price).toFixed(2)}.`;
    console.log(message); // "10 items cost $2.50."
    /*
    此代码在模板字面量的一部分执行了一次计算, count 与 price 变量相乘,再使用.toFixed() 方法将结果格式化为两位小数。而在第二个替换位之前的美元符号被照常输出,因为没有左花括号紧随其后。
    */
    

    模板字面量本身也是 JS 表达式,意味着你可以将模板字面量嵌入到另一个模板字面量内部。

    let name = "Nicholas",
        message = `Hello, ${
    `my name is ${ name }`
    }.`;
    console.log(message); // "Hello, my name is Nicholas."
    
    • 标签化模板

    一个模板标签( template tag )能对模板字面量进行转换并返回最终的字符串值,标签在模板的起始处被指定,即在第一个 ` 之前。

    let message = tag`Hello world`;
    // tag 就是会被应用到 `Hello world` 模板字面量上的模板标签。
    

    定义标签

    一个标签( tag )仅是一个函数,它被调用时接收需要处理的模板字面量数据。
    标签所接收的数据被划分为独立片段,并且必须将它们组合起来以创建结果。
    第一个参数是个数组,包含被 JS 解释过的字面量字符串,随后的参数是每个替换位的解释值。
    标签函数的参数一般定义为剩余参数形式,以便更容易处理数据。

    function tag(literals, ...substitutions) {
        // 返回一个字符串
    }
    
    let count = 10,
    price = 0.25,
    message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;
    /*
    passthru() 函数,该函数将会接收到三个参数。
    首先是一个literals 数组,包含如下元素:
    在首个替换位之前的空字符串( "" );
    首个替换位与第二个替换位之间的字符串( " items cost $" );
    第二个替换位之后的字符串( "." )。
    substitutions.length === literals.length - 1 的值总是 true
    */
    
    function passthru(literals, ...substitutions) {
        let result = "";
        // 仅使用 substitution 的元素数量来进行循环
        for (let i = 0; i < substitutions.length; i++) {
            result += literals[i];
            result += substitutions[i];
        }
        // 添加最后一个字面量
        result += literals[literals.length - 1];
        return result;
    }
    let count = 10,
        price = 0.25,
        message = passthru `${count} items cost $${(count * price).toFixed(2)}.`;
    console.log(message); // "10 items cost $2.50."
    
    /*
    执行与模板字面量的默认行为相同的转换操作。
    在循环中使用 substituions.length 而不是 literals.length 来避免 substituions 数组的越界。
    它能工作是由于 ES6 对 literals 和 substituions 的良好定义。
    
    */
    

    使用模板字面量中的原始值

    模板标签能访问字符串的原始信息,主要指的是可以访问字符在转义之前的形式。
    可使用内置的 String.raw() 标签。

    let message1 = `Multiline\nstring`,
        message2 = String.raw `Multiline\nstring`;
    console.log(message1); // "Multiline
    // string"
    console.log(message2); // "Multiline\\nstring"
    

    字符串的原始信息同样会被传递给模板标签。
    标签函数的第一个参数为包含额外属性 raw 的数组,而 raw 属性则是含有与每个字面量值等价的原始值的数组。

    例如, literals[0] 的值总是等价于包含字符串原始信息的 literals.raw[0] 的值

    function raw(literals, ...substitutions) {
        let result = "";
        // 仅使用 substitution 的元素数量来进行循环
        for (let i = 0; i < substitutions.length; i++) {
            result += literals.raw[i]; // 改为使用原始值
            result += substitutions[i];
        }
        // 添加最后一个字面量
        result += literals.raw[literals.length - 1];
        return result;
    }
    let message = raw `Multiline\nstring`;
    console.log(message); // "Multiline\\nstring"
    console.log(message.length); // 17
    /*
    这里使用 literals.raw 而非 literals 来输出结果字符串。
    这意味着任何转义字符都会以原始的形式返回。
    当你想在输出的字符串中包含转义字符时,原始字符串会很有帮助。
    */
    
    • 总结

    完整的 Unicode 支持允许 JS 以合理的方式处理 UTF-16 字符。通过 codePointAt() 与
    String.fromCodePoint() 在代码点和字符之间转换的能力,是字符串操作的一大进步。正则
    表达式新增的 u 标志使得直接操作代码点而不是 16 位字符变为可能,而 normalize() 方
    法则允许进行更恰当的字符串比较。
    ES6 也添加了操作字符串的新方法,允许你更容易识别子字符串,而不用管它在父字符串中
    的位置。正则表达式同样引入了许多功能。
    模板字面量是 ES6 的一项重要补充,允许你创建领域专用语言( DSL )让字符串的创建更容
    易。能将变量直接嵌入到模板字面量中,意味着开发者在组合长字符串与变量时,有了一种
    比字符串拼接更为安全的工具。
    内置的多行字符串支持,是普通 JS 字符串绝对无法做到的,这使得模板字面量成为凌驾于前
    者之上的有用升级。尽管在模板字面量中允许直接使用换行,你依然可以使用 \n 或其它字
    符转义序列。
    模板标签是创建 DSL 最重要的部分。标签是接收模板字面量片段作为参数的函数,你可以使
    用它们来返回合适的字符串。这些数据包括了字面量、等价的原始值以及替换位的值,标签
    使用这些信息片段来决定输出。

    相关文章

      网友评论

          本文标题:ES6-02 字符串与正则表达式

          本文链接:https://www.haomeiwen.com/subject/efkqyftx.html