JavaScript高级程序设计(第4版)
上QQ阅读APP看书,第一时间看更新

5.3 原始值包装类型

为了方便操作原始值,ECMAScript提供了3种特殊的引用类型:Boolean、Number和String。这些类型具有本章介绍的其他引用类型一样的特点,但也具有与各自原始类型对应的特殊行为。每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。来看下面的例子:

    let s1 = "some text";
    let s2 = s1.substring(2);

在这里,s1是一个包含字符串的变量,它是一个原始值。第二行紧接着在s1上调用了substring()方法,并把结果保存在s2中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。具体来说,当第二行访问s1时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下3步:

(1)创建一个String类型的实例;

(2)调用实例上的特定方法;

(3)销毁实例。

可以把这3步想象成执行了如下3行ECMAScript代码:

    let s1 = new String("some text");
    let s2 = s1.substring(2);
    s1 = null;

这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上3步也会在后台发生,只不过使用的是Boolean和Number包装类型而已。

引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过new实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:

    let s1 = "some text";
    s1.color = "red";
    console.log(s1.color);   // undefined

这里的第二行代码尝试给字符串s1添加了一个color属性。可是,第三行代码访问color属性时,它却不见了。原因就是第二行代码运行时会临时创建一个String对象,而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里创建了自己的String对象,但这个对象没有color属性。

可以显式地使用Boolean、Number和String构造函数创建原始值包装对象。不过应该在确实必要时再这么做,否则容易让开发者疑惑,分不清它们到底是原始值还是引用值。在原始值包装类型的实例上调用typeof会返回"object",所有原始值包装对象都会转换为布尔值true。

另外,Object构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。比如:

    let obj = new Object("some text");
    console.log(obj instanceof String);   // true

如果传给Object的是字符串,则会创建一个String的实例。如果是数值,则会创建Number的实例。布尔值则会得到Boolean的实例。

注意,使用new调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:

    let value = "25";
    let number = Number(value);     // 转型函数
    console.log(typeof number);     // "number"
    let obj = new Number(value);    // 构造函数
    console.log(typeof obj);         // "object"

在这个例子中,变量number中保存的是一个值为25的原始数值,而变量obj中保存的是一个Number的实例。

虽然不推荐显式创建原始值包装类型的实例,但它们对于操作原始值的功能是很重要的。每个原始值包装类型都有相应的一套方法来方便数据操作。

5.3.1 Boolean

Boolean是对应布尔值的引用类型。要创建一个Boolean对象,就使用Boolean构造函数并传入true或false,如下例所示:

    let booleanObject = new Boolean(true);

Boolean的实例会重写valueOf()方法,返回一个原始值true或false。toString()方法被调用时也会被覆盖,返回字符串"true"或"false"。不过,Boolean对象在ECMAScript中用得很少。不仅如此,它们还容易引起误会,尤其是在布尔表达式中使用Boolean对象时,比如:

    let falseObject = new Boolean(false);
    let result = falseObject && true;
    console.log(result); // true
    let falseValue = false;
    result = falseValue && true;
    console.log(result); // false

在这段代码中,我们创建一个值为false的Boolean对象。然后,在一个布尔表达式中通过&&操作将这个对象与一个原始值true组合起来。在布尔算术中,false && true等于false。可是,这个表达式是对falseObject对象而不是对它表示的值(false)求值。前面刚刚说过,所有对象在布尔表达式中都会自动转换为true,因此falseObject在这个表达式里实际上表示一个true值。那么true && true当然是true。

除此之外,原始值和引用值(Boolean对象)还有几个区别。首先,typeof操作符对原始值返回"boolean",但对引用值返回"object"。同样,Boolean对象是Boolean类型的实例,在使用instaceof操作符时返回true,但对原始值则返回false,如下所示:

    console.log(typeof falseObject);                // object
    console.log(typeof falseValue);                  // boolean
    console.log(falseObject instanceof Boolean); // true
    console.log(falseValue instanceof Boolean);   // false

理解原始布尔值和Boolean对象之间的区别非常重要,强烈建议永远不要使用后者。

5.3.2 Number

Number是对应数值的引用类型。要创建一个Number对象,就使用Number构造函数并传入一个数值,如下例所示:

    let numberObject = new Number(10);

