在 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)