JavaScript即学即用教程[2]-字符串操作

我们观察整个计算机领域,这么多年,在研究的内容无非是这几种: 文本,图片,视频. 字符串的处理,至今仍然是一门很深的学问,如自然语言处理技术。所以我们学习js中的字符串基本处理方法,也是很有价值的。

字符串在底层的数据结构是一种数组,所以基本上文字处理操作函数,是对数组处理算法的一种综合和封装的运用。

创建字符串

字符串声明很简单,直接用双引号,或单引号包裹即可。

特殊的字符字面量-转义序列

非打印字符不能直接用字符字面量表示,需要用转义符号来表达. 例如:

1
2
3
4
5
6
\n 换行
\t 制表符
\b 退格
\r 回车
\\ 斜杠自身
\' 引号

另外要知道,字符字面量还有一种十六进制编码值的表示法,比如你知道这个字符在ASCII码表的16进制值或unicode码表中的编码值,那么可以用 \xnn \unnnn的方式来表示。例如:

1
2
\xa0 表示十进制160,他是latin1字符集(扩展了ASC码)里面的表象空格的这个字符。
\u3000 表示十六进制的12288,他是unicode字符集里的全角空格。上面的 `\xa0` 一般情况下其 unicode 码也是 `00a0`, 因此其 unicode 表示法为 `\u00a0`, 且 `\xa0 === \u00a0`

备注: 在 JavaScript 里面,一个中文字和英文字,取 length 的时候,都是返回 1.

其他类型转String类型

首先,在JavaScript里面任何类型都可以有办法转换为String类型。转换办法有2种:

  1. 对象自身的toString方法
  2. 全局的String函数

toString方法几乎可以应用在任何类型,但是除了null和undefined。我认为可以把toString理解为对象的方法(实际上也是的),所以JavaScript里面凡是调用时是对象或能自动包装为对象的,则都拥有toString方法,如:

1
2
3
4
true.toString() 返回'true'
10.toString(2) 返回'1010',二进制输出
10.toString(10) 返回十进制 '10'
null.toString() 会报错

而String函数在调用时,其实他会优先调用toString函数来返回值。碰上null和undefined则自己处理:

1
2
3
var str = String(123) 返回'123'
String(null) 返回 'null'
String(undefined) 返回 'undefined'

不可变

像其他语言中一样,String在内存中应该是不可变的。

故,所有string的API操作函数,没有任何一个是对原始字符串进行修改的,哪怕是replace()也是返回一个新的字符串。字符串自身的toString方法,也是返回一个新的字符串副本。

API

原型方法:

  • str.length, 获取字符串的长度
  • charAt(index), 取某个索引位置的字符
  • charCodeAt(index), 取某个索引位置的字符ASC码(实际是UCS码,在ES6里是unicode码)
  • indexOf(str, [fromIndex]), 取某个字符或字符串所在的索引位置, 找不到输出-1。 同样还有个lastIndexOf
  • search(str), 跟indexOf一样,只是既可以字符串,又可以搜正则。search无法找到所有匹配的子串的位置。
  • slice(index, index), 沾出一部分字符串来,从index到另一个index,如果第二个参数没有,就到末尾. 前包后不包。这个跟substring函数一样,且能支持负数(-1表示最后一个元素的下标,以此类推),所以直接学习slice就可以了。
  • substr(index,len), 从某个位置开始截取一定数量的字符。
  • match(regexp), 可以返回匹配的子串的数组,匹配不到返回null. 正则带g可以返回所有匹配的数组,不带则返回一个项的数组。(所以match好像无法找出匹配到的子捕获组的内容,这样的话,我们貌似只能通过regexp.exec那个函数来实现拿出捕获组的内容了)。通过match拿到结果数组,我们可以对匹配到的所有内容数组进行后续的map,forEach等操作
  • replace(regexp|substr, newSubstr|function), 替换某个子串为另外一个子串,或者新的子串用自己的函数来处理。带g才能替换所有匹配到的子串。
  • split(separatorStr|regexp), 可以用指定分隔符,或者正则,来把字符串隔成多个子串的数组。
  • toLowerCase,toUpperCase: 变小写变大写
  • concat(), 连接一个新的字符串,返回连接后的字符串。
  • padStart/padEnd 这俩方法后来新加的,挺有用的,可以用来给定特定的字符来对齐字符串
  • repeat 重复某个字符串n次
  • startWith(str)/endWith(), 判断是否以某个子串开头。
  • str.includes(searchString[, position]) 判断是否包含某个子串(内部实现可以用indexOf是否为-1来作为判定)

  • String.fromCharCode(), 可以把数值转换为字符串。适合你知道一个unicode码,想直接以unicode的形式输入这个字符的时候。当然,如果真有这种需求,也可以直接在双引号内使用\u+unicode这样的字面量的形式。

replace

replace函数配合正则可以进行更强大的匹配替换。

  • 默认replace只能替换匹配到的第一个子串。如果想全部替换则可以使用正则表达式,并带上g参数
1
text.replace(/at/g, 'amd')

带了g参数正则的replace,相当于正则会对字符串进行多次匹配,直到正则游标进行到无法匹配为止,因此可以对字符串中符合正则的子串进行全部替换。

在上面的替換字符串’amd’中,可以使用這些特定的符号来构造你要生成的目标字符串:

1
2
3
4
5
6
$$ // 表示 $ 这个符号自身
$& // 表示你匹配到的完整子串
$' // 表示匹配到的子串的左侧部分的字符串
$` // 表示匹配到的子串的右侧部分的字符串
$n // 即$1, $2等,表示捕获组
$nn // 如果捕获组比较多,就用2位数来表达

更精细的replace控制

有时,需要在替换字符串时加入一些控制逻辑,此时可以把replace第二个参数设置为function函数:

1
2
3
4
5
text.replace(/[<>"&]/g, function (matched, pos, originalText) {
// 其中matched表示每次匹配到的子字符串。 如果正则有捕获组,则matched后面还有n个捕获组参数,例如有2个捕获组你就可以这样命名形参: function (matched, $1, $2, pos, originalText) {}
// pos表示匹配到的子串的起始坐标
// originalText表示源串
})

应用

  1. 找出一个字符串中所有等于abc的子串坐标
1

  1. 找出一个字符串中所有等于abc的单词的坐标

  2. 找出一个字符串中所有等于abc的单词的个数

  3. 统计一个字符串中各个单词出现的频数(wordcount)

  4. 统计一个字符串中出现次数最多的那个单词(对wordcount结果进行取最大值?)

  5. 实现一个trim函数,去掉字符串左右的空格

  1. 表单项不能以xxx开头

    1
    2
    3
    if (str.indexOf(0) == '-') {
    alert('error')
    }
  2. 判断一个字符串里有没有数字
    利用search函数,支持正则。而indexOf不支持。

    1
    2
    3
    if (str.search(/[0-9]/) != -1) {
    alert('有数字')
    }
  3. 寻找出字符串里符号要求的某个子串
    要想满足这个要求,比如找出一个字符串里以a开头的单词,这种需求,用String的其他方法都无法满足。
    用search确实可以找到第一个位置,却无法找到所有的,自然就无法取出这些子串。
    而通过在match正则匹配,就可以对字符串进行子串匹配搜索,并且拿到结果。

    1
    str.match(/a\w+\s/)
  4. replace字符串里某些字符替换成另外一种逻辑
    String的replace函数就比较适合这种场景了,而且这种场景在现实中很常见。

  5. 像服务端提交数据时,把年和月份连接提来提交
    利用String的concat方法, 或者直接用+号运算符进行字符串拼接。

    1
    str1.concat(str2)
  1. 防范xss攻击,实现HTML转义
    利用了replace的高级用法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    this.REGX_HTML_ENCODE = /"|&|'|<|>|[\x00-\x20]|[\x7F-\xFF]|[\u0100-\u2700]/g;
    function encode(s){
    return (typeof s != "string") ? s :
    s.replace(this.REGX_HTML_ENCODE, function($0){
    var c = $0.charCodeAt(0), r = ["&#"];
    c = (c == 0x20) ? 0xA0 : c;
    r.push(c); r.push(";");
    return r.join("");
    });
    };

这里的0x20这个ASC值是ASCII码里面的空格,之所以换成0xA0是因为html实体里面定义了0xA0表示空格,所以这里做了切换。其实用0x20也没什么问题。http://www.w3school.com.cn/tags/html_ref_entities.html

  1. 实现首字母大写的几种方式,如例子 “abc def hig” 变为 “Abc Def Hig”

解决思路:可以想象为是对每个单词进行首字母大写(比如先split整个句子为单词的数组,再遍历处理)

针对单词:

1
2
3
4
5
6
// 方法1:对于单个单词,直接取第一个字符,再拼上slice后面的子串
`${str[0].toUpperCase()}${str.slice(1)}`
等同于:
str.charAt(0).toUpperCase() + str.slice(1)
// 同样的原理,有人提出可以用es6的函数参数的结构赋值---只是用这个特性来拿到第一个字符和后面的子串. 最终的算法还是这么 加一下
let firstUpperCase = ([first, ...rest]) => first.toUpperCase() + rest.join('')

针对句子的话,其实如果是分割为单词数组,每个单词同样可以使用上面的算法,我们来看下句子还有哪些更方便的算法:

1
2
3
4
5
// 方法2: 把句子分割为单词数组,对每个单词:把第一个单词找出,并replace为其大写形式
//选取首个字符 (str就是一个单词)
var char=str.charAt(0);
//将单子首字符替换为大写 (感觉还不如直接slice拼一下)
str=str.replace(char,function(s){return s.toUpperCase();}); // replace第二个参数是个函数,接收参数s为当次匹配到的结果
1
2
3
4
5
6
7
8
9
// 方法3: 上面是直接拿出第一个字符再replace,我们还直接用正则找出第一个字符
str.toLowerCase().replace(/^\S/g,function(s){return s.toUpperCase();}); // 这个智能解决句子的首字母。
// 或者找到句子里每个单词来替换首字母 (不过\b会匹配到 I'm 的m,会认为m前面也是个边界)
str.toLowerCase().replace(/\b[a-z]/g,function(s){return s.toUpperCase();})
String.prototype.firstUpperCase = function(){
return this.replace(/\b(\w)(\w*)/g, function($0, $1, $2) {
return $1.toUpperCase() + $2.toLowerCase();
});
}// 这个其实也存在I'm的问题

那么,到底最好用的方法是哪个呢? 我觉得有这两个:

1
2
3
4
5
6
7
8
9
10
// 要么分割为单词数组,再正则替换
arr.forEach(str => {
return str.toLowerCase().replace(/^[a-z]/g, (L) => L.toUpperCase());
}).join(' ')
// 要么直接正则replace
function firstUpperCase(str) {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
}


Refer

学习下html转义可参考:
http://www.cnblogs.com/snowinmay/p/3224375.html
http://blog.chinaunix.net/uid-20511797-id-3118652.html
http://www.cnblogs.com/sxz2008/p/6518367.html