与Boolean类型一样,Number类型重写了valueOf()、toLocaleString()和toString()方法。valueOf()方法返回Number对象表示的原始数值,另外两个方法返回数值字符串。toString()方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串,如下所示:

    let num = 10;
    console.log(num.toString());    // "10"
    console.log(num.toString(2));   // "1010"
    console.log(num.toString(8));   // "12"
    console.log(num.toString(10)); // "10"
    console.log(num.toString(16)); // "a"

除了继承的方法,Number类型还提供了几个用于将数值格式化为字符串的方法。

toFixed()方法返回包含指定小数点位数的数值字符串,如:

    let num = 10;
    console.log(num.toFixed(2));//"10.00"

这里的toFixed()方法接收了参数2,表示返回的数值字符串要包含两位小数。结果返回值为"10.00",小数位填充了0。如果数值本身的小数位超过了参数指定的位数,则四舍五入到最接近的小数位:

    let num = 10.005;
    console.log(num.toFixed(2));//"10.01"

toFixed()自动舍入的特点可以用于处理货币。不过要注意的是,多个浮点数值的数学计算不一定得到精确的结果。比如,0.1 + 0.2 = 0.30000000000000004。

注意 toFixed()方法可以表示有0~20个小数位的数值。某些浏览器可能支持更大的范围,但这是通常被支持的范围。

另一个用于格式化数值的方法是toExponential(),返回以科学记数法(也称为指数记数法)表示的数值字符串。与toFixed()一样,toExponential()也接收一个参数,表示结果中小数的位数。来看下面的例子:

    let num = 10;
    console.log(num.toExponential(1));  //"1.0e+1"

这段代码的输出为"1.0e+1"。一般来说,这么小的数不用表示为科学记数法形式。如果想得到数值最适当的形式,那么可以使用toPrecision()。

toPrecision()方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。来看几个例子:

    let num = 99;
    console.log(num.toPrecision(1));//"1e+2"
    console.log(num.toPrecision(2));//"99"
    console.log(num.toPrecision(3));//"99.0"

在这个例子中,首先要用1位数字表示数值99,得到"1e+2",也就是100。因为99不能只用1位数字来精确表示,所以这个方法就将它舍入为100,这样就可以只用1位数字(及其科学记数法形式)来表示了。用2位数字表示99得到"99",用3位数字则是"99.0"。本质上,toPrecision()方法会根据数值和精度来决定调用toFixed()还是toExponential()。为了以正确的小数位精确表示数值,这3个方法都会向上或向下舍入。

注意 toPrecision()方法可以表示带1~21个小数位的数值。某些浏览器可能支持更大的范围,但这是通常被支持的范围。

与Boolean对象类似,Number对象也为数值提供了重要能力。但是,考虑到两者存在同样的潜在问题,因此并不建议直接实例化Number对象。在处理原始数值和引用数值时,typeof和instacnceof操作符会返回不同的结果,如下所示:

    let numberObject = new Number(10);
    let numberValue = 10;
    console.log(typeof numberObject);                // "object"
    console.log(typeof numberValue);                  // "number"
    console.log(numberObject instanceof Number);   // true
    console.log(numberValue instanceof Number);    // false

原始数值在调用typeof时始终返回"number",而Number对象则返回"object"。类似地,Number对象是Number类型的实例,而原始数值不是。

isInteger()方法与安全整数

ES6新增了Number.isInteger()方法,用于辨别一个数值是否保存为整数。有时候,小数位的0可能会让人误以为数值是一个浮点值:

    console.log(Number.isInteger(1));     // true
    console.log(Number.isInteger(1.00)); // true
    console.log(Number.isInteger(1.01)); // false

IEEE 754数值格式有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。这个数值范围从Number.MIN_SAFE_INTEGER(-253+ 1)到Number.MAX_SAFE_INTEGER(253-1)。对超出这个范围的数值,即使尝试保存为整数,IEEE 754编码格式也意味着二进制值可能会表示一个完全不同的数值。为了鉴别整数是否在这个范围内,可以使用Number.isSafeInteger()方法:

    console.log(Number.isSafeInteger(-1 * (2 ** 53)));        // false
    console.log(Number.isSafeInteger(-1 * (2 ** 53) + 1));   // true
    console.log(Number.isSafeInteger(2 ** 53));                // false
    console.log(Number.isSafeInteger((2 ** 53) -1));         // true

