实例对象方法扩展

实例对象方法扩展 #

codePointAt() #

产生原因 #

Javascript内部,字符以UTF-16的格式存储,每个字符固定为2个字节。

对于那些需要4个字节存储的字符(Unicode 编号大于0xFFFF的字符),Javascript会认为它们是个字符。

(function (log) {
    const s = '𠮷a';
    log(s.length) // 3
    log(s.charAt(0)) // �
    log(s.charAt(1)) // �

    log(s.charCodeAt(0)) // 55362
    log(s.charCodeAt(1)) // 57271
})(console.log)

上面代码中,汉字𠮷(注意,这个字不是吉祥)的码点0x20BB7UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。

对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt方法无法读取整个字符charCodeAt方法只能分别返回前两个字节和后两个字节


优化效果 #

ES6提供了codePointAt 方法,能够正确处理4个字节存储的字符,返回一个字符的Unicode码点。

(function (log) {
    const s = '𠮷a';

    log(s.codePointAt(0)) // 134071
    log(s.codePointAt(1)) // 57271
    log(s.codePointAt(2)) // 97
})(console.log)

codePointAt方法的参数,是字符字符串中的位置(从0开始)。

JavaScript𠮷a视为三个字符,codePointAt 方法在第一个字符上,正确地识别了𠮷,返回了它的十进制码点 134071(即十六进制的20BB7)。

在第二个字符(即𠮷的后两个字节)和第三个字符a上,codePointAt方法的结果与charCodeAt方法相同。

总之,codePointAt方法可以正确返回四字节UTF-16字符的Unicode码点。

对于那些两个字节存储的常规字符,它返回结果与charCodeAt方法相同。


结合for-of #

你可能注意到了,codePointAt方法的参数,仍然是不正确的。

比如,下面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2

解决这个问题的一个办法是使用for...of循环,因为它会正确识别 32 位的 UTF-16 字符。

(function (log) {
    const s = '𠮷a';

    log(s.codePointAt(0)) // 134071
    log(s.codePointAt(2)) // 97

    for (let ch of s) {
        log(ch.codePointAt(0));
    }
    // 134071
    // 97
})(console.log)

进制转换 #

codePointAt方法返回的是码点十进制值,如果想要其他进制的值,可以使用toString方法转换一下。

(function (log) {
    const s = '𠮷a';
    log(s.codePointAt(0)) // 134071
    log(s.codePointAt(2)) // 97

    log(s.codePointAt(0).toString(2)) // 100000101110110111
    log(s.codePointAt(2).toString(2)) // 1100001

    log(s.codePointAt(0).toString(8)) // 405667
    log(s.codePointAt(2).toString(8)) // 141

    log(s.codePointAt(0).toString(10)) // 134071
    log(s.codePointAt(2).toString(10)) // 97

    log(s.codePointAt(0).toString(16)) // 20bb7
    log(s.codePointAt(2).toString(16)) // 61

    log(s.codePointAt(0).toString(32)) // 42tn
    log(s.codePointAt(2).toString(32)) // 31
})(console.log)

判断是否是多字节 #

codePointAt 方法是测试一个字符由两个字节还是四个字节组成的最简单方法。

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

    log(is32Bit('𠮷')); // true
    log(is32Bit('a')); // false
})(console.log)

normalize() #

产生原因 #

许多欧洲语言有语调符号重音符号。为了表示它们,Unicode 提供了两种方法。

  • 一种是直接提供带重音符号的字符,比如Ǒ\u01D1)。
  • 另一种是提供合成符号combining character),即原字符重音符号的合成,两个字符合成一个字符,比如O\u004F)和ˇ\u030C)合成Ǒ\u004F\u030C)。

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

(function (log, S) {
    log('\u01D1' === '\u004F\u030C'); //false

    log('\u01D1'.length); // 1
    log('\u004F\u030C'.length); // 2
})(console.log, String)

上面代码表示,JavaScript合成字符视为两个字符,导致两种表示方法不相等。


优化效果 #

ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

(function (log, S) {
    const type1 = '\u01D1'.normalize();
    const type2 = '\u004F\u030C'.normalize();
    log(type1 === type2); // true
})(console.log, String)

参数详解 #

normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。

