在 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)
上面代码中,汉字𠮷(注意,这个字不是吉祥的吉)的码点是0x20BB7,UTF-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方法相同。
你可能注意到了,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)
许多欧洲语言有语调符号和重音符号。为了表示它们,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(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 之间的小数,取整以后等于-0,repeat视同为 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的参数是数组,Map,WeakMap,Set,WeakSet或者对象,则会统一当作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)
如果一个正则表达式在字符串里面有多个匹配,现在一般使用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)