5.3.3 String

String是对应字符串的引用类型。要创建一个String对象,使用String构造函数并传入一个数值,如下例所示:

    letstringObject=newString("helloworld");

String对象的方法可以在所有字符串原始值上调用。3个继承的方法valueOf()、toLocaleString()和toString()都返回对象的原始字符串值。

每个String对象都有一个length属性,表示字符串中字符的数量。来看下面的例子:

    let stringValue = "hello world";
    console.log(stringValue.length);//"11"

这个例子输出了字符串"hello world"中包含的字符数量:11。注意,即使字符串中包含双字节字符(而不是单字节的ASCII字符),也仍然会按单字符来计数。

String类型提供了很多方法来解析和操作字符串。

1.JavaScript字符

JavaScript字符串由16位码元(code unit)组成。对多数字符来说,每16位码元对应一个字符。换句话说,字符串的length属性表示字符串包含多少16位码元:

    let message = "abcde";
    console.log(message.length); // 5

此外,charAt()方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方法查找指定索引位置的16位码元,并返回该码元对应的字符:

    let message = "abcde";
    console.log(message.charAt(2)); // "c"

JavaScript字符串使用了两种Unicode编码混合的策略:UCS-2和UTF-16。对于可以采用16位编码的字符(U+0000~U+FFFF),这两种编码实际上是一样的。

注意 要深入了解关于字符编码的内容,推荐Joel Spolsky写的博客文章:“The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses! )”。

另一个有用的资源是Mathias Bynens的博文:“JavaScript's Internal Character Encoding: UCS-2 or UTF-16? ”。

使用charCodeAt()方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定。比如:

    let message = "abcde";
    // Unicode "Latin small letter C"的编码是U+0063
    console.log(message.charCodeAt(2));   // 99
    // 十进制99 等于十六进制63
    console.log(99 === 0x63);               // true

fromCharCode()方法用于根据给定的UTF-16码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:

    // Unicode "Latin small letter A"的编码是U+0061
    // Unicode "Latin small letter B"的编码是U+0062
    // Unicode "Latin small letter C"的编码是U+0063
    // Unicode "Latin small letter D"的编码是U+0064
    // Unicode "Latin small letter E"的编码是U+0065
    console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65));   // "abcde"
    // 0x0061 === 97
    // 0x0062 === 98
    // 0x0063 === 99
    // 0x0064 === 100
    // 0x0065 === 101
    console.log(String.fromCharCode(97, 98, 99, 100, 101));             // "abcde"

对于U+0000~U+FFFF范围内的字符,length、charAt()、charCodeAt()和fromCharCode()返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用16位表示的,而这几个方法也都基于16位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。

这个对应关系在扩展到Unicode增补字符平面时就不成立了。问题很简单,即16位只能唯一表示65536个字符。这对于大多数语言字符集是足够了,在Unicode中称为基本多语言平面(BMP)。为了表示更多的字符,Unicode采用了一个策略,即每个字符使用另外16位去选择一个增补平面。这种每个字符使用两个16位码元的策略称为代理对。

在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。比如,下面的例子中使用了一个笑脸表情符号,也就是一个使用代理对编码的字符:

    // "smiling face with smiling eyes" 表情符号的编码是U+1F60A
    // 0x1F60A === 128522
    let message = "ab☺de";
    console.log(message.length);             // 6
    console.log(message.charAt(1));         // b
    console.log(message.charAt(2));         // <? >
    console.log(message.charAt(3));         // <? >
    console.log(message.charAt(4));         // d
    console.log(message.charCodeAt(1));    // 98
    console.log(message.charCodeAt(2));    // 55357
    console.log(message.charCodeAt(3));    // 56842
    console.log(message.charCodeAt(4));    // 100
    console.log(String.fromCodePoint(0x1F60A)); // ☺
    console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab☺de

这些方法仍然将16位码元当作一个字符,事实上索引2和索引3对应的码元应该被看成一个代理对,只对应一个字符。fromCharCode()方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个Unicode笑脸字符。