形参值形参值解释
NFC默认参数,表示标准等价合成Normalization Form Canonical Composition),返回多个简单字符合成字符。所谓标准等价指的是视觉语义上的等价。
NFD表示标准等价分解Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符
NFKC表示兼容等价合成Normalization Form Compatibility Composition),返回合成字符。所谓兼容等价指的是语义上存在等价,但视觉上不等价,比如喜喜。(这只是用来举例,normalize方法不能识别中文。)
NFKD表示兼容等价分解Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符
(function (log, S) {
    log('\u004F\u030C'.normalize('NFC').length); // 1
    log('\u004F\u030C'.normalize('NFD').length); // 2
})(console.log, String)

上面代码表示,NFC参数返回字符的合成形式NFD参数返回字符的分解形式。

不过,normalize方法目前不能识别三个三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。


判断字符串包含 #

传统上,Javascript中只有indexOf方法,可用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法

方法名方法解释
contains(findStr,findIndex)返回布尔值,表示了是否找到参数字符串
startsWith(findStr,findIndex)返回布尔值,表示参数字符串是否正在源字符串的头部
endsWith(findStr,findIndex)返回布尔值,表示参数字符串是否正在源字符串的尾部
  • findStr,表示被检索的参数字符串

  • findIndex,表示开始搜索的位置,endsWith的行为与其他两个方法有所不同,它针对前findIndex个字符,而其他两个方法则针对从findIndex个位置直到字符串结束的字符。


(function (log, S) {
    const s1 = 'Hello world!';

    log(s1.startsWith('Hello')); // true
    log(s1.endsWith('!')); // true
    log(s1.includes('o')); // true

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

    const s2 = 'Hello world!';

    log(s2.startsWith('world', 6)); // true
    log(s2.endsWith('Hello', 5)); // true
    log(s2.includes('Hello', 6)); // false
})(console.log, String)

repeat #

repeat(repeatIndex)返回一个新字符串,表示将原字符串重复repeatIndex

基础使用 #


(function (log, S) {
    log('x'.repeat(3)); // 'xxx'
    log('hello'.repeat(2)); // 'hellohello'
    log('na'.repeat(0)); // ''
})(console.log, String)

特殊形参值处理 #

参数值为小数

如果repeat的参数是小数,会被取整,注意这个取整对当前值不做任何四舍五入


(function (log, S) {
    log('na'.repeat(2.3)); // 'nana'
    log('na'.repeat(2.5)); // 'nana'
    log('na'.repeat(2.9)); // 'nana'

    log('na'.repeat(2.3999)); // 'nana'
    log('na'.repeat(2.599)); // 'nana'
    log('na'.repeat(2.999)); // 'nana'
})(console.log, String)

参数值为0

如果repeat的参数是+0,-0,0,NaN,-1~0之间或者0~1之间值,会返回一个空字符串

0-1 之间的小数,则等同于 0,这是因为会先进行取整运算。

0-1 之间的小数取整以后等于-0repeat视同为 0

(function (log, S) {
    log('na'.repeat(+0)); // ''
    log('na'.repeat(-0)); // ''
    log('na'.repeat(-0)); // ''

    log('na'.repeat(NaN)); // ''

    log('na'.repeat(+0.2)); // ''
    log('na'.repeat(+0.5)); // ''
    log('na'.repeat(+0.8)); // ''

    log('na'.repeat(-0.2)); // ''
    log('na'.repeat(-0.5)); // ''
    log('na'.repeat(-0.8)); // ''
})(console.log, String)

参数值为负数或者Infinity

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

(function (log, S) {
    log('na'.repeat(Infinity)); // RangeError: Invalid count value
    log('na'.repeat(-1)); // RangeError: Invalid count value
})(console.log, String)

参数值为字符串

如果repeat的参数是字符串,则会先转换成数字

  • 如果字符串中没有包含有效数字,则当作0处理。
  • 如果字符串中包含有效数字,同时又有特殊字符\s\S或者字母等,则当作0处理。
  • 如果字符串中包含有效数字,同时仅有空格\t或者\n等,当作有效参数进行使用。
  • 如果字符串中包含其他进制值字符串,转换成十进制数值之后,当作有效参数进行使用。
  • 如果字符串中包含符号字符串,转换为数值之后,如果为大于等于1正数,当作有效参数进行使用;如果为-1~1,则当作0,进行使用;如果为小于等于-1负数,则会抛出异常RangeError: Invalid count value
