# 概述
在JavaScript中,所谓字符串就是包含在英文“双引号”或‘单引号’中的内容,可以是数字、运算符号、各国语言、特殊编码字符,甚至还能是HTML的标签。也就是说,只要符合在双引号或单引号中这个要求,计算机文本中的内容基本上都可以成为一个字符串,ES6新引入了模板字符串,使用反引号(`)标识。
获取一个字符串的途径非常多,特别是在进行表单操作的的时候,通过表单输入框,无论表单类型,哪怕是在输入一个纯数字的时候,得到的都是一个字符串。还有通过JavaScript获取DOM元素节点中文本的 innerText
和 textContent
属性得到的字符也是一个字符串。
我们先来看从一系列的表单元素中获取到的值的数据类型的例子:
<!-- HTML 部分 --> <form> <input type="text" value="123"><br> <input type="tel" value="15228885771"><br> <input type="email" value="lihy_online@163.com"><br> <input type="color" value="#ffffff"><br> </form> <!-- JavaScript 部分 --> <script type="text/javascript"> // 获取所有的表单元素 var aIpt = document.getElementsByTagName(`input`); var iptType, valType; // 循环遍历aIpt元素 for(var i = 0; i < aIpt.length; i++){ // console.log(aIpt[i]); // 获取表单类型 iptType = aIpt[i].type; // 获取值类型 valType = typeof aIpt[i].value; console.log(`表单类型为:${iptType},数据类型为:${valType}`); } // 输出结果如下: // 表单类型为:text, 数据类型为:string // 表单类型为:tel, 数据类型为:string // 表单类型为:email,数据类型为:string // 表单类型为:color,数据类型为:string </script>
再来看一个通过 innerText
属性和 textContent
属性获取到的文本内容的数据类型的例子:
<!-- HTML 部分 --> <p>Hello, world!</p> <p>lihy_online@163.com</p> <!-- JavaScript 部分 --> <script type="text/javascript"> var aP = document.getElementsByTagName(`p`); for(let i = 0; i < aP.length; i++) { console.log(`通过innerHTML获取到的值为:${aP[i].innerHTML},值类型为:${typeof aP[i].innerHTML}`); console.log(`通过textContent获取到的值为:${aP[i].textContent},值类型为:${typeof aP[i].textContent}`); } // 输出结果如下: // 通过innerHTML获取到的值为:Hello, world!,值类型为:string // 通过textContent获取到的值为:Hello, world!,值类型为:string // 通过innerHTML获取到的值为:lihy_online@163.com,值类型为:string // 通过textContent获取到的值为:lihy_online@163.com,值类型为:string </script>
# 字符串引号使用规则
在使用字符串的引号时需要注意,如果一个字符串已经使用过引号,若再在字符串内使用引号需要使用另外一个引号。即双引号内使用单引号,单引号内使用双引号,或者使用转义符 \
对同样的引号进行转换,多层引号嵌套,该规则同样适用,如:
var str1 = "Hello, I'm Petter!"; var str2 = 'How do you think about "JavaScript"?'; var str3 = "设置字体的'颜色'代码是:<p style=\"color:red;\">";
需要了解的一个字符串常识就是,字符串不能直接分成多行去写,否则浏览器会报错。如果字符串过长,需要进行换行使视觉层次上更加地清晰,那需要这样去写:
var str = "这是一个" + "分行写的" + "字符串"; console.log(str); // 输出:这是一个分行写的字符串
# 字符串与数组
通过上一章中学习的内容可以发现 parseInt()
和 parseFloat()
方法可以将字符串转换为数值,同时也能对数组元素中的第一个元素进行数值转化。这说明在某种程度上,字符串和数组是有一点联系的。实际上,数组拥有的很多属性,字符串也同样具备。来看这样一个输出例子:
var str = 'ABCDE'; var arr = ['A', 'B', 'C', 'D', 'E']; console.log(str.length); // 5 console.log(arr.length); // 5
通过上例可以清晰地发现,字符串和数组是何等的相似。实际上,数组和字符串是可以互相转换的,这就需要用到两个方法:split()
和 join()
:
// 1、split 将字符串转换为数组 var result; var str = "HTML,CSS,JavaScript,jQuery"; result = str.split(); console.log(result); // ["HTML,CSS,JavaScript,jQuery"] result = str.split(','); console.log(result); // ["HTML", "CSS", "JavaScript", "jQuery"] // 2、join 将数组转换为字符串 var arr = ["152", "2888", "5771"]; result = arr.join(); console.log(result); // "152,2888,5771" result = arr.join('-'); console.log(result); // "152-2888-5771"
从上例可以分析出 split()
方法可以将一个字符串转换为一个数组,若添加一个用引号引起来的参数,得出的数组就会以该参数进行分割(上例中使用的是“,”作为参数,用其它字符作为参数同样可行),若所给参数是一个空引号"",则形成的数组会合并为一个数组元素项。同样的,join()
方法是将一个数组元素转换为一个字符串,根据给出的参数对结果中的字符串进行分割,或者不进行分割。当然,这两个方法也可以不给出参数,用它们内置的默认值进行处理,如:
直接用索引下标(str[index]
)访问一个字符串的方式,是在ECMAScript第3版中新增的方式,对IE8之前的浏览器是不支持的,如果要兼容老版本浏览器建议使用 charAt()
这个方法。除了浏览器兼容性的区别外,这两个方法还有一个区别就是,直接使用字符串索引的方法去查找一个不存在的字符返回的是 dundefined
,而通过 charAt()
方法得到的是一个空字符串(很多时候我们更希望出现这个结果)。
var str = 'ABCDE'; console.log(str[0]); // A console.log(str[5]); // undefined console.log(str.charAt(0)); // A console.log(str.charAt(5)); // ""
# 字符串与Unicode
反斜杠 \
在字符串中有特殊用途,用来表示一些特殊的字符,所以又称 转义操作符(简称:转义符),以下是一些常用转义符的表示法:(小括号中的值表示法为Unicode)
\0(或:\u0000)用于表示:null \b(或:\u0008)用于表示:后退键 \f(或:\u000C)用于表示:换页符 \n(或:\u000A)用于表示:换行符 \r(或:\u000D)用于表示:回车键 \t(或:\u0009)用于表示:制表符 \v(或:\u000B)用于表示:垂直制表符 \'(或:\u0027)用于表示:单引号 \"(或:\u0022)用于表示:双引号 \\ (或:\u005C)用于表示:反斜杠
当然,转义符远不止上面列出的这些。你需要注意的是,在非特殊字符前面加上转义符 \
,那“\”会被省略掉,如果需要输出“\”,那就需要写成双斜杠“\ \”的形式。
console.log('\欢迎来到王者荣耀!'); // "欢迎来到王者荣耀!" console.log('\t欢迎来到王者荣耀!'); // " 欢迎来到王者荣耀!" console.log('欢迎来到\\王者荣耀\\!'); // "欢迎来到\王者荣耀\!"
1、字符的 Unicode 表示法
JavaScript 允许采用 \uxxxx
形式表示一个字符,其中 xxxx
表示字符的 Unicode 码点,每个Unicode码都有各自对应的字符,如 \u00A9
就是用于输出版权符号“©”的。
在JavaScript引擎内部,所有字符都用Unicode表示,它不仅以Unicode储存字符,还允许直接在程序中使用Unicode编号表示字符。解析代码的时候,JavaScript会自动识别一个字符是字面形式表示,还是Unicode形式表示。输出给用户的时候,所有字符都会转成字面形式。其实也就是说,任何JavaScript中允许的字符都是可以通过Unicode来表示的。
var str = "\u738b\u8005\u8363\u8000"; str; // "王者荣耀"
我们还需要知道,每个字符在JavaScript内部都是以16位(即2个字节)的UTF-16格式储存。也就是说,JavaScript的单位字符长度固定为16位长度,即2个字节。因此,\uxxxx
这种表示法只限于码点在\u0000
~\uFFFF
之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
"\u20BB7" // " 7" "\uD842\uDFB7" // "?"
上面代码表示,如果直接在\u
后面跟上超过0xFFFF
的数值(比如\u20BB7
),JavaScript会理解成 \u20BB+7
。由于 \u20BB
是一个不可打印字符,所以只会显示一个空格,后面跟着一个7
。ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。
"\u{20BB7}" // "?"
2、str.charCodeAt(idx)
在ES5中,如果想将一个现成的字符转换为十六进制的Unicode的话,需要通过 str.charCodeAt(idx)
方法获取字符数字编码值,其中,str 为原字符串,idx为需要获取数字编码的字符对应的下标。然后再通过 toString(16) 将刚才得到的数字编码值转化为一个十六进制的字符,并在这个十六进制的字符的前面拼接上 \u,就可以得到一个十六进制表示法的Unicode了。
/** * 函数封装:将字符串转为Unicode编码 */ Object.prototype.toUnicodeString = function(str) { let s = str || this.valueOf() ; let result = ``; for (var i = 0; i < s.length; i++) { result += `\\u${s.str.charCodeAt(i).toString(16)}`; } return result; } `王者荣耀`.toUnicodeString() // "\u738b\u8005\u8363\u8000"
但你需要注意,正如上一小节所说,JavaScript内部,字符以UTF-16的格式储存,每个字符固定为2
个字节。对于那些需要4
个字节储存的字符(Unicode码点大于0xFFFF
的字符),JavaScript会认为它们是两个字符。
var s = "?"; s.length // 2 s.charAt(0) // "�" s.charAt(1) // "�" s.charCodeAt(0) // 55362 s.charCodeAt(1) // 57271
上面代码中,汉字“?”(注意,这个字不是”吉祥“的”吉“)的码点是0x20BB7
,UTF-16编码为 0xD842 0xDFB7
(十进制为55362 57271
),需要4
个字节储存。对于这种4
个字节的字符,JavaScript不能正确处理,字符串长度会误判为2,而且 charAt 方法无法读取整个字符,charCodeAt 方法只能分别返回前两个字节和后两个字节的值。
3、String.fromCharCode()
在ES5中,我们可以通过String.fromCharCode(numCode)
(这里的“numCode”为转换出的数字编码),又重新将数字编码转换为原来的字符串:
var str = "帅"; var numCode = str.charCodeAt(0); // 24069 var oriStr = String.fromCharCode(numCode); // "帅"
这个方法不能识别32位的UTF-16字符(即Unicode编号大于0xFFFF
的字符)。
String.fromCharCode(0x20BB7) // // "ஷ"
# 字符串包装对象
在 JavaScript中对象主要分为两大类,一类称作“包装对象”,一类称作“内置对象”。从编码角度讲,它们的区别是在使用了 Object()
方法后是否还全等于自身,若不等于自身,那就是一个包装对象;若等于自身,那就是一个内置对象。
var str = "Henrry Lee"; str === Object(str); // false 包装对象 var obj = {name: "Henrry Lee"}; obj === Object(obj); // true 内置对象
字符串是 JavaScript 原生提供的三个包装对象之一(另外两个是数值与布尔类型),用来生成字符串的包装对象。既然作为对象的字符串肯定就有自己的方法。如 valueOf()
方法就是用于返回对象所包装的那个字符串。字符串的包装对象是一个类似数组的对象(但仍然不是数组)。
上面提到了,只要是对象,就有自己的特定的方法,有些方法是所有对象公有的,有些方法是该类对象私有的。共有的方法无论是包装对象还是内置对象都可以使用,而私有的方法,只有指定类型的对象可以使用(有的方法可以有两种或以上的数据类型可以使用,如后面会讲到的 concat()
等方法)。
# 字符串对象方法
1、定义字符串
var str = "Hello, world!"; var str = 'Hello, world!'; var str = `Hello, world!`; // ES6 // 包装对象 var str = new String("Hi!"); var str = new Object("Hi!");
2、字符串长度
length 属性,用于返回字符串长度。
var str = "China!"; str.length; // 6
3、字符串查询
3.1、charAt()
charAt 方法返回指定位置的字符,参数是从0开始编号的下标。其语法形式为:charAt(idx)
var s = new String("abc"); s.charAt(0); // a s.charAt(s.length - 1); // c // 使用数组的下标语法 s.charAt(0); // a s[0]; // a // 如果参数为负数,或大于等于字符串的长度,charAt返回空字符串。 s.charAt(-1); // "" s.charAt(10); // ""
3.2、indexOf() 、 lastIndexOf()
这两个方法用于确定一个字符串在另一个字符串中的位置,返回一个整数,表示匹配开始的位置。如果返回-1
,就表示不匹配。两者的区别在于,indexOf 从字符串头部开始匹配,lastIndexOf 从尾部开始匹配。
var str = 'Hello, world!'; str.indexOf('l'); // 2 str.indexOf('world!'); // 7 str.lastIndexOf('l') // 10
它们还可以接受第二个参数,对于 indexOf 方法,第二个参数表示从该位置开始向后匹配;对于lastIndexOf,第二个参数表示从该位置起向前匹配。
var str = 'Hello, world!'; str.indexOf('l', 5); // 10 str.lastIndexOf('l', 5) // 3
3.3、match()
match 方法用于字符串查询,如果没有找到,则返回null,如果找到,返回一个数组。
var str = "abc"; str.match("ab"); // ["ab"] str.match("ac"); // null
3.4、search()
search 方法的用法等同于 indexOf/lastIndexOf,但是其返回值为匹配到的第一个位置。如果没有找到匹配,则返回 -1
。
var str = 'Hello, china!'; str.search('china'); // 7 str.search('world'); // -1
3.5、查询头部/尾部
var website = "http://www.baidu.com"; // 查询头部 // reg -> /^条件/ console.log(/^http/.test(website)); // 查询尾部 // reg -> /条件$/ console.log(/com$/.test(website)); // 查询是否包含 // reg -> /条件/ console.log(/baidu/.test(website));
4、字符串拼接
concat 方法用于连接两个字符串,返回一个新字符串,不改变原字符串。其语法形式为:concat(str1, str2, str3…)
var s1 = 'a', s2 = 'b'; s1.concat(s2); // "ab" var s3 = 'c'; s1.concat(s2, s3); // "abc"
tips:如果参数不是字符串,concat 方法会将其先转为字符串,然后再连接。
拼接字符串,也可以使用+
号。
var str1 = 'Hello, '; var str2 = 'China!'; var str3 = str1 + str2; // "Hello, China!";
5、字符串截取
5.1、slice()
slice 方法用于字符串截取,其语法形式为:slice(start, end)
它的第一个参数是开始位置,第二个参数是结束位置(不含该位置)。
'JavaScript'.slice(0, 4) // "Java"
如果省略第二个参数,则表示从指定位置开始截取到末尾。
'JavaScript'.slice(4) // "Script"
如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度。
'JavaScript'.slice(-6) // "Script" 'JavaScript'.slice(0, -6) // "Java" 'JavaScript'.slice(-2, -1) // "p"
如果第一个参数大于第二个参数,slice 方法返回一个空字符串。
'JavaScript'.slice(2, 1) // ""
5.2、subString()
substring 方法用于字符串截取,不改变原字符串。它与slice作用相同,使用方法类似,但有一些奇怪的规则,因此不建议使用这个方法,优先使用slice。其语法形式为:subString(start, end)
它的第一个参数是开始位置,第二个参数是结束位置(不含该位置)。
'JavaScript'.substring(0, 4) // "Java"
如果省略第二个参数,则表示从指定位置开始截取到末尾。
'JavaScript'.substring(4) // "Script"
如果第二个参数大于第一个参数,substring 方法会自动更换两个参数的位置。
'JavaScript'.substring(10, 4) // "Script" // 等同于 'JavaScript'.substring(4, 10) // "Script"
如果参数是负数,substring 方法会自动将负数转为0。
'Javascript'.substring(-3) // "JavaScript" 'JavaScript'.substring(4, -3) // "Java"
3.3、substr()
substr 方法用于字符串截取,不改变原字符串。其语法形式为:subStr(start, end)
它的第一个参数是开始位置,第二个参数是结束位置(不含该位置)。
'JavaScript'.substr(4, 6) // "Script"
如果省略第二个参数,则表示子字符串一直到原字符串的结束。
'JavaScript'.substr(4) // "Script"
如果第一个参数是负数,表示倒数计算的字符位置。如果第二个参数是负数,将被自动转为0,因此会返回空字符串。
'JavaScript'.substr(-6) // "Script" 'JavaScript'.substr(4, -1) // ""
提示:字符串截取优先使用 slice 方法。
6、字符串去除空格
trim 方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串。
' Hello world!'.trim() // "Hello world!"
7、字符串大小写转换
toLowerCase 方法用于将一个字符串全部转为小写,toUpperCase 则是全部转为大写。它们都返回一个新字符串,不改变原字符串。
'Hello World'.toLowerCase(); // "hello world" 'Hello World'.toUpperCase(); // "HELLO WORLD"
这个方法也可以将布尔值或数组转为大写字符串,但是需要通过call方法使用。
String.prototype.toUpperCase.call(true) // 'TRUE' String.prototype.toUpperCase.call(['a', 'b', 'c']) // 'A,B,C'
8、字符串比较
JavaScript允许使用 >
、<
或 ==
运算比较字符串,返回一个布尔。其在比较时是通过字符的 Unicode 编码进行比较的。
var compare = (s1, s2) => { if (s1 > s2) { console.log(`${s1} > ${s2}`); }else if(s1 == s2) { console.log(`${s1} = ${s2}`); }else { console.log(`${s1} < ${s2}`); } } compare('a', 'b'); // "a < b" compare('a', 'A'); // "a > A" compare('ab', 'ac'); // "ab < ac" compare('ab', 'ab'); // "ab = ab"
有一种情况需要注意,就是计算机的区域设置。因为用"<
"和">
"来比较字符串时,并不会考虑当地的排序规则,比如在西班牙语中,按照传统的排序,"ch"将作为一个字符排在"c"和"d"之间。localeCompare()
提供了一种方式,可以帮助你使用默认区域设置下的字符排序规则。
localeCompare 用本地特定的顺序来比较两个字符串。其语法形式为: stringObject.localeCompare(target)
,如果 stringObject 小于 target,则 localeCompare() 返回小于 0 的数。如果 stringObject 大于 target,则该方法返回大于 0 的数。如果两个字符串相等,或根据本地排序规则没有区别,该方法返回 0。
'apple'.localeCompare('banana') // -1 'apple'.localeCompare('apple') // 0
提示:ECMAscript 标准并没有规定如何进行本地特定的比较操作,它只规定该函数采用底层操作系统提供的排序规则。
接下来我们将用本地特定排序规则对字符串数组进行排序:
var strings; // 要排序的字符串数组,假设已经得到初始化 strings.sort(function(a,b) { return a.localeCompare(b) }); // 调用sort()方法进行排序
9、字符串替换
replace 方法用于字符串替换,其语法形式为:replace(search, replacement)
var str = "Hello, world!"; str.replace("world", "china"); // "Hello, china!"
10、将字符串切割为数组
split 方法将一个字符串根据某个字符串切割为数组,其语法形式为:split(str)
var str = "HTML,CSS,JavaScript"; str.split(","); // ["HTML","CSS","JavaScript"]
# Base64转码
Base64是一种编码方法,可以将任意字符转成可打印字符。这种编码方法,一开始的作用是为了不显示特殊字符,简化程序的处理。但因为这种编码方式比较复杂,现在使用JavaScript的开发者都少有涉及这种编码方式,所以更多时候都将其作为一种加密手段在使用。该编码提供两个方法来转换字符:
-
btoa()
将字符串或二进制值转为Base64编码
-
atob()
将Base64编码转为原来的编码
我们先来看一个将普通字符串转换为Base64格式编码,再将该格式转换为普通字符串在控制台中输出的例子:
可以从示例中的前两个结果很直观的发现,我们首先是用btoa()将一串普通的字符串“Hello, world!”转换成为了Base64码“YXVsZW5jZQ==”,然后我们将得出的Base64码通过atob()方法将得到的Base64码再次成功地转换成为了普通的字符串“Hello, world!”。示例中的后两个结果我们是对二进制数“0b1001”进行了相同方式的转换,最后得到字符串“9”(若用parseInt(0b1001),得出的结果为数字9,而不是字符串“9”)。可见这两种方法是可以将字符串和Base64码在两种格式之间互相转换的,但是要注意的是,通过atob()这种方法是不能还原出数值型的值的,这为我们拼接密码字符串提供了便利(因为“+”运算符只能做拼接运算了)。但在模型情况下需要得出的结果是一个数值型的值,那就得用“数值的转换”提供的三种方法来进行转换了。
但是在使用这两个方法的时候需要注意的是,这两个方法不支持对非ASC II字符转换为Base64码,否则在浏览器中会报错。如果要对非ASC II的字符进行Base64转码,需要用到两个对URI组件编码的函数:“encodeURIComponent()”函数和“decodeURIComponent()”函数,前者能将字符串作为 URI 组件进行编码,后者能将encodeURIComponent() 函数编码的 URI 进行解码。如例:
利用这个原理,就可以将非ASC II的稍微重要的机密字符串进行这样简单的加密传输,在传输的终端再进行一次解密。这样的方式对于熟悉计算机底层语言的黑客来说也许算不上什么高级手段,但对于一般用户和普通开发人员而言,这样的加密手段肯定是会让他们摸不着北的。但是,稍微高级一点的加密算法通常都不是用一两种编码转换或者几种二进制运算的方式来编写的,这些高级加密算法非常之复杂,令很多从事黑客技术多年的人都“望而生怯”。
作为一个前端开发者,也许掌握那些高级加密手段一时间没有什么用。但在现在的用户体验设计主流思维方向都在追求着一种便捷性、易用性、效率性,哪怕曾经对安全要求特别高的金融行业、金融软件在现今都让“6位纯数字密码”取代了之前强制要求的“数字+字母组合密码”,类似微信支付、京东支付等这些金融类或电子商务类的App(Application,现在在计算机术语中通常指“移动端的应用软件”)甚至出现了免密支付。这和曾经要求类似“密码至少要求8位,需要以英文字母+数字+符号组成”这样的要求“背道而驰”了,这让很多用户反而对密码的安全产生了顾虑。
其实针对这个问题大可不必有太多顾虑,因为现在的支付平台都提供了在线冻结支付相关的功能,只要一个电话核对用户相关的信息,只要基本能符合(不要求所有问题都能答得上)即可冻结支付的账户,这为手机不小心遗失提供了一个安全的便捷保障。如果手机还在你手里那就更不用担心了,因为现在的支付平台都是和设备绑定的,也就是同样一个密码只能在同一台设备上进行使用,如果要使用另外一台设备上使用,或者是IP段于原设备不符,都是需要提供绑定设备提供的短信验证码的。所以,就算是支付密码被别人知道了,他们拿去也是什么事也干不了的。而且现在的“6位数字密码”的键盘在绝大部分App上都并非是手机上提供的“原生”数字键盘,而是程序员“自己做”的,这样一来木马程序就无法获取到用户通过键盘输入使产生的“键值”,也就无法直接地获知用户到底输入的是什么,甚至根本就不认为用户正在进行着输入的行为,截获用户输入就更无从谈起了。