为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用codePointAt()来代替charCodeAt()。跟使用charCodeAt()时类似,codePointAt()接收16位码元的索引并返回该索引位置上的码点(code point)。码点是Unicode中一个字符的完整标识。比如,"c"的码点是0x0063,而"☺"的码点是0x1F60A。码点可能是16位,也可能是32位,而codePointAt()方法可以从指定码元位置识别完整的码点。

    let message = "ab☺de";
    console.log(message.codePointAt(1)); // 98
    console.log(message.codePointAt(2)); // 128522
    console.log(message.codePointAt(3));//56842
    console.log(message.codePointAt(4)); // 100

注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代理对的码点:

    console.log([..."ab☺de"]); // ["a", "b", "☺", "d", "e"]

与charCodeAt()有对应的codePointAt()一样,fromCharCode()也有一个对应的fromCodePoint()。这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:

    console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101));   // ab☺de
    console.log(String.fromCodePoint(97, 98, 128522, 100, 101));         // ab☺de

2.normalize()方法

某些Unicode字符可以有多种编码方式。有的字符既可以通过一个BMP字符表示,也可以通过一个代理对表示。比如:

    // U+00C5:上面带圆圈的大写拉丁字母A
    console.log(String.fromCharCode(0x00C5));             // Å
    // U+212B:长度单位“埃”
    console.log(String.fromCharCode(0x212B));             // Å
    // U+004:大写拉丁字母A
    // U+030A:上面加个圆圈
    console.log(String.fromCharCode(0x0041, 0x030A));   // Å

比较操作符不在乎字符看起来是什么样的,因此这3个字符互不相等。

    let a1 = String.fromCharCode(0x00C5),
        a2 = String.fromCharCode(0x212B),
        a3 = String.fromCharCode(0x0041, 0x030A);
    console.log(a1, a2, a3); // Å, Å, Å
    console.log(a1 === a2);   // false
    console.log(a1 === a3);   // false
    console.log(a2 === a3);   // false

为解决这个问题,Unicode提供了4种规范化形式,可以将类似上面的字符规范化为一致的格式,无论底层字符的代码是什么。这4种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、NFKD(Normalization Form KD)和NFKC(Normalization Form KC)。可以使用normalize()方法对字符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:"NFD"、"NFC"、"NFKD"或"NFKC"。

注意这4种规范化形式的具体细节超出了本书范围,有兴趣的读者可以自行参考UAX 15#: Unicode Normalization Forms中的1.2节“Normalization Forms”。

通过比较字符串与其调用normalize()的返回值,就可以知道该字符串是否已经规范化了:

    let a1 = String.fromCharCode(0x00C5),
        a2 = String.fromCharCode(0x212B),
        a3 = String.fromCharCode(0x0041, 0x030A);
    // U+00C5 是对0+212B进行NFC/NFKC规范化之后的结果
    console.log(a1 === a1.normalize("NFD"));   // false
    console.log(a1 === a1.normalize("NFC"));   // true
    console.log(a1 === a1.normalize("NFKD")); // false
    console.log(a1 === a1.normalize("NFKC")); // true
    // U+212B是未规范化的
    console.log(a2 === a2.normalize("NFD"));   // false
    console.log(a2 === a2.normalize("NFC"));   // false
    console.log(a2 === a2.normalize("NFKD")); // false
    console.log(a2 === a2.normalize("NFKC")); // false
    // U+0041/U+030A是对0+212B进行NFD/NFKD规范化之后的结果
    console.log(a3 === a3.normalize("NFD"));   // true
    console.log(a3 === a3.normalize("NFC"));   // false
    console.log(a3 === a3.normalize("NFKD")); // true
    console.log(a3 === a3.normalize("NFKC")); // false

选择同一种规范化形式可以让比较操作符返回正确的结果:

    let a1 = String.fromCharCode(0x00C5),
        a2 = String.fromCharCode(0x212B),
        a3 = String.fromCharCode(0x0041, 0x030A);
    console.log(a1.normalize("NFD") === a2.normalize("NFD"));     // true
    console.log(a2.normalize("NFKC") === a3.normalize("NFKC"));   // true
    console.log(a1.normalize("NFC") === a3.normalize("NFC"));     // true

3.字符串操作方法

本节介绍几个操作字符串值的方法。首先是concat(),用于将一个或多个字符串拼接成一个新字符串。来看下面的例子:

    let stringValue = "hello ";
    let result = stringValue.concat("world");
    console.log(result);        // "hello world"
    console.log(stringValue); // "hello"