(function (log, S) {
    log(1, 'na'.repeat('na')); // 1 ''
    log(2, 'na'.repeat('3na')); // 2 ''
    log(3, 'na'.repeat('3,')); // 3 ''
    log(3, 'na'.repeat('3\s')); // 3 ''
    log(3, 'na'.repeat('3\S')); // 3 ''

    log(4, 'na'.repeat('3 ')); // 4 'nanana'
    log(4, 'na'.repeat('3\t')); // 4 'nanana'
    log(4, 'na'.repeat('\n3\n')); // 4 'nanana'

    log(5, 'na'.repeat('03')); // 5 'nanana'
    log(5, 'na'.repeat('0b1')); // 5 'na'
    log(5, 'na'.repeat('0o3')); // 5 'nanana'
    log(5, 'na'.repeat('0x3')); // 5 'nanana'

    // log('na'.repeat('-3')); // RangeError: Invalid count value

    log(6, 'na'.repeat('+3')); // 6 'nanana'
})(console.log, String)

参数值为日期格式

如果repeat的参数是日期格式,则会先转换成数字

但是由于转换的一般超过了可用的字符串长度,所以一般会直接报错RangeError: Invalid string length

如果是传入日期格式单独转换天数日期值repeat可以正常使用。

(function (log, S) {
    const d = Reflect.construct(Date,[]);
    log(d.getTime()); // 1549895917860
    log(d.getDate()); // 11
    log(d.getDay()); // 1

    // log('na'.repeat(d)); // RangeError: Invalid string length
    // log('na'.repeat(d.getTime())); // RangeError: Invalid string length
    log('na'.repeat(d.getDate())); // nanananananananananana
    log('na'.repeat(d.getDay())); // na
})(console.log, String)

参数值为数组或者对象

如果repeat的参数是数组MapWeakMapSetWeakSet或者对象,则会统一当作0进行处理,返回空字符串

(function (log, S) {
    log(1, 'na'.repeat([])); // 1 ''
    log(1, 'na'.repeat([1, 2, 3])); // 1 ''
    log(1, 'na'.repeat([{
        z: 1
    }])); // 1 ''

    log(2, 'na'.repeat({})); // 2 ''
    log(2, 'na'.repeat({
        z: 1
    })); // 2 ''

    log(3, 'na'.repeat(new Map())); // 3 ''

    log(4, 'na'.repeat(new WeakMap())); // 4 ''

    log(5, 'na'.repeat(new Set())); // 5 ''

    log(6, 'na'.repeat(new WeakSet())); // 6 ''
})(console.log, String)

字符串补全 #

基础使用 #

ES2017 引入了字符串补全长度的功能。

如果某个字符串不够指定长度,会在头部尾部补全。

方法名方法解释
padStart(maxFillLength,fillStr)返回最后补全的字符串,用于头部补全
padEnd(maxFillLength,fillStr)返回最后补全的字符串,尾部补全
  • maxFillLength是字符串补全生效的最大长度
  • fillStr是用来补全字符串
(function (log, S) {
    const s = 'm';
    const fillStr = 'ab';
    log(s.padStart(5, fillStr)); // ababm
    log(s.padStart(4, fillStr)); // abam

    log(s.padEnd(5, fillStr)); // mabab
    log(s.padEnd(4, fillStr)); // maba
})(console.log, String)

最大长度值忽略情况 #

如果原字符串长度,等于或大于最大长度,则字符串补全不生效,返回原字符串

(function (log, S) {
    const s = 'mm';
    const fillStr = 'ab';
    log(s.padStart(2, fillStr)); // 'mm'
    log(s.padEnd(2, fillStr)); // 'mm'
})(console.log, String)

补全字符串截取 #

如果用来补全的字符串原字符串,两者的长度之和超过了最大长度,则会截去超出位数补全字符串

(function (log, S) {
    const s = 'mm';
    const fillStr = '0123456789';
    log(s.padStart(10, fillStr)); // 01234567mm
    log(s.padEnd(10, fillStr)); // mm01234567
})(console.log, String)

补全字符串忽略 #

如果省略第二个参数,默认使用空格补全长度。

(function (log, S) {
    const s = 'mm';
    log(s.padStart(10)); //         mm
    log(s.padEnd(10)); // mm        
})(console.log, String)

