Object对象
概述
JavaScript 原生提供 Object
对象(注意起首的 O
是大写),本章介绍该对象原生的各种方法。
JavaScript 的所有其他对象都继承自 Object
对象,即那些对象都是 Object
的实例。
Object
对象的原生方法分成两类:Object
本身的方法与 Object
的实例方法。
(1)Object
对象本身的方法
所谓“本身的方法”就是直接定义在 Object
对象的方法。
1 | Object.print = function (o) { console.log(o) }; |
上面代码中,print
方法就是直接定义在 Object
对象上。
(2)Object
的实例方法
所谓实例方法就是定义在 Object
原型对象 Object.prototype
上的方法。它可以被 Object
实例直接使用。
1 | Object.prototype.print = function () { |
上面代码中,Object.prototype
定义了一个 print
方法,然后生成一个 Object
的实例 obj
。obj
直接继承了 Object.prototype
的属性和方法,可以直接使用 obj.print
调用 print
方法。也就是说,obj
对象的 print
方法实质上就是调用 Object.prototype.print
方法。
以下先介绍 Object
作为函数的用法,然后再介绍 Object
对象的原生方法,分成对象自身的方法(又称为“静态方法”)和实例方法两部分。
Object()
Object
本身是一个函数,可以当作工具方法使用,将任意值转为对象。这个方法常用于保证某个值一定是对象。
如果参数为空(或者为 undefined
和 null
),Object()
返回一个空对象。
1 | var obj = Object(); |
上面代码的含义,是将 undefined
和 null
转为对象,结果得到了一个空对象 obj
。
instanceof
运算符用来验证,一个对象是否为指定的构造函数的实例。obj instanceof Object
返回 true
,就表示 obj
对象是 Object
的实例。
如果参数是原始类型的值,Object
方法将其转为对应的包装对象的实例(参见《原始类型的包装对象》一章)。
1 | var obj = Object(1); |
上面代码中,Object
函数的参数是各种原始类型的值,转换成对象就是原始类型值对应的包装对象。
如果 Object
方法的参数是一个对象,它总是返回该对象,即不用转换。
1 | var arr = []; |
利用这一点,可以写一个判断变量是否为对象的函数。
1 | function isObject(value) { |
Object 构造函数
Object
不仅可以当作工具函数使用,还可以当作构造函数使用,即前面可以使用 new
命令。
Object
构造函数的首要用途,是直接通过它来生成新对象。
1 | var obj = new Object(); |
注意,通过
var obj = new Object()
的写法生成新对象,与字面量的写法var obj = {}
是等价的。或者说,后者只是前者的一种简便写法。
Object
构造函数的用法与工具方法很相似,几乎一模一样。使用时,可以接受一个参数,如果该参数是一个对象,则直接返回这个对象;如果是一个原始类型的值,则返回该值对应的包装对象(详见《包装对象》一章)。
1 | var o1 = {a: 1}; |
虽然用法相似,但是 Object(value)
与 new Object(value)
两者的语义是不同的,Object(value)
表示将 value
转成一个对象,new Object(value)
则表示新生成一个对象,它的值是 value
。
Object 的静态方法
所谓“静态方法”,是指部署在 Object
对象自身的方法。
Object.keys(),Object.getOwnPropertyNames()
Object.keys
方法和 Object.getOwnPropertyNames
方法都用来遍历对象的属性。
Object.keys
方法的参数是一个对象,返回一个数组。该数组的成员都是该对象自身的(而不是继承的)所有属性名。
1 | var obj = { |
Object.getOwnPropertyNames
方法与 Object.keys
类似,也是接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名。
1 | var obj = { |
对于一般的对象来说,Object.keys()
和 Object.getOwnPropertyNames()
返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。Object.keys
方法只返回可枚举的属性(详见《对象属性的描述对象》一章),Object.getOwnPropertyNames
方法还返回不可枚举的属性名。
1 | var a = ['Hello', 'World']; |
上面代码中,数组的 length
属性是不可枚举的属性,所以只出现在 Object.getOwnPropertyNames
方法的返回结果中。
由于 JavaScript 没有提供计算对象属性个数的方法,所以可以用这两个方法代替。
1 | var obj = { |
一般情况下,几乎总是使用 Object.keys
方法,遍历对象的属性。
其他方法
除了上面提到的两个方法,Object
还有不少其他静态方法,将在后文逐一详细介绍。
(1)对象属性模型的相关方法
Object.getOwnPropertyDescriptor()
:获取某个属性的描述对象。Object.defineProperty()
:通过描述对象,定义某个属性。Object.defineProperties()
:通过描述对象,定义多个属性。
(2)控制对象状态的方法
Object.preventExtensions()
:防止对象扩展。Object.isExtensible()
:判断对象是否可扩展。Object.seal()
:禁止对象配置。Object.isSealed()
:判断一个对象是否可配置。Object.freeze()
:冻结一个对象。Object.isFrozen()
:判断一个对象是否被冻结。
(3)原型链相关方法
Object.create()
:该方法可以指定原型对象和属性,返回一个新的对象。Object.getPrototypeOf()
:获取对象的Prototype
对象。
Object 的实例方法
除了静态方法,还有不少方法定义在 Object.prototype
对象。它们称为实例方法,所有 Object
的实例对象都继承了这些方法。
Object
实例对象的方法,主要有以下六个。
Object.prototype.valueOf()
:返回当前对象对应的值。Object.prototype.toString()
:返回当前对象对应的字符串形式。Object.prototype.toLocaleString()
:返回当前对象对应的本地字符串形式。Object.prototype.hasOwnProperty()
:判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。Object.prototype.isPrototypeOf()
:判断当前对象是否为另一个对象的原型。Object.prototype.propertyIsEnumerable()
:判断某个属性是否可枚举。
本节介绍前四个方法,另外两个方法将在后文相关章节介绍。
Object.prototype.valueOf()
valueOf
方法的作用是返回一个对象的“值”,默认情况下返回对象本身。
1 | var obj = new Object(); |
上面代码比较 obj.valueOf()
与 obj
本身,两者是一样的。
valueOf
方法的主要用途是,JavaScript 自动类型转换时会默认调用这个方法(详见《数据类型转换》一章)。
1 | var obj = new Object(); |
上面代码将对象 obj
与数字 1
相加,这时 JavaScript 就会默认调用 valueOf()
方法,求出 obj
的值再与 1
相加。所以,如果自定义 valueOf
方法,就可以得到想要的结果。
1 | var obj = new Object(); |
上面代码自定义了 obj
对象的 valueOf
方法,于是 1 + obj
就得到了 3
。这种方法就相当于用自定义的 obj.valueOf
,覆盖 Object.prototype.valueOf
。
Object.prototype.toString()
toString
方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串。
1 | var o1 = new Object(); |
上面代码表示,对于一个对象调用 toString
方法,会返回字符串 [object Object]
,该字符串说明对象的类型。
字符串 [object Object]
本身没有太大的用处,但是通过自定义 toString
方法,可以让对象在自动类型转换时,得到想要的字符串形式。
1 | var obj = new Object(); |
上面代码表示,当对象用于字符串加法时,会自动调用 toString
方法。由于自定义了 toString
方法,所以返回字符串 hello world
。
数组、字符串、函数、Date 对象都分别部署了自定义的 toString
方法,覆盖了 Object.prototype.toString
方法。
1 | [1, 2, 3].toString() // "1,2,3" |
上面代码中,数组、字符串、函数、Date 对象调用 toString
方法,并不会返回 [object Object]
,因为它们都自定义了 toString
方法,覆盖原始方法。
toString() 的应用:判断数据类型
Object.prototype.toString
方法返回对象的类型字符串,因此可以用来判断一个值的类型。
1 | var obj = {}; |
上面代码调用空对象的 toString
方法,结果返回一个字符串 object Object
,其中第二个 Object
表示该值的构造函数。这是一个十分有用的判断数据类型的方法。
由于实例对象可能会自定义 toString
方法,覆盖掉 Object.prototype.toString
方法,所以为了得到类型字符串,最好直接使用 Object.prototype.toString
方法。通过函数的 call
方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型。
1 | Object.prototype.toString.call(value) |
上面代码表示对 value
这个值调用 Object.prototype.toString
方法。
不同数据类型的 Object.prototype.toString
方法返回值如下。
- 数值:返回
[object Number]
。 - 字符串:返回
[object String]
。 - 布尔值:返回
[object Boolean]
。 - undefined:返回
[object Undefined]
。 - null:返回
[object Null]
。 - 数组:返回
[object Array]
。 - arguments 对象:返回
[object Arguments]
。 - 函数:返回
[object Function]
。 - Error 对象:返回
[object Error]
。 - Date 对象:返回
[object Date]
。 - RegExp 对象:返回
[object RegExp]
。 - 其他对象:返回
[object Object]
。
这就是说,Object.prototype.toString
可以看出一个值到底是什么类型。
1 | Object.prototype.toString.call(2) // "[object Number]" |
利用这个特性,可以写出一个比 typeof
运算符更准确的类型判断函数。
1 | var type = function (o){ |
在上面这个 type
函数的基础上,还可以加上专门判断某种类型数据的方法。
1 | var type = function (o){ |
Object.prototype.toLocaleString()
Object.prototype.toLocaleString
方法与 toString
的返回结果相同,也是返回一个值的字符串形式。
1 | var obj = {}; |
这个方法的主要作用是留出一个接口,让各种不同的对象实现自己版本的 toLocaleString
,用来返回针对某些地域的特定的值。
1 | var person = { |
上面代码中,toString()
方法返回对象的一般字符串形式,toLocaleString()
方法返回本地的字符串形式。
目前,主要有三个对象自定义了 toLocaleString
方法。
- Array.prototype.toLocaleString()
- Number.prototype.toLocaleString()
- Date.prototype.toLocaleString()
举例来说,日期的实例对象的 toString
和 toLocaleString
返回值就不一样,而且 toLocaleString
的返回值跟用户设定的所在地域相关。
1 | var date = new Date(); |
Object.prototype.hasOwnProperty()
Object.prototype.hasOwnProperty
方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性。
1 | var obj = { |
上面代码中,对象 obj
自身具有 p
属性,所以返回 true
。toString
属性是继承的,所以返回 false
。
属性描述对象
概述
JavaScript提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”(attributes object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。
下面是属性描述对象的一个例子。
1 | { |
属性描述对象提供6个元属性。
(1)value
value
是该属性的属性值,默认为 undefined
。
(2)writable
writable
是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为 true
。
(3)enumerable
enumerable
是一个布尔值,表示该属性是否可遍历,默认为 true
。如果设为 false
,会使得某些操作(比如 for...in
循环、Object.keys()
)跳过该属性。
(4)configurable
configurable
是一个布尔值,表示属性的可配置性,默认为 true
。如果设为 false
,将阻止某些操作改写属性描述对象,比如无法删除该属性,也不得改变各种元属性(value
属性除外)。也就是说,configurable
属性控制了属性描述对象的可写性。
(5)get
get
是一个函数,表示该属性的取值函数(getter),默认为 undefined
。
(6)set
set
是一个函数,表示该属性的存值函数(setter),默认为 undefined
。
Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor()
方法可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名。
1 | var obj = { p: 'a' }; |
上面代码中,Object.getOwnPropertyDescriptor()
方法获取 obj.p
的属性描述对象。
注意,Object.getOwnPropertyDescriptor()
方法只能用于对象自身的属性,不能用于继承的属性。
1 | var obj = { p: 'a' }; |
上面代码中,toString
是 obj
对象继承的属性,Object.getOwnPropertyDescriptor()
无法获取。
Object.getOwnPropertyNames()
Object.getOwnPropertyNames
方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。
1 | var obj = Object.defineProperties({}, { |
上面代码中,obj.p1
是可遍历的,obj.p2
是不可遍历的。Object.getOwnPropertyNames
会将它们都返回。
这跟 Object.keys
的行为不同,Object.keys
只返回对象自身的可遍历属性的全部属性名。
1 | Object.keys([]) // [] |
上面代码中,数组自身的 length
属性是不可遍历的,Object.keys
不会返回该属性。第二个例子的 Object.prototype
也是一个对象,所有实例对象都会继承它,它自身的属性都是不可遍历的。
Object.defineProperty(),Object.defineProperties()
Object.defineProperty()
方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下。
1 | Object.defineProperty(object, propertyName, attributesObject) |
Object.defineProperty
方法接受三个参数,依次如下。
- object:属性所在的对象
- propertyName:字符串,表示属性名
- attributesObject:属性描述对象
举例来说,定义 obj.p
可以写成下面这样。
1 | var obj = Object.defineProperty({}, 'p', { |
上面代码中,Object.defineProperty()
方法定义了 obj.p
属性。由于属性描述对象的 writable
属性为 false
,所以 obj.p
属性不可写。注意,这里的 Object.defineProperty
方法的第一个参数是 {}
(一个新建的空对象),p
属性直接定义在这个空对象上面,然后返回这个对象,这是 Object.defineProperty()
的常见用法。
如果属性已经存在,Object.defineProperty()
方法相当于更新该属性的属性描述对象。
如果一次性定义或修改多个属性,可以使用 Object.defineProperties()
方法。
1 | var obj = Object.defineProperties({}, { |
上面代码中,Object.defineProperties()
同时定义了 obj
对象的三个属性。其中,p3
属性定义了取值函数 get
,即每次读取该属性,都会调用这个取值函数。
注意,一旦定义了取值函数 get
(或存值函数 set
),就不能将 writable
属性设为 true
,或者同时定义 value
属性,否则会报错。
1 | var obj = {}; |
上面代码中,同时定义了 get
属性和 value
属性,以及将 writable
属性设为 true
,就会报错。
Object.defineProperty()
和 Object.defineProperties()
参数里面的属性描述对象,writable
、configurable
、enumerable
这三个属性的默认值都为 false
。
1 | var obj = {}; |
上面代码中,定义 obj.foo
时用了一个空的属性描述对象,就可以看到各个元属性的默认值。
Object.prototype.propertyIsEnumerable()
实例对象的 propertyIsEnumerable()
方法返回一个布尔值,用来判断某个属性是否可遍历。注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回 false
。
1 | var obj = {}; |
上面代码中,obj.p
是可遍历的,而 obj.toString
是继承的属性。
元属性
属性描述对象的各个属性称为“元属性”,因为它们可以看作是控制属性的属性。
value
value
属性是目标属性的值。
1 | var obj = {}; |
上面代码是通过 value
属性,读取或改写 obj.p
的例子。
writable
writable
属性是一个布尔值,决定了目标属性的值(value)是否可以被改变。
1 | var obj = {}; |
上面代码中,obj.a
的 writable
属性是 false
。然后,改变 obj.a
的值,不会有任何效果。
注意,正常模式下,对 writable
为 false
的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对 a
属性重新赋予一个同样的值。
1 | 'use strict'; |
上面代码是严格模式,对 obj.a
任何赋值行为都会报错。
如果原型对象的某个属性的 writable
为 false
,那么子对象将无法自定义这个属性。
1 | var proto = Object.defineProperty({}, 'foo', { |
上面代码中,proto
是原型对象,它的 foo
属性不可写。obj
对象继承 proto
,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。
但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视。
1 | var proto = Object.defineProperty({}, 'foo', { |
enumerable
enumerable
(可遍历性)返回一个布尔值,表示目标属性是否可遍历。
JavaScript 的早期版本,for...in
循环是基于 in
运算符的。我们知道,in
运算符不管某个属性是对象自身的还是继承的,都会返回 true
。
1 | var obj = {}; |
上面代码中,toString
不是 obj
对象自身的属性,但是 in
运算符也返回 true
,这导致了 toString
属性也会被 for...in
循环遍历。
这显然不太合理,后来就引入了“可遍历性”这个概念。只有可遍历的属性,才会被 for...in
循环遍历,同时还规定 toString
这一类实例对象继承的原生属性,都是不可遍历的,这样就保证了 for...in
循环的可用性。
具体来说,如果一个属性的 enumerable
为 false
,下面三个操作不会取到该属性。
for..in
循环Object.keys
方法JSON.stringify
方法
因此,enumerable
可以用来设置“秘密”属性。
1 | var obj = {}; |
上面代码中,obj.x
属性的 enumerable
为 false
,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但不是真正的私有属性,还是可以直接获取它的值。
注意,for...in
循环包括继承的属性,Object.keys
方法不包括继承的属性。如果需要获取对象自身的所有属性,不管是否可遍历,可以使用 Object.getOwnPropertyNames
方法。
另外,JSON.stringify
方法会排除 enumerable
为 false
的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的 enumerable
设为 false
。
configurable
configurable
(可配置性)返回一个布尔值,决定了是否可以修改属性描述对象。也就是说,configurable
为 false
时,writable
、enumerable
和 configurable
都不能被修改了。
1 | var obj = Object.defineProperty({}, 'p', { |
上面代码中,obj.p
的 configurable
属性为 false
。然后,改动 writable
、enumerable
、configurable
,结果都报错。
注意,writable
属性只有在 false
改为 true
时会报错,true
改为 false
是允许的。
1 | var obj = Object.defineProperty({}, 'p', { |
value
属性的情况比较特殊。只要 writable
和 configurable
有一个为 true
,就允许改动 value
。
1 | var o1 = Object.defineProperty({}, 'p', { |
另外,writable
为 false
时,直接对目标属性赋值,不报错,但不会成功。
1 | var obj = Object.defineProperty({}, 'p', { |
上面代码中,obj.p
的 writable
为 false
,对 obj.p
直接赋值不会生效。如果是严格模式,还会报错。
可配置性决定了目标属性是否可以被删除(delete)。
1 | var obj = Object.defineProperties({}, { |
上面代码中,obj.p1
的 configurable
是 true
,所以可以被删除,obj.p2
就无法删除。
存取器
除了直接定义以外,属性还可以用存取器(accessor)定义。其中,存值函数称为 setter
,使用属性描述对象的 set
属性;取值函数称为 getter
,使用属性描述对象的 get
属性。
一旦对目标属性定义了存取器,那么存取的时候,都将执行对应的函数。利用这个功能,可以实现许多高级特性,比如定制属性的读取和赋值行为。
1 | var obj = Object.defineProperty({}, 'p', { |
上面代码中,obj.p
定义了 get
和 set
属性。obj.p
取值时,就会调用 get
;赋值时,就会调用 set
。
JavaScript 还提供了存取器的另一种写法。
1 | // 写法二 |
上面两种写法,虽然属性 p
的读取和赋值行为是一样的,但是有一些细微的区别。第一种写法,属性 p
的 configurable
和 enumerable
都为 false
,从而导致属性 p
是不可遍历的;第二种写法,属性 p
的 configurable
和 enumerable
都为 true
,因此属性 p
是可遍历的。实际开发中,写法二更常用。
注意,取值函数 get
不能接受参数,存值函数 set
只能接受一个参数(即属性的值)。
存取器往往用于,属性的值依赖对象内部数据的场合。
1 | var obj ={ |
上面代码中,next
属性的存值函数和取值函数,都依赖于内部属性 $n
。
对象的拷贝
有时,我们需要将一个对象的所有属性,拷贝到另一个对象,可以用下面的方法实现。
1 | var extend = function (to, from) { |
上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝值。
1 | extend({}, { |
为了解决这个问题,我们可以通过 Object.defineProperty
方法来拷贝属性。
1 | var extend = function (to, from) { |
上面代码中,hasOwnProperty
那一行用来过滤掉继承的属性,否则可能会报错,因为 Object.getOwnPropertyDescriptor
读不到继承属性的属性描述对象。
控制对象状态
有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法,最弱的一种是 Object.preventExtensions
,其次是 Object.seal
,最强的是 Object.freeze
。
Object.preventExtensions()
Object.preventExtensions
方法可以使得一个对象无法再添加新的属性。
1 | var obj = new Object(); |
上面代码中,obj
对象经过 Object.preventExtensions
以后,就无法添加新属性了。
Object.isExtensible()
Object.isExtensible
方法用于检查一个对象是否使用了 Object.preventExtensions
方法。也就是说,检查是否可以为一个对象添加属性。
1 | var obj = new Object(); |
上面代码中,对 obj
对象使用 Object.preventExtensions
方法以后,再使用 Object.isExtensible
方法,返回 false
,表示已经不能添加新属性了。
Object.seal()
Object.seal
方法使得一个对象既无法添加新属性,也无法删除旧属性。
1 | var obj = { p: 'hello' }; |
上面代码中,obj
对象执行 Object.seal
方法以后,就无法添加新属性和删除旧属性了。
Object.seal
实质是把属性描述对象的 configurable
属性设为 false
,因此属性描述对象不再能改变了。
1 | var obj = { |
上面代码中,使用 Object.seal
方法之后,属性描述对象的 configurable
属性就变成了 false
,然后改变 enumerable
属性就会报错。
Object.seal
只是禁止新增或删除属性,并不影响修改某个属性的值。
1 | var obj = { p: 'a' }; |
上面代码中,Object.seal
方法对 p
属性的 value
无效,是因为此时 p
属性的可写性由 writable
决定。
Object.isSealed()
Object.isSealed
方法用于检查一个对象是否使用了 Object.seal
方法。
1 | var obj = { p: 'a' }; |
这时,Object.isExtensible
方法也返回 false
。
1 | var obj = { p: 'a' }; |
Object.freeze()
Object.freeze
方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。
1 | var obj = { |
上面代码中,对 obj
对象进行 Object.freeze()
以后,修改属性、新增属性、删除属性都无效了。这些操作并不报错,只是默默地失败。如果在严格模式下,则会报错。
Object.isFrozen()
Object.isFrozen
方法用于检查一个对象是否使用了 Object.freeze
方法。
1 | var obj = { |
使用 Object.freeze
方法以后,Object.isSealed
将会返回 true
,Object.isExtensible
返回 false
。
1 | var obj = { |
Object.isFrozen
的一个用途是,确认某个对象没有被冻结后,再对它的属性赋值。
1 | var obj = { |
上面代码中,确认 obj
没有被冻结后,再对它的属性赋值,就不会报错了。
局限性
上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。
1 | var obj = new Object(); |
上面代码中,对象 obj
本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在 obj
上读到。
一种解决方案是,把 obj
的原型也冻结住。
1 | var obj = new Object(); |
另外一个局限是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。
1 | var obj = { |
上面代码中,obj.bar
属性指向一个数组,obj
对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的。
Array对象
构造函数
Array
是 JavaScript 的原生对象,同时也是一个构造函数,可以用它生成新的数组。
1 | var arr = new Array(2); |
上面代码中,Array()
构造函数的参数 2
,表示生成一个两个成员的数组,每个位置都是空值。
如果没有使用 new
关键字,运行结果也是一样的。
1 | var arr = Array(2); |
考虑到语义性,以及与其他构造函数用法保持一致,建议总是加上 new
。
Array()
构造函数有一个很大的缺陷,不同的参数个数会导致不一致的行为。
1 | // 无参数时,返回一个空数组 |
可以看到,Array()
作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组字面量是更好的做法。
1 | // bad |
注意,如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回 undefined
,但实际上该位置没有任何值。虽然这时可以读取到 length
属性,但是取不到键名。
1 | var a = new Array(3); |
上面代码中,a
是 Array()
生成的一个长度为3的空数组,b
是一个三个成员都是 undefined
的数组,这两个数组是不一样的。读取键值的时候,a
和 b
都返回 undefined
,但是 a
的键名(成员的序号)都是空的,b
的键名是有值的。
静态方法
Array.isArray()
Array.isArray
方法返回一个布尔值,表示参数是否为数组。它可以弥补 typeof
运算符的不足。
1 | var arr = [1, 2, 3]; |
上面代码中,typeof
运算符只能显示数组的类型是 Object
,而 Array.isArray
方法可以识别数组。
实例方法
valueOf(),toString()
valueOf
方法是一个所有对象都拥有的方法,表示对该对象求值。不同对象的 valueOf
方法不尽一致,数组的 valueOf
方法返回数组本身。
1 | var arr = [1, 2, 3]; |
toString
方法也是对象的通用方法,数组的 toString
方法返回数组的字符串形式。
1 | var arr = [1, 2, 3]; |
push(),pop()
push
方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
1 | var arr = []; |
上面代码使用 push
方法,往数组中添加了四个成员。
pop
方法用于删除数组的最后一个元素,并返回该元素。注意,该方法会改变原数组。
1 | var arr = ['a', 'b', 'c']; |
对空数组使用 pop
方法,不会报错,而是返回 undefined
。
1 | [].pop() // undefined |
push
和 pop
结合使用,就构成了“后进先出”的栈结构(stack)。
1 | var arr = []; |
上面代码中,3
是最后进入数组的,但是最早离开数组。
shift(),unshift()
shift()
方法用于删除数组的第一个元素,并返回该元素。注意,该方法会改变原数组。
1 | var a = ['a', 'b', 'c']; |
上面代码中,使用 shift()
方法以后,原数组就变了。
shift()
方法可以遍历并清空一个数组。
1 | var list = [1, 2, 3, 4]; |
上面代码通过 list.shift()
方法每次取出一个元素,从而遍历数组。它的前提是数组元素不能是 0
或任何布尔值等于 false
的元素,因此这样的遍历不是很可靠。
push()
和 shift()
结合使用,就构成了“先进先出”的队列结构(queue)。
unshift()
方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
1 | var a = ['a', 'b', 'c']; |
unshift()
方法可以接受多个参数,这些参数都会添加到目标数组头部。
1 | var arr = [ 'c', 'd' ]; |
join()
join()
方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔。
1 | var a = [1, 2, 3, 4]; |
如果数组成员是 undefined
或 null
或空位,会被转成空字符串。
1 | [undefined, null].join('#') |
通过 call
方法,这个方法也可以用于字符串或类似数组的对象。
1 | Array.prototype.join.call('hello', '-') |
concat()
concat
方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。
1 | ['hello'].concat(['world']) |
除了数组作为参数,concat
也接受其他类型的值作为参数,添加到目标数组尾部。
1 | [1, 2, 3].concat(4, 5, 6) |
如果数组成员包括对象,concat
方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用。
1 | var obj = { a: 1 }; |
上面代码中,原数组包含一个对象,concat
方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变。
reverse()
reverse
方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组。
1 | var a = ['a', 'b', 'c']; |
slice()
slice()
方法用于提取目标数组的一部分,返回一个新数组,原数组不变。
1 | arr.slice(start, end); |
它的第一个参数为起始位置(从0开始,会包括在返回的新数组之中),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。
1 | var a = ['a', 'b', 'c']; |
上面代码中,最后一个例子 slice()
没有参数,实际上等于返回一个原数组的拷贝。
如果 slice()
方法的参数是负数,则表示倒数计算的位置。
1 | var a = ['a', 'b', 'c']; |
上面代码中,-2
表示倒数计算的第二个位置,-1
表示倒数计算的第一个位置。
如果第一个参数大于等于数组长度,或者第二个参数小于第一个参数,则返回空数组。
1 | var a = ['a', 'b', 'c']; |
slice()
方法的一个重要应用,是将类似数组的对象转为真正的数组。
1 | Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 }) |
上面代码的参数都不是数组,但是通过 call
方法,在它们上面调用 slice()
方法,就可以把它们转为真正的数组。
splice()
splice()
方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。
1 | arr.splice(start, count, addElement1, addElement2, ...); |
splice
的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。
1 | var a = ['a', 'b', 'c', 'd', 'e', 'f']; |
上面代码从原数组4号位置,删除了两个数组成员。
1 | var a = ['a', 'b', 'c', 'd', 'e', 'f']; |
上面代码除了删除成员,还插入了两个新成员。
起始位置如果是负数,就表示从倒数位置开始删除。
1 | var a = ['a', 'b', 'c', 'd', 'e', 'f']; |
上面代码表示,从倒数第四个位置 c
开始删除两个成员。
如果只是单纯地插入元素,splice
方法的第二个参数可以设为 0
。
1 | var a = [1, 1, 1]; |
如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。
1 | var a = [1, 2, 3, 4]; |
sort()
sort
方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。
1 | ['d', 'c', 'b', 'a'].sort() |
上面代码的最后两个例子,需要特殊注意。sort()
方法不是按照大小排序,而是按照字典顺序。也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以 101
排在 11
的前面。
如果想让 sort
方法按照自定义方式排序,可以传入一个函数作为参数。
1 | [10111, 1101, 111].sort(function (a, b) { |
上面代码中,sort
的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于 0
,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面。
1 | [ |
注意,自定义的排序函数应该返回数值,否则不同的浏览器可能有不同的实现,不能保证结果都一致。
1 | // bad |
上面代码中,前一种排序算法返回的是布尔值,这是不推荐使用的。后一种是数值,才是更好的写法。
map()
map()
方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。
1 | var numbers = [1, 2, 3]; |
上面代码中,numbers
数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化。
map()
方法接受一个函数作为参数。该函数调用时,map()
方法向它传入三个参数:当前成员、当前位置和数组本身。
1 | [1, 2, 3].map(function(elem, index, arr) { |
上面代码中,map()
方法的回调函数有三个参数,elem
为当前成员的值,index
为当前成员的位置,arr
为原数组([1, 2, 3]
)。
map()
方法还可以接受第二个参数,用来绑定回调函数内部的 this
变量(详见《this 变量》一章)。
1 | var arr = ['a', 'b', 'c']; |
上面代码通过 map()
方法的第二个参数,将回调函数内部的 this
对象,指向 arr
数组。
如果数组有空位,map()
方法的回调函数在这个位置不会执行,会跳过数组的空位。
1 | var f = function (n) { return 'a' }; |
上面代码中,map()
方法不会跳过 undefined
和 null
,但是会跳过空位。
forEach()
forEach()
方法与 map()
方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach()
方法不返回值,只用来操作数据。这就是说,如果数组遍历的目的是为了得到返回值,那么使用 map()
方法,否则使用 forEach()
方法。
forEach()
的用法与 map()
方法一致,参数是一个函数,该函数同样接受三个参数:当前值、当前位置、整个数组。
1 | function log(element, index, array) { |
上面代码中,forEach()
遍历数组不是为了得到返回值,而是为了在屏幕输出内容,所以不必使用 map()
方法。
forEach()
方法也可以接受第二个参数,绑定参数函数的 this
变量。
1 | var out = []; |
上面代码中,空数组 out
是 forEach()
方法的第二个参数,结果,回调函数内部的 this
关键字就指向 out
。
注意,forEach()
方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用 for
循环。
1 | var arr = [1, 2, 3]; |
上面代码中,执行到数组的第二个成员时,就会中断执行。forEach()
方法做不到这一点。
forEach()
方法也会跳过数组的空位。
1 | var log = function (n) { |
上面代码中,forEach()
方法不会跳过 undefined
和 null
,但会跳过空位。
filter()
filter()
方法用于过滤数组成员,满足条件的成员组成一个新数组返回。
它的参数是一个函数,所有数组成员依次执行该函数,返回结果为 true
的成员组成一个新数组返回。该方法不会改变原数组。
1 | [1, 2, 3, 4, 5].filter(function (elem) { |
上面代码将大于 3
的数组成员,作为一个新数组返回。
1 | var arr = [0, 1, 'a', false]; |
上面代码中,filter()
方法返回数组 arr
里面所有布尔值为 true
的成员。
filter()
方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组。
1 | [1, 2, 3, 4, 5].filter(function (elem, index, arr) { |
上面代码返回偶数位置的成员组成的新数组。
filter()
方法还可以接受第二个参数,用来绑定参数函数内部的 this
变量。
1 | var obj = { MAX: 3 }; |
上面代码中,过滤器 myFilter()
内部有 this
变量,它可以被 filter()
方法的第二个参数 obj
绑定,返回大于 3
的成员。
some(),every()
这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。
它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。
some
方法是只要一个成员的返回值是 true
,则整个 some
方法的返回值就是 true
,否则返回 false
。
1 | var arr = [1, 2, 3, 4, 5]; |
上面代码中,如果数组 arr
有一个成员大于等于3,some
方法就返回 true
。
every
方法是所有成员的返回值都是 true
,整个 every
方法才返回 true
,否则返回 false
。
1 | var arr = [1, 2, 3, 4, 5]; |
上面代码中,数组 arr
并非所有成员大于等于 3
,所以返回 false
。
注意,对于空数组,some
方法返回 false
,every
方法返回 true
,回调函数都不会执行。
1 | function isEven(x) { return x % 2 === 0 } |
some
和 every
方法还可以接受第二个参数,用来绑定参数函数内部的 this
变量。
reduce(),reduceRight()
reduce()
方法和 reduceRight()
方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce()
是从左到右处理(从第一个成员到最后一个成员),reduceRight()
则是从右到左(从最后一个成员到第一个成员),其他完全一样。
1 | [1, 2, 3, 4, 5].reduce(function (a, b) { |
上面代码中,reduce()
方法用来求出数组所有成员的和。reduce()
的参数是一个函数,数组每个成员都会依次执行这个函数。如果数组有 n 个成员,这个参数函数就会执行 n - 1 次。
- 第一次执行:
a
是数组的第一个成员1
,b
是数组的第二个成员2
。 - 第二次执行:
a
为上一轮的返回值3
,b
为第三个成员3
。 - 第三次执行:
a
为上一轮的返回值6
,b
为第四个成员4
。 - 第四次执行:
a
为上一轮返回值10
,b
为第五个成员5
。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值15
。
reduce()
方法和 reduceRight()
方法的第一个参数都是一个函数。该函数接受以下四个参数。
- 累积变量。第一次执行时,默认为数组的第一个成员;以后每次执行时,都是上一轮的返回值。
- 当前变量。第一次执行时,默认为数组的第二个成员;以后每次执行时,都是下一个成员。
- 当前位置。一个整数,表示第二个参数(当前变量)的位置,默认为
1
。 - 原数组。
这四个参数之中,只有前两个是必须的,后两个则是可选的。
1 | [1, 2, 3, 4, 5].reduce(function ( |
如果要对累积变量指定初值,可以把它放在 reduce()
方法和 reduceRight()
方法的第二个参数。
1 | [1, 2, 3, 4, 5].reduce(function (a, b) { |
上面代码指定参数 a
的初值为10,所以数组从10开始累加,最终结果为25。注意,这时 b
是从数组的第一个成员开始遍历,参数函数会执行5次。
建议总是加上第二个参数,这样比较符合直觉,每个数组成员都会依次执行 reduce()
方法的参数函数。另外,第二个参数可以防止空数组报错。
1 | function add(prev, cur) { |
上面代码中,由于空数组取不到累积变量的初始值,reduce()
方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。
下面是一个 reduceRight()
方法的例子。
1 | function subtract(prev, cur) { |
上面代码中,reduce()
方法相当于 3
减去 2
再减去 1
,reduceRight
方法相当于 1
减去 2
再减去 3
。
由于这两个方法会遍历数组,所以实际上可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。
1 | function findLongest(entries) { |
上面代码中,reduce()
的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员。
indexOf(),lastIndexOf()
indexOf
方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回 -1
。
1 | var a = ['a', 'b', 'c']; |
indexOf
方法还可以接受第二个参数,表示搜索的开始位置。
1 | ['a', 'b', 'c'].indexOf('a', 1) // -1 |
上面代码从1号位置开始搜索字符 a
,结果为 -1
,表示没有搜索到。
lastIndexOf
方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回 -1
。
1 | var a = [2, 5, 9, 2]; |
注意,这两个方法不能用来搜索 NaN
的位置,即它们无法确定数组成员是否包含 NaN
。
1 | [NaN].indexOf(NaN) // -1 |
这是因为这两个方法内部,使用严格相等运算符(===
)进行比较,而 NaN
是唯一一个不等于自身的值。
链式使用
上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用。
1 | var users = [ |
上面代码中,先产生一个所有 Email 地址组成的数组,然后再过滤出以 t
开头的 Email 地址,最后将它打印出来。
包装对象
定义
对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。
所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的 Number
、String
、Boolean
三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
1 | var v1 = new Number(123); |
上面代码中,基于原始类型的值,生成了三个对应的包装对象。可以看到,v1
、v2
、v3
都是对象,且与对应的简单类型值不相等。
包装对象的设计目的,首先是使得“对象”这种类型可以覆盖 JavaScript 所有的值,整门语言有一个通用的数据模型,其次是使得原始类型的值也有办法调用自己的方法。
Number
、String
和 Boolean
这三个原生对象,如果不作为构造函数调用(即调用时不加 new
),而是作为普通函数调用,常常用于将任意类型的值转为数值、字符串和布尔值。
1 | // 字符串转为数值 |
上面这种数据类型的转换,详见《数据类型转换》一节。
总结一下,这三个对象作为构造函数使用(带有 new
)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有 new
),可以将任意类型的值,转为原始类型的值。
实例方法
三种包装对象各自提供了许多实例方法,详见后文。这里介绍两种它们共同具有、从 Object
对象继承的方法:valueOf()
和 toString()
。
valueOf()
valueOf()
方法返回包装对象实例对应的原始类型的值。
1 | new Number(123).valueOf() // 123 |
toString()
toString()
方法返回对应的字符串形式。
1 | new Number(123).toString() // "123" |
原始类型与实例对象的自动转换
某些场合,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例。
比如,字符串可以调用 length
属性,返回字符串的长度。
1 | 'abc'.length // 3 |
上面代码中,abc
是一个字符串,本身不是对象,不能调用 length
属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用 length
属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。
1 | var str = 'abc'; |
上面代码中,字符串 abc
的包装对象提供了多个属性,length
只是其中之一。
自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。
1 | var s = 'Hello World'; |
上面代码为字符串 s
添加了一个 x
属性,结果无效,总是返回 undefined
。
另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象 String.prototype
上定义。
自定义方法
除了原生的实例方法,包装对象还可以自定义方法和属性,供原始类型的值直接调用。
比如,我们可以新增一个 double
方法,使得字符串和数字翻倍。
1 | String.prototype.double = function () { |
上面代码在 String
和 Number
这两个对象的原型上面,分别自定义了一个方法,从而可以在所有实例对象上调用。注意,最后一行的 123
外面必须要加上圆括号,否则后面的点运算符(.
)会被解释成小数点。
Boolean对象
概述
Boolean
对象是 JavaScript 的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象实例。
1 | var b = new Boolean(true); |
上面代码的变量 b
是一个 Boolean
对象的实例,它的类型是对象,值为布尔值 true
。
注意,false
对应的包装对象实例,布尔运算结果也是 true
。
1 | if (new Boolean(false)) { |
上面代码的第一个例子之所以得到 true
,是因为 false
对应的包装对象实例是一个对象,进行逻辑运算时,被自动转化成布尔值 true
(因为所有对象对应的布尔值都是 true
)。而实例的 valueOf
方法,则返回实例对应的原始值,本例为 false
。
Boolean 函数的类型转换作用
Boolean
对象除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时 Boolean
就是一个单纯的工具方法。
1 | Boolean(undefined) // false |
上面代码中几种得到 true
的情况,都值得认真记住。
顺便提一下,使用双重的否运算符(!
)也可以将任意值转为对应的布尔值。
1 | !!undefined // false |
最后,对于一些特殊值,Boolean
对象前面加不加 new
,会得到完全相反的结果,必须小心。
1 | if (Boolean(false)) { |
Number 对象
概述
Number
对象是数值对应的包装对象,可以作为构造函数使用,也可以作为工具函数使用。
作为构造函数时,它用于生成值为数值的对象。
1 | var n = new Number(1); |
上面代码中,Number
对象作为构造函数使用,返回一个值为 1
的对象。
作为工具函数时,它可以将任何类型的值转为数值。
1 | Number(true) // 1 |
上面代码将布尔值 true
转为数值 1
。Number
作为工具函数的用法,详见《数据类型转换》一章。
静态属性
Number
对象拥有以下一些静态属性(即直接定义在 Number
对象上的属性,而不是定义在实例上的属性)。
Number.POSITIVE_INFINITY
:正的无限,指向Infinity
。Number.NEGATIVE_INFINITY
:负的无限,指向-Infinity
。Number.NaN
:表示非数值,指向NaN
。Number.MIN_VALUE
:表示最小的正数(即最接近0的正数,在64位浮点数体系中为5e-324
),相应的,最接近0的负数为-Number.MIN_VALUE
。Number.MAX_SAFE_INTEGER
:表示能够精确表示的最大整数,即9007199254740991
。Number.MIN_SAFE_INTEGER
:表示能够精确表示的最小整数,即-9007199254740991
。
1 | Number.POSITIVE_INFINITY // Infinity |
实例方法
Number
对象有4个实例方法,都跟将数值转换成指定格式有关。
Number.prototype.toString()
Number
对象部署了自己的 toString
方法,用来将一个数值转为字符串形式。
1 | (10).toString() // "10" |
toString
方法可以接受一个参数,表示输出的进制。如果省略这个参数,默认将数值先转为十进制,再输出字符串;否则,就根据参数指定的进制,将一个数字转化成某个进制的字符串。
1 | (10).toString(2) // "1010" |
上面代码中,10
一定要放在括号里,这样表明后面的点表示调用对象属性。如果不加括号,这个点会被 JavaScript 引擎解释成小数点,从而报错。
1 | 10.toString(2) |
只要能够让 JavaScript 引擎不混淆小数点和对象的点运算符,各种写法都能用。除了为 10
加上括号,还可以在 10
后面加两个点,JavaScript 会把第一个点理解成小数点(即 10.0
),把第二个点理解成调用对象属性,从而得到正确结果。
1 | 10..toString(2) |
这实际上意味着,可以直接对一个小数使用 toString
方法。
1 | 10.5.toString() // "10.5" |
通过方括号运算符也可以调用 toString
方法。
1 | 10['toString'](2) // "1010" |
toString
方法只能将十进制的数,转为其他进制的字符串。如果要将其他进制的数,转回十进制,需要使用 parseInt
方法。
Number.prototype.toFixed()
toFixed()
方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。
1 | (10).toFixed(2) // "10.00" |
上面代码中,10
和 10.005
先转成2位小数,然后转成字符串。其中 10
必须放在括号里,否则后面的点会被处理成小数点。
toFixed()
方法的参数为小数位数,有效范围为0到100,超出这个范围将抛出 RangeError 错误。
由于浮点数的原因,小数 5
的四舍五入是不确定的,使用的时候必须小心。
1 | (10.055).toFixed(2) // 10.05 |
Number.prototype.toExponential()
toExponential
方法用于将一个数转为科学计数法形式。
1 | (10).toExponential() // "1e+1" |
toExponential
方法的参数是小数点后有效数字的位数,范围为0到100,超出这个范围,会抛出一个 RangeError 错误。
Number.prototype.toPrecision()
Number.prototype.toPrecision()
方法用于将一个数转为指定位数的有效数字。
1 | (12.34).toPrecision(1) // "1e+1" |
该方法的参数为有效数字的位数,范围是1到100,超出这个范围会抛出 RangeError 错误。
该方法用于四舍五入时不太可靠,跟浮点数不是精确储存有关。
1 | (12.35).toPrecision(3) // "12.3" |
Number.prototype.toLocaleString()
Number.prototype.toLocaleString()
方法接受一个地区码作为参数,返回一个字符串,表示当前数字在该地区的当地书写形式。
1 | (123).toLocaleString('zh-Hans-CN-u-nu-hanidec') |
该方法还可以接受第二个参数配置对象,用来定制指定用途的返回字符串。该对象的 style
属性指定输出样式,默认值是 decimal
,表示输出十进制形式。如果值为 percent
,表示输出百分数。
1 | (123).toLocaleString('zh-Hans-CN', { style: 'percent' }) |
如果 style
属性的值为 currency
,则可以搭配 currency
属性,输出指定格式的货币字符串形式。
1 | (123).toLocaleString('zh-Hans-CN', { style: 'currency', currency: 'CNY' }) |
如果 Number.prototype.toLocaleString()
省略了参数,则由浏览器自行决定如何处理,通常会使用操作系统的地区设定。注意,该方法如果使用浏览器不认识的地区码,会抛出一个错误。
1 | (123).toLocaleString('123') // 出错 |
自定义方法
与其他对象一样,Number.prototype
对象上面可以自定义方法,被 Number
的实例继承。
1 | Number.prototype.add = function (x) { |
上面代码为 Number
对象实例定义了一个 add
方法。在数值上调用某个方法,数值会自动转为 Number
的实例对象,所以就可以调用 add
方法了。由于 add
方法返回的还是数值,所以可以链式运算。
1 | Number.prototype.subtract = function (x) { |
上面代码在 Number
对象的实例上部署了 subtract
方法,它可以与 add
方法链式调用。
我们还可以部署更复杂的方法。
1 | Number.prototype.iterate = function () { |
上面代码在 Number
对象的原型上部署了 iterate
方法,将一个数值自动遍历为一个数组。
注意,数值的自定义方法,只能定义在它的原型对象 Number.prototype
上面,数值本身是无法自定义属性的。
1 | var n = 1; |
上面代码中,n
是一个原始类型的数值。直接在它上面新增一个属性 x
,不会报错,但毫无作用,总是返回 undefined
。这是因为一旦被调用属性,n
就自动转为 Number
的实例对象,调用结束后,该对象自动销毁。所以,下一次调用 n
的属性时,实际取到的是另一个对象,属性 x
当然就读不出来。
String对象
概述
String
对象是 JavaScript 原生提供的三个包装对象之一,用来生成字符串对象。
1 | var s1 = 'abc'; |
上面代码中,变量 s1
是字符串,s2
是对象。由于 s2
是字符串对象,s2.valueOf
方法返回的就是它所对应的原始字符串。
字符串对象是一个类似数组的对象(很像数组,但不是数组)。
1 | new String('abc') |
上面代码中,字符串 abc
对应的字符串对象,有数值键(0
、1
、2
)和 length
属性,所以可以像数组那样取值。
除了用作构造函数,String
对象还可以当作工具方法使用,将任意类型的值转为字符串。
1 | String(true) // "true" |
上面代码将布尔值 true
和数值 5
,分别转换为字符串。
静态方法
String.fromCharCode()
String
对象提供的静态方法(即定义在对象本身,而不是定义在对象实例的方法),主要是 String.fromCharCode()
。该方法的参数是一个或多个数值,代表 Unicode 码点,返回值是这些码点组成的字符串。
1 | String.fromCharCode() // "" |
上面代码中,String.fromCharCode
方法的参数为空,就返回空字符串;否则,返回参数对应的 Unicode 字符串。
注意,该方法不支持 Unicode 码点大于 0xFFFF
的字符,即传入的参数不能大于 0xFFFF
(即十进制的 65535)。
1 | String.fromCharCode(0x20BB7) |
上面代码中,String.fromCharCode
参数 0x20BB7
大于 0xFFFF
,导致返回结果出错。0x20BB7
对应的字符是汉字 𠮷
,但是返回结果却是另一个字符(码点 0x0BB7
)。这是因为 String.fromCharCode
发现参数值大于 0xFFFF
,就会忽略多出的位(即忽略 0x20BB7
里面的 2
)。
这种现象的根本原因在于,码点大于 0xFFFF
的字符占用四个字节,而 JavaScript 默认支持两个字节的字符。这种情况下,必须把 0x20BB7
拆成两个字符表示。
1 | String.fromCharCode(0xD842, 0xDFB7) |
上面代码中,0x20BB7
拆成两个字符 0xD842
和 0xDFB7
(即两个两字节字符,合成一个四字节字符),就能得到正确的结果。码点大于 0xFFFF
的字符的四字节表示法,由 UTF-16 编码方法决定。
实例属性
String.prototype.length
字符串实例的 length
属性返回字符串的长度。
1 | 'abc'.length // 3 |
实例方法
String.prototype.charAt()
charAt
方法返回指定位置的字符,参数是从 0
开始编号的位置。
1 | var s = new String('abc'); |
这个方法完全可以用数组下标替代。
1 | 'abc'.charAt(1) // "b" |
如果参数为负数,或大于等于字符串的长度,charAt
返回空字符串。
1 | 'abc'.charAt(-1) // "" |
String.prototype.charCodeAt()
charCodeAt()
方法返回字符串指定位置的 Unicode 码点(十进制表示),相当于 String.fromCharCode()
的逆操作。
1 | 'abc'.charCodeAt(1) // 98 |
上面代码中,abc
的 1
号位置的字符是 b
,它的 Unicode 码点是 98
。
如果没有任何参数,charCodeAt
返回首字符的 Unicode 码点。
1 | 'abc'.charCodeAt() // 97 |
如果参数为负数,或大于等于字符串的长度,charCodeAt
返回 NaN
。
1 | 'abc'.charCodeAt(-1) // NaN |
注意,charCodeAt
方法返回的 Unicode 码点不会大于65536(0xFFFF),也就是说,只返回两个字节的字符的码点。如果遇到码点大于 65536 的字符(四个字节的字符),必须连续使用两次 charCodeAt
,不仅读入 charCodeAt(i)
,还要读入 charCodeAt(i+1)
,将两个值放在一起,才能得到准确的字符。
String.prototype.concat()
concat
方法用于连接两个字符串,返回一个新字符串,不改变原字符串。
1 | var s1 = 'abc'; |
该方法可以接受多个参数。
1 | 'a'.concat('b', 'c') // "abc" |
如果参数不是字符串,concat
方法会将其先转为字符串,然后再连接。
1 | var one = 1; |
上面代码中,concat
方法将参数先转成字符串再连接,所以返回的是一个三个字符的字符串。作为对比,加号运算符在两个运算数都是数值时,不会转换类型,所以返回的是一个两个字符的字符串。
String.prototype.slice()
slice()
方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置)。
1 | 'JavaScript'.slice(0, 4) // "Java" |
如果省略第二个参数,则表示子字符串一直到原字符串结束。
1 | 'JavaScript'.slice(4) // "Script" |
如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度。
1 | 'JavaScript'.slice(-6) // "Script" |
如果第一个参数大于第二个参数(正数情况下),slice()
方法返回一个空字符串。
1 | 'JavaScript'.slice(2, 1) // "" |
String.prototype.substring()
substring
方法用于从原字符串取出子字符串并返回,不改变原字符串,跟 slice
方法很相像。它的第一个参数表示子字符串的开始位置,第二个位置表示结束位置(返回结果不含该位置)。
1 | 'JavaScript'.substring(0, 4) // "Java" |
如果省略第二个参数,则表示子字符串一直到原字符串的结束。
1 | 'JavaScript'.substring(4) // "Script" |
如果第一个参数大于第二个参数,substring
方法会自动更换两个参数的位置。
1 | 'JavaScript'.substring(10, 4) // "Script" |
上面代码中,调换 substring
方法的两个参数,都得到同样的结果。
如果参数是负数,substring
方法会自动将负数转为0。
1 | 'JavaScript'.substring(-3) // "JavaScript" |
上面代码中,第二个例子的参数 -3
会自动变成 0
,等同于 'JavaScript'.substring(4, 0)
。由于第二个参数小于第一个参数,会自动互换位置,所以返回 Java
。
由于这些规则违反直觉,因此不建议使用 substring
方法,应该优先使用 slice
。
String.prototype.substr()
substr
方法用于从原字符串取出子字符串并返回,不改变原字符串,跟 slice
和 substring
方法的作用相同。
substr
方法的第一个参数是子字符串的开始位置(从0开始计算),第二个参数是子字符串的长度。
1 | 'JavaScript'.substr(4, 6) // "Script" |
如果省略第二个参数,则表示子字符串一直到原字符串的结束。
1 | 'JavaScript'.substr(4) // "Script" |
如果第一个参数是负数,表示倒数计算的字符位置。如果第二个参数是负数,将被自动转为0,因此会返回空字符串。
1 | 'JavaScript'.substr(-6) // "Script" |
上面代码中,第二个例子的参数 -1
自动转为 0
,表示子字符串长度为 0
,所以返回空字符串。
总结:如果需要支持负数索引,推荐使用slice方法;如果不需要支持负数索引,并且涉及到参数位置的调换较为复杂时,可以选择substring方法;如果只是简单地按起始索引和长度截取字符串,则可以使用substr方法。
String.prototype.indexOf(),String.prototype.lastIndexOf()
indexOf
方法用于确定一个字符串在另一个字符串中第一次出现的位置,返回结果是匹配开始的位置。如果返回 -1
,就表示不匹配。
1 | 'hello world'.indexOf('o') // 4 |
indexOf
方法还可以接受第二个参数,表示从该位置开始向后匹配。
1 | 'hello world'.indexOf('o', 6) // 7 |
lastIndexOf
方法的用法跟 indexOf
方法一致,主要的区别是 lastIndexOf
从尾部开始匹配,indexOf
则是从头部开始匹配。
1 | 'hello world'.lastIndexOf('o') // 7 |
另外,lastIndexOf
的第二个参数表示从该位置起向前匹配。
1 | 'hello world'.lastIndexOf('o', 6) // 4 |
String.prototype.trim()
trim
方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串。
1 | ' hello world '.trim() |
该方法去除的不仅是空格,还包括制表符(\t
、\v
)、换行符(\n
)和回车符(\r
)。
1 | '\r\nabc \t'.trim() // 'abc' |
String.prototype.toLowerCase(),String.prototype.toUpperCase()
toLowerCase
方法用于将一个字符串全部转为小写,toUpperCase
则是全部转为大写。它们都返回一个新字符串,不改变原字符串。
1 | 'Hello World'.toLowerCase() |
String.prototype.match()
match
方法用于确定原字符串是否匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。如果没有找到匹配,则返回 null
。
1 | 'cat, bat, sat, fat'.match('at') // ["at"] |
返回的数组还有 index
属性和 input
属性,分别表示匹配字符串开始的位置和原始字符串。
1 | var matches = 'cat, bat, sat, fat'.match('at'); |
match
方法还可以使用正则表达式作为参数,详见《正则表达式》一章。
String.prototype.search(),String.prototype.replace()
search
方法的用法基本等同于 match
,但是返回值为匹配的第一个位置。如果没有找到匹配,则返回 -1
。
1 | 'cat, bat, sat, fat'.search('at') // 1 |
search
方法还可以使用正则表达式作为参数,详见《正则表达式》一节。
replace
方法用于替换匹配的子字符串,一般情况下只替换第一个匹配(除非使用带有 g
修饰符的正则表达式)。
1 | 'aaa'.replace('a', 'b') // "baa" |
replace
方法还可以使用正则表达式作为参数,详见《正则表达式》一节。
String.prototype.split()
split
方法按照给定规则分割字符串,返回一个由分割出来的子字符串组成的数组。
1 | 'a|b|c'.split('|') // ["a", "b", "c"] |
如果分割规则为空字符串,则返回数组的成员是原字符串的每一个字符。
1 | 'a|b|c'.split('') // ["a", "|", "b", "|", "c"] |
如果省略参数,则返回数组的唯一成员就是原字符串。
1 | 'a|b|c'.split() // ["a|b|c"] |
如果满足分割规则的两个部分紧邻着(即两个分割符中间没有其他字符),则返回数组之中会有一个空字符串。
1 | 'a||c'.split('|') // ['a', '', 'c'] |
如果满足分割规则的部分处于字符串的开头或结尾(即它的前面或后面没有其他字符),则返回数组的第一个或最后一个成员是一个空字符串。
1 | '|b|c'.split('|') // ["", "b", "c"] |
split
方法还可以接受第二个参数,限定返回数组的最大成员数。
1 | 'a|b|c'.split('|', 0) // [] |
上面代码中,split
方法的第二个参数,决定了返回数组的成员数。
split
方法还可以使用正则表达式作为参数,详见《正则表达式》一节。
String.prototype.localeCompare()
localeCompare
方法用于比较两个字符串。它返回一个整数,如果小于0,表示第一个字符串小于第二个字符串;如果等于0,表示两者相等;如果大于0,表示第一个字符串大于第二个字符串。
1 | 'apple'.localeCompare('banana') // -1 |
该方法的最大特点,就是会考虑自然语言的顺序。举例来说,正常情况下,大写的英文字母小于小写字母。
1 | 'B' > 'a' // false |
上面代码中,字母 B
小于字母 a
。因为 JavaScript 采用的是 Unicode 码点比较,B
的码点是66,而 a
的码点是97。
但是,localeCompare
方法会考虑自然语言的排序情况,将 B
排在 a
的前面。
1 | 'B'.localeCompare('a') // 1 |
上面代码中,localeCompare
方法返回整数1,表示 B
较大。
localeCompare
还可以有第二个参数,指定所使用的语言(默认是英语),然后根据该语言的规则进行比较。
1 | 'ä'.localeCompare('z', 'de') // -1 |
上面代码中,de
表示德语,sv
表示瑞典语。德语中,ä
小于 z
,所以返回 -1
;瑞典语中,ä
大于 z
,所以返回 1
。
Math 对象
静态属性
Math
对象的静态属性,提供以下一些数学常数。
Math.E
:常数e
。Math.LN2
:2 的自然对数。Math.LN10
:10 的自然对数。Math.LOG2E
:以 2 为底的e
的对数。Math.LOG10E
:以 10 为底的e
的对数。Math.PI
:常数π
。Math.SQRT1_2
:0.5 的平方根。Math.SQRT2
:2 的平方根。
1 | Math.E // 2.718281828459045 |
这些属性都是只读的,不能修改。
静态方法
Math
对象提供以下一些静态方法。
Math.abs()
:绝对值Math.ceil()
:向上取整Math.floor()
:向下取整Math.max()
:最大值Math.min()
:最小值Math.pow()
:幂运算Math.sqrt()
:平方根Math.log()
:自然对数Math.exp()
:e
的指数Math.round()
:四舍五入Math.random()
:随机数
Math.abs()
Math.abs
方法返回参数值的绝对值。
1 | Math.abs(1) // 1 |
Math.max(),Math.min()
Math.max
方法返回参数之中最大的那个值,Math.min
返回最小的那个值。如果参数为空, Math.min
返回 Infinity
, Math.max
返回 -Infinity
。
1 | Math.max(2, -1, 5) // 5 |
Math.floor(),Math.ceil()
Math.floor
方法返回小于或等于参数值的最大整数(地板值)。
1 | Math.floor(3.2) // 3 |
Math.ceil
方法返回大于或等于参数值的最小整数(天花板值)。
1 | Math.ceil(3.2) // 4 |
这两个方法可以结合起来,实现一个总是返回数值的整数部分的函数。
1 | function ToInteger(x) { |
上面代码中,不管正数或负数,ToInteger
函数总是返回一个数值的整数部分。
Math.round()
Math.round
方法用于四舍五入。
1 | Math.round(0.1) // 0 |
注意,它对负数的处理(主要是对 0.5
的处理)。
1 | Math.round(-1.1) // -1 |
Math.pow()
Math.pow
方法返回以第一个参数为底数、第二个参数为指数的幂运算值。
1 | // 等同于 2 ** 2 |
下面是计算圆面积的方法。
1 | var radius = 20; |
Math.sqrt()
Math.sqrt
方法返回参数值的平方根。如果参数是一个负值,则返回 NaN
。
1 | Math.sqrt(4) // 2 |
Math.log()
Math.log
方法返回以 e
为底的自然对数值。
1 | Math.log(Math.E) // 1 |
如果要计算以10为底的对数,可以先用 Math.log
求出自然对数,然后除以 Math.LN10
;求以2为底的对数,可以除以 Math.LN2
。
1 | Math.log(100)/Math.LN10 // 2 |
Math.exp()
Math.exp
方法返回常数 e
的参数次方。
1 | Math.exp(1) // 2.718281828459045 |
Math.random()
Math.random()
返回0到1之间的一个伪随机数,可能等于0,但是一定小于1。
1 | Math.random() // 0.7151307314634323 |
任意范围的随机数生成函数如下。
1 | function getRandomArbitrary(min, max) { |
任意范围的随机整数生成函数如下。
1 | function getRandomInt(min, max) { |
返回随机字符的例子如下。
1 | function random_str(length) { |
上面代码中,random_str
函数接受一个整数作为参数,返回变量 ALPHABET
内的随机字符所组成的指定长度的字符串。
三角函数方法
Math
对象还提供一系列三角函数方法。
Math.sin()
:返回参数的正弦(参数为弧度值)Math.cos()
:返回参数的余弦(参数为弧度值)Math.tan()
:返回参数的正切(参数为弧度值)Math.asin()
:返回参数的反正弦(返回值为弧度值)Math.acos()
:返回参数的反余弦(返回值为弧度值)Math.atan()
:返回参数的反正切(返回值为弧度值)
1 | Math.sin(0) // 0 |
Date对象
Date
对象是 JavaScript 原生的时间库。它以国际标准时间(UTC)1970年1月1日00:00:00作为时间的零点,可以表示的时间范围是前后各1亿天(单位为毫秒)。
普通函数的用法
Date
对象可以作为普通函数直接调用,返回一个代表当前时间的字符串。
1 | Date() |
注意,即使带有参数,Date
作为普通函数使用时,返回的还是当前时间。
1 | Date(2000, 1, 1) |
上面代码说明,无论有没有参数,直接调用 Date
总是返回当前时间。
构造函数的用法
Date
还可以当作构造函数使用。对它使用 new
命令,会返回一个 Date
对象的实例。如果不加参数,实例代表的就是当前时间。
1 | var today = new Date(); |
Date
实例有一个独特的地方。其他对象求值的时候,都是默认调用 .valueOf()
方法,但是 Date
实例求值的时候,默认调用的是 toString()
方法。这导致对 Date
实例求值,返回的是一个字符串,代表该实例对应的时间。
1 | var today = new Date(); |
上面代码中,today
是 Date
的实例,直接求值等同于调用 toString
方法。
作为构造函数时,Date
对象可以接受多种格式的参数,返回一个该参数对应的时间实例。
1 | // 参数为时间零点开始计算的毫秒数 |
关于 Date
构造函数的参数,有几点说明。
第一点,参数可以是负整数,代表1970年元旦之前的时间。
1 | new Date(-1378218728000) |
第二点,只要是能被 Date.parse()
方法解析的字符串,都可以当作参数。
1 | new Date('2013-2-15') |
上面多种日期字符串的写法,返回的都是同一个时间。
第三,参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,这时至少需要两个参数,因为如果只使用“年”这一个参数,Date
会将其解释为毫秒数。
1 | new Date(2013) |
上面代码中,2013被解释为毫秒数,而不是年份。
1 | new Date(2013, 0) |
上面代码中,不管有几个参数,返回的都是2013年1月1日零点。
最后,各个参数的取值范围如下。
- 年:使用四位数年份,比如
2000
。如果写成两位数或个位数,则加上1900
,即10
代表1910年。如果是负数,表示公元前。 - 月:
0
表示一月,依次类推,11
表示12月。 - 日:
1
到31
。 - 小时:
0
到23
。 - 分钟:
0
到59
。 - 秒:
0
到59
- 毫秒:
0
到999
。
注意,月份从 0
开始计算,但是,天数从 1
开始计算。另外,除了日期的默认值为 1
,小时、分钟、秒钟和毫秒的默认值都是 0
。
这些参数如果超出了正常范围,会被自动折算。比如,如果月设为 15
,就折算为下一年的4月。
1 | new Date(2013, 15) |
上面代码的第二个例子,日期设为 0
,就代表上个月的最后一天。
参数还可以使用负数,表示扣去的时间。
1 | new Date(2013, -1) |
上面代码中,分别对月和日使用了负数,表示从基准日扣去相应的时间。
日期的运算
类型自动转换时,Date
实例如果转为数值,则等于对应的毫秒数;如果转为字符串,则等于对应的日期字符串。所以,两个日期实例对象进行减法运算时,返回的是它们间隔的毫秒数;进行加法运算时,返回的是两个字符串连接而成的新字符串。
1 | var d1 = new Date(2000, 2, 1); |
静态方法
Date.now()
Date.now
方法返回当前时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数,相当于 Unix 时间戳乘以1000。
1 | Date.now() // 1364026285194 |
Date.parse()
Date.parse
方法用来解析日期字符串,返回该时间距离时间零点(1970年1月1日 00:00:00)的毫秒数。
日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准,即 YYYY-MM-DDTHH:mm:ss.sssZ
格式,其中最后的 Z
表示时区。但是,其他格式也可以被解析,请看下面的例子。
1 | Date.parse('Aug 9, 1995') |
上面的日期字符串都可以解析。
如果解析失败,返回 NaN
。
1 | Date.parse('xxx') // NaN |
Date.UTC()
Date.UTC
方法接受年、月、日等变量作为参数,返回该时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数。
1 | // 格式 |
该方法的参数用法与 Date
构造函数完全一致,比如月从 0
开始计算,日期从 1
开始计算。区别在于 Date.UTC
方法的参数,会被解释为 UTC 时间(世界标准时间),Date
构造函数的参数会被解释为当前时区的时间。
实例方法
Date
的实例对象,有几十个自己的方法,除了 valueOf
和 toString
,可以分为以下三类。
to
类:从Date
对象返回一个字符串,表示指定的时间。get
类:获取Date
对象的日期和时间。set
类:设置Date
对象的日期和时间。
Date.prototype.valueOf()
valueOf
方法返回实例对象距离时间零点(1970年1月1日00:00:00 UTC)对应的毫秒数,该方法等同于 getTime
方法。
1 | var d = new Date(); |
预期为数值的场合,Date
实例会自动调用该方法,所以可以用下面的方法计算时间的间隔。
1 | var start = new Date(); |
to 类方法
(1)Date.prototype.toString()
toString
方法返回一个完整的日期字符串。
1 | var d = new Date(2013, 0, 1); |
因为 toString
是默认的调用方法,所以如果直接读取 Date
实例,就相当于调用这个方法。
(2)Date.prototype.toUTCString()
toUTCString
方法返回对应的 UTC 时间,也就是比北京时间晚8个小时。
1 | var d = new Date(2013, 0, 1); |
(3)Date.prototype.toISOString()
toISOString
方法返回对应时间的 ISO8601 写法。
1 | var d = new Date(2013, 0, 1); |
注意,toISOString
方法返回的总是 UTC 时区的时间。
(4)Date.prototype.toJSON()
toJSON
方法返回一个符合 JSON 格式的 ISO 日期字符串,与 toISOString
方法的返回结果完全相同。
1 | var d = new Date(2013, 0, 1); |
(5)Date.prototype.toDateString()
toDateString
方法返回日期字符串(不含小时、分和秒)。
1 | var d = new Date(2013, 0, 1); |
(6)Date.prototype.toTimeString()
toTimeString
方法返回时间字符串(不含年月日)。
1 | var d = new Date(2013, 0, 1); |
(7)本地时间
以下三种方法,可以将 Date 实例转为表示本地时间的字符串。
Date.prototype.toLocaleString()
:完整的本地时间。Date.prototype.toLocaleDateString()
:本地日期(不含小时、分和秒)。Date.prototype.toLocaleTimeString()
:本地时间(不含年月日)。
下面是用法实例。
1 | var d = new Date(2013, 0, 1); |
这三个方法都有两个可选的参数。
1 | dateObj.toLocaleString([locales[, options]]) |
这两个参数中,locales
是一个指定所用语言的字符串,options
是一个配置对象。下面是 locales
的例子,分别采用 en-US
和 zh-CN
语言设定。
1 | var d = new Date(2013, 0, 1); |
options
配置对象有以下属性。
dateStyle
:可能的值为full
、long
、medium
、short
。timeStyle
:可能的值为full
、long
、medium
、short
。month
:可能的值为numeric
、2-digit
、long
、short
、narrow
。year
:可能的值为numeric
、2-digit
。weekday
:可能的值为long
、short
、narrow
。day
、hour
、minute
、second
:可能的值为numeric
、2-digit
。timeZone
:可能的值为 IANA 的时区数据库。timeZooneName
:可能的值为long
、short
。hour12
:24小时周期还是12小时周期,可能的值为true
、false
。
下面是用法实例。
1 | var d = new Date(2013, 0, 1); |
get 类方法
Date
对象提供了一系列 get*
方法,用来获取实例对象某个方面的值。
getTime()
:返回实例距离1970年1月1日00:00:00的毫秒数,等同于valueOf
方法。getDate()
:返回实例对象对应每个月的几号(从1开始)。getDay()
:返回星期几,星期日为0,星期一为1,以此类推。getFullYear()
:返回四位的年份。getMonth()
:返回月份(0表示1月,11表示12月)。getHours()
:返回小时(0-23)。getMilliseconds()
:返回毫秒(0-999)。getMinutes()
:返回分钟(0-59)。getSeconds()
:返回秒(0-59)。getTimezoneOffset()
:返回当前时间与 UTC 的时区差异,以分钟表示,返回结果考虑到了夏令时因素。
所有这些 get*
方法返回的都是整数,不同方法返回值的范围不一样。
- 分钟和秒:0 到 59
- 小时:0 到 23
- 星期:0(星期天)到 6(星期六)
- 日期:1 到 31
- 月份:0(一月)到 11(十二月)
1 | var d = new Date('January 6, 2013'); |
上面代码中,最后一行返回 -480
,即 UTC 时间减去当前时间,单位是分钟。-480
表示 UTC 比当前时间少480分钟,即当前时区比 UTC 早8个小时。
下面是一个例子,计算本年度还剩下多少天。
1 | function leftDays() { |
上面这些 get*
方法返回的都是当前时区的时间,Date
对象还提供了这些方法对应的 UTC 版本,用来返回 UTC 时间。
getUTCDate()
getUTCFullYear()
getUTCMonth()
getUTCDay()
getUTCHours()
getUTCMinutes()
getUTCSeconds()
getUTCMilliseconds()
1 | var d = new Date('January 6, 2013'); |
上面代码中,实例对象 d
表示当前时区(东八时区)的1月6日0点0分0秒,这个时间对于当前时区来说是1月6日,所以 getDate
方法返回6,对于 UTC 时区来说是1月5日,所以 getUTCDate
方法返回5。
set 类方法
Date
对象提供了一系列 set*
方法,用来设置实例对象的各个方面。
setDate(date)
:设置实例对象对应的每个月的几号(1-31),返回改变后毫秒时间戳。setFullYear(year [, month, date])
:设置四位年份。setHours(hour [, min, sec, ms])
:设置小时(0-23)。setMilliseconds()
:设置毫秒(0-999)。setMinutes(min [, sec, ms])
:设置分钟(0-59)。setMonth(month [, date])
:设置月份(0-11)。setSeconds(sec [, ms])
:设置秒(0-59)。setTime(milliseconds)
:设置毫秒时间戳。
这些方法基本是跟 get*
方法一一对应的,但是没有 setDay
方法,因为星期几是计算出来的,而不是设置的。另外,需要注意的是,凡是涉及到设置月份,都是从0开始算的,即 0
是1月,11
是12月。
1 | var d = new Date ('January 6, 2013'); |
set*
方法的参数都会自动折算。以 setDate()
为例,如果参数超过当月的最大天数,则向下一个月顺延,如果参数是负数,表示从上个月的最后一天开始减去的天数。
1 | var d1 = new Date('January 6, 2013'); |
上面代码中,d1.setDate(32)
将日期设为1月份的32号,因为1月份只有31号,所以自动折算为2月1日。d2.setDate(-1)
表示设为上个月的倒数第二天,即12月30日。
set
类方法和 get
类方法,可以结合使用,得到相对时间。
1 | var d = new Date(); |
set*
系列方法除了 setTime()
,都有对应的 UTC 版本,即设置 UTC 时区的时间。
setUTCDate()
setUTCFullYear()
setUTCHours()
setUTCMilliseconds()
setUTCMinutes()
setUTCMonth()
setUTCSeconds()
1 | var d = new Date('January 6, 2013'); |
上面代码中,本地时区(东八时区)的1月6日0点0分,是 UTC 时区的前一天下午16点。设为 UTC 时区的22点以后,就变为本地时区的上午6点。
RegExp 对象
RegExp对象提供正则表达式的功能。
概述
正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用来按照“给定模式”匹配文本。比如,正则表达式给出一个 Email 地址的模式,然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。
新建正则表达式有两种方法。一种是使用字面量,以斜杠表示开始和结束。
1 | var regex = /xyz/; |
另一种是使用 RegExp
构造函数。
1 | var regex = new RegExp('xyz'); |
上面两种写法是等价的,都新建了一个内容为 xyz
的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。
RegExp
构造函数还可以接受第二个参数,表示修饰符(详细解释见下文)。
1 | var regex = new RegExp('xyz', 'i'); |
上面代码中,正则表达式 /xyz/
有一个修饰符 i
。
实例属性
正则对象的实例属性分成两类。
一类是修饰符相关,用于了解设置了什么修饰符。
RegExp.prototype.ignoreCase
:返回一个布尔值,表示是否设置了i
修饰符。RegExp.prototype.global
:返回一个布尔值,表示是否设置了g
修饰符。RegExp.prototype.multiline
:返回一个布尔值,表示是否设置了m
修饰符。RegExp.prototype.flags
:返回一个字符串,包含了已经设置的所有修饰符,按字母排序。
上面四个属性都是只读的。
1 | var r = /abc/igm; |
另一类是与修饰符无关的属性,主要是下面两个。
RegExp.prototype.lastIndex
:返回一个整数,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义,详细介绍请看后文。RegExp.prototype.source
:返回正则表达式的字符串形式(不包括反斜杠),该属性只读。
1 | var r = /abc/igm; |
实例方法
RegExp.prototype.test()
正则实例对象的 test
方法返回一个布尔值,表示当前模式是否能匹配参数字符串。
1 | /cat/.test('cats and dogs') // true |
上面代码验证参数字符串之中是否包含 cat
,结果返回 true
。
如果正则表达式带有 g
修饰符,则每一次 test
方法都从上一次结束的位置开始向后匹配。
1 | var r = /x/g; |
上面代码的正则表达式使用了 g
修饰符,表示是全局搜索,会有多个结果。接着,三次使用 test
方法,每一次开始搜索的位置都是上一次匹配的后一个位置。
带有 g
修饰符时,可以通过正则对象的 lastIndex
属性指定开始搜索的位置。
1 | var r = /x/g; |
上面代码指定从字符串的第五个位置开始搜索,这个位置为空,所以返回 false
。同时,lastIndex
属性重置为 0
,所以第二次执行 r.test(s)
会返回 true
。
注意,带有 g
修饰符时,正则表达式内部会记住上一次的 lastIndex
属性,这时不应该更换所要匹配的字符串,否则会有一些难以察觉的错误。
1 | var r = /bb/g; |
上面代码中,由于正则表达式 r
是从上一次的 lastIndex
位置开始匹配,导致第二次执行 test
方法时出现预期以外的结果。
lastIndex
属性只对同一个正则表达式有效,所以下面这样写是错误的。
1 | var count = 0; |
上面代码会导致无限循环,因为 while
循环的每次匹配条件都是一个新的正则表达式,导致 lastIndex
属性总是等于0。
如果正则模式是一个空字符串,则匹配所有字符串。
1 | new RegExp('').test('abc') |
RegExp.prototype.exec()
正则实例对象的 exec()
方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回 null
。
1 | var s = '_x_x'; |
上面代码中,正则对象 r1
匹配成功,返回一个数组,成员是匹配结果;正则对象 r2
匹配失败,返回 null
。
如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的 length
属性等于组匹配的数量再加1。
1 | var s = '_x_x'; |
上面代码的 exec()
方法,返回一个数组。第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果。
exec()
方法的返回数组还包含以下两个属性:
input
:整个原字符串。index
:模式匹配成功的开始位置(从0开始计数)。
1 | var r = /a(b+)a/; |
上面代码中的 index
属性等于1,是因为从原字符串的第二个位置开始匹配成功。
如果正则表达式加上 g
修饰符,则可以使用多次 exec()
方法,下一次搜索的位置从上一次匹配成功结束的位置开始。
1 | var reg = /a/g; |
上面代码连续用了四次 exec()
方法,前三次都是从上一次匹配结束的位置向后匹配。当第三次匹配结束以后,整个字符串已经到达尾部,匹配结果返回 null
,正则实例对象的 lastIndex
属性也重置为 0
,意味着第四次匹配将从头开始。
利用 g
修饰符允许多次匹配的特点,可以用一个循环完成全部匹配。
1 | var reg = /a/g; |
上面代码中,只要 exec()
方法不返回 null
,就会一直循环下去,每次输出匹配的位置和匹配的文本。
正则实例对象的 lastIndex
属性不仅可读,还可写。设置了 g
修饰符的时候,只要手动设置了 lastIndex
的值,就会从指定位置开始匹配。
字符串的实例方法
字符串的实例方法之中,有4种与正则表达式有关。
String.prototype.match()
:返回一个数组,成员是所有匹配的子字符串。String.prototype.search()
:按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。String.prototype.replace()
:按照给定的正则表达式进行替换,返回替换后的字符串。String.prototype.split()
:按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员。
String.prototype.match()
字符串实例对象的 match
方法对字符串进行正则匹配,返回匹配结果。
1 | var s = '_x_x'; |
从上面代码可以看到,字符串的 match
方法与正则对象的 exec
方法非常类似:匹配成功返回一个数组,匹配失败返回 null
。
如果正则表达式带有 g
修饰符,则该方法与正则对象的 exec
方法行为不同,会一次性返回所有匹配成功的结果。
1 | var s = 'abba'; |
设置正则表达式的 lastIndex
属性,对 match
方法无效,匹配总是从字符串的第一个字符开始。
1 | var r = /a|b/g; |
上面代码表示,设置正则对象的 lastIndex
属性是无效的。
String.prototype.search()
字符串对象的 search
方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回 -1
。
1 | '_x_x'.search(/x/) |
上面代码中,第一个匹配结果出现在字符串的 1
号位置。
String.prototype.replace()
字符串对象的 replace
方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。
1 | str.replace(search, replacement) |
正则表达式如果不加 g
修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。
1 | 'aaa'.replace('a', 'b') // "baa" |
上面代码中,最后一个正则表达式使用了 g
修饰符,导致所有的 a
都被替换掉了。
replace
方法的一个应用,就是消除字符串首尾两端的空格。
1 | var str = ' #id div.class '; |
replace
方法的第二个参数可以使用美元符号 $
,用来指代所替换的内容。
$&
:匹配的子字符串。- `$``:匹配结果前面的文本。
$'
:匹配结果后面的文本。$n
:匹配成功的第n
组内容,n
是从1开始的自然数。$$
:指代美元符号$
。
1 | 'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1') |
上面代码中,第一个例子是将匹配的组互换位置,第二个例子是改写匹配的值。
replace
方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值。
1 | '3 and 5'.replace(/[0-9]+/g, function (match) { |
作为 replace
方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子。
1 | var prices = { |
上面代码的捕捉模式中,有四个括号,所以会产生四个组匹配,在匹配函数中用 $1
到 $4
表示。匹配函数的作用是将价格插入模板中。
String.prototype.split()
字符串对象的 split
方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。
1 | str.split(separator, [limit]) |
该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数。
1 | // 非正则分隔 |
上面代码使用正则表达式,去除了子字符串的逗号后面的空格。
1 | // 例一 |
上面代码的分割规则是0次或多次的 a
,由于正则默认是贪婪匹配,所以例一的第一个分隔符是 aaa
,第二个分割符是 a
,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是 aaa
,第二个分隔符是0个 a
(即空字符),第三个分隔符是 a
,所以将字符串分成四个部分。
如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。
1 | 'aaa*a*'.split(/(a*)/) |
上面代码的正则表达式使用了括号,第一个组匹配是 aaa
,第二个组匹配是 a
,它们都作为数组成员返回。
匹配规则
正则表达式的规则很复杂,下面一一介绍这些规则。
字面量字符和元字符
大部分字符在正则表达式中,就是字面的含义,比如 /a/
匹配 a
,/b/
匹配 b
。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的 a
和 b
),那么它们就叫做“字面量字符”(literal characters)。
1 | /dog/.test('old dog') // true |
上面代码中正则表达式的 dog
,就是字面量字符,所以 /dog/
匹配 old dog
,因为它就表示 d
、o
、g
三个字母连在一起。
除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个。
(1)点字符(.)
点字符(.
)匹配除回车(\r
)、换行(\n
) 、行分隔符(\u2028
)和段分隔符(\u2029
)以外的所有字符。注意,对于码点大于 0xFFFF
字符,点字符不能正确匹配,会认为这是两个字符。
1 | /c.t/ |
上面代码中,c.t
匹配 c
和 t
之间包含任意一个字符的情况,只要这三个字符在同一行,比如 cat
、c2t
、c-t
等等,但是不匹配 coot
。
(2)位置字符
位置字符用来提示字符所处的位置,主要有两个字符。
^
表示字符串的开始位置$
表示字符串的结束位置
1 | // test必须出现在开始位置 |
(3)选择符(|
)
竖线符号(|
)在正则表达式中表示“或关系”(OR),即 cat|dog
表示匹配 cat
或 dog
。
1 | /11|22/.test('911') // true |
上面代码中,正则表达式指定必须匹配 11
或 22
。
多个选择符可以联合使用。
1 | // 匹配fred、barney、betty之中的一个 |
选择符会包括它前后的多个字符,比如 /ab|cd/
指的是匹配 ab
或者 cd
,而不是指匹配 b
或者 c
。如果想修改这个行为,可以使用圆括号。
1 | /a( |\t)b/.test('a\tb') // true |
上面代码指的是,a
和 b
之间有一个空格或者一个制表符。
其他的元字符还包括 \
、*
、+
、?
、()
、[]
、{}
等,将在下文解释。
转义符
正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配 +
,就要写成 \+
。
1 | /1+1/.test('1+1') |
上面代码中,第一个正则表达式之所以不匹配,因为加号是元字符,不代表自身。第二个正则表达式使用反斜杠对加号转义,就能匹配成功。
正则表达式中,需要反斜杠转义的,一共有12个字符:^
、.
、[
、$
、(
、)
、|
、*
、+
、?
、{
和 \
。需要特别注意的是,如果使用 RegExp
方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。
1 | (new RegExp('1\+1')).test('1+1') |
上面代码中,RegExp
作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义。
特殊字符
正则表达式对一些不能打印的特殊字符,提供了表达方法。
\cX
表示Ctrl-[X]
,其中的X
是A-Z之中任一个英文字母,用来匹配控制字符。[\b]
匹配退格键(U+0008),不要与\b
混淆。\n
匹配换行键。\r
匹配回车键。\t
匹配制表符 tab(U+0009)。\v
匹配垂直制表符(U+000B)。\f
匹配换页符(U+000C)。\0
匹配null
字符(U+0000)。\xhh
匹配一个以两位十六进制数(\x00
-\xFF
)表示的字符。\uhhhh
匹配一个以四位十六进制数(\u0000
-\uFFFF
)表示的 Unicode 字符。
字符类
字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如 [xyz]
表示 x
、y
、z
之中任选一个匹配。
1 | /[abc]/.test('hello world') // false |
上面代码中,字符串 hello world
不包含 a
、b
、c
这三个字母中的任一个,所以返回 false
;字符串 apple
包含字母 a
,所以返回 true
。
有两个字符在字符类中有特殊含义。
(1)脱字符(^)
如果方括号内的第一个字符是 [^]
,则表示除了字符类之中的字符,其他字符都可以匹配。比如,[^xyz]
表示除了 x
、y
、z
之外都可以匹配。
1 | /[^abc]/.test('bbc news') // true |
上面代码中,字符串 bbc news
包含 a
、b
、c
以外的其他字符,所以返回 true
;字符串 bbc
不包含 a
、b
、c
以外的其他字符,所以返回 false
。
如果方括号内没有其他字符,即只有 [^]
,就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.
)是不包括换行符的。
1 | var s = 'Please yes\nmake my day!'; |
上面代码中,字符串 s
含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式 [^]
包含一切字符,所以匹配成功。
注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。
(2)连字符(-)
某些情况下,对于连续序列的字符,连字符(-
)用来提供简写形式,表示字符的连续范围。比如,[abc]
可以写成 [a-c]
,[0123456789]
可以写成 [0-9]
,同理 [A-Z]
表示26个大写字母。
1 | /a-z/.test('b') // false |
上面代码中,当连字号(dash)不出现在方括号之中,就不具备简写的作用,只代表字面的含义,所以不匹配字符 b
。只有当连字号用在方括号之中,才表示连续的字符序列。
以下都是合法的字符类简写形式。
1 | [0-9.,] |
上面代码中最后一个字符类 [1-31]
,不代表 1
到 31
,只代表 1
到 3
。
连字符还可以用来指定 Unicode 字符的范围。
1 | var str = "\u0130\u0131\u0132"; |
上面代码中,\u0128-\uFFFF
表示匹配码点在 0128
到 FFFF
之间的所有字符。
另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是 [A-z]
,表面上它是选中从大写的 A
到小写的 z
之间52个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。
1 | /[A-z]/.test('\\') // true |
上面代码中,由于反斜杠(’')的ASCII码在大写字母与小写字母之间,结果会被选中。
预定义模式
预定义模式指的是某些常见模式的简写方式。
\d
匹配0-9之间的任一数字,相当于[0-9]
。\D
匹配所有0-9以外的字符,相当于[^0-9]
。\w
匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
。\W
除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
。\s
匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]
。\S
匹配非空格的字符,相当于[^ \t\r\n\v\f]
。\b
匹配词的边界。\B
匹配非词边界,即在词的内部。
下面是一些例子。
1 | // \s 的例子 |
上面代码中,\s
表示空格,所以匹配结果会包括空格。\b
表示词的边界,所以 world
的词首必须独立(词尾是否独立未指定),才会匹配。同理,\B
表示非词的边界,只有 world
的词首不独立,才会匹配。
通常,正则表达式遇到换行符(\n
)就会停止匹配。
1 | var html = "<b>Hello</b>\n<i>world!</i>"; |
上面代码中,字符串 html
包含一个换行符,结果点字符(.
)不匹配换行符,导致匹配结果可能不符合原意。这时使用 \s
字符类,就能包括换行符。
1 | var html = "<b>Hello</b>\n<i>world!</i>"; |
上面代码中,[\S\s]
指代一切字符。
重复类
模式的精确匹配次数,使用大括号({}
)表示。{n}
表示恰好重复 n
次,{n,}
表示至少重复 n
次,{n,m}
表示重复不少于 n
次,不多于 m
次。
1 | /lo{2}k/.test('look') // true |
上面代码中,第一个模式指定 o
连续出现2次,第二个模式指定 o
连续出现2次到5次之间。
量词符
量词符用来设定某个模式出现的次数。
?
问号表示某个模式出现0次或1次,等同于{0, 1}
。*
星号表示某个模式出现0次或多次,等同于{0,}
。+
加号表示某个模式出现1次或多次,等同于{1,}
。
1 | // t 出现0次或1次 |
贪婪模式
上一小节的三个量词符,默认情况下都是最大可能匹配,即匹配到下一个字符不满足匹配规则为止。这被称为贪婪模式。
1 | var s = 'aaa'; |
上面代码中,模式是 /a+/
,表示匹配1个 a
或多个 a
,那么到底会匹配几个 a
呢?因为默认是贪婪模式,会一直匹配到字符 a
不出现为止,所以匹配结果是3个 a
。
除了贪婪模式,还有非贪婪模式,即最小可能匹配。只要一发现匹配,就返回结果,不要往下检查。如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。
1 | var s = 'aaa'; |
上面例子中,模式结尾添加了一个问号 /a+?/
,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配,+?
表示只要发现一个 a
,就不再往下匹配了。
除了非贪婪模式的加号(+?
),还有非贪婪模式的星号(*?
)和非贪婪模式的问号(??
)。
+?
:表示某个模式出现1次或多次,匹配时采用非贪婪模式。*?
:表示某个模式出现0次或多次,匹配时采用非贪婪模式。??
:表格某个模式出现0次或1次,匹配时采用非贪婪模式。
1 | 'abb'.match(/ab*/) // ["abb"] |
上面例子中,/ab*/
表示如果 a
后面有多个 b
,那么匹配尽可能多的 b
;/ab*?/
表示匹配尽可能少的 b
,也就是0个 b
。
修饰符
修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。
修饰符可以单个使用,也可以多个一起使用。
1 | // 单个修饰符 |
(1)g 修饰符
默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g
修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。
1 | var regex = /b/; |
上面代码中,正则模式不含 g
修饰符,每次都是从字符串头部开始匹配。所以,连续做了三次匹配,都返回 true
。
1 | var regex = /b/g; |
上面代码中,正则模式含有 g
修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串 abba
只有两个 b
,所以前两次匹配结果为 true
,第三次匹配结果为 false
。
(2)i 修饰符
默认情况下,正则对象区分字母的大小写,加上 i
修饰符以后表示忽略大小写(ignoreCase)。
1 | /abc/.test('ABC') // false |
上面代码表示,加了 i
修饰符以后,不考虑大小写,所以模式 abc
匹配字符串 ABC
。
(3)m 修饰符
m
修饰符表示多行模式(multiline),会修改 ^
和 $
的行为。默认情况下(即不加 m
修饰符时),^
和 $
匹配字符串的开始处和结尾处,加上 m
修饰符以后,^
和 $
还会匹配行首和行尾,即 ^
和 $
会识别换行符(\n
)。
1 | /world$/.test('hello world\n') // false |
上面的代码中,字符串结尾处有一个换行符。如果不加 m
修饰符,匹配不成功,因为字符串的结尾不是 world
;加上以后,$
可以匹配行尾。
1 | /^b/m.test('a\nb') // true |
上面代码要求匹配行首的 b
,如果不加 m
修饰符,就相当于 b
只能处在字符串的开始处。加上 m
修饰符以后,换行符 \n
也会被认为是一行的开始。
组匹配
(1)概述
正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。
1 | /fred+/.test('fredd') // true |
上面代码中,第一个模式没有括号,结果 +
只表示重复字母 d
,第二个模式有括号,结果 +
就表示匹配 fred
这个词。
下面是另外一个分组捕获的例子。
1 | var m = 'abcabc'.match(/(.)b(.)/); |
上面代码中,正则表达式 /(.)b(.)/
一共使用两个括号,第一个括号捕获 a
,第二个括号捕获 c
。
注意,使用组匹配时,不宜同时使用 g
修饰符,否则 match
方法不会捕获分组的内容。
1 | var m = 'abcabc'.match(/(.)b(.)/g); |
上面代码使用带 g
修饰符的正则表达式,结果 match
方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的 exec
方法,配合循环,才能读到每一轮匹配的组捕获。
1 | var str = 'abcabc'; |
正则表达式内部,还可以用 \n
引用括号匹配的内容,n
是从1开始的自然数,表示对应顺序的括号。
1 | /(.)b(.)\1b\2/.test("abcabc") |
上面的代码中,\1
表示第一个括号匹配的内容(即 a
),\2
表示第二个括号匹配的内容(即 c
)。
下面是另外一个例子。
1 | /y(..)(.)\2\1/.test('yabccab') // true |
括号还可以嵌套。
1 | /y((..)\2)\1/.test('yabababab') // true |
上面代码中,\1
指向外层括号,\2
指向内层括号。
组匹配非常有用,下面是一个匹配网页标签的例子。
1 | var tagName = /<([^>]+)>[^<]*<\/\1>/; |
上面代码中,圆括号匹配尖括号之中的标签,而 \1
就表示对应的闭合标签。
上面代码略加修改,就能捕获带有属性的标签。
1 | var html = '<b class="hello">Hello</b><i>world</i>'; |
(2)非捕获组
(?:x)
称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。
非捕获组的作用请考虑这样一个场景,假定需要匹配 foo
或者 foofoo
,正则表达式就应该写成 /(foo){1, 2}/
,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为 /(?:foo){1, 2}/
,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容。
请看下面的例子。
1 | var m = 'abc'.match(/(?:.)b(.)/); |
上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容。
下面是用来分解网址的正则表达式。
1 | // 正常匹配 |
上面的代码中,前一个正则表达式是正常匹配,第一个括号返回网络协议;后一个正则表达式是非捕获匹配,返回结果中不包括网络协议。
(3)先行断言
x(?=y)
称为先行断言(Positive look-ahead),x
只有在 y
前面才匹配,y
不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成 /\d+(?=%)/
。
“先行断言”中,括号里的部分是不会返回的。
1 | var m = 'abc'.match(/b(?=c)/); |
上面的代码使用了先行断言,b
在 c
前面所以被匹配,但是括号对应的 c
不会被返回。
(4)先行否定断言
x(?!y)
称为先行否定断言(Negative look-ahead),x
只有不在 y
前面才匹配,y
不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成 /\d+(?!%)/
。
1 | /\d+(?!\.)/.exec('3.14') |
上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是 14
。
“先行否定断言”中,括号里的部分是不会返回的。
1 | var m = 'abd'.match(/b(?!c)/); |
上面的代码使用了先行否定断言,b
不在 c
前面所以被匹配,而且括号对应的 d
不会被返回。
JSON对象
JSON格式
JSON 格式(JavaScript Object Notation 的缩写)是一种用于数据交换的文本格式,2001年由 Douglas Crockford 提出,目的是取代繁琐笨重的 XML 格式。
相比 XML 格式,JSON 格式有两个显著的优点:书写简单,一目了然;符合 JavaScript 原生语法,可以由解释引擎直接处理,不用另外添加解析代码。所以,JSON 迅速被接受,已经成为各大网站交换数据的标准格式,并被写入标准。
每个 JSON 对象就是一个值,可能是一个数组或对象,也可能是一个原始类型的值。总之,只能是一个值,不能是两个或更多的值。
JSON 对值的类型和格式有严格的规定。
- 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
- 原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和
null
(不能使用NaN
,Infinity
,-Infinity
和undefined
)。- 字符串必须使用双引号表示,不能使用单引号。
- 对象的键名必须放在双引号里面。
- 数组或对象最后一个成员的后面,不能加逗号。
以下都是合法的 JSON。
1 | ["one", "two", "three"] |
以下都是不合法的 JSON。
1 | { name: "张三", 'age': 32 } // 属性名必须使用双引号 |
注意,null
、空数组和空对象都是合法的 JSON 值。
JSON 对象
JSON
对象是 JavaScript 的原生对象,用来处理 JSON 格式数据。它有两个静态方法:JSON.stringify()
和 JSON.parse()
。
JSON.stringify()
基本用法
JSON.stringify()
方法用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式,并且可以被 JSON.parse()
方法还原。
1 | JSON.stringify('abc') // ""abc"" |
上面代码将各种类型的值,转成 JSON 字符串。
注意,对于原始类型的字符串,转换结果会带双引号。
1 | JSON.stringify('foo') === "foo" // false |
上面代码中,字符串 foo
,被转成了 "\"foo\""
。这是因为将来还原的时候,内层双引号可以让 JavaScript 引擎知道,这是一个字符串,而不是其他类型的值。
1 | JSON.stringify(false) // "false" |
上面代码中,如果不是内层的双引号,将来还原的时候,引擎就无法知道原始值是布尔值还是字符串。
如果对象的属性是 undefined
、函数或 XML 对象,该属性会被 JSON.stringify()
过滤。
1 | var obj = { |
上面代码中,对象 obj
的 a
属性是 undefined
,而 b
属性是一个函数,结果都被 JSON.stringify
过滤。
如果数组的成员是 undefined
、函数或 XML 对象,则这些值被转成 null
。
1 | var arr = [undefined, function () {}]; |
上面代码中,数组 arr
的成员是 undefined
和函数,它们都被转成了 null
。
正则对象会被转成空对象。
1 | JSON.stringify(/foo/) // "{}" |
JSON.stringify()
方法会忽略对象的不可遍历的属性。
1 | var obj = {}; |
上面代码中,bar
是 obj
对象的不可遍历属性,JSON.stringify
方法会忽略这个属性。
第二个参数
JSON.stringify()
方法还可以接受一个数组,作为第二个参数,指定参数对象的哪些属性需要转成字符串。
1 | var obj = { |
上面代码中,JSON.stringify()
方法的第二个参数指定,只转 prop1
和 prop2
两个属性。
这个类似白名单的数组,只对对象的属性有效,对数组无效。
1 | JSON.stringify(['a', 'b'], ['0']) |
上面代码中,第二个参数指定 JSON 格式只转 0
号属性,实际上对数组是无效的,只对对象有效。
第二个参数还可以是一个函数,用来更改 JSON.stringify()
的返回值。
1 | function f(key, value) { |
上面代码中的 f
函数,接受两个参数,分别是被转换的对象的键名和键值。如果键值是数值,就将它乘以 2
,否则就原样返回。
注意,这个处理函数是递归处理所有的键。
1 | var obj = {a: {b: 1}}; |
上面代码中,对象 obj
一共会被 f
函数处理三次,输出的最后那行是 JSON.stringify()
的默认输出。第一次键名为空,键值是整个对象 obj
;第二次键名为 a
,键值是 {b: 1}
;第三次键名为 b
,键值为1。
递归处理中,每一次处理的对象,都是前一次返回的值。
1 | var obj = {a: 1}; |
上面代码中,f
函数修改了对象 obj
,接着 JSON.stringify()
方法就递归处理修改后的对象 obj
。
如果处理函数返回 undefined
或没有返回值,则该属性会被忽略。
1 | function f(key, value) { |
上面代码中,a
属性经过处理后,返回 undefined
,于是该属性被忽略了。
第三个参数
JSON.stringify()
还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性。
默认返回的是单行字符串,对于大型的 JSON 对象,可读性非常差。第三个参数使得每个属性单独占据一行,并且将每个属性前面添加指定的前缀(不超过10个字符)。
1 | // 默认输出 |
上面例子中,第三个属性 \t
在每个属性前面添加一个制表符,然后分行显示。
第三个属性如果是一个数字,则表示每个属性前面添加的空格(最多不超过10个)。
1 | JSON.stringify({ p1: 1, p2: 2 }, null, 2); |
参数对象的 toJSON() 方法
如果参数对象有自定义的 toJSON()
方法,那么 JSON.stringify()
会使用这个方法的返回值作为参数,而忽略原对象的其他属性。
下面是一个普通的对象。
1 | var user = { |
现在,为这个对象加上 toJSON()
方法。
1 | var user = { |
上面代码中,JSON.stringify()
发现参数对象有 toJSON()
方法,就直接使用这个方法的返回值作为参数,而忽略原对象的其他参数。
Date
对象就有一个自己的 toJSON()
方法。
1 | var date = new Date('2015-01-01'); |
上面代码中,JSON.stringify()
发现处理的是 Date
对象实例,就会调用这个实例对象的 toJSON()
方法,将该方法的返回值作为参数。
toJSON()
方法的一个应用是,将正则对象自动转为字符串。因为 JSON.stringify()
默认不能转换正则对象,但是设置了 toJSON()
方法以后,就可以转换正则对象了。
1 | var obj = { |
上面代码在正则对象的原型上面部署了 toJSON()
方法,将其指向 toString()
方法,因此转换成 JSON 格式时,正则对象就先调用 toJSON()
方法转为字符串,然后再被 JSON.stringify()
方法处理。
JSON.parse()
JSON.parse()
方法用于将 JSON 字符串转换成对应的值。
1 | JSON.parse('{}') // {} |
如果传入的字符串不是有效的 JSON 格式,JSON.parse()
方法将报错。
1 | JSON.parse("'String'") // illegal single quotes |
上面代码中,双引号字符串中是一个单引号字符串,因为单引号字符串不符合 JSON 格式,所以报错。
为了处理解析错误,可以将 JSON.parse()
方法放在 try...catch
代码块中。
1 | try { |
JSON.parse()
方法可以接受一个处理函数,作为第二个参数,用法与 JSON.stringify()
方法类似。
1 | function f(key, value) { |
上面代码中,JSON.parse()
的第二个参数是一个函数,如果键名是 a
,该函数会将键值加上10。