在这个例子中,对stringValue调用concat()方法的结果是得到"hello world",但stringValue的值保持不变。concat()方法可以接收任意多个参数,因此可以一次性拼接多个字符串,如下所示:

    let stringValue = "hello ";
    let result = stringValue.concat("world", "!");
    console.log(result);        // "hello world!"
    console.log(stringValue); // "hello"

这个修改后的例子将字符串"world"和"! "追加到了"hello "后面。虽然concat()方法可以拼接字符串,但更常用的方式是使用加号操作符(+)。而且多数情况下,对于拼接多个字符串来说,使用加号更方便。

ECMAScript提供了3个从字符串中提取子字符串的方法:slice()、substr()和substring()。这3个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。对slice()和substring()而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。对substr()而言,第二个参数表示返回的子字符串数量。任何情况下,省略第二个参数都意味着提取到字符串末尾。与concat()方法一样,slice()、substr()和substring()也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。来看下面的例子:

    let stringValue = "hello world";
    console.log(stringValue.slice(3));         // "lo world"
    console.log(stringValue.substring(3));    // "lo world"
    console.log(stringValue.substr(3));        // "lo world"
    console.log(stringValue.slice(3, 7));     // "lo w"
    console.log(stringValue.substring(3,7)); // "lo w"
    console.log(stringValue.substr(3, 7));    // "lo worl"

在这个例子中,slice()、substr()和substring()是以相同方式被调用的,而且多数情况下返回的值也相同。如果只传一个参数3,则所有方法都将返回"lo world",因为"hello"中"l"位置为3。如果传入两个参数3和7,则slice()和substring()返回"lo w"(因为"world"中"o"在位置7,不包含),而substr()返回"lo worl",因为第二个参数对它而言表示返回的字符数。

当某个参数是负值时,这3个方法的行为又有不同。比如,slice()方法将所有负值参数都当成字符串长度加上负参数值。

而substr()方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为0。substring()方法会将所有负参数值都转换为0。看下面的例子:

    let stringValue = "hello world";
    console.log(stringValue.slice(-3));           // "rld"
    console.log(stringValue.substring(-3));      // "hello world"
    console.log(stringValue.substr(-3));          // "rld"
    console.log(stringValue.slice(3, -4));        // "lo w"
    console.log(stringValue.substring(3, -4));   // "hel"
    console.log(stringValue.substr(3, -4));      // "" (empty string)

这个例子明确演示了3个方法的差异。在给slice()和substr()传入负参数时,它们的返回结果相同。这是因为-3会被转换为8(长度加上负参数),实际上调用的是slice(8)和substr(8)。而substring()方法返回整个字符串,因为-3会转换为0。

在第二个参数是负值时,这3个方法各不相同。slice()方法将第二个参数转换为7,实际上相当于调用slice(3, 7),因此返回"lo w"。而substring()方法会将第二个参数转换为0,相当于调用substring(3, 0),等价于substring(0, 3),这是因为这个方法会将较小的参数作为起点,将较大的参数作为终点。对substr()来说,第二个参数会被转换为0,意味着返回的字符串包含零个字符,因而会返回一个空字符串。

4.字符串位置方法

有两个方法用于在字符串中定位子字符串:indexOf()和lastIndexOf()。这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)。两者的区别在于,indexOf()方法从字符串开头开始查找子字符串,而lastIndexOf()方法从字符串末尾开始查找子字符串。来看下面的例子:

    let stringValue = "hello world";
    console.log(stringValue.indexOf("o"));      // 4
    console.log(stringValue.lastIndexOf("o")); // 7

这里,字符串中第一个"o"的位置是4,即"hello"中的"o"。最后一个"o"的位置是7,即"world"中的"o"。如果字符串中只有一个"o",则indexOf()和lastIndexOf()返回同一个位置。

这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。这意味着,indexOf()会从这个参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;lastIndexOf()则会从这个参数指定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。下面看一个例子:

    let stringValue = "hello world";
    console.log(stringValue.indexOf("o", 6));      // 7
    console.log(stringValue.lastIndexOf("o", 6)); // 4

