重学JavaScript系列——(五)基本引用类型
博主以扎实JavaScript基础为目的,以《JavaScript高级程序设计(第四版)》为核心参考资料,以一个“复习者”的角度有针对性地来创作这期专栏。文章加入了博主的很多思考和开发经验,关注初学JavaScript时容易忽略的地方,着重总结了ECMAScript新标准知识点的特性和应用场景。最终,本专栏将覆盖完整的JavaScript知识体系,以辅佐各路豪杰在开发路上的稳步前进。
专栏传送门:https://blog.csdn.net/huoyihengyuan/category_10586561.html
文章目录
引用值(或者对象)是某个特定 引用类型的实例。在ECMAScript中,引用类型是把数据和功能组织到一起的结构, 经常被人错误地称作“类”。虽然从技术上讲,JavaScript是一门面向对象语言,但ECMAScript缺少传统面向对象编程语言的基本结构,包括类和接口。引用类型有时候被称为 对象定义,因为它们描述了自己的对象应有的属性和方法。
引用类型虽然有点像类,但是跟类不是一个概念。为了避免混淆,后买呢不会使用术语类。
对象被认为是某个特定引用类型的实例。新对象通过new操作符后跟一个构造函数(constructor)来创建。
5.1 Date
ECMAScript的Date参考了Java早期版本的java.util.Date。为此Date类型将日期保存为自协调世界时(UTC,Universal time Coordinated)时间1970年1月1日午夜(零时)至今所经过的毫秒数。使用这种存储格式,Date类型科技精确表示1970年1月1日之前以及之后285616年的日期。
在使用Date对象时,我们记不住那么多表示时间的API,但可以记住几个常用的API。
Date.parse()和Date.UTC()都能接受一个表示日期的参数,然后返回1970.01.01之后的毫秒数。Date.now()返回方法执行时的毫秒数,这个方法方便我们测试代码运行时间。
5.1.1 继承的方法
与其他类型一样,Date类型重写了toLocaleString()、toString()、valueOf()方法。对前两个方法来说,它们返回日期和时间,现代浏览器已经在这两个方法的输出上趋于一致,但是由于旧浏览器上是不同的。这些差异意味着toLocaleString()和toString()可能只对调试有用,不能用于显示。
Date的valueOf()返回日期的毫秒表示,可以直接利用操作符(如大于号和小于号)来直接使用它返回的值。
let date1 = new Date(2019,0,1)
let date2 = new Date(2019,1,1)
console.log(date2>date1)//true
console.log(date2<date1)//false
5.1.2 其他Date的API方法
Date的API提供了很多格式化方法或取得日期值的特定部分,有兴趣或有需要的朋友可以自行查阅更详细的表格,这个就不再过多阐述,毕竟我们也不可能记住所有的日期API。
Date的API最令我头大的是,它们某些特定值有的是从0开始的,有的是从1开始计数的,容易混淆。除了手动查阅API外,社区中也出现了不错的Date相关的工具库,但在工作时要小心变成工具库的搬运工(手动滑稽)。
5.2 RegExp
ECMAScript通过RegExp类型支持正则表达式。正则表达式使用类似Perl的简介语法来创建:
let expression = /pattern/flag
这个正则表达式的pattern(模式)可以是任何简单或复杂的正则表达式,包括字符类、限定符、分组、向前查找和反向引用。每个正则表达式可以带零个或多个flags(标记),用于控制正则表达式的行为。下面给出了表示匹配模式的标记。
- g:全局模式,表示查找字符串的全部内容。
- i:不区分大小写,表示在查找匹配时忽略pattern和字符串的大小写。
- m:多行模式,表示查找到一行文本末尾时会继续查找。
- y:粘附模式,表示只查找从lastIndex开始及之后的字符串。
- u:Unicode模式,启用Unicode匹配。
- s:doAll模式,表示元字符.可以匹配任何字符(包括\n和\r)。
与其他语言中的正则表达式类似,所有元字符在模式中也必须转意:
{ [ ( \ ^ $ | ) ] } ? * + .
创建正则表达式除了使用简介语法之外,也能直接使用RegExp构造函数来创建。由于构造函数需要传入字符串,某些情况下需要二次转义,这个需要特别注意不要混淆。
字面量模式 | 对应的字符串 |
---|---|
/\[bc\]at/ | "\\[bc\\]at" |
\.at | "\\.at" |
\w\\hello\\123 | "\\w\\\\hellp\\\\123" |
5.2.1 RegExp实例属性
每个RegExp实例都有下列属性,提供有关模式的各方面信息。
- global:布尔值,表示是否设置了g标记。
- ignoreCase:布尔值,表示是否设置了i标记。
- unicode:布尔值,表示是否设置了u标记。
- sticky:布尔值,表示是否设置了y标记。
- lastIndex:整数,表示在源字符串中下一次搜索的开始位置,始终从0开始。
- multiline:布尔值,表示是否设置了m标记。
- doAll:布尔值,表示是否设置了s标记。
- source:正则表达式的字面量字符串(不是构造函数的字符串),没有开头和结尾的斜杠。
- flags:正则表达式的标记字符串。
通过这些属性可以全面了解正则表达式的信息,但是实际开发中用得不多,因为模式声明中包含这些信息。
举个例子:
let pattern1 = /\[bc\]at/i
let pattern2 = new RegExp("\\[bc\\]at","i")
console.log(pattern1.source)//"\[bc\]at"
console.log(pattern2.source)//"\[bc\]at"
两个模式分别通过字面量和构造函数创建,但是source属性是相通的,返回的是规范化之后的字面量表示。
5.2.2 RegExp实例方法
RegExp的主要方法是exec(),传入要匹配的字符串,在调用后返回第一个匹配信息,并包含index属性(当前匹配位置的下标)和input属性(传入的字符串)等。
如果设置全局标记g,可以通过多次调用exec来依次匹配符合模式的子串,每次调用exec都会更新lastIndex到下一次准备匹配到位置。
如果设置粘附模式y,则覆盖g标记,含义是只从lastIndex到位置上查找,如果找不到就重置为0。
不过在JavaScript中,我们更常用字符串的match或matchAll方法来匹配字符串,分别返回数组或迭代器,方便我们拿到指定模式的值。
正则表达式的valueOf()方法返回正则表达式本身。
5.2.3 RegExp的构造函数属性
构造函数本身也有几个属性,并且都有几个简写:
全名 | 简写 | 说明 |
---|---|---|
input | $_ | 最后搜索的字符串 |
lastMatch | $& | 最后搜索的字符串 |
lastParen | $+ | 最后搜索的最后匹配的捕获组 |
leftContext | $` | input字符串中出现在lastMatch前面的文本 |
rightContext | $’ | input字符串中出现在lastMatch后面的文本 |
使用简写模式时,要加上中括号,因为大多数简写模式都不是合法的ECMAScript标识符:
RegExp.$_
RegExp.["$`"]
RegExp.["$'"]
RegExp.["$&"]
RegExp.["$+"]
RegExp.["$*"]
RegExp构造函数的所有属性都没有任何的Web标准出处,因此不要在生产环境中使用它们。(总结:暂时性风险过高,不推荐使用)
5.2.4 模式局限
虽然ECMAScript现在对正则表达式支持已经有了长足进步,但还是有一些高级特性没有被支持:
- \A和\Z锚(分别匹配字符串的开始和结尾)
- 联合及交叉类
- 原子组
- x(忽略空格)匹配模式
- 条件式匹配
- 正则表达式注释
虽然还有一些局限,但ECMAScript正则表达式的日常开发的已经可以覆盖大多数应用场景。
5.3 原始值包装类型/基本包装类型
为了方便操作原始值,ECMAScript提供了三种特殊的引用类型:String、Number和Boolean。
当对一个原始类型的值调用函数时,后台会自动创建对应的包装类型进行操作,紧接着销毁该包装类型。
5.3.1 Boolean
Boolean我们一般不必显式地调用,而是直接使用原始值。
5.3.2 Number
num.toString(10)代表转化为10进制
num.toString(2)代表转化为2进制
...
num.toFixed(2)//保留两位小数
num.toExponential(1)//科学计数法,小数保留1位
//根据数值和精度来自动确定应该调用toFixed/toExponential
num.toPrecision()
Number.isInteger()用于辨别一个数值是否是整数。(1.00也算作整数)
IEEE754有一个特殊的数值范围,从Number.MIN_SAFE_INTEGER(-253+1)到Number.MAX_SAFE_INTEGER(253-1)。可以用Number.isSafeInteger()判断是否在这个范围内。
5.3.3 String
(1)JavaScript字符
JavaScript字符串由16位码元(code unit)组成,对于多数字符来说,每16位码元对应一个字符,字符串的length属性表示字符串包含多少16位码元。
可以通过String.fromCodePoint()传入数字(不只十进制表达)得到对应ASCII的字符,如果传入一个数字列表,则会得到一个对应字符拼接的字符串。
相反,如果期望得到字符串对应的ASCII的十进制表示,可以用codePointAt()。
String.fromCharCode()和charCodeAt()是另一组达到相同目的的函数,但是他们每次只能识别一个码元,某些字符(如笑脸符号😊)是由代理对字符(两个码元组成的字符),这样一来,String.fromCharCode()和charCodeAt()就不能准确处理代理对字符。而String.fromCodePoint()和codePointAt()却可以正确处理,所以应用场景更广阔一些。
(2)normalize()方法
某些Unicode字符可以有多种编码方式,有时候一些特殊字符外观相同,但通过“===”比较却不相等,这是因为它们使用的规范化形式不同,分别是NFD、NFC、NFKD、NFKC。此时就可以通过normalize()进行规范化。
规范化形式的具体细节超出了JavaScript的范围,在工作场景中也不常见,感兴趣的朋友可自行google。
(3)字符串操作方法
stringValue.concat()和字符串使用“+”拼接的效果一样,顺便说一下,前者可以传入多个参数。
ECMAScript中slice()、subStr()、substring()都能提取字串,它们的使用细节略有不同,但应用场景几乎一致,所以熟记一种即可。
(4)字符串位置方法
indexOf()和lastIndexOf()分别从初始和末尾定位子串的位置下标,并且都允许传入第二个数值参数,代表从指定下标开始查找。
利用第二个参数,可以遍历所有子串,实现类似正则表达式的功能。
(5)字符串包含方法
ECMAScript6添加3个字符串包含的方法并返回布尔值,startsWith()和endsWith()分别检查是否包含在头和尾,includes()则可检查到任意位置。
(6)trim()方法
trim()用于消除字符串的头尾空格,另外trimLeft()和trimRight()可以分别消除头和尾空格。
trimStart()和trimEnd()可以实现相同效果。
(7)repeat()方法
stringValue.repeat(5)代表字符串重复多少遍,并返回一个副本。
(8)padStart()和padEnd()方法。
这两个方法会复制字符串,如果小于指定长度,会在相应的一边填充复制的字符串,直至满足长度。默认字符串为空格。
这个应用场景尤其适合日期时间中需要把“1,2,3”转为“01,02,03”的场景。
(9)字符串迭代与解构
字符串原型上暴露了一个@@iterator方法,所以它可以被扩展运算符和for-of循环使用,除此之外,我们也可以手动的使用迭代器。
关于@@itetator在之前的文章(重学JavaScript系列——(三)语言基础/Symbol部分)中已经介绍过,这里不再过多阐述。
(10)字符串大小写转换
用toLocaleLowerCase()和toLocaleUpperCase()来替代toLowerCase()和toUpperCase(),对于少数语言(如土耳其语)会有更好的兼容性,可以放心使用。
(11)字符串模式匹配方法
字符串和正则表达式的结合可谓是如虎添翼,match()、matchAll()、search()、replace()方法是最常用的模式匹配方法,不再介绍。
(12)localeCompare()方法
按照字母表顺序比较两个字符串。这个方法的独特之处是实现了所在的地区(国家和语言)决定了这个方法如何比较字符串。在美国,英语是ECMAScript的标准语言,它会区分大小写,大写字母排在小写字母前面,但其他地区未必是这种情况。
(13)HTML方法
stringValue.blod()会生成<b>string</b>
的标签,类似这种生成标签字符串的HTML方法还有很多,但现在基本上已经没有人使用了,了解即可,不必深究。
5.4 单例内置对象
5.4.1 Global
ECMA-262规定Global是一种兜底对象。事实上,不存在全局对象和全局函数这种东西,在全局作用域中的变量和函数都会变成Global的对象属性。
(1)URL编码方法
encodeURI()不会编码属于URL组件的特殊字符,比如冒号、斜杠、问号、井号。encodeURIComponent()会编码它发现的所有非标准字符。
相应地,decodeURI()和decodeURIComponent()用于解码。
(2)eval()方法
eval()是一个完整的ECMAScript解释器,它传入要执行的JavaScript代码的字符串。通过eval()执行的代码属于该调用所在上下文,和上下文具拥有相同作用域链。这就意味着包含在上下文中的变量可以在eval()的调用内部引用。
通过eval()定义的任何变量都不会被提升,这是应为在解释代码时,它们是被包含在一个字符串中的,只是在eval执行的时候才会被创建。
在严格模式下,eval()内部创建的变量和函数无法被外部访问到。
为了减少XSS隐患,请谨慎使用eval()。
(3)Global对象属性
Global对象有很多属性,像之前提到的undefined、NaN、Infinity等特殊值,此外,所有原生引用类型构造函数,比如Object和Function等等皆是Global的属性。
(4)window对象
ECMA-262没有规定直接访问Global对象的方式,但浏览器将window对象实现为Globel对象的代理。
另一种获取Global对象的方式是使用下面的代码:
let global = function(){
return this
}()
5.4.2 Math
Math经常被用于处理数学逻辑的场景,我们应该再熟悉不过了。
Math对象本身有一些属性作为特殊值(如Math.PI等),也提供了一些常用的方法min()、max()、ceil()、floor()、round()。
Math.random()方法方便我们创建“随机逻辑”。
注意,如果为了加密而需要生成随机数(给生成器的输入需要较高的不确定性),那么建议使用window.crypto.getRandomValues()。
Math还有很多其他方法,需要的时候拿来即用即可。
小结
JavaScript中的对象成为引用值,几种内置的引用类型用于创建特定类型的对象,它和传统面向对象编程语音的类类似,但实现不同。
Date用于日期相关、RegExp用于正则相关、包装类型方便我们操作基本类型。
当代码开始执行时,全局上下文中会存在两个内置的对象Global和Math。
基本引用类型的存在,无疑使JavaScript变得更灵活,它使数据和功能结合在一起。