常见用途 #

数值补全指定位数

padStart()padEnd常见用途是为数值补全指定位数

下面代码生成 10 位的数值字符串

(function (log, S) {
    const fillLength = 10;
    const fillStr = '0';
    log('1'.padStart(fillLength, fillStr)); // '0000000001'
    log('12'.padStart(fillLength, fillStr)); // '0000000012'
    log('123456'.padStart(fillLength, fillStr)); // '0000123456'

    log('1'.padEnd(fillLength, fillStr)); // '1000000000'
    log('12'.padEnd(fillLength, fillStr)); // '1200000000'
    log('123456'.padEnd(fillLength, fillStr)); // '1234560000'
})(console.log, String)

提示字符串格式

padStart()padEnd的还可以用作日期格式的字符串格式提示

下面代码生成年月日日期格式的字符串。

(function (log, S) {

    log('12'.padStart(10, 'YYYY-MM-DD')); // 'YYYY-MM-12'
    log('09-12'.padStart(10, 'YYYY-MM-DD')); // 'YYYY-09-12'

    // log('12'.padEnd(10, 'YYYY-MM-DD')); // 12YYYY-MM-
    // log('09-12'.padEnd(10, 'YYYY-MM-DD')); // 09-12YYYY-
    log('2019'.padEnd(10, '-MM-DD')); // '2019-MM-DD'
})(console.log, String)

matchAll #

产生原因 #

如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出。

(function (log, S) {

    const regex = /t(e)(st(\d?))/g;
    const string = 'test1test2test3';

    const matches = [];
    let match;
    while (match = regex.exec(string)) {
      matches.push(match);
    }

    log(matches);
    //     [ [ 'test1',
    //     'e',
    //     'st1',
    //     '1',
    //     index: 0,
    //     input: 'test1test2test3',
    //     groups: undefined ],
    //   [ 'test2',
    //     'e',
    //     'st2',
    //     '2',
    //     index: 5,
    //     input: 'test1test2test3',
    //     groups: undefined ],
    //   [ 'test3',
    //     'e',
    //     'st3',
    //     '3',
    //     index: 10,
    //     input: 'test1test2test3',
    //     groups: undefined ] ]
})(console.log, String)

上面代码中,while循环取出每一轮的正则匹配,一共三轮。


优化结果 #

目前有一个提案,目前还未实现,增加了String.prototype.matchAll方法,可以一次性取出所有匹配

不过,它返回的是一个遍历器(Iterator),而不是数组

(function (log, S) {
    // g 修饰符加不加都可以
    const regex = /t(e)(st(\d?))/g;
    const string = 'test1test2test3';

    if (string.matchAll) {
        for (const match of string.matchAll(regex)) {
            log(match);
        }
        // ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
        // ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
        // ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
    } else {
        log('not support');
    }

    // not support
})(console.log, String)

上面代码中,由于string.matchAll(regex)返回的是遍历器,所以可以用for...of循环取出。

相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源


遍历器转换 #

遍历器转为数组是非常简单的,使用...运算符和Array.from方法就可以了。

(function (log, S) {
    // g 修饰符加不加都可以
    const regex = /t(e)(st(\d?))/g;
    const string = 'test1test2test3';

    if (string.matchAll) {
        const matches = string.matchAll(regex);
        // 转为数组方法一
        log([...matches]);
        //     [ [ 'test1',
        //     'e',
        //     'st1',
        //     '1',
        //     index: 0,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test2',
        //     'e',
        //     'st2',
        //     '2',
        //     index: 5,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test3',
        //     'e',
        //     'st3',
        //     '3',
        //     index: 10,
        //     input: 'test1test2test3',
        //     groups: undefined ] ]

        // 转为数组方法二
        log(Array.from(matches));
        //     [ [ 'test1',
        //     'e',
        //     'st1',
        //     '1',
        //     index: 0,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test2',
        //     'e',
        //     'st2',
        //     '2',
        //     index: 5,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test3',
        //     'e',
        //     'st3',
        //     '3',
        //     index: 10,
        //     input: 'test1test2test3',
        //     groups: undefined ] ]

    } else {
        log('not support');
    }

    // not support
})(console.log, String)

Build by Loppo 0.6.16