在传入第二个参数6以后,结果跟前面的例子恰好相反。这一次,indexOf()返回7,因为它从位置6(字符"w")开始向后搜索字符串,在位置7找到了"o"。而lastIndexOf()返回4,因为它从位置6开始反向搜索至字符串开头,因此找到了"hello"中的"o"。像这样使用第二个参数并循环调用indexOf()或lastIndexOf(),就可以在字符串中找到所有的目标子字符串,如下所示:

    let stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
    let positions = new Array();
    let pos = stringValue.indexOf("e");
    while(pos > -1) {
      positions.push(pos);
      pos = stringValue.indexOf("e", pos + 1);
    }
    console.log(positions); // [3,24,32,35,52]

这个例子逐步增大开始搜索的位置,通过indexOf()遍历了整个字符串。首先取得第一个"e"的位置,然后进入循环,将上一次的位置加1再传给indexOf(),确保搜索到最后一个子字符串实例之后。每个位置都保存在positions数组中,可供以后使用。

5.字符串包含方法

ECMAScript 6增加了3个用于判断字符串中是否包含另一个字符串的方法:startsWith()、endsWith()和includes()。这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值。它们的区别在于,startsWith()检查开始于索引0的匹配项,endsWith()检查开始于索引(string.length - substring.length)的匹配项,而includes()检查整个字符串:

    let message = "foobarbaz";
    console.log(message.startsWith("foo"));   // true
    console.log(message.startsWith("bar"));   // false
    console.log(message.endsWith("baz"));     // true
    console.log(message.endsWith("bar"));     // false
    console.log(message.includes("bar"));     // true
    console.log(message.includes("qux"));     // false

startsWith()和includes()方法接收可选的第二个参数,表示开始搜索的位置。如果传入第二个参数,则意味着这两个方法会从指定位置向着字符串末尾搜索,忽略该位置之前的所有字符。下面是一个例子:

    let message = "foobarbaz";
    console.log(message.startsWith("foo"));      // true
    console.log(message.startsWith("foo", 1));   // false
    console.log(message.includes("bar"));         // true
    console.log(message.includes("bar", 4));     // false

endsWith()方法接收可选的第二个参数,表示应该当作字符串末尾的位置。如果不提供这个参数,那么默认就是字符串长度。如果提供这个参数,那么就好像字符串只有那么多字符一样:

    let message = "foobarbaz";
    console.log(message.endsWith("bar"));      // false
    console.log(message.endsWith("bar", 6));   // true

6.trim()方法

ECMAScript在所有字符串上都提供了trim()方法。这个方法会创建字符串的一个副本,删除前、后所有空格符,再返回结果。比如:

    let stringValue = "   hello world   ";
    let trimmedStringValue = stringValue.trim();
    console.log(stringValue);           // "   hello world "
    console.log(trimmedStringValue);   // "hello world"

由于trim()返回的是字符串的副本,因此原始字符串不受影响,即原本的前、后空格符都会保留。

另外,trimLeft()和trimRight()方法分别用于从字符串开始和末尾清理空格符。

7.repeat()方法

ECMAScript在所有字符串上都提供了repeat()方法。这个方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果。

    let stringValue = "na ";
    console.log(stringValue.repeat(16)+ "batman");
    // na na na na na na na na na na na na na na na na batman

8.padStart()和padEnd()方法

padStart()和padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格(U+0020)。

    let stringValue = "foo";
    console.log(stringValue.padStart(6));         // "    foo"
    console.log(stringValue.padStart(9, "."));   // "......foo"
    console.log(stringValue.padEnd(6));           // "foo    "
    console.log(stringValue.padEnd(9, "."));     // "foo......"

可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串。

    let stringValue = "foo";
    console.log(stringValue.padStart(8, "bar")); // "barbafoo"
    console.log(stringValue.padStart(2));          // "foo"
    console.log(stringValue.padEnd(8, "bar"));    // "foobarba"
    console.log(stringValue.padEnd(2));             // "foo"

9.字符串迭代与解构

字符串的原型上暴露了一个@@iterator方法,表示可以迭代字符串的每个字符。可以像下面这样手动使用迭代器:

    let message = "abc";
    let stringIterator = message[Symbol.iterator]();
    console.log(stringIterator.next());   // {value: "a", done: false}
    console.log(stringIterator.next());   // {value: "b", done: false}
    console.log(stringIterator.next());   // {value: "c", done: false}
    console.log(stringIterator.next());   // {value: undefined, done: true}

在for-of循环中可以通过这个迭代器按序访问每个字符:

    for (const c of "abcde") {
      console.log(c);
    }
    // a
    // b
    // c
    // d
    // e

有了这个迭代器之后,字符串就可以通过解构操作符来解构了。比如,可以更方便地把字符串分割为字符数组:

    let message = "abcde";
    console.log([...message]); // ["a", "b", "c", "d", "e"]

10.字符串大小写转换

下一组方法涉及大小写转换,包括4个方法:toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()。toLowerCase()和toUpperCase()方法是原来就有的方法,与java.lang.String中的方法同名。toLocaleLowerCase()和toLocaleUpperCase()方法旨在基于特定地区实现。在很多地区,地区特定的方法与通用的方法是一样的。但在少数语言中(如土耳其语), Unicode大小写转换需应用特殊规则,要使用地区特定的方法才能实现正确转换。下面是几个例子:

    let stringValue = "hello world";
    console.log(stringValue.toLocaleUpperCase());  //"HELLOWORLD"
    console.log(stringValue.toUpperCase());         //"HELLOWORLD"
    console.log(stringValue.toLocaleLowerCase());  //"helloworld"
    console.log(stringValue.toLowerCase());         //"helloworld"

这里,toLowerCase()和toLocaleLowerCase()都返回hello world,而toUpperCase()和toLocaleUpperCase()都返回HELLO WORLD。通常,如果不知道代码涉及什么语言,则最好使用地区特定的转换方法。

11.字符串模式匹配方法

String类型专门为在字符串中实现模式匹配设计了几个方法。第一个就是match()方法,这个方法本质上跟RegExp对象的exec()方法相同。match()方法接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象。来看下面的例子:

    let text = "cat, bat, sat, fat";
    let pattern = /.at/;
    // 等价于pattern.exec(text)
    let matches = text.match(pattern);
    console.log(matches.index);        // 0
    console.log(matches[0]);           // "cat"
    console.log(pattern.lastIndex);   // 0

match()方法返回的数组与RegExp对象的exec()方法返回的数组是一样的:第一个元素是与整个模式匹配的字符串,其余元素则是与表达式中的捕获组匹配的字符串(如果有的话)。

另一个查找模式的字符串方法是search()。这个方法唯一的参数与match()方法一样:正则表达式字符串或RegExp对象。这个方法返回模式第一个匹配的位置索引,如果没找到则返回-1。search()始终从字符串开头向后匹配模式。看下面的例子:

    let text = "cat, bat, sat, fat";
    letpos=text.search(/at/);
    console.log(pos);  //1

这里,search(/at/)返回1,即"at"的第一个字符在字符串中的位置。

为简化子字符串替换操作,ECMAScript提供了replace()方法。这个方法接收两个参数,第一个参数可以是一个RegExp对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记,如下面的例子所示:

    let text = "cat, bat, sat, fat";
    letresult=text.replace("at", "ond");
    console.log(result);  //"cond, bat, sat, fat"
    result=text.replace(/at/g, "ond");
    console.log(result);  //"cond, bond, sond, fond"

在这个例子中,字符串"at"先传给replace()函数,而替换文本是"ond"。结果是"cat"被修改为"cond",而字符串的剩余部分保持不变。通过将第一个参数改为带全局标记的正则表达式,字符串中的所有"at"都被替换成了"ond"。

第二个参数是字符串的情况下,有几个特殊的字符序列,可以用来插入正则表达式操作的值。ECMA-262中规定了下表中的值。

使用这些特殊的序列,可以在替换文本中使用之前匹配的内容,如下面的例子所示:

    let text = "cat, bat, sat, fat";
    result=text.replace(/(.at)/g, "word($1)");
    console.log(result);  //word(cat), word(bat), word(sat), word(fat)

这里,每个以"at"结尾的词都会被替换成"word"后跟一对小括号,其中包含捕获组匹配的内容$1。

replace()的第二个参数可以是一个函数。在只有一个匹配项时,这个函数会收到3个参数:与整个模式匹配的字符串、匹配项在字符串中的开始位置,以及整个字符串。在有多个捕获组的情况下,每个匹配捕获组的字符串也会作为参数传给这个函数,但最后两个参数还是与整个模式匹配的开始位置和原始字符串。这个函数应该返回一个字符串,表示应该把匹配项替换成什么。使用函数作为第二个参数可以更细致地控制替换过程,如下所示:

    function htmlEscape(text) {
      return text.replace(/[<>"&]/g, function(match, pos, originalText) {
        switch(match) {
          case "<":
            return "&lt; ";
          case ">":
            return "&gt; ";
          case "&":
            return "&amp; ";
          case "\"":
            return "&quot; ";
        }
      });
    }
    console.log(htmlEscape("<p class=\"greeting\">Hello world! </p>"));
    // "&lt; p class=&quot; greeting&quot; &gt; Hello world! </p>"

这里,函数htmlEscape()用于将一段HTML中的4个字符替换成对应的实体:小于号、大于号、和号,还有双引号(都必须经过转义)。实现这个任务最简单的办法就是用一个正则表达式查找这些字符,然后定义一个函数,根据匹配的每个字符分别返回特定的HTML实体。

最后一个与模式匹配相关的字符串方法是split()。这个方法会根据传入的分隔符将字符串拆分成数组。作为分隔符的参数可以是字符串,也可以是RegExp对象。(字符串分隔符不会被这个方法当成正则表达式。)还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。来看下面的例子:

    let colorText = "red, blue, green, yellow";
    let colors1 = colorText.split(", ");         // ["red", "blue", "green", "yellow"]
    let colors2 = colorText.split(", ", 2);     // ["red", "blue"]
    let colors3 = colorText.split(/[^, ]+/);    // ["", ", ", ", ", ", ", ""]

在这里,字符串colorText是一个逗号分隔的颜色名称符串。调用split(", ")会得到包含这些颜色名的数组,基于逗号进行拆分。要把数组元素限制为2个,传入第二个参数2即可。最后,使用正则表达式可以得到一个包含逗号的数组。注意在最后一次调用split()时,返回的数组前后包含两个空字符串。这是因为正则表达式指定的分隔符出现在了字符串开头("red")和末尾("yellow")。

12.localeCompare()方法

最后一个方法是localeCompare(),这个方法比较两个字符串,返回如下3个值中的一个。

❑ 如果按照字母表顺序,字符串应该排在字符串参数前头,则返回负值。(通常是-1,具体还要看与实际值相关的实现。)

❑ 如果字符串与字符串参数相等,则返回0。

❑ 如果按照字母表顺序,字符串应该排在字符串参数后头,则返回正值。(通常是1,具体还要看与实际值相关的实现。)

下面是一个例子:

    let stringValue = "yellow";
    console.log(stringValue.localeCompare("brick"));   // 1
    console.log(stringValue.localeCompare("yellow")); // 0
    console.log(stringValue.localeCompare("zoo"));     // -1

在这里,字符串"yellow"与3个不同的值进行了比较:"brick"、"yellow"和"zoo"。"brick"按字母表顺序应该排在"yellow"前头,因此localeCompare()返回1。"yellow"等于"yellow",因此"localeCompare()"返回0。最后,"zoo"在"yellow"后面,因此localeCompare()返回-1。强调一下,因为返回的具体值可能因具体实现而异,所以最好像下面的示例中一样使用localeCompare():

    function determineOrder(value) {
      let result = stringValue.localeCompare(value);
      if (result < 0) {
        console.log(`The string 'yellow' comes before the string '${value}'.`);
      } else if (result > 0) {
        console.log(`The string 'yellow' comes after the string '${value}'.`);
      } else {
        console.log(`The string 'yellow' is equal to the string '${value}'.`);
      }
    }
    determineOrder("brick");
    determineOrder("yellow");
    determineOrder("zoo");

这样一来,就可以保证在所有实现中都能正确判断字符串的顺序了。

localeCompare()的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字符串。在美国,英语是ECMAScript实现的标准语言,localeCompare()区分大小写,大写字母排在小写字母前面。但其他地区未必是这种情况。

13.HTML方法

早期的浏览器开发商认为使用JavaScript动态生成HTML标签是一个需求。因此,早期浏览器扩展了规范,增加了辅助生成HTML标签的方法。下表总结了这些HTML方法。不过,这些方法基本上已经没有人使用了,因为结果通常不是语义化的标记。