内置对象
内置对象篇
一、Object 对象
1、概述
JavaScript 原生提供Object
对象(注意起首的O
是大写),本章介绍该对象原生的各种方法。
JavaScript 的所有其他对象都继承自Object
对象,即那些对象都是Object
的实例。
Object
对象的原生方法分成两类:Object
本身的方法与Object
的实例方法。
(1)Object 对象本身的方法
所谓“本身的方法”就是直接定义在Object
对象的方法。
Object.print = function (o) {
console.log(o)
}
上面代码中,print
方法就是直接定义在Object
对象上。
(2)Object 的实例方法
所谓实例方法就是定义在Object
原型对象Object.prototype
上的方法。它可以被Object
实例直接使用。
Object.prototype.print = function () {
console.log(this)
}
var obj = new Object()
obj.print() // Object
上面代码中,Object.prototype
定义了一个print
方法,然后生成一个Object
的实例obj
。obj
直接继承了Object.prototype
的属性和方法,可以直接使用obj.print
调用print
方法。也就是说,obj
对象的print
方法实质上就是调用Object.prototype.print
方法。
关于原型对象object.prototype
的详细解释,参见《面向对象编程》章节。这里只要知道,凡是定义在Object.prototype
对象上面的属性和方法,将被所有实例对象共享就可以了。
以下先介绍Object
作为函数的用法,然后再介绍Object
对象的原生方法,分成对象自身的方法(又称为“静态方法”)和实例方法两部分。
2、Object() 当作工具方法(不通过 new 创建)
Object()
本身是一个函数,可以当作工具方法使用,将任意值转为对象。这个方法常用于保证某个值一定是对象。
如果参数为空(或者为undefined
和null
),Object()
返回一个空对象。
var obj = Object() // obj => {}
// 等同于
obj = Object(undefined)
obj = Object(null)
obj instanceof Object // true
上面代码的含义,是将undefined
和null
转为对象,结果得到了一个空对象obj
。
instanceof
运算符用来验证,一个对象是否为指定的构造函数的实例。obj instanceof Object
返回true
,就表示obj
对象是Object
的实例。
如果参数是原始类型的值,Object
方法将其转为对应的包装对象的实例(参见《原始类型的包装对象》一章)。
// 包装对象实例
var obj = Object(1) // obj => Number{1}
obj instanceof Object // true in s tan ce of 判断对象类型
obj instanceof Number // true
var obj = Object('foo') // obj => String{‘foo’}
obj instanceof Object // true
obj instanceof String // true
var obj = Object(true) // obj => Boolean{‘foo’}
obj instanceof Object // true
obj instanceof Boolean // true
上面代码中,Object
函数的参数是各种原始类型的值,转换成对象就是原始类型值对应的包装对象。
如果Object
方法的参数是一个对象,它总是返回该对象,即不用转换。
var arr = []
var obj = Object(arr) // obj => []
obj === arr // true
var value = {}
var obj = Object(value) // obj => {}
obj === value // true
var fn = function () {}
var obj = Object(fn) // obj => function(){}
obj === fn // true
利用这一点,可以写一个判断变量是否为对象的函数。
function isObject(value){
return value === Object(value)
}
isObject([]) // true
isObject({}) // true
isObject('123') // false
3、Object 构造函数(通过 new 创建)
Object
构造函数的首要用途,是直接通过它来生成新对象。
var obj = new Object() // 等同于 var obj = {}
注意,通过
var obj = new Object()
的写法生成新对象,与字面量的写法var obj = {}
是等价的。或者说,后者只是前者的一种简便写法。
Object
构造函数的用法与工具方法很相似,几乎一模一样。使用时,可以接受一个参数,如果该参数是一个对象,则直接返回这个对象;如果是一个原始类型的值,则返回该值对应的包装对象(详见《包装对象》一章)。
var o1 = {a: 1};
var o2 = new Object(o1);
o1 === o2 // true
var obj = new Object(123);
obj instanceof Number // true
虽然用法相似,但是Object(value)
与new Object(value)
两者的语义是不同的,Object(value)
表示将value
转成一个对象,new Object(value)
则表示新生成一个对象,它的值是value
。
4、Object 的静态方法
所谓“静态方法”,是指部署在Object
对象自身的方法。
Object.keys()
Object.keys()
方法和Object.getOwnPropertyNames()
方法都用来遍历对象的属性。
Object.keys()
方法的参数是一个对象,返回一个数组。该数组的成员都是该对象自身的(而不是继承的)所有属性名。
var obj = {
p1: 123,
p2: 456
}
Object.keys(obj) // ['p1','p2'] 注意是属性名
Object.getOwnPropertyNames()
Object.getOwnPropertyNames
方法与Object.keys
类似,也是接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名。
var obj = {
p1: 123,
p2: 456
}
Object.getOwnPropertyNames(obj) // ['p1','p2']
// getOwnPropertyNames 中文含义:获取自己的属性名
对于一般的对象来说,Object.keys()
和Object.getOwnPropertyNames()
返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。Object.keys
方法只返回可枚举的属性(详见《对象属性的描述对象》一章),Object.getOwnPropertyNames
方法还返回不可枚举的属性名。
var a = ['hello', 'world']
Object.keys(a) // ['0',‘1’] 如果对象是数组时返回索引字符串
Object.getOwnPropertyNames(a) // [‘0’,‘1’,'length']
上面代码中,数组的length
属性是不可枚举的属性,所以只出现在Object.getOwnPropertyNames
方法的返回结果中。
由于 JavaScript 没有提供计算对象属性个数的方法,所以可以用这两个方法代替。
获取对象属性个数长度
var obj = {
p1: 123,
p2: 456
};
Object.keys(obj).length // 2
Object.getOwnPropertyNames(obj).length // 2
一般情况下,几乎总是使用Object.keys
方法,遍历对象的属性。
Object 的其他静态方法
除了上面提到的两个方法,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
对象。
、
有部分方法为列入此文档,更多方法详情:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
5、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
方法的作用是返回一个对象的“值”,默认情况下返回对象本身。
var obj = new Object()
obj.valueof() === obj //true
上面代码比较obj.valueOf()
与obj
本身,两者是一样的。
valueOf
方法的主要用途是,JavaScript 自动类型转换时会默认调用这个方法(详见《数据类型转换》一章)。
var obj = Object()
1 + obj //"1[object Object]"
上面代码将对象obj
与数字1
相加,这时 JavaScript 就会自动调用valueOf()
方法,求出obj
的值再与1
相加。所以,如果自定义valueOf
方法,就可以得到想要的结果。
var obj = new Object()
obj.valueof = function () {
return 2
}
1 + obj // 3
上面代码自定义了obj
对象的valueOf
方法,于是1 + obj
就得到了3
。这种方法就相当于用自定义的obj.valueOf
,覆盖Object.prototype.valueOf
。
Object.prototype.toString()
toString
方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串。
var o1 = new Object()
o1.toString() // "[object Object]"
var o2 = { a: 2 }
o2.toString() // "[object Object]"
上面代码表示,对于一个对象调用toString
方法,会返回字符串[object Object]
,该字符串说明对象的类型。
字符串[object Object]
本身没有太大的用处,但是通过自定义toString
方法,可以让对象在自动类型转换时,得到想要的字符串形式。
var obj = new Object()
obj.toString = function () {
return 'hello'
}
obj + ' ' + 'world' // "hello world"
上面代码表示,当对象用于字符串加法时,会自动调用toString
方法。由于自定义了toString
方法,所以返回字符串hello world
。
数组、字符串、函数、Date 对象都分别部署了自定义的toString
方法,覆盖了Object.prototype.toString
方法。
;[1, 2, 3].toString() // "1,2,3"
'123'
.toString()(
// "123"
function () {
return 123
}
)
.toString()(
// "function () {
// return 123;
// }"
new Date()
)
.toString()
// "Tue May 10 2016 09:11:31 GMT+0800 (CST)"
上面代码中,数组、字符串、函数、Date 对象调用toString
方法,并不会返回[object Object]
,因为它们都自定义了toString
方法,覆盖原始方法。
toString() 的应用:判断数据类型
Object.prototype.toString
方法返回对象的类型字符串,因此可以用来判断一个值的类型。
var obj = {};
obj.toString() // "[object Object]"
上面代码调用空对象的toString
方法,结果返回一个字符串object Object
,其中第二个Object
表示该值的构造函数。这是一个十分有用的判断数据类型的方法。
由于实例对象可能会自定义toString
方法,覆盖掉Object.prototype.toString
方法,所以为了得到类型字符串,最好直接使用Object.prototype.toString
方法。通过函数的call
方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型。
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
可以看出一个值到底是什么类型。
Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"
利用这个特性,可以写出一个比typeof
运算符更准确的类型判断函数。
var type = function (o) {
var s = Object.prototype.toString.call(o)
return s.match(/\[object (.*?)\]/)[1].toLowerCase()
}
type({}) // "object"
type([]) // "array"
type(5) // "number"
type(null) // "null"
type() // "undefined"
type(/abcd/) // "regex"
type(new Date()) // "date"
在上面这个type
函数的基础上,还可以加上专门判断某种类型数据的方法。
var type = function (o) {
var s = Object.prototype.toString.call(o)
return s.match(/\[object (.*?)\]/)[1].toLowerCase()
}
;[
'Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase()
}
})
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true
Object.prototype.toLocaleString()
Object.prototype.toLocaleString
方法与toString
的返回结果相同,也是返回一个值的字符串形式。
var obj = {}
obj.toString(obj) // "[object Object]"
obj.toLocaleString(obj) // "[object Object]"
这个方法的主要作用是留出一个接口,让各种不同的对象实现自己版本的toLocaleString
,用来返回针对某些地域的特定的值。
var person = {
toString: function () {
return 'Henry Norman Bethune'
},
toLocaleString: function () {
return '白求恩'
}
}
person.toString() // Henry Norman Bethune
person.toLocaleString() // 白求恩
上面代码中,toString()
方法返回对象的一般字符串形式,toLocaleString()
方法返回本地的字符串形式。
目前,主要有三个对象自定义了toLocaleString
方法。
- Array.prototype.toLocaleString()
- Number.prototype.toLocaleString()
- Date.prototype.toLocaleString()
举例来说,日期的实例对象的toString
和toLocaleString
返回值就不一样,而且toLocaleString
的返回值跟用户设定的所在地域相关。
var date = new Date()
date.toString() // "Sun Oct 06 2019 15:50:04 GMT+0800 (中国标准时间)"
date.toLocaleString() // "2019/10/6 下午3:50:04"
Object.prototype.hasOwnProperty()
Object.prototype.hasOwnProperty
方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性。
检查对象是否包含有某个属性,返回布尔值
var obj = {
p: 123
};
obj.hasOwnProperty('p') // true
obj.hasOwnProperty('toString') // false
上面代码中,对象obj
自身具有p
属性,所以返回true
。toString
属性是继承的,所以返回false
。
二、属性描述对象
1、概述
JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”(attributes object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。
下面是属性描述对象的一个例子。
{
value: 123, // 该属性的属性值,默认undefined
writable: false, // 属性值是否可改变,默认为true
enumerable: true, // 该属性是否可遍历,默认为true
configurable: false, //属性描述对象的可配置性,默认为true
get: undefined, // 该属性的取值函数(getter),默认为undefined
set: undefined // 该属性的存值函数(setter),默认为undefined
}
属性描述对象提供 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
。
2、Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor()
方法可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名。
var obj = { p: 'a' }
Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a", // 属性值'a'
// writable: true, // 属性值'a' 可改写
// enumerable: true, // 'p'属性可被遍历
// configurable: true // 属性描述对象的可配置性
// }
// getOwnPropertyDescriptor 中文含义:获取自己的属性描述符
注意,Object.getOwnPropertyDescriptor()
方法只能用于对象自身的属性,不能用于继承的属性。
var obj = { p: 'a' }
Object.getOwnPropertyDescriptor(obj, 'toString')
// undefined
上面代码中,toString
是obj
对象继承的属性,Object.getOwnPropertyDescriptor()
无法获取。
3、Object.getOwnPropertyNames()
Object.getOwnPropertyNames
方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。
返回对象的全部属性名
var obj = Object.defineProperties(
{},
{
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false }
}
)
Object.getOwnPropertyNames(obj)
// ["p1", "p2"]
上面代码中,obj.p1
是可遍历的,obj.p2
是不可遍历的。Object.getOwnPropertyNames
会将它们都返回。
这跟Object.keys
的行为不同,Object.keys
只返回对象自身的可遍历属性的全部属性名。
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype) /*[
"constructor", // 构造函数
"__defineGetter__",
"__defineSetter__",
"hasOwnProperty", // 检测属性名
"__lookupGetter__",
"__lookupSetter__",
"isPrototypeOf",
"propertyIsEnumerable",
"toString", // 返回类型字符串
"valueOf",
"__proto__", // 原型链
"toLocaleString"
]
*/
上面代码中,数组自身的length
属性是不可遍历的,Object.keys
不会返回该属性。第二个例子的Object.prototype
也是一个对象,所有实例对象都会继承它,它自身的属性都是不可遍历的。
4、Object.defineProperty(),Object.defineProperties()
Object.defineProperty()
方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下。
Object.defineProperty(object, propertyName, attributesObject)
Object.defineProperty
方法接受三个参数,依次如下。
- object:属性所在的对象
- propertyName:字符串,表示属性名
- attributesObject:属性描述对象
举例来说,定义obj.p
可以写成下面这样。
var obj = Object.defineProperty({}, 'p', {
value: 123,
writable: false, // 设置为false,无法被改写
enumerable: true, // 可遍历
configurable: false // 属性描述不可再被配置
})
obj.p // 123
obj.p = 246 // 由于writable为false,改写无效
obj.p // 123
上面代码中,Object.defineProperty()
方法定义了obj.p
属性。由于属性描述对象的writable
属性为false
,所以obj.p
属性不可写。注意,这里的Object.defineProperty
方法的第一个参数是{}
(一个新建的空对象),p
属性直接定义在这个空对象上面,然后返回这个对象,这是Object.defineProperty()
的常见用法。
如果属性已经存在,Object.defineProperty()
方法相当于更新该属性的属性描述对象。
如果一次性定义或修改多个属性,可以使用Object.defineProperties()
方法。
var obj = Object.defineProperties(
{},
{
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: {
get: function () {
return this.p1 + this.p2
},
enumerable: true,
configurable: true
}
}
)
obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"
上面代码中,Object.defineProperties()
同时定义了obj
对象的三个属性。其中,p3
属性定义了取值函数get
,即每次读取该属性,都会调用这个取值函数。
注意,一旦定义了取值函数get
(或存值函数set
),就不能将writable
属性设为true
,或者同时定义value
属性,否则会报错。
var obj = {}
Object.defineProperty(obj, 'p', {
value: 123,
get: function () {
return 456
}
})
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value
Object.defineProperty(obj, 'p', {
writable: true,
get: function () {
return 456
}
})
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute
上面代码中,同时定义了get
属性和value
属性,以及将writable
属性设为true
,就会报错。
Object.defineProperty()
和Object.defineProperties()
参数里面的属性描述对象,writable
、configurable
、enumerable
这三个属性的默认值都为false
。
var obj = {}
Object.defineProperty(obj, 'foo', {})
Object.getOwnPropertyDescriptor(obj, 'foo')
// { // 默认值
// value: undefined,
// writable: false,
// enumerable: false,
// configurable: false
// }
上面代码中,定义obj.foo
时用了一个空的属性描述对象,就可以看到各个元属性的默认值。
5、Object.prototype.propertyIsEnumerable()
实例对象的propertyIsEnumerable()
方法返回一个布尔值,用来判断某个属性是否可遍历。注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回false
。
var obj = {}
obj.p = 123
obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false
上面代码中,obj.p
是可遍历的,而obj.toString
是继承的属性。
6、元属性
属性描述对象的各个属性称为“元属性”,因为它们可以看作是控制属性的属性。
value 目标属性的值
value
属性是目标属性的值。
var obj = {}
obj.p = 123
Object.getOwnPropertyDescriptor(obj, 'p').value
// 123
Object.defineProperty(obj, 'p', { value: 246 })
obj.p // 246
上面代码是通过value
属性,读取或改写obj.p
的例子。
writable 可写性
writable
属性是一个布尔值,决定了目标属性的值(value)是否可以被改变。
var obj = {}
Object.defineProperty(obj, 'a', {
value: 37,
writable: false
})
obj.a // 37
obj.a = 25 // 可写性为false,值无法被修改
obj.a // 37
上面代码中,obj.a
的writable
属性是false
。然后,改变obj.a
的值,不会有任何效果。
注意,正常模式下,对writable
为false
的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对a
属性重新赋予一个同样的值。
'use strict';
var obj = {};
Object.defineProperty(obj, 'a', {
value: 37,
writable: false
});
obj.a = 37;
// Uncaught TypeError: Cannot assign to read only property 'a' of object
上面代码是严格模式,对obj.a
任何赋值行为都会报错。
如果原型对象的某个属性的writable
为false
,那么子对象将无法自定义这个属性。
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
})
var obj = Object.create(proto) // obj对象继承proto对象
obj.foo = 'b'
obj.foo // 'a'
上面代码中,proto
是原型对象,它的foo
属性不可写。obj
对象继承proto
,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。
但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视。
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
})
var obj = Object.create(proto)
Object.defineProperty(obj, 'foo', {
value: 'b'
})
obj.foo // "b"
enumerable 可遍历性
enumerable
(可遍历性)返回一个布尔值,表示目标属性是否可遍历。
JavaScript 的早期版本,for...in
循环是基于in
运算符的。我们知道,in
运算符不管某个属性是对象自身的还是继承的,都会返回true
。
var obj = {}
'toString' in obj // true
上面代码中,toString
不是obj
对象自身的属性,但是in
运算符也返回true
,这导致了toString
属性也会被for...in
循环遍历。
这显然不太合理,后来就引入了“可遍历性”这个概念。只有可遍历的属性,才会被for...in
循环遍历,同时还规定toString
这一类实例对象继承的原生属性,都是不可遍历的,这样就保证了for...in
循环的可用性。
具体来说,如果一个属性的enumerable
为false
,下面三个操作不会取到该属性。
for..in
循环Object.keys
方法JSON.stringify
方法
因此,enumerable
可以用来设置“秘密”属性。
var obj = {}
Object.defineProperty(obj, 'x', {
value: 123,
enumerable: false
})
obj.x // 123
for (var key in obj) {
console.log(key)
}
// undefined
Object.keys(obj) // []
JSON.stringify(obj) // "{}"
上面代码中,obj.x
属性的enumerable
为false
,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但不是真正的私有属性,还是可以直接获取它的值。
注意,for...in
循环包括继承的属性,Object.keys
方法不包括继承的属性。如果需要获取对象自身的所有属性,不管是否可遍历,可以使用Object.getOwnPropertyNames
方法。
另外,JSON.stringify
方法会排除enumerable
为false
的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的enumerable
设为false
。
configurable 可配置性
configurable
(可配置性)返回一个布尔值,决定了是否可以修改属性描述对象。也就是说,configurable
为false
时,value
、writable
、enumerable
和configurable
都不能被修改了。
var obj = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
enumerable: false,
configurable: false
})
Object.defineProperty(obj, 'p', { value: 2 })
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', { writable: true })
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', { enumerable: true })
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', { configurable: true })
// TypeError: Cannot redefine property: p
上面代码中,obj.p
的configurable
为false
。然后,改动value
、writable
、enumerable
、configurable
,结果都报错。
注意,writable
只有在false
改为true
会报错,true
改为false
是允许的。
var obj = Object.defineProperty({}, 'p', {
writable: true,
configurable: false
})
Object.defineProperty(obj, 'p', { writable: false })
// 修改成功
至于value
,只要writable
和configurable
有一个为true
,就允许改动。
var o1 = Object.defineProperty({}, 'p', {
value: 1,
writable: true,
configurable: false
})
Object.defineProperty(o1, 'p', { value: 2 })
// 修改成功
var o2 = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
configurable: true
})
Object.defineProperty(o2, 'p', { value: 2 })
// 修改成功
另外,writable
为false
时,直接给目标属性赋值,不报错,但不会成功。
var obj = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
configurable: false
})
obj.p = 2
obj.p // 1
上面代码中,obj.p
的writable
为false
,对obj.p
直接赋值不会生效。如果是严格模式,还会报错。
可配置性决定了目标属性是否可以被删除(delete)。
var obj = Object.defineProperties(
{},
{
p1: { value: 1, configurable: true },
p2: { value: 2, configurable: false }
}
)
delete obj.p1 // true
delete obj.p2 // false
obj.p1 // undefined
obj.p2 // 2
上面代码中,obj.p1
的configurable
是true
,所以可以被删除,obj.p2
就无法删除。
7、存取器(get 和 set)
除了直接定义以外,属性还可以用存取器(accessor)定义。其中,存值函数称为setter
,使用属性描述对象的set
属性;取值函数称为getter
,使用属性描述对象的get
属性。
一旦对目标属性定义了存取器,那么存取的时候,都将执行对应的函数。利用这个功能,可以实现许多高级特性,比如某个属性禁止赋值。
var obj = Object.defineProperty({}, 'p', {
get: function () {
// 获取属性时执行
return 'getter' // 这里相当于定义了p属性的值,如果设置了get则不能再设置value
},
set: function (value) {
// 设置属性时执行
console.log('setter: ' + value)
}
})
obj.p // "getter"
obj.p = 123 // "setter: 123"
上面代码中,obj.p
定义了get
和set
属性。obj.p
取值时,就会调用get
;赋值时,就会调用set
。
JavaScript 还提供了存取器的另一种写法。
var obj = {
get p(){ // 相当于为obj对象创建一个p属性,并且赋值为‘getter’
return 'getter'
},
ser p(value){
console.log('setter'+value)
}
}
上面的写法与定义属性描述对象是等价的,而且使用更广泛。
注意,取值函数get
不能接受参数,存值函数set
只能接受一个参数(即属性的值)。
存取器往往用于,属性的值依赖对象内部数据的场合。
var obj = {
$n: 5,
get next() {
return this.$n++
},
set next(n) {
if (n >= this.$n) this.$n = n
else throw new Error('新的值必须大于当前值')
}
}
obj.next // 5
obj.next = 10
obj.next // 10
obj.next = 5
// Uncaught Error: 新的值必须大于当前值
上面代码中,next
属性的存值函数和取值函数,都依赖于内部属性$n
。
vue 框架就是通过存取器来实现的数据响应式
8、对象的拷贝
有时,我们需要将一个对象的所有属性,拷贝到另一个对象,可以用下面的方法实现。
var extend = function (to, from) {
for (var property in from) {
to[property] = from[property]
}
return to
}
extend({}, { a: 1 }) // {a:1}
上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝到属性和值,并不能拷贝到存取器
extend({}, {
get a() { return 1 }
})
// {a: 1} // 这里并没有拷贝到get方法
为了解决这个问题,我们可以通过Object.defineProperty
方法来拷贝属性。
var extend = function (to, from) {
for (var property in from) {
if (!from.hasOwnProperty()) continue //过滤继承属性 continue 终止本次循环,继续剩余循环
Object.defineProperty(
to,
property,
Object.getOwnPropertyDescriptor(from, property)
)
}
return to
}
extend(
{},
{
get a() {
return 1
}
}
) // {get a(){return 1}}
上面代码中,hasOwnProperty
那一行用来过滤掉继承的属性,否则可能会报错,因为Object.getOwnPropertyDescriptor
读不到继承属性的属性描述对象。
9、控制对象读写状态
有时需要冻结对象的读写状态,防止对象被改变。
JavaScript 提供了三种冻结方法:
最弱的一种是
Object.preventExtensions
(不能新增,但能被删除和修改)其次是
Object.seal
(不能新增和删除,但能修改)最强的是
Object.freeze
(不能新增、删除和修改)
Object.preventExtensions() 防止新增
var obj = new Object()
Object.preventExtensions(obj) // prevent Extensions 中文: 防止扩展
Object.defineProperty(obj, 'p', {
value: 'hello'
}) // TypeError:cannot define property:p, object is not extensible
obj.p = 1
obj.p // undefined
上面代码中,obj
对象经过Object.preventExtensions
以后,就无法添加新属性了。
Object.isExtensible() 是可新增的?
Object.isExtensible
方法用于检查一个对象是否使用了Object.preventExtensions
方法。也就是说,检查是否可以为一个对象添加属性。
var obj = new Object()
Object.isExtensible(obj) // true is Extensible 中文: 是可扩展的?
Object.preventExtensions(obj)
Object.isExtensible(obj) // false
上面代码中,对obj
对象使用Object.preventExtensions
方法以后,再使用Object.isExtensible
方法,返回false
,表示已经不能添加新属性了。
Object.seal() 密封
Object.seal
方法使得一个对象既无法添加新属性,也无法删除旧属性。
var obj = { p: 'hello' }
Object.seal(obj) // seal 中文: 密封
delete obj.p // 删除属性无效
obj.p // hello
obj.p = 'hi' // 可修改值
obj.p // hi
obj.x = 'world' // 新增属性无效
obj.x // undefined
上面代码中,obj
对象执行Object.seal
方法以后,就无法添加新属性和删除旧属性了。
Object.seal
实质是把属性描述对象的configurable
属性设为false
,因此属性描述对象不再能改变了。
var obj = {p: 'a'}
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.seal(obj)
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
// value: "a",
// writable: true,
// enumerable: true,
// configurable: false
// }
Object.defineProperty(obj,'p',{
enumerable:
})
// TypeError: Cannot redefine property: p
上面代码中,使用Object.seal
方法之后,属性描述对象的configurable
属性就变成了false
,然后改变enumerable
属性就会报错。
Object.seal
只是禁止新增或删除属性,并不影响修改某个属性的值。
var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p // 'b'
上面代码中,Object.seal
方法对p
属性的value
无效,是因为此时p
属性的可写性由writable
决定。
Object.isSealed() 是密封的?
Object.isSealed
方法用于检查一个对象是否使用了Object.seal
方法。
var obj = { p: 'a' }
Object.seal(obj)
Object.isSealed(obj) // true is Sealed 中文: 是密封的?
这时,Object.isExtensible
方法也返回false
。
var obj = { p: 'a' }
Object.seal(obj)
Object.isExtensible(obj) // false
isSealed 为 true,是密封的。isExtensible 为 false,不是可扩展的
Object.freeze() 冻结
Object.freeze
方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。
var obj = { p: 'hello' }
Object.freeze(obj)
obj.p = 'world'
obj.p // hello
obj.t = 'hello'
obj.t // nudefined
delete obj.p // false
obj.p // hello
上面代码中,对obj
对象进行Object.freeze()
以后,修改属性、新增属性、删除属性都无效了。这些操作并不报错,只是默默地失败。如果在严格模式下,则会报错。
Object.isFrozen() 是冻结的?
Object.isFrozen
方法用于检查一个对象是否使用了Object.freeze
方法。
var obj = {
p: 'hello'
}
Object.freeze(obj)
Object.isFrozen(obj) // true // 是冻结的
使用Object.freeze
方法以后,Object.isSealed
将会返回true
,Object.isExtensible
返回false
。
var obj = {
p: 'hello'
}
Object.freeze(obj)
Object.isSealed(obj) // true // 是密封的
Object.isExtensible(obj) // false // 不是可扩展的
Object.isFrozen
的一个用途是,确认某个对象没有被冻结后,再对它的属性赋值。
var obj = {
p: 'hello'
}
Object.freeze(obj)
if (!Object.isFrozen(obj)) {
obj.p = 'world'
}
上面代码中,确认obj
没有被冻结后,再对它的属性赋值,就不会报错了。
局限性
上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。
var obj = new Object()
Object.preventExtensions(obj)
var proto = Object.getPrototypeOf(obj) // 获取原型对象
proto.t = 'hello'
obj.t
// hello
上面代码中,对象obj
本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在obj
上读到。
一种解决方案是,把obj
的原型也冻结住。
var obj = new Object()
Object.preventExtensions(obj)
var proto = Object.getPrototypeOf(obj)
Object.preventExtensions(proto)
proto.t = 'hello'
obj.t // undefined
另外一个局限是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。
var obj = {
foo: 1,
bar: ['a', 'b']
}
Object.freeze(obj)
obj.bar.push('c')
obj.bar // ["a", "b", "c"]
上面代码中,obj.bar
属性指向一个数组,obj
对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的。
三、Array 对象
1、Array 构造函数
Array
是 JavaScript 的原生对象,同时也是一个构造函数,可以用它生成新的数组。
var arr = new Array(2) // Array是原生对象,也是构造函数; 2表示生成2个成员
arr.length // 2
arr // [ empty x 2 ]
上面代码中,Array
构造函数的参数2
,表示生成一个两个成员的数组,每个位置都是空值。
如果没有使用new
,运行结果也是一样的。
var arr = new Array(2)
// 等同于
var arr = Array(2)
Array
构造函数有一个很大的缺陷,就是不同的参数,会导致它的行为不一致。
// 无参数时,返回一个空数组
new Array() // []
// 单个正整数参数,表示返回的新数组的长度
new Array(1) // [ empty ]
new Array(2) // [ empty x 2 ]
// 非正整数的数值作为参数,会报错
new Array(3.2) // RangeError: Invalid array length
new Array(-3) // RangeError: Invalid array length
// 单个非数值(比如字符串、布尔值、对象等)作为参数,
// 则该参数是返回的新数组的成员
new Array('abc') // ['abc']
new Array([1]) // [Array[1]]
// 多参数时,所有参数都是返回的新数组的成员
new Array(1, 2) // [1, 2]
new Array('a', 'b', 'c') // ['a', 'b', 'c']
可以看到,Array
作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组字面量是更好的做法。
// bad
var arr = new Array(1, 2)
// good
var arr = [1, 2]
注意,如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回undefined
,但实际上该位置没有任何值。虽然可以取到length
属性,但是取不到键名。
var a = new Array(3)
var b = [undefined, undefined, undefined]
a.length // 3
b.length // 3
a[0] // undefined
b[0] // undefined
0 in a // false // in运算符用于数组时,前面的参数为数组索引
0 in b // true
上面代码中,a
是一个长度为 3 的空数组,b
是一个三个成员都是undefined
的数组。读取键值的时候,a
和b
都返回undefined
,但是a
的键位都是空的,b
的键位是有值的。
2、静态方法
Array.isArray()
Array.isArray
方法返回一个布尔值,表示参数是否为数组。它可以弥补typeof
运算符的不足。
var arr = [1, 2, 3]
typeof arr // object // 使用typeof返回的是object
Array.isArray(arr) // true
上面代码中,typeof
运算符只能显示数组的类型是Object
,而Array.isArray
方法可以识别数组。
3、实例方法
valueOf() 原数组,toString() 转字符串
valueOf
方法是一个所有对象都拥有的方法,表示对该对象求值。不同对象的valueOf
方法不尽一致,数组的valueOf
方法返回数组本身。
var arr = [1, 2, 3]
arr.valueOf() // [1,2,3]
toString
方法也是对象的通用方法,数组的toString
方法返回数组的字符串形式。
var arr = [1, 2, 3]
arr.toString() // "1,2,3"
var arr = [1, 2, 3, [4, 5, 6]]
arr.toString() // "1,2,3,4,5,6"
push() 末尾添加,pop() 删除末尾
push
方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
var arr = []
arr.push(1) // 1
arr.push('a') // 2
arr.push(true, {}) // 4
arr // [1,'a',true,{}]
// 注:push()方法 返回添加后的数组长度,会改变原数组
上面代码使用push
方法,往数组中添加了四个成员。
pop
方法用于删除数组的最后一个元素,并返回该元素。注意,该方法会改变原数组。
var arr = ['a', 'b', 'c']
arr.pop() // 'c' 删除最后一个元素,并返回该元素
arr // ['a','b']
对空数组使用pop
方法,不会报错,而是返回undefined
。
;[].pop() // undefined
push
和pop
结合使用,就构成了“后进先出”的栈结构(stack)。
var arr = []
arr.push(1, 2)
arr.push(3)
arr.pop()
arr // [1, 2]
上面代码中,3
是最后进入数组的,但是最早离开数组。
unshift() 首位添加, shift() 删除首位
unshift()
方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
var a = ['a', 'b', 'c']
a.unshift('x') // 4
a // ['x', 'a', 'b', 'c']
unshift()
方法可以接受多个参数,这些参数都会添加到目标数组头部。
var arr = ['c', 'd']
arr.unshift('a', 'b') // 4
arr // [ 'a', 'b', 'c', 'd' ]
shift()
方法用于删除数组的第一个元素,并返回该元素。注意,该方法会改变原数组。
var a = ['a', 'b', 'c']
a.shift() // 'a' 删除第一个元素,并返回该元素
a // ['b','c']
上面代码中,使用shift()
方法以后,原数组就变了。
shift()
方法可以遍历并清空一个数组。
var list = [1, 2, 3, 4]
var item
while ((item = list.shift())) {
console.log(item)
}
list // []
上面代码通过list.shift()
方法每次取出一个元素,从而遍历数组。它的前提是数组元素不能是0
或任何布尔值等于false
的元素,因此这样的遍历不是很可靠。
push()
和shift()
结合使用,就构成了“先进先出”的队列(堆)结构(queue)。
var queue = []
queue.push('1')
queue.push('2')
queue.push('3')
queue.shift()
queue // ['2','3']
push() 最后位置添加元素,unshift() 第一位置添加元素
pop() 删除最后一个元素,shift() 删除第一个元素
join() 连接每个成员-转字符串
join()
方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔。
数组转字符串
var a = [1, 2, 3, 4]
a.join('') // '1234'
a.join(' ') // '1 2 3 4'
a.join('|') // '1|2|3|4‘
a.join() // '1,2,3,4'
如果数组成员是undefined
或null
或空位,会被转成空字符串。
[undefined, null].join('#')
// '#'
['a',, 'b'].join('-')
// 'a--b'
通过call
方法,这个方法也可以用于字符串或类似数组的对象。
Array.prototype.join.call('hello', '-')
// "h-e-l-l-o"
// 等价 'hello'.split('').join('-')
var obj = { 0: 'a', 1: 'b', length: 2 } // 数组的变形对象
Array.prototype.join.call(obj, '-')
// 'a-b'
concat() 合并数组
concat
方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。
['hello'].concat(['world'])
// ['hello','world']
['hello'].concat(['world'],['!'])
// ["hello", "world", "!"]
[].concat({a: 1}, {b: 2})
// [{ a: 1 }, { b: 2 }]
[2].concat({a: 1})
// [2, {a: 1}]
[].concat(1,2)
// [1,2]
除了数组作为参数,concat
也接受其他类型的值作为参数,添加到目标数组尾部。
;[1, 2, 3].concat(4, 5, 6)
// [1, 2, 3, 4, 5, 6]
如果数组成员包括对象,concat
方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用。
var obj = { a: 1 }
var oldArray = [obj] // 浅拷贝
var newArray = oldArray.concat()
obj.a = 2 // 改变原对象以后,新数组跟着改变
newArray[0].a // 2
上面代码中,原数组包含一个对象,concat
方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变。
reverse() 反转
reverse
方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组。
var a = [1, 2, 3, 4]
a.reverse() // [4,3,2,1]
a // [4,3,2,1]
slice() 部分-提取部分成员
slice
方法用于提取目标数组的一部分,返回一个新数组,原数组不变。
arr.slice(start, end)
它的第一个参数为起始位置(从 0 开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。
var a = ['a', 'b', 'c']
a.slice(0) // ['a','b','c'] 从0到最后一个成员
a.slice(1) // ['b','c'] 从1到最后一个成员
a.slice(1, 2) // ['b'] 从1到2成员,不包含2成员
a.slice(2, 6) // ['c'] 从2到6成员,超出的不计算在内
a.slice() //['a','b','c'] 无参数,默认全部,多用于拷贝数组到新的数组
上面代码中,最后一个例子slice
没有参数,实际上等于返回一个原数组的拷贝。
如果slice
方法的参数是负数,则表示倒数计算的位置。
var a = ['a', 'b', 'c']
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]
上面代码中,-2
表示倒数计算的第二个位置,-1
表示倒数计算的第一个位置。
如果第一个参数大于等于数组长度,或者第二个参数小于第一个参数,则返回空数组。
var a = ['a', 'b', 'c']
a.slice(4) // []
a.slice(2, 1) // []
应用-类数组转数组
slice`方法的一个重要应用,是将类似数组的对象转为真正的数组。
Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll('div'))
Array.prototype.slice.call(arguments)
上面代码的参数都不是数组,但是通过call
方法,在它们上面调用slice
方法,就可以把它们转为真正的数组。
splice() 从第几个开始,删除几个,并添加成员,返回被删除成员。
splice
方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。
也可以不删除成员同时在指定位置添加新成员
arr.splice(start, count, addElement1, addElement2, ...);
// 第几个开始,删除几个,加入的新成员...(非必需)
splice
的第一个参数是删除的起始位置(从 0 开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。
var a = [1, 2, 3, 4, 5, 6]
a.splice(2, 2) // [3,4] 返回被删除成员
a[(1, 2, 5, 6)]
上面代码从原数组 2 号位置,删除了 2 个数组成员。
var a = ['a', 'b', 'c', 'd', 'e', 'f']
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
上面代码从原数组 4 号位置删除 2 个成员,并插入了[1 , 2]新成员。
起始位置如果是负数,就表示从倒数位置开始删除。
var a = ['a', 'b', 'c', 'd', 'e', 'f']
a.splice(-4, 2) // ["c", "d"] 负数表示倒数
上面代码表示,从倒数第四个位置c
开始删除两个成员。
如果只是单纯地插入元素,splice
方法的第二个参数可以设为0
。
var a = [1, 1, 1]
a.splice(1, 0, 'a') // [] // 不删除成员,并加入新成员到指定位置
a // [1, 'a', 1, 1]
应用-拆分数组
如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。
var a = [1, 2, 3, 4]
var a2 = a.splice(2) // [3, 4]
a // [1, 2]
a2 // [3, 4]
sort() 排序
sort
方法对数组成员进行排序,默认是按照字典顺序排序。排序后,将改变原数组。
;['d', 'c', 'b', 'a']
.sort()
[
// ['a', 'b', 'c', 'd']
(4, 3, 2, 1)
].sort()
[
// [1, 2, 3, 4]
('abc', 'ba', 'ea', 'ae')
].sort()
[
// ["abc", "ae", "ba", "ea"]
(11, 101)
].sort()
[
// [101, 11]
(10111, 1101, 111)
].sort()
// [10111, 1101, 111]
上面代码的最后三个例子,需要特殊注意。sort()
方法不是按照大小排序,而是按照字典顺序。也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101
排在11
的前面。
先转成字符串,再从 0 位置开始对比对应位置上的值的顺序
如果想让sort
方法按照自定义方式排序,可以传入一个函数作为参数。
;[10111, 1101, 111]
.sort(function (a, b) {
// 返回值大于0,则a排于b之后,否则反之。
return a - b // 升序
})
[
// [111, 1101, 10111]
(10111, 1101, 111)
].sort(function (a, b) {
return b - a // 降序
})
// [10111,1101, 111]
上面代码中,sort
的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于0
,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面。
应用-自定义排序
;[
{ name: '张三', age: 30 },
{ name: '李四', age: 24 },
{ name: '王五', age: 28 }
]
.sort(function (o1, o2) {
return o1.age - o2.age
})
[
// [
// { name: "李四", age: 24 },
// { name: "王五", age: 28 },
// { name: "张三", age: 30 }
// ]
// 自定义排序
({ day: '星期日' }, { day: '星期一' }, { day: '星期五' })
].sort(function (o1, o2) {
return computedDay(o1.day.charAt(2)) - computedDay(o2.day.charAt(2))
}) //[{ day: '星期一' }, { day: '星期五' }, { day: '星期日' }]
function computedDay(v) {
return v === '一'
? 1
: v === '二'
? 2
: v === '三'
? 3
: v === '四'
? 4
: v === '五'
? 5
: v === '六'
? 6
: 7
}
注意,自定义的排序函数应该返回数值,否则不同的浏览器可能有不同的实现,不能保证结果都一致。
// bad
;[1, 4, 2, 6, 0, 6, 2, 6]
.sort((a, b) => a > b) // 返回布尔值,不推荐
[
// good
(1, 4, 2, 6, 0, 6, 2, 6)
].sort((a, b) => a - b) // 返回数值,推荐
上面代码中,前一种排序算法返回的是布尔值,这是不推荐使用的。后一种是数值,才是更好的写法。
map() 映射 每个成员 并返回
map
方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。不改变原数组。
var numbers = [1, 2, 3]
numbers.map(function (n) {
return n + 1
})
// [2, 3, 4]
numbers
// [1, 2, 3]
上面代码中,numbers
数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化。
map
方法接受一个函数作为参数。该函数调用时,map
方法向它传入三个参数:当前成员、当前索引、数组本身。
;[1, 2, 3].map(function (elem, index, arr) {
// 当前成员、当前索引、数组本身
return elem * index
})
// [0, 2, 6]
上面代码中,map
方法的回调函数有三个参数,elem
为当前成员的值,index
为当前成员的索引,arr
为原数组([1, 2, 3]
)。
map
方法还可以接受第二个参数,用来绑定回调函数内部的this
变量(详见《this 变量》一章)。
var arr = ['a', 'b', 'c']
;[1, 2].map(function (e) {
return this[e] // this指向arr
}, arr) // 使内部this指向arr
// ['b', 'c']
上面代码通过map
方法的第二个参数,将回调函数内部的this
对象,指向arr
数组。
如果数组有空位,map
方法的回调函数在这个位置不会执行,会跳过数组的空位。
var f = function (n) { return 'a' };
[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1, , 2].map(f) // ["a", , "a"]
上面代码中,map
方法不会跳过undefined
和null
,但是会跳过空位。
forEach() 循环每个成员
forEach
方法与map
方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach
方法不返回值,只用来操作数据。这就是说,如果数组遍历的目的是为了得到返回值,那么使用map
方法,否则使用forEach
方法。
forEach
的用法与map
方法一致,参数是一个函数,该函数同样接受三个参数:当前成员、当前索引、整个数组。
function log(element, index, array) {
// 当前成员、当前索引、数组本身
console.log('[' + index + '] = ' + element)
}
;[2, 5, 9].forEach(log)
// [0] = 2
// [1] = 5
// [2] = 9
上面代码中,forEach
遍历数组不是为了得到返回值,而是为了在屏幕输出内容,所以不必使用map
方法。
forEach
方法也可以接受第二个参数,绑定参数函数的this
变量。
var out = []
;[1, 2, 3].forEach(function (elem) {
this.push(elem * elem) // this指向为out
}, out) // 使内部this指向out
out // [1, 4, 9]
上面代码中,空数组out
是forEach
方法的第二个参数,结果,回调函数内部的this
关键字就指向out
。
注意,forEach
方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用for
循环。
var arr = [1, 2, 3]
for (var i = 0; i < arr.length; i++) {
if (arr[i] === 2) break
console.log(arr[i])
}
// 1
上面代码中,执行到数组的第二个成员时,就会中断执行。forEach
方法做不到这一点。
forEach
方法也会跳过数组的空位。
var log = function (n) {
console.log(n + 1);
};
[1, undefined, 2].forEach(log)
// 2
// NaN
// 3
[1, null, 2].forEach(log)
// 2
// 1 // null+1 => 1
// 3
[1, , 2].forEach(log)
// 2
// 3
上面代码中,forEach
方法不会跳过undefined
和null
,但会跳过空位。
filter() 过滤
filter
方法用于过滤数组成员,满足条件的成员组成一个新数组返回。
它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true
的成员组成一个新数组返回。
该方法不会改变原数组。
;[1, 2, 3, 4, 5].filter(function (elem) {
return elem > 3
})
// [4,5]
上面代码将大于3
的数组成员,作为一个新数组返回。
var arr = [0, 1, 'a', false]
arr.filter(Boolean)
// [1, "a"]
上面代码中,filter
方法返回数组arr
里面所有布尔值为true
的成员。
filter
方法的参数函数可以接受三个参数**:当前成员,当前位置、整个数组。**
;[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
// 当前成员,当前位置、整个数组
return index % 2 === 0
})
// [1, 3, 5]
上面代码返回偶数位置的成员组成的新数组。
filter
方法还可以接受第二个参数,用来绑定参数函数内部的this
变量。
var obj = { MAX: 3 }
var myFilter = function (item) {
if (item > this.MAX) return true // this指向obj
}
var arr = [2, 8, 3, 4, 1, 3, 2, 9]
arr.filter(myFilter, obj) // [8, 4, 9] // 使myFilter内部this指向obj
上面代码中,过滤器myFilter
内部有this
变量,它可以被filter
方法的第二个参数obj
绑定,返回大于3
的成员。
some()某个,every()每个
这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。
它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置、整个数组,然后返回一个布尔值。
some
方法是只要一个成员的返回值是true
,则整个some
方法的返回值就是true
,否则返回false
。
var arr = [1, 2, 3, 4, 5]
arr.some(function (elem, index, arr) {
// 返回值中有一个为true则此方法返回true
return elem >= 3
})
// true
上面代码中,如果数组arr
有一个成员大于等于 3,some
方法就返回true
。
every
方法是所有成员的返回值都是true
,整个every
方法才返回true
,否则返回false
。
var arr = [1, 2, 3, 4, 5]
arr.every(function (elem, index, arr) {
// 每个返回值都为true,此方法才返回true,否则返回false
return elem >= 3
})
// false
上面代码中,数组arr
并非所有成员大于等于3
,所以返回false
。
注意,对于空数组,some
方法返回false
,every
方法返回true
,回调函数都不会执行。
function isEven(x) { return x % 2 === 0 } // 不会执行此函数
[].some(isEven) // false
[].every(isEven) // true
some
和every
方法还可以接受第二个参数,用来绑定参数函数内部的this
变量。
var arr = [1, 2, 3]
var obj = { MAX: 0 }
function fn(elem) {
return elem > this.MAX // this指向obj
}
arr.some(fn, obj) // true
arr.every(fn, obj) // true
reduce() 累计器,reduceRight() 反向累计器
reduce
方法和reduceRight
方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce
是从左到右处理(从第一个成员到最后一个成员),reduceRight
则是从右到左(从最后一个成员到第一个成员),其他完全一样。
;[1, 2, 3, 4, 5].reduce(function (a, b, c, d) {
// 记住a是默认是第一个成员,然后是上一次运算的返回值。
console.log(a, b)
console.log(c)
return a + b
})
// 1 2
// 3 3
// 6 4
// 10 5
// 最终结果 15
上面代码中,reduce
方法求出数组所有成员的和。第一次执行,a
是数组的第一个成员1
,b
是数组的第二个成员2
。第二次执行,a
为上一轮的返回值3
,b
为第三个成员3
。第三次执行,a
为上一轮的返回值6
,b
为第四个成员4
。第四次执行,a
为上一轮返回值10
,b
为第五个成员5
。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值15
。
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
// 说明:中括号代表可选参数
reduce
方法和reduceRight
方法的第一个参数都是一个callback
函数。该函数接受以下四个参数。
- 累积变量,默认为数组的第一个成员
- 当前变量,默认为数组的第二个成员
- 当前位置(默认从 1 开始), 如果提供了
initialValue
,则起始索引号为 0,否则为 1。 (可选) - 原数组(可选)
initialValue
可选
作为第一次调用 callback函数时
的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。
如果要对累积变量指定初值,可以把它放在reduce
方法和reduceRight
方法的第二个参数。
;[1, 2, 3, 4, 5].reduce(function (a, b) {
return a + b
}, 10)
// 25
上面代码指定参数a
的初值为 10,所以数组从 10 开始累加,最终结果为 25。注意,这时b
是从数组的第一个成员开始遍历。
上面的第二个参数相当于设定了默认值,处理空数组时尤其有用。
function add(prev, cur) {
return prev + cur;
}
[].reduce(add) // 报错
// TypeError: Reduce of empty array with no initial value
[].reduce(add, 1)
// 1
上面代码中,由于空数组取不到初始值,reduce
方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。
下面是一个reduceRight
方法的例子。
function subtract(prev, cur) {
return prev - cur
}
;[3, 2, 1]
.reduce(subtract) // 0
[(3, 2, 1)].reduceRight(subtract) // -4
上面代码中,reduce
方法相当于3
减去2
再减去1
,reduceRight
方法相当于1
减去2
再减去3
。
应用-找出字符最长的数组成员
由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。
function findLongest(entries) {
return entries.reduce(function (longest, entry) {
return entry.length > longest.length ? entry : longest;
}, '');
}
findLongest(['aaa', 'bb', 'c']) // "aaa"
上面代码中,reduce
的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员。
应用-按属性对 object 分类
var people = [
{ name: 'Alice', age: 21 },
{ name: 'Max', age: 20 },
{ name: 'Jane', age: 20 }
]
function groupBy(objectArray, property) {
return objectArray.reduce(function (acc, obj) {
var key = obj[property]
if (!acc[key]) {
// 当{}中没有对应分类属性时,先创建改属性
acc[key] = []
}
acc[key].push(obj)
return acc
}, {}) // 初始值为{}
}
var groupedPeople = groupBy(people, 'age')
// groupedPeople is:
// {
// 20: [
// { name: 'Max', age: 20 },
// { name: 'Jane', age: 20 }
// ],
// 21: [{ name: 'Alice', age: 21 }]
// }
更多例子:< https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce >
indexOf() 搜索成员第一个出现的位置,lastIndexOf() 搜索成员最后出现的位置
indexOf
方法返回给定元素在数组中第一次出现的位置,如果没有则返回-1
。
indexOf 语法
arr.indexOf(searchElement[, fromIndex = 0])
var a = [1, 2, 3, 4, 5]
a.indexOf(3) // 2 3在数组a中第一次出现的位置为2
a.indexOf(6) // -1 6在数组a中没有出现,返回-1
indexOf
方法还可以接受第二个参数,表示搜索的开始位置。
;['a', 'b', 'c'].indexOf('a', 1) // -1 从1位置开始搜索a,没有搜索到
上面代码从 1 号位置开始搜索字符a
,结果为-1
,表示没有搜索到。
lastIndexOf()语法
arr.lastIndexOf(searchElement[, fromIndex = arr.length - 1])
lastIndexOf
方法返回给定元素在数组中最后一次出现的位置,如果没有则返回-1
。
var a = [2, 3, 4, 2]
a.lastIndexOf(2) //3 2在数组a中最后一次出现的位置为3
a.lastIndexOf(5) //-1
注意,这两个方法不能用来搜索NaN
的位置,即它们无法确定数组成员是否包含NaN
。
;[NaN]
.indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1
这是因为这两个方法内部,使用严格相等运算符(===
)进行比较,而NaN
是唯一一个不等于自身的值。
链式使用
上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用。
var users = [
{ name: 'tom', email: 'tom@example.com' },
{ name: 'peter', email: 'peter@example.com' }
]
users
.map(function (user) {
return user.email
}) // 对每个成员进行遍历,使用返回值组成一个新的数组并返回
.filter(function (email) {
// 过滤返回值为false的成员,并返回一个过滤掉的新数组
return /^t/.test(email)
})
.forEach(function (email) {
// 遍历数组
console.log(email)
})
// "tom@example.com"
上面代码中,先产生一个所有 Email 地址组成的数组,然后再过滤出以t
开头的 Email 地址,最后将它打印出来。
方法的返回值能满足下一个方法的调用,即可使用链式调用。
四、包装对象
1、定义
对象是 JavaScript 语言中最主要的数据类型,三种原始类型的值——数字、字符串、布尔值 在一定条件下,也会自动转为对象,也就是原始类型的包装对象(wrapper)
所谓“包装对象”,指的是与数字、字符串、布尔值分别相对应的Number
、String
、Boolean
三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
var v1 = new Number(123)
var v2 = new String('abc')
var v3 = new Boolean(true)
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false
上面代码中,基于原始类型的值,生成了三个对应的包装对象。可以看到,v1
、v2
、v3
都是对象,且与对应的简单类型值不相等。
包装对象的设计目的,首先是使得“对象”这种类型可以覆盖 JavaScript 所有的值,整门语言有一个通用的数据模型,其次是使得原始类型的值也有办法调用自己的方法。
Number
、String
和Boolean
这三个原生对象,如果不作为构造函数调用(即调用时不加new
),而是作为普通函数调用,常常用于将任意类型的值转为数值、字符串和布尔值。
// 字符串转为数值
Number('123') // 123
// 数值转为字符串
String(123) // "123"
// 数值转为布尔值
Boolean(123) // true
上面这种数据类型的转换,详见《数据类型转换》一节。
总结一下,这三个对象作为构造函数使用(带有new
)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new
),可以将任意类型的值,转为原始类型的值。
2、实例方法
三种包装对象各自提供了许多实例方法,详见后文。这里介绍两种它们共同具有、从Object
对象继承的方法:valueOf()
和toString()
。
2.1 valueOf()
valueOf()
方法返回包装对象实例对应的原始类型的值。
new Number(123).valueOf() // 123
new String('abc').valueOf() // "abc"
new Boolean(true).valueOf() // true
2.2 toString()
toString()
方法返回对应的字符串形式。
new Number(123).toString() // "123"
new String('abc').toString() // "abc"
new Boolean(true).toString() // "true"
3、原始类型与实例对象的自动转换
某些场合,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例。
比如,字符串可以调用length
属性,返回字符串的长度。
'abc'.length // 3
上面代码中,abc
是一个字符串,本身不是对象,不能调用length
属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length
属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。
var str = 'abc'
str.length // 3
//等同于
var strObj = new String(str)
// String{
// 0:'a',1:'b',2:'c',length:3,[[PrimitiveValue]]: "abc"
// }
strObj.length // 3
上面代码中,字符串abc
的包装对象提供了多个属性,length
只是其中之一。
自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。
var s = 'Hello World'
s.x = 123
s.x // undefined
上面代码为字符串s
添加了一个x
属性,结果无效,总是返回undefined
。
另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象String.prototype
上定义(参见《面向对象编程》章节)。
4、自定义方法
除了原生的实例方法,包装对象还可以自定义方法和属性,供原始类型的值直接调用。
比如,我们可以新增一个double
方法,使得字符串和数字翻倍。
String.prototype.double = function () {
// String是一个方法,String.prototype本质是一个实例对象
return this.valueOf() + this.valueOf()
}
'abc'.double()
// abcabc
Number.prototype.double = function () {
return this.valueOf() + this.valueOf()
}
;(123).double() // 246 // 不加括号时点运算符会被解释成小数点
上面代码在String
和Number
这两个对象的原型上面,分别自定义了一个方法,从而可以在所有实例对象上调用。注意,最后一张的123
外面必须要加上圆括号,否则后面的点运算符(.
)会被解释成小数点。
五、Boolean 对象
1、概述
Boolean
对象是 JavaScript 的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象实例。
var b = new Boolean(true)
typeof b // object
b.valueOf() // true
上面代码的变量b
是一个Boolean
对象的实例,它的类型是对象,值为布尔值true
。
注意,false
对应的包装对象实例,布尔运算结果也是true
。
if (new Boolean(false)) {
// 返回的是一个对象,所有对象对应的布尔值都是true
console.log('true')
} // true
if (new Boolean(false).valueOf()) {
// 它的值为false
console.log('true')
} // 无输出
上面代码的第一个例子之所以得到true
,是因为false
对应的包装对象实例是一个对象,进行逻辑运算时,被自动转化成布尔值true
(因为所有对象对应的布尔值都是true
)。而实例的valueOf
方法,则返回实例对应的原始值,本例为false
。
2、Boolean 函数的类型转换作用
Boolean
对象除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时Boolean
就是一个单纯的工具方法。
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean('') // false
Boolean(NaN) // false
Boolean(1) // true
Boolean('false') // true
Boolean([]) // true
Boolean({}) // true
Boolean(function () {}) // true
Boolean(/foo/) // true
上面代码中几种得到true
的情况,都值得认真记住。
直接使用上面方法括号内的参数进行 if 逻辑运行相当于调用了 Boolean 方法并传入参数
顺便提一下,使用双重的否运算符(!
)也可以将任意值转为对应的布尔值。
!!undefined // false
!!null // false
!!0 // false
!!'' // false
!!NaN // false
!!1 // true
!!'false' // true
!![] // true
!!{} // true
!!function () {} // true
!!/foo/ // true
最后,对于一些特殊值,Boolean
对象前面加不加new
,会得到完全相反的结果,必须小心。
if (Boolean(false)) {
console.log('true')
} // 无输出
if (new Boolean(false)) {
// 加new 转为对象,对象为true
console.log('true')
} // true
if (Boolean(null)) {
console.log('true')
} // 无输出
if (new Boolean(null)) {
// 加new 转为对象,对象为true
console.log('true')
} // true
六、Number 对象
1、概述
Number
对象是数值对应的包装对象,可以作为构造函数使用,也可以作为工具函数使用。
作为构造函数时,它用于生成值为数值的对象。
var n = new Number(1) // 作为构造函数时,生成值为数值的对象
typeof n // object
n.valueOf() // 1
上面代码中,Number
对象作为构造函数使用,返回一个值为1
的对象。
作为工具函数时,它可以将任何类型的值转为数值。
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number([]) // 0
Number('') // 0
Number({}) // NaN
Number(undefined) // NaN
Number('123') // 123
上面代码将布尔值true
转为数值1
。Number
作为工具函数的用法,详见《数据类型转换》一章。
2、静态属性
Number
对象拥有以下一些静态属性(即直接定义在Number
对象上的属性,而不是定义在实例上的属性)。
Number.POSITIVE_INFINITY
:正的无限,指向Infinity
。Number.NEGATIVE_INFINITY
:负的无限,指向-Infinity
。Number.NaN
:表示非数值,指向NaN
。Number.MAX_VALUE
:表示最大的正数1.7976931348623157e+308
,相应的,最小的负数为-Number.MAN_VALUE
。Number.MIN_VALUE
:表示最小的正数(即最接近 0 的正数,在 64 位浮点数体系中为5e-324
),相应的,最接近 0 的负数为-Number.MIN_VALUE
。Number.MAX_SAFE_INTEGER
:表示能够精确表示的最大整数,即9007199254740991
。Number.MIN_SAFE_INTEGER
:表示能够精确表示的最小整数,即-9007199254740991
。
Number.POSITIVE_INFINITY // Infinity
Number.NEGATIVE_INFINITY // -Infinity
Number.NaN // NaN
Number.MAX_VALUE
// 1.7976931348623157e+308
Number.MAX_VALUE < Infinity
// true
Number.MIN_VALUE
// 5e-324
Number.MIN_VALUE > 0
// true
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_SAFE_INTEGER // -9007199254740991
3、实例方法
Number
对象有4 个实例方法,都跟将数值转换成指定格式有关。
3.1 Number.prototype.toString() 可转成多种进制字符串
Number
对象部署了自己的toString
方法,用来将一个数值转为字符串形式。
;(10).toString() // "10"
toString
方法可以接受一个参数,表示输出的进制。如果省略这个参数,默认将数值先转为十进制,再输出字符串;否则,就根据参数指定的进制,将一个数字转化成某个进制的字符串。
;(10)
.toString(2)(
// "1010"
10
)
.toString(8)(
// "12"
10
)
.toString(16) // "a"
上面代码中,10
一定要放在括号里,这样表明后面的点表示调用对象属性。如果不加括号,这个点会被 JavaScript 引擎解释成小数点,从而报错。
10.toString(2) // 不加括号会把点解释成小数点,从而报错
// SyntaxError: Unexpected token ILLEGAL
只要能够让 JavaScript 引擎不混淆小数点和对象的点运算符,各种写法都能用。除了为10
加上括号,还可以在10
后面加两个点,JavaScript 会把第一个点理解成小数点(即10.0
),把第二个点理解成调用对象属性,从而得到正确结果。
;(10).toString(2)
// "1010"
// 其他方法还包括
;(10).toString(2) // "1010"
;(10.0).toString(2) // "1010"
这实际上意味着,可以直接对一个小数使用toString
方法。
;(10.5).toString() // "10.5"
;(10.5).toString(2) // "1010.1"
;(10.5).toString(8) // "12.4"
;(10.5).toString(16) // "a.8"
通过方括号运算符也可以调用toString
方法。
;(10)['toString'](2) // "1010"
toString
方法只能将十进制的数,转为其他进制的字符串。如果要将其他进制的数,转回十进制,需要使用parseInt
方法。
3.2 Number.prototype.toFixed() 转成保留指定小数位数的字符串
toFixed()
方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。
;(10).toFixed(2) // "10.00"
;(10.005).toFixed(2) // "10.01"
上面代码中,10
和10.005
先转成 2 位小数,然后转成字符串。其中10
必须放在括号里,否则后面的点会被处理成小数点。
toFixed()
方法的参数为小数位数,有效范围为 0 到 20,超出这个范围将抛出 RangeError 错误。
由于浮点数的原因,小数5
的四舍五入是不确定的,使用的时候必须小心。
;(10.055)
.toFixed(2)(
// 10.05
10.005
)
.toFixed(2) // 10.01
3.3 Number.prototype.toExponential() 转成科学计数法形式字符串
toExponential
方法用于将一个数转为科学计数法形式。
;(10)
.toExponential()(
// "1e+1"
10
)
.toExponential(1)(
// "1.0e+1"
10
)
.toExponential(2)(
// "1.00e+1"
1234
)
.toExponential()(
// "1.234e+3"
1234
)
.toExponential(1)(
// "1.2e+3"
1234
)
.toExponential(2) // "1.23e+3"
toExponential
方法的参数是小数点后有效数字的位数,范围为 0 到 20,超出这个范围,会抛出一个 RangeError 错误。
3.4 Number.prototype.toPrecision()转为指定位数的有效数字
Number.prototype.toPrecision()
方法用于将一个数转为指定位数的有效数字。
;(12.34)
.toPrecision(1)(
// "1e+1"
12.34
)
.toPrecision(2)(
// "12"
12.34
)
.toPrecision(3)(
// "12.3"
12.34
)
.toPrecision(4)(
// "12.34"
12.34
)
.toPrecision(5) // "12.340"
该方法的参数为有效数字的位数,范围是 1 到 21,超出这个范围会抛出 RangeError 错误。
该方法用于四舍五入时不太可靠,跟浮点数不是精确储存有关。
;(12.35)
.toPrecision(3)(
// "12.3"
12.25
)
.toPrecision(3)(
// "12.3"
12.15
)
.toPrecision(3)(
// "12.2"
12.45
)
.toPrecision(3) // "12.4"
4.5 Number.prototype.toLocaleString()
Number.prototype.toLocaleString()
方法接受一个地区码作为参数,返回一个字符串,表示当前数字在该地区的当地书写形式。
;(123).toLocaleString('zh-Hans-CN-u-nu-hanidec')
// "一二三"
该方法还可以接受第二个参数配置对象,用来定制指定用途的返回字符串。该对象的style
属性指定输出样式,默认值是decimal
,表示输出十进制形式。如果值为percent
,表示输出百分数。
;(123).toLocaleString('zh-Hans-CN', { style: 'persent' })
// "12,300%"
如果style
属性的值为currency
,则可以搭配currency
属性,输出指定格式的货币字符串形式。
;(123)
.toLocaleString('zh-Hans-CN', { style: 'currency', currency: 'CNY' })(
// "¥123.00"
123
)
.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })(
// "123,00 €"
123
)
.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
// "$123.00"
如果Number.prototype.toLocaleString()
省略了参数,则由浏览器自行决定如何处理,通常会使用操作系统的地区设定。注意,该方法如果使用浏览器不认识的地区码,会抛出一个错误。
;(123).toLocaleString('123') // 出错
应用-添加千位分隔符
;(123456.789).toLocaleString()
// 输出 "123,456.789",常用。
;(1234566).toLocaleString('zh-Hans-CN-u-nu-hanidec')
// 输出"一,二三四,五六六"
;(1234566).toLocaleString('zh-Hans-CN-u-nu-hanidec', { useGrouping: true })
// 给.toLocaleString()加第二个参数,依然输出"一,二三四,五六六"
// 不带千位分隔符
;(1234566).toLocaleString('zh-Hans-CN-u-nu-hanidec', { useGrouping: false })
// 输出"一二三四五六六",这个比较有用,也就是最简单的将阿拉伯数字变成汉字的办法。
4、自定义方法
与其他对象一样,Number.prototype
对象上面可以自定义方法,被Number
的实例继承。
Number.prototype.add = function (x) {
return this + x
}
;(8)['add'](2) // 10
上面代码为Number
对象实例定义了一个add
方法。在数值上调用某个方法,数值会自动转为Number
的实例对象,所以就可以调用add
方法了。由于add
方法返回的还是数值,所以可以链式运算。
Number.prototype.subtract = function (x) {
return this - x
}
;(8).add(2).subtract(4)
// 6
上面代码在Number
对象的实例上部署了subtract
方法,它可以与add
方法链式调用。
我们还可以部署更复杂的方法。
Number.prototype.iterate = function () {
var result = []
for (var i = 0; i <= this; i++) {
result.push(i)
}
return result
}
;(8).iterate()
// [0, 1, 2, 3, 4, 5, 6, 7, 8]
上面代码在Number
对象的原型上部署了iterate
方法,将一个数值自动遍历为一个数组。
注意,数值的自定义方法,只能定义在它的原型对象Number.prototype
上面,数值本身是无法自定义属性的。
var n = 1
n.x = 1
n.x // undefined
上面代码中,n
是一个原始类型的数值。直接在它上面新增一个属性x
,不会报错,但毫无作用,总是返回undefined
。这是因为一旦被调用属性,n
就自动转为Number
的实例对象,调用结束后,该对象自动销毁。所以,下一次调用n
的属性时,实际取到的是另一个对象,属性x
当然就读不出来。
七、String 对象
1、概述
String
对象是 JavaScript 原生提供的三个包装对象之一,用来生成字符串对象。
var s1 = 'abc'
var s2 = new String('abc') // 使用new关键字会变成包装对象
typeof s1 // "string"
typeof s2 // "object"
s2.valueOf() // "abc"
上面代码中,变量s1
是字符串,s2
是对象。由于s2
是字符串对象,s2.valueOf
方法返回的就是它所对应的原始字符串。
字符串对象是一个类似数组的对象(很像数组,但不是数组)。
var strObj = new String('abc')
// String {0: "a", 1: "b", 2: "c", length: 3}
strObj[1] // "b"
上面代码中,字符串abc
对应的字符串对象,有数值键(0
、1
、2
)和length
属性,所以可以像数组那样取值。
除了用作构造函数,String
对象还可以当作工具方法使用,将任意类型的值转为字符串。
String(true) // "true"
String(false) // "false"
String(5) // "5"
String(null) // 'null'
String(undefined) //'undefined'
String({}) // '[object Object]'
String([]) // ''
String(['1', '2']) // '1,2'
上面代码将各种类型的值,分别转换为字符串。
2、静态方法
String.fromCharCode()
String
对象提供的静态方法(即定义在对象本身,而不是定义在对象实例的方法),主要是String.fromCharCode()
。该方法的参数是一个或多个数值,代表 Unicode 码点,返回值是这些码点组成的字符串。
String.fromCharCode() // ""
String.fromCharCode(97) // "a"
String.fromCharCode(104, 101, 108, 108, 111)
// "hello"
上面代码中,String.fromCharCode
方法的参数为空,就返回空字符串;否则,返回参数对应的 Unicode 字符串。
注意,该方法不支持 Unicode 码点大于0xFFFF
的字符,即传入的参数不能大于0xFFFF
(即十进制的 65535)。
String.fromCharCode(0x20bb7)
// "ஷ"
String.fromCharCode(0x20bb7) === String.fromCharCode(0x0bb7)
// true
上面代码中,String.fromCharCode
参数0x20BB7
大于0xFFFF
,导致返回结果出错。0x20BB7
对应的字符是汉字𠮷
,但是返回结果却是另一个字符(码点0x0BB7
)。这是因为String.fromCharCode
发现参数值大于0xFFFF
,就会忽略多出的位(即忽略0x20BB7
里面的2
)。
这种现象的根本原因在于,码点大于0xFFFF
的字符占用四个字节,而 JavaScript 默认支持两个字节的字符。这种情况下,必须把0x20BB7
拆成两个字符表示。
String.fromCharCode(0xd842, 0xdfb7)
// "𠮷"
上面代码中,0x20BB7
拆成两个字符0xD842
和0xDFB7
(即两个两字节字符,合成一个四字节字符),就能得到正确的结果。码点大于0xFFFF
的字符的四字节表示法,由 UTF-16 编码方法决定。
3、实例属性
String.prototype.length
字符串实例的length
属性返回字符串的长度。
'abc'.length // 3
4、实例方法
4.1 String.prototype.charAt() 返回指定位置的字符
charAt
方法返回指定位置的字符,参数是从0
开始编号的位置。
var s = new String('abc')
s.charAt(1) // "b"
s.charAt(s.length - 1) // "c"
这个方法完全可以用数组下标替代。
'abc'.charAt(1) // "b"
'abc'[1] // "b"
如果参数为负数,或大于等于字符串的长度,charAt
返回空字符串。
'abc'.charAt(-1) // ""
'abc'.charAt(3) // ""
4.2 String.prototype.charCodeAt()返回字符串指定位置的 Unicode 码点(十进制表示)
charCodeAt
方法返回字符串指定位置的 Unicode 码点(十进制表示),相当于String.fromCharCode()
的逆操作。
'abc'.charCodeAt(1) // 98
上面代码中,abc
的1
号位置的字符是b
,它的 Unicode 码点是98
。
如果没有任何参数,charCodeAt
返回首字符的 Unicode 码点。
'abc'.charCodeAt() // 97
如果参数为负数,或大于等于字符串的长度,charCodeAt
返回NaN
。
'abc'.charCodeAt(-1) // NaN
'abc'.charCodeAt(4) // NaN
注意,charCodeAt
方法返回的 Unicode 码点不会大于 65536(0xFFFF),也就是说,只返回两个字节的字符的码点。如果遇到码点大于 65536 的字符(四个字节的字符),必需连续使用两次charCodeAt
,不仅读入charCodeAt(i)
,还要读入charCodeAt(i+1)
,将两个值放在一起,才能得到准确的字符。
4.3 String.prototype.concat() 合并字符串,返回新字符串,不改变原字符串
concat
方法用于连接两个字符串,返回一个新字符串,不改变原字符串。
var s1 = 'abc';
var s2 = 'def';
s1.concat(s2) // "abcdef"
s1 // "abc"
该方法可以接受多个参数。
'a'.concat('b', 'c') // "abc"
如果参数不是字符串,concat
方法会将其先转为字符串,然后再连接。
var one = 1;
var two = 2;
var three = '3';
''.concat(one, two, three) // "123"
one + two + three // "33"
上面代码中,concat
方法将参数先转成字符串再连接,所以返回的是一个三个字符的字符串。作为对比,加号运算符在两个运算数都是数值时,不会转换类型,所以返回的是一个两个字符的字符串。
4.4 String.prototype.slice() 取出部分子字符串,不改变原字符串
slice
方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置)。
'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) // ""
4.5 String.prototype.substring() 和 slice 类似
substring
方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice
方法很相像。它的第一个参数表示子字符串的开始位置,第二个位置表示结束位置(返回结果不含该位置)。
'JavaScript'.substring(0, 4) // "Java"
如果省略第二个参数,则表示子字符串一直到原字符串的结束。
'JavaScript'.substring(4) // "Script" 省略参数二,会取到结束
如果第一个参数大于第二个参数,substring
方法会自动更换两个参数的位置。
'JavaScript'.substring(10, 4) // "Script" // substring自动更换位置,slice则是返回空字符
// 等同于
'JavaScript'.substring(4, 10) // "Script"
上面代码中,调换substring
方法的两个参数,都得到同样的结果。
如果参数是负数,substring
方法会自动将负数转为 0。
'JavaScript'.substring(-3) // "JavaScript" // substring负数自动转为0,slice负数表示倒数
'JavaScript'.substring(4, -3) // "Java"
上面代码中,第二个例子的参数-3
会自动变成0
,等同于'JavaScript'.substring(4, 0)
。由于第二个参数小于第一个参数,会自动互换位置,所以返回Java
。
由于这些规则违反直觉,因此不建议使用substring
方法,应该优先使用slice
。
小总结:substring() 和 slice() 的区别,有两个。
- 负数参数:substring 负数自动转为 0,slice 负数表示倒数
- 参数一大于参数二:substring 自动更换位置,slice 则是返回空字符
4.6 String.prototype.substr() 和 slice、substring 类似,区别是参数二表示长度
substr
方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice
和substring
方法的作用相同。
substr
方法的第一个参数是子字符串的开始位置(从 0 开始计算),第二个参数是子字符串的长度。
'JavaScript'.substr(4, 6) // "Script"
如果省略第二个参数,则表示子字符串一直到原字符串的结束。
'JavaScript'.substr(4) // "Script" 省略参数二,会取到结束
如果第一个参数是负数,表示倒数计算的字符位置。如果第二个参数是负数,将被自动转为 0,因此会返回空字符串。
'JavaScript'.substr(-6) // "Script" 参数一是负数表示倒数
'JavaScript'.substr(4, -1) // "" 参数二是负数转为0
上面代码中,第二个例子的参数-1
自动转为0
,表示子字符串长度为0
,所以返回空字符串。
4.7 String.prototype.indexOf() 搜索子串第一个出现的位置,String.prototype.lastIndexOf() 搜索子串最后出现的位置
indexOf
方法用于确定一个字符串在另一个字符串中第一次出现的位置,返回结果是匹配开始的位置。如果返回-1
,就表示不匹配。
'hello world'.indexOf('o') // 4
'JavaScript'.indexOf('script') // -1
indexOf
方法还可以接受第二个参数,表示从该位置开始向后匹配。
'hello world'.indexOf('o', 6) // 7
lastIndexOf
方法的用法跟indexOf
方法一致,主要的区别是lastIndexOf
从尾部开始匹配,indexOf
则是从头部开始匹配。
'hello world'.lastIndexOf('o') // 7
另外,lastIndexOf
的第二个参数表示从该位置起向前匹配。
'hello world'.lastIndexOf('o', 6) // 4
4.8 String.prototype.trim() ''修剪'‘两端空格,不改变原字符串
trim
方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串。
' hello world '.trim()
// "hello world"
该方法去除的不仅是空格,还包括制表符(\t
、\v
)、换行符(\n
)和回车符(\r
)。
'\r\nabc \t'.trim() // 'abc'
4.9 String.prototype.toLowerCase() 全转小写,String.prototype.toUpperCase() 全转大写,都不改变原字符串
toLowerCase
方法用于将一个字符串全部转为小写,toUpperCase
则是全部转为大写。它们都返回一个新字符串,不改变原字符串。
'Hello World'.toLowerCase()
// "hello world"
'Hello World'.toUpperCase()
// "HELLO WORLD"
4.10 String.prototype.match() 匹配子串,返回数组(数组带 index、input 属性)
match
方法用于确定原字符串是否匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。如果没有找到匹配,则返回null
。
'cat, bat, sat, fat'.match('at') // ["at"]
// ["at", index: 1, input: "cat, bat, sat, fat", groups: undefined]
'catbatsatbat'.match('bat') // ["bat"]
//["bat", index: 3, input: "catbatsatbat", groups: undefined]
// index属性为bat首次出现的索引
'cat, bat, sat, fat'.match('xt') // null
返回的数组还有**index
属性和input
属性**,分别表示匹配字符串开始的位置和原始字符串。
var matches = 'cat, bat, sat, fat'.match('at')
matches.index // 1 子串首次出现的索引位置
matches.input // "cat, bat, sat, fat"
match
方法还可以使用正则表达式作为参数,详见《正则表达式》一章。
4.11 String.prototype.search() 搜索子串第一次出现的位置,没有则返-1,String.prototype.replace() 替换子串,参数二替换参数一,不改变原字符串
search
方法的用法基本等同于match
,但是返回值为匹配的第一个位置。如果没有找到匹配,则返回-1
。
'cat, bat, sat, fat'.search('at') // 1
search
方法还可以使用正则表达式作为参数,详见《正则表达式》一节。
replace
方法用于替换匹配的子字符串,一般情况下只替换第一个匹配(除非使用带有g
修饰符的正则表达式)。
var str = 'aaa'
str.replace('a', 'b') // "baa" 一般情况只替换第一个,除非带有g的正则
str // 'aaa' 不改变原字符串
str.replace('c', 'b') // 'aaa' 匹配不到返回原字符串
replace
方法还可以使用正则表达式作为参数,详见《正则表达式》一节。
4.12 String.prototype.split() 按给定规则分割字符串,返回数组
split
方法按照给定规则分割字符串,返回一个由分割出来的子字符串组成的数组。
'a|b|c'.split('|') // ["a", "b", "c"]
如果分割规则为空字符串,则返回数组的成员是原字符串的每一个字符。
'a|b|c'.split('') // ["a", "|", "b", "|", "c"]
如果省略参数,则返回数组的唯一成员就是原字符串。
'a|b|c'.split() // ["a|b|c"]
如果满足分割规则的两个部分紧邻着(即两个分割符中间没有其他字符),则返回数组之中会有一个空字符串。
'a||c'.split('|') // ['a', '', 'c']
如果满足分割规则的部分处于字符串的开头或结尾(即它的前面或后面没有其他字符),则返回数组的第一个或最后一个成员是一个空字符串。
'|b|c'.split('|') // ["", "b", "c"]
'a|b|'.split('|') // ["a", "b", ""]
split
方法还可以接受第二个参数,限定返回数组的最大成员数(数组长度)。
'a|b|c'.split('|', 0) // []
'a|b|c'.split('|', 1) // ["a"]
'a|b|c'.split('|', 2) // ["a", "b"]
'a|b|c'.split('|', 3) // ["a", "b", "c"]
'a|b|c'.split('|', 4) // ["a", "b", "c"]
上面代码中,split
方法的第二个参数,决定了返回数组的成员数。
split
方法还可以使用正则表达式作为参数,详见《正则表达式》一节。
4.13 String.prototype.localeCompare() 比较两个字符串,返回整数。
localeCompare
方法 用于比较两个字符串。它返回一个整数,如果小于 0,表示第一个字符串小于第二个字符串;如果等于 0,表示两者相等;如果大于 0,表示第一个字符串大于第二个字符串。
'apple'.localeCompare('banana') // -1
'apple'.localeCompare('apple') // 0
该方法的最大特点,就是会考虑自然语言的顺序。举例来说,正常情况下,大写的英文字母小于小写字母。
'B' > 'a' // false
上面代码中,字母B
小于字母a
。因为 JavaScript 采用的是 Unicode 码点比较,B
的码点是 66,而a
的码点是 97。
但是,localeCompare
方法会考虑自然语言的排序情况,将B
排在a
的前面。
'B'.localeCompare('a') // 1
上面代码中,localeCompare
方法返回整数 1,表示B
较大。
localeCompare
还可以有第二个参数,指定所使用的语言(默认是英语),然后根据该语言的规则进行比较。
'ä'.localeCompare('z', 'de') // -1
'ä'.localeCompare('z', 'sv') // 1
上面代码中,de
表示德语,sv
表示瑞典语。德语中,ä
小于z
,所以返回-1
;瑞典语中,ä
大于z
,所以返回1
。
八、Math 对象
Math
是 JavaScript 的原生对象,提供各种数学功能。该对象不是构造函数,不能生成实例,所有的属性和方法都必须在Math
对象上调用。
1、静态属性
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 的平方根。
Math.E // 2.718281828459045
Math.LN2 // 0.6931471805599453
Math.LN10 // 2.302585092994046
Math.LOG2E // 1.4426950408889634
Math.LOG10E // 0.4342944819032518
Math.PI // 3.141592653589793
Math.SQRT1_2 // 0.7071067811865476
Math.SQRT2 // 1.4142135623730951
这些属性都是只读的,不能修改。
2、静态方法
Math
对象提供以下一些静态方法。
Math.abs()
:绝对值Math.ceil()
:向上取整Math.floor()
:向下取整Math.max()
:最大值Math.min()
:最小值Math.pow()
:指数运算Math.sqrt()
:平方根Math.log()
:自然对数Math.exp()
:e
的指数Math.round()
:四舍五入Math.random()
:随机数
2.1 Math.abs() 绝对值
Math.abs
方法返回参数值的绝对值。
Math.abs(1) // 1
Math.abs(-1) // 1
2.2 Math.max() 最大值,Math.min()最 小值
Math.max
方法返回参数之中最大的那个值,Math.min
返回最小的那个值。如果参数为空, Math.min
返回Infinity
, Math.max
返回-Infinity
。
Math.max(2, -1, 5) // 5
Math.min(2, -1, 5) // -1
Math.min() // Infinity
Math.max() // -Infinity
2.3 Math.floor() 向下取整,Math.ceil() 向上取整
Math.floor
方法返回小于参数值的最大整数(地板值)。
Math.floor(3.2) // 3
Math.floor(-3.2) // -4
Math.ceil
方法返回大于参数值的最小整数(天花板值)。
Math.ceil(3.2) // 4
Math.ceil(-3.2) // -3
这两个方法可以结合起来,实现一个总是返回数值的整数部分的函数。
function ToInteger(x) {
x = Number(x)
return x < 0 ? Math.ceil(x) : Math.floor(x)
}
ToInteger(3.2) // 3
ToInteger(3.5) // 3
ToInteger(3.8) // 3
ToInteger(-3.2) // -3
ToInteger(-3.5) // -3
ToInteger(-3.8) // -3
上面代码中,不管正数或负数,ToInteger
函数总是返回一个数值的整数部分。
2.4 Math.round() 四舍五入
Math.round
方法用于四舍五入。
Math.round(0.1) // 0
Math.round(0.5) // 1
Math.round(0.6) // 1
注意,它对负数的处理(主要是对.5
的处理)。
Math.round(-1.1) // -1
Math.round(-1.5) // -1
Math.round(-1.6) // -2
2.5 Math.pow() 指数运算 (次方运算)
Math.pow
方法返回以第一个参数为底数、第二个参数为幂的指数值。
// 等同于 2 ** 2
Math.pow(2, 2) // 4 2的2次方
// 等同于 2 ** 3
Math.pow(2, 3) // 8 2的3次方
下面是计算圆面积的方法。
var radius = 20
var area = Math.PI * Math.pow(radius, 2) // 1256.6370614359173
2.6 Math.sqrt() 平方根
Math.sqrt
方法返回参数值的平方根。如果参数是一个负值,则返回NaN
。
Math.sqrt(4) // 2
Math.sqrt(-4) // NaN
2.7 Math.log() 自然对数
Math.log
方法返回以e
为底的自然对数值。
Math.log(Math.E) // 1
Math.log(10) // 2.302585092994046
如果要计算以 10 为底的对数,可以先用Math.log
求出自然对数,然后除以Math.LN10
;求以 2 为底的对数,可以除以Math.LN2
。
Math.log(100) / Math.LN10 // 2
Math.log(8) / Math.LN2 // 3
e
的指数
2.8 Math.exp() Math.exp
方法返回常数e
的参数次方。
Math.exp(1) // 2.718281828459045
Math.exp(3) // 20.085536923187668
2.9 Math.random() 0 到 1 的随机数(可能等于 0,但一定小于 1)
Math.random()
返回0 到 1 之间的一个伪随机数,可能等于 0,但是一定小于 1。
Math.random() // 0.7151307314634323
任意范围的随机数生成函数如下。
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min
}
getRandomArbitrary(1.5, 6.5)
// 2.4942810038223864
任意范围的随机整数生成函数如下。
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
getRandomInt(1, 6) // 5
返回随机字符的例子如下。
function random_str(length) {
var ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALPHABET += 'abcdefghijklmnopqrstuvwxyz'
ALPHABET += '0123456789-_'
var str = ''
for (var i = 0; i < length; ++i) {
var rand = Math.floor(Math.random() * ALPHABET.length)
str += ALPHABET.substring(rand, rand + 1)
}
return str
}
random_str(6) // "NdQKOr" 参数为指定字符长度
上面代码中,random_str
函数接受一个整数作为参数,返回变量ALPHABET
内的随机字符所组成的指定长度的字符串。
2.10 三角函数方法
Math
对象还提供一系列三角函数方法。
Math.sin()
:返回参数的正弦(参数为弧度值)Math.cos()
:返回参数的余弦(参数为弧度值)Math.tan()
:返回参数的正切(参数为弧度值)Math.asin()
:返回参数的反正弦(返回值为弧度值)Math.acos()
:返回参数的反余弦(返回值为弧度值)Math.atan()
:返回参数的反正切(返回值为弧度值)
Math.sin(0) // 0
Math.cos(0) // 1
Math.tan(0) // 0
Math.sin(Math.PI / 2) // 1
Math.asin(1) // 1.5707963267948966
Math.acos(1) // 0
Math.atan(1) // 0.7853981633974483
三角函数的应用相关知识:< https://www.w3cplus.com/javascript/trigonometry-you-must-know.html >
九、Date 对象
Date
对象是 JavaScript 原生的时间库。它以国际标准时间(UTC)1970 年 1 月 1 日 00:00:00 作为时间的零点,可以表示的时间范围是前后各 1 亿天(单位为毫秒)。
1、普通函数的用法
Date
对象可以作为普通函数直接调用,返回一个代表当前时间的字符串。
Date()
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)" 字符串类型
注意,即使带有参数,Date
作为普通函数使用时,返回的还是当前时间。
Date(2000, 1, 1) // 作为普通函数使用时带有参数返回的也是当前时间
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"
上面代码说明,无论有没有参数,直接调用Date
总是返回当前时间。
2、构造函数的用法
Date
还可以当作构造函数使用。对它使用new
命令,会返回一个Date
对象的实例。如果不加参数,实例代表的就是当前时间。
var today = new Date()
Date
实例有一个独特的地方。其他对象求值的时候,都是默认调用.valueOf()
方法,但是Date
实例求值的时候,默认调用的是toString()
方法。这导致对Date
实例求值,返回的是一个字符串,代表该实例对应的时间。
var today = new Date()
today
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"
// 等同于
today.toString()
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"
上面代码中,today
是Date
的实例,直接求值等同于调用toString
方法。
作为构造函数时,Date
对象可以接受多种格式的参数,返回一个该参数对应的时间实例。
// 参数为时间零点开始计算的毫秒数
new Date(1378218728000)
// Tue Sep 03 2013 22:32:08 GMT+0800 (CST)
// 参数为日期字符串
new Date('January 6, 2013')
// Sun Jan 06 2013 00:00:00 GMT+0800 (CST)
// 参数为多个整数,
// 代表年、月、日、小时、分钟、秒、毫秒
new Date(2013, 0, 1, 0, 0, 0, 0)
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
关于Date
构造函数的参数,有几点说明。
第一点,参数可以是负整数,代表 1970 年元旦之前的时间。
new Date(-1378218728000)
// Fri Apr 30 1926 17:27:52 GMT+0800 (CST)
第二点,只要是能被Date.parse()
方法解析的字符串,都可以当作参数。
new Date('2013-2-15')
new Date('2013/2/15')
new Date('02/15/2013')
new Date('2013-FEB-15')
new Date('FEB, 15, 2013')
new Date('FEB 15, 2013')
new Date('February, 15, 2013')
new Date('February 15, 2013')
new Date('15 Feb 2013')
new Date('15, February, 2013')
// Fri Feb 15 2013 00:00:00 GMT+0800 (CST)
上面多种日期字符串的写法,返回的都是同一个时间。
第三,参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,这时至少需要两个参数,因为如果只使用“年”这一个参数,Date
会将其解释为毫秒数。
new Date(2013) // 只有一个数字的参数会解释为毫秒数
// Thu Jan 01 1970 08:00:02 GMT+0800 (CST)
上面代码中,2013 被解释为毫秒数,而不是年份。
new Date(2013, 0)
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 1)
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 1, 0)
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 1, 0, 0, 0, 0)
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
上面代码中,不管有几个参数,返回的都是 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 月。
new Date(2013, 15)
// Tue Apr 01 2014 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 0)
// Mon Dec 31 2012 00:00:00 GMT+0800 (CST)
上面代码的第二个例子,日期设为0
,就代表上个月的最后一天。
参数还可以使用负数,表示扣去的时间。
new Date(2013, -1)
// Sat Dec 01 2012 00:00:00 GMT+0800 (CST)
new Date(2013, 0, -1)
// Sun Dec 30 2012 00:00:00 GMT+0800 (CST)
上面代码中,分别对月和日使用了负数,表示从基准日扣去相应的时间。
3、日期的运算
类型自动转换时,Date
实例如果转为数值,则等于对应的毫秒数;如果转为字符串,则等于对应的日期字符串。所以,两个日期实例对象进行减法运算时,返回的是它们间隔的毫秒数;进行加法运算时,返回的是两个字符串连接而成的新字符串。
var d1 = new Date(2000, 2, 1)
var d2 = new Date(2000, 3, 1)
d2 - d1 // 减法运算返回毫秒数
// 2678400000
d2 + d1 // 加法运算返回日期字符串
// "Sat Apr 01 2000 00:00:00 GMT+0800 (CST)Wed Mar 01 2000 00:00:00 GMT+0800 (CST)"
4、静态方法
4.1 Date.now() 当前时间戳
Date.now
方法返回当前时间距离时间零点(1970 年 1 月 1 日 00:00:00 UTC)的毫秒数(即时间戳),相当于 Unix 时间戳乘以 1000。
Date.now() // 1364026285194
4.2 Date.parse() 解析日期字符串,返回该时间点的时间戳
Date.parse
方法用来解析日期字符串,返回该时间距离时间零点(1970 年 1 月 1 日 00:00:00)的毫秒数。
日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准,即YYYY-MM-DDTHH:mm:ss.sssZ
格式,其中最后的Z
表示时区。但是,其他格式也可以被解析,请看下面的例子。
Date.parse('Aug 9, 1995')
Date.parse('January 26, 2011 13:51:50')
Date.parse('Mon, 25 Dec 1995 13:30:00 GMT')
Date.parse('Mon, 25 Dec 1995 13:30:00 +0430')
Date.parse('2011-10-10')
Date.parse('2011-10-10T14:48:00')
上面的日期字符串都可以解析。
如果解析失败,返回NaN
。
Date.parse('xxx') // NaN
4.3 Date.UTC() 以年、月、日等变量作为参数,返回该时间点的时间戳
Date.UTC
方法接受年、月、日等变量作为参数,返回该时间距离时间零点(1970 年 1 月 1 日 00:00:00 UTC)的毫秒数。
// 格式
Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
// 用法
Date.UTC(2011, 0, 1, 2, 3, 4, 567)
// 1293847384567
该方法的参数用法与Date
构造函数完全一致,比如月从0
开始计算,日期从1
开始计算。区别在于Date.UTC
方法的参数,会被解释为 UTC 时间(世界标准时间),Date
构造函数的参数会被解释为当前时区的时间。
5、实例方法
Date
的实例对象,有几十个自己的方法,除了valueOf
和toString
,可以分为以下三类。
to
类:从Date
对象返回一个字符串,表示指定的时间。get
类:获取Date
对象的日期和时间。set
类:设置Date
对象的日期和时间。
5.1 Date.prototype.valueOf() 返回时间戳,等同于 getTime()。(减法运算默认调用)
valueOf
方法返回实例对象距离时间零点(1970 年 1 月 1 日 00:00:00 UTC)对应的毫秒数,该方法等同于getTime
方法。
var d = new Date()
d.valueOf() // 1362790014817
d.getTime() // 1362790014817
预期为数值的场合,Date
实例会自动调用该方法,所以可以用下面的方法计算时间的间隔。
var start = new Date()
// ...
var end = new Date()
var elapsed = end - start // 自动调用valueOf()方法,返回时间戳的减法运算
5.2 to 类方法(返回字符串相关)
(1)Date.prototype.toString() 返回一个完整的日期字符串(直接读取实例默认调用此方法)
toString
方法返回一个完整的日期字符串。
var d = new Date(2013, 0, 1);
d.toString()
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"
d
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"
因为toString
是默认的调用方法,所以如果直接读取Date
实例,就相当于调用这个方法。
(2)Date.prototype.toUTCString() 返回对应的 UTC 时间(世界标准时),比北京时间晚 8 小时
toUTCString
方法返回对应的 UTC 时间,也就是比北京时间晚 8 个小时。
var d = new Date(2013, 0, 1)
d.toUTCString()
// "Mon, 31 Dec 2012 16:00:00 GMT"
(3)Date.prototype.toISOString() 返回对应时间的 ISO8601 写法(UTC 时间)
toISOString
方法返回对应时间的 ISO8601 写法。
var d = new Date(2013, 0, 1)
d.toISOString()
// "2012-12-31T16:00:00.000Z"
注意,toISOString
方法返回的总是 UTC 时区的时间。
toISOString
方法的返回结果完全相同。
(4)Date.prototype.toJSON() 返回符合 JSON 格式字符串,与toJSON
方法返回一个符合 JSON 格式的 ISO 日期字符串,与toISOString
方法的返回结果完全相同。
var d = new Date(2013, 0, 1)
d.toJSON()
// "2012-12-31T16:00:00.000Z"
(5)Date.prototype.toDateString() 返回日期字符串(不含时分秒)
toDateString
方法返回日期字符串(不含小时、分和秒)。
var d = new Date(2013, 0, 1)
d.toDateString() // "Tue Jan 01 2013"
(6)Date.prototype.toTimeString() 返回时间字符串(不含年月日)
toTimeString
方法返回时间字符串(不含年月日)。
var d = new Date(2013, 0, 1)
d.toTimeString() // "00:00:00 GMT+0800 (CST)"
(7)本地时间
以下三种方法,可以将 Date 实例转为表示本地时间的字符串。
Date.prototype.toLocaleString()
:完整的本地时间。Date.prototype.toLocaleDateString()
:本地日期(不含小时、分和秒)。Date.prototype.toLocaleTimeString()
:本地时间(不含年月日)。
下面是用法实例。
var d = new Date(2013, 0, 1)
d.toLocaleString()
// 中文版浏览器为"2013年1月1日 上午12:00:00"
// 英文版浏览器为"1/1/2013 12:00:00 AM"
d.toLocaleDateString()
// 中文版浏览器为"2013年1月1日"
// 英文版浏览器为"1/1/2013"
d.toLocaleTimeString()
// 中文版浏览器为"上午12:00:00"
// 英文版浏览器为"12:00:00 AM"
这三个方法都有两个可选的参数。
dateObj.toLocaleString([locales[, options]])
dateObj.toLocaleDateString([locales[, options]])
dateObj.toLocaleTimeString([locales[, options]])
这两个参数中,locales
是一个指定所用语言的字符串,options
是一个配置对象。下面是locales
的例子。
var d = new Date(2013, 0, 1)
d.toLocaleString('en-US') // "1/1/2013, 12:00:00 AM"
d.toLocaleString('zh-CN') // "2013/1/1 上午12:00:00"
d.toLocaleDateString('en-US') // "1/1/2013"
d.toLocaleDateString('zh-CN') // "2013/1/1"
d.toLocaleTimeString('en-US') // "12:00:00 AM"
d.toLocaleTimeString('zh-CN') // "上午12:00:00"
下面是options
的例子。
var d = new Date(2013, 0, 1)
// 时间格式
// 下面的设置是,星期和月份为完整文字,年份和日期为数字
d.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
// "Tuesday, January 1, 2013"
// 指定时区
d.toLocaleTimeString('en-US', {
timeZone: 'UTC',
timeZoneName: 'short'
})
// "4:00:00 PM UTC"
d.toLocaleTimeString('en-US', {
timeZone: 'Asia/Shanghai',
timeZoneName: 'long'
})
// "12:00:00 AM China Standard Time"
// 小时周期为12还是24
d.toLocaleTimeString('en-US', {
hour12: false
})
// "00:00:00"
d.toLocaleTimeString('en-US', {
hour12: true
})
// "12:00:00 AM"
5.3 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 的时区差异,以分钟表示,返回结果考虑到了夏令时因素。(与 UTC 差异)
所有这些get*
方法返回的都是整数,不同方法返回值的范围不一样。
- 分钟和秒:0 到 59
- 小时:0 到 23
- 星期:0(星期天)到 6(星期六)
- 日期:1 到 31
- 月份:0(一月)到 11(十二月)
var d = new Date('January 6, 2013')
d.getDate() // 6
d.getMonth() // 0
d.getFullYear() // 2013
d.getTimezoneOffset() // -480 UTC时间减去当前时间,单位是分钟
上面代码中,最后一行返回-480
,即 UTC 时间减去当前时间,单位是分钟。-480
表示 UTC 比当前时间少 480 分钟,即当前时区比 UTC 早 8 个小时。
例子:计算本年度还剩下多少天
function leftDays() {
var today = new Date()
var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999)
var msPerDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
return Math.round((endYear.getTime() - today.getTime()) / msPerDay)
}
上面这些get*
方法返回的都是当前时区的时间,Date
对象还提供了这些方法对应的 UTC 版本,用来返回 UTC 时间。
获取 UTC 时区相关方法:
getUTCDate()
getUTCFullYear()
getUTCMonth()
getUTCDay()
getUTCHours()
getUTCMinutes()
getUTCSeconds()
getUTCMilliseconds()
var d = new Date('January 6, 2013')
d.getDate() // 6
d.getUTCDate() // 5
上面代码中,实例对象d
表示当前时区(东八时区)的 1 月 6 日 0 点 0 分 0 秒,这个时间对于当前时区来说是 1 月 6 日,所以getDate
方法返回 6,对于 UTC 时区来说是 1 月 5 日,所以getUTCDate
方法返回 5。
5.4 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 月。
var d = new Date('January 6, 2013')
d // Sun Jan 06 2013 00:00:00 GMT+0800 (CST)
d.setDate(9) // 1357660800000
d // Wed Jan 09 2013 00:00:00 GMT+0800 (CST)
set*
方法的参数都会自动折算。以setDate
为例,如果参数超过当月的最大天数,则向下一个月顺延,如果参数是负数,表示从上个月的最后一天开始减去的天数。
var d1 = new Date('January 6, 2013')
d1.setDate(32) // 1359648000000
d1 // Fri Feb 01 2013 00:00:00 GMT+0800 (CST)
var d2 = new Date('January 6, 2013')
d.setDate(-1) // 1356796800000
d // Sun Dec 30 2012 00:00:00 GMT+0800 (CST)
**set
类方法和get
类方法,可以结合使用,得到相对时间。**
var d = new Date()
// 将日期向后推1000天
d.setDate(d.getDate() + 1000)
// 将时间设为6小时后
d.setHours(d.getHours() + 6)
// 将年份设为去年
d.setFullYear(d.getFullYear() - 1)
set*
系列方法除了setTime()
,都有对应的 UTC 版本,即设置 UTC 时区的时间。
setUTCDate()
setUTCFullYear()
setUTCHours()
setUTCMilliseconds()
setUTCMinutes()
setUTCMonth()
setUTCSeconds()
var d = new Date('January 6, 2013')
d.getUTCHours() // 16
d.setUTCHours(22) // 1357423200000
d // Sun Jan 06 2013 06:00:00 GMT+0800 (CST)
上面代码中,本地时区(东八时区)的 1 月 6 日 0 点 0 分,是 UTC 时区的前一天下午 16 点。设为 UTC 时区的 22 点以后,就变为本地时区的上午 6 点。
十、RegExp 对象
RegExp
对象提供正则表达式的功能。
1、概述
正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用来按照“给定模式”匹配文本。比如,正则表达式给出一个 Email 地址的模式,然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。
新建正则表达式有两种方法:
一种是使用字面量,以斜杠表示开始和结束。
var regex = /xyz/ // 效率较高,且直观,推荐。
另一种是使用RegExp
构造函数。
var regex = new RegExp('xyz')
上面两种写法是等价的,都新建了一个内容为xyz
的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。
RegExp
构造函数还可以接受第二个参数,表示修饰符(详细解释见下文)。
var regex = new RegExp('xyz', 'i') // i为修饰符
// 等价于
var regex = /xyz/i
上面代码中,正则表达式/xyz/
有一个修饰符i
。
2、实例属性
正则对象的实例属性分成两类。
一类是修饰符相关,用于了解设置了什么修饰符。
RegExp.prototype.ignoreCase
:返回一个布尔值,表示是否设置了i
修饰符。RegExp.prototype.global
:返回一个布尔值,表示是否设置了g
修饰符。RegExp.prototype.multiline
:返回一个布尔值,表示是否设置了m
修饰符。RegExp.prototype.flags
:返回一个字符串,包含了已经设置的所有修饰符,按字母排序。
上面四个属性都是只读的。
var r = /abc/gim
r.ignoreCase // true 是否设置i修饰符
r.global // true 是否设置g修饰符
r.multiline // true 是否设置m修饰符
r.flags // 'gim' 字符串,包含设置的所有修饰符,按字母排序
另一类是与修饰符无关的属性,主要是下面两个。
RegExp.prototype.lastIndex
:返回一个整数,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义,详细介绍请看后文。RegExp.prototype.source
:返回正则表达式的字符串形式(不包括反斜杠),该属性只读。
var r = /abc/gim
r.lastIndex // 0
r.source // "abc" 正则表达式的字符串形式
3、实例方法
3.1 RegExp.prototype.test() 当前模式是否匹配参数字符串,返回布尔值
正则实例对象的test
方法返回一个布尔值,表示当前模式是否能匹配参数字符串。
;/cat/.test('cats and dogs') // true 验证字符串是否包含cat
上面代码验证参数字符串之中是否包含cat
,结果返回true
。
如果正则表达式带有g
修饰符,则每一次test
方法都从上一次结束的位置开始向后匹配。
var r = /x/g // 带有g全局匹配修饰符
var s = '_x_x'
r.lastIndex // 0 下一次开始搜索的位置
r.test(s) // true 内部原理:从0位置开始匹配,匹配到'x'在1位置,返回true,并把lastIndex属性设置为2,
r.lastIndex // 2 下一次开始搜索的位置
r.test(s) // true 内部原理:从2位置开始匹配,匹配到'x'在3位置,返回true,并把lastIndex属性设置为4
r.lastIndex // 4 下一次开始搜索的位置
r.test(s) // false 内部原理:从4位置开始匹配,匹配不到’x‘,返回false
上面代码的正则表达式使用了g
修饰符,表示是全局搜索,会有多个结果。接着,三次使用test
方法,每一次开始搜索的位置都是上一次匹配的后一个位置。
带有g
修饰符时,可以通过正则对象的lastIndex
属性指定开始搜索的位置。
var r = /x/g
var s = '_x_x'
r.lastIndex = 4 // lastIndex属性可读写,改写后,test方法将从该位置开始匹配
r.test(s) // false
r.lastIndex // 0 重置为0
r.test(s) // true
上面代码指定从字符串的第五个位置开始搜索,这个位置为空,所以返回false
。同时,lastIndex
属性重置为0
,所以第二次执行r.test(s)
会返回true
。
注意,带有g
修饰符时,正则表达式内部会记住上一次的lastIndex
属性,这时不应该更换所要匹配的字符串,否则会有一些难以察觉的错误。
var r = /bb/g
r.test('bb') // true 由于带有g修饰符,匹配后会把lastIndex属性修改为了2
r.test('-bb-') // false 从2位置开始匹配'bb',并没有匹配到
上面代码中,由于正则表达式r
是从上一次的lastIndex
位置开始匹配,导致第二次执行test
方法时出现预期以外的结果。
lastIndex
属性只对同一个正则表达式有效,所以下面这样写是错误的。
var count = 0
while (/a/g.test('babaa')) count++
上面代码会导致无限循环,因为while
循环的每次匹配条件都是一个新的正则表达式,导致lastIndex
属性总是等于 0。
如果正则模式是一个空字符串,则匹配所有字符串。
new RegExp('').test('abc')
// true
3.2 RegExp.prototype.exec() 返回匹配结果,如匹配则返回一个数组,成员是匹配到的子字符串,否则返回 null
正则实例对象的exec
方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回null
。
var s = '_x_x'
var r1 = /x/
var r2 = /y/
r1.exec(s) // ["x"] 返回一个成员
r2.exec(s) // null
上面代码中,正则对象r1
匹配成功,返回一个数组,成员是匹配结果;正则对象r2
匹配失败,返回null
。
如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的length
属性等于组匹配的数量再加 1。
var s = '_x_x'
var r = /_(x)/ // 含有圆括号,组匹配
r.exec(s) // ["_x", "x"] 返回多个成员,第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果
上面代码的exec
方法,返回一个数组。第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果。
exec
方法的返回数组还包含以下两个属性:
input
:整个原字符串。index
:整个模式匹配成功的开始位置(从 0 开始计数)。
var r = /a(b+)a/
var arr = r.exec('_abbba_aba_')
arr // ["abbba", "bbb"] // 第一个成员是整个匹配的结果,第二个成员是圆括号匹配的结果
arr.index // 1 表示是在1位置开始匹配成功的
arr.input // "_abbba_aba_"
上面代码中的index
属性等于 1,是因为从原字符串的第二个位置开始匹配成功。
如果正则表达式加上g
修饰符,则可以使用多次exec
方法,下一次搜索的位置从上一次匹配成功结束的位置开始。
var reg = /a/g // 带'g'修饰符
var str = 'abc_abc_abc'
var r1 = reg.exec(str)
r1 // ["a"]
r1.index // 0 在0位置匹配成功
reg.lastIndex // 1 下一次匹配开始位置为1
var r2 = reg.exec(str)
r2 // ["a"]
r2.index // 4 在4位置匹配成功
reg.lastIndex // 5 下一次匹配开始位置为5
var r3 = reg.exec(str)
r3 // ["a"]
r3.index // 8 在8位置匹配成功
reg.lastIndex // 9 下一次匹配开始位置为9
var r4 = reg.exec(str)
r4 // null 没有匹配到返回null
reg.lastIndex // 0 下一次匹配位置为0
上面代码连续用了四次exec
方法,前三次都是从上一次匹配结束的位置向后匹配。当第三次匹配结束以后,整个字符串已经到达尾部,匹配结果返回null
,正则实例对象的lastIndex
属性也重置为0
,意味着第四次匹配将从头开始。
利用g
修饰符允许多次匹配的特点,可以用一个循环完成全部匹配。
var reg = /a/g
var str = 'abc_abc_abc'
while (true) {
var match = reg.exec(str)
if (!match) break
console.log('#' + match.index + ':' + match[0])
}
// #0:a
// #4:a
// #8:a
上面代码中,只要exec
方法不返回null
,就会一直循环下去,每次输出匹配的位置和匹配的文本。
正则实例对象的lastIndex
属性不仅可读,还可写。设置了g
修饰符的时候,只要手动设置了lastIndex
的值,就会从指定位置开始匹配。
4、字符串的实例方法
字符串的实例方法之中,有 4 种与正则表达式有关。
String.prototype.match()
:返回一个数组,成员是所有匹配的子字符串。String.prototype.search()
:按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。String.prototype.replace()
:按照给定的正则表达式进行替换,返回替换后的字符串。String.prototype.split()
:按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员。
4.1 String.prototype.match() 匹配,返回匹配结果数组或 null
字符串实例对象的match
方法对字符串进行正则匹配,返回匹配结果。
var s = '_x_x'
var r1 = /x/
var r2 = /y/
s.match(r1) // ["x"] // 未带g修饰符,匹配到一个即返回结果
s.match(r2) // null
从上面代码可以看到,字符串的match
方法与正则对象的exec
方法非常类似:匹配成功返回一个数组,匹配失败返回null
。
如果正则表达式带有g
修饰符,则该方法与正则对象的exec
方法行为不同,会一次性返回所有匹配成功的结果。
var s = 'abba'
var r = /a/g
s.match(r) // ["a", "a"] 带g修饰符,会返回全部结果到数组成员
r.exec(s) // ["a"] 正则实例的exec方法只返回一个
设置正则表达式的lastIndex
属性,对match
方法无效,匹配总是从字符串的第一个字符开始。
var r = /a|b/g
r.lastIndex = 7
'xaxb'.match(r) // ['a', 'b']
r.lastIndex // 0
上面代码表示,设置正则对象的lastIndex
属性是无效的。
4.2 String.prototype.search() 返回第一个满足匹配条件的位置,不满足则返-1
字符串对象的search
方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1
。
'_x_x'.search(/x/)
// 1
上面代码中,第一个匹配结果出现在字符串的1
号位置。
4.3 String.prototype.replace() 替换匹配的值,参数一是子串或正则,参数二是替换内容
字符串对象的replace
方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。
str.replace(search, replacement)
正则表达式如果不加g
修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。
'aaa'.replace('a', 'b') // "baa"
'aaa'.replace(/a/, 'b') // "baa" 不带g修饰符,只匹配并替换第一个
'aaa'.replace(/a/g, 'b') // "bbb" 带g修饰符,匹配全局,并替换全部匹配到的值
上面代码中,最后一个正则表达式使用了g
修饰符,导致所有的b
都被替换掉了。
replace
方法的一个应用,就是消除字符串首尾两端的空格。
var str = ' #id div.class '
str.replace(/^\s+|\s+$/g, '')
// "#id div.class"
replace
方法的第二个参数可以使用美元符号$
,用来指代所替换的内容。
$&
:匹配的子字符串。- $`:匹配结果前面的文本。
$'
:匹配结果后面的文本。$n
:匹配成功的第n
组内容,n
是从 1 开始的自然数。$$
:指代美元符号$
。
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1')
// "world hello"
/*说明:
$n 用于组内容,$2为匹配到的第二组,即world; $1为匹配到的第一组,即hello
*/
'abc'.replace('b', "[$`-$&-$'-$$]")
// "a[a-b-c-$]c"
/*说明:
$`为匹配结果 b 前面的文本,即 a
$&为匹配的子字符串,即 b
$’为匹配结果 b 后面的文本,即 c
$$代表$符号本身
*/
上面代码中,第一个例子是将匹配的组互换位置,第二个例子是改写匹配的值。
replace
方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值。
'3 and 5'.replace(/[0-9]+/g, function (match) {
return 2 * match // match参数是匹配到的内容
})
// "6 and 10"
var a = 'The quick brown fox jumped over the lazy dog.'
var pattern = /quick|brown|lazy/gi
a.replace(pattern, function replacer(match) {
return match.toUpperCase()
})
// The QUICK BROWN fox jumped over the LAZY dog.
作为replace
方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子。
var prices = {
p1: '$1.99',
p2: '$9.99',
p3: '$5.00'
}
var template =
'<span id="p1"></span>' + '<span id="p2"></span>' + '<span id="p3"></span>'
template.replace(
/(<span id=")(.*?)(">)(<\/span>)/g,
function (match, $1, $2, $3, $4, index, str) {
return $1 + $2 + $3 + prices[$2] + $4
}
)
// "<span id="p1">$1.99</span><span id="p2">$9.99</span><span id="p3">$5.00</span>"
// 说明:
// match 整个匹配到的内容:<span id="p*"></span>
// 以下参数分别是匹配到的组内容,有多少组就对应多少个参数
// $1 <span id="
// $2 p1~p3
// $3 ">
// $4 </span>
// 后面还可以接受两个参数:
// 倒数第二个是:捕捉到的内容(即match的内容)在整个字符串中的位置
// 最后一个是:原字符串
上面代码的捕捉模式中,有四个括号,所以会产生四个组匹配,在匹配函数中用$1
到$4
表示。匹配函数的作用是将价格插入模板中。
4.4 String.prototype.split() 按给定规则分割字符串,返回数组
字符串对象的split
方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。
str.split(separator, [limit])
该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数(数组长度)。
// 非正则分隔
'a, b,c, d'.split(',')
// [ 'a', ' b', 'c', ' d' ]
// 正则分隔,去除多余的空格
'a, b,c, d'.split(/, */)
// [ 'a', 'b', 'c', 'd' ]
// 指定返回数组的最大成员(数组长度)
'a, b,c, d'.split(/, */, 2)[('a', 'b')]
上面代码使用正则表达式,去除了子字符串的逗号后面的空格。
// 例一
'aaa*a*'.split(/a*/)
// [ '', '*', '*' ] 分隔符,即被过滤的符号,
// 例二
'aaa**a*'.split(/a*/)
// ["", "*", "*", "*"]
上面代码的分割规则是 0 次或多次的a
,由于正则默认是贪婪匹配,所以例一的第一个分隔符是aaa
,第二个分割符是a
,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是aaa
,第二个分隔符是 0 个a
(即空字符),第三个分隔符是a
,所以将字符串分成四个部分。
如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。
'aaa*a*'.split(/(a*)/)
// [ '', 'aaa', '*', 'a', '*' ]
上面代码的正则表达式使用了括号,第一个组匹配是aaa
,第二个组匹配是a
,它们都作为数组成员返回。
5、匹配规则
搭配菜鸟教程一起学习更容易理解:https://www.runoob.com/regexp/regexp-tutorial.html
正则表达式在线测试与常用正则表达式:http://c.runoob.com/front-end/854
正则表达式的规则很复杂,下面一一介绍这些规则。
5.1 字面量字符和元字符
大部分字符在正则表达式中,就是字面的含义,比如/a/
匹配a
,/b/
匹配b
。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的a
和b
),那么它们就叫做“字面量字符”(literal characters)。
/dog/.test('old dog') // true
上面代码中正则表达式的dog
,就是字面量字符,所以/dog/
匹配old dog
,因为它就表示d
、o
、g
三个字母连在一起。
除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个。
(1)点字符(.) 类似通配
点字符(.
)匹配除回车(\r
)、换行(\n
) 、行分隔符(\u2028
)和段分隔符(\u2029
)以外的任何单个字符。注意,对于码点大于0xFFFF
字符,点字符不能正确匹配,会认为这是两个字符。
;/c.t/ //匹配c和t之间包含任意一个字符的情况,注意:一个.只匹配一个字符
'cet c2t c-t c.t coot c我t'.match(/c.t/g)
// ["cet", "c2t", "c-t", "c.t", "c我t"]
'cet c2t c-t c.t coot c我t'.match(/c..t/g)
//["coot"]
上面代码中,c.t
匹配c
和t
之间包含任意一个字符的情况,只要这三个字符在同一行,比如cat
、c2t
、c-t
等等,但是不匹配coot
。
(2)位置字符(^) ($) 开始位置和结束位置
位置字符用来提示字符所处的位置,主要有两个字符。
^
表示字符串的开始位置$
表示字符串的结束位置
// test必须出现在开始位置
/^test/.test('test123') // true
// test必须出现在结束位置
/test$/.test('new test') // true
// 从开始位置到结束位置只有test
/^test$/.test('test') // true
/^test$/.test('test test') // false
|
) 表示''或关系''
(3)选择符(竖线符号(|
)在正则表达式中表示“或关系”(OR),即cat|dog
表示匹配cat
或dog
。
;/11|22/.test('911') // true
上面代码中,正则表达式指定必须匹配11
或22
。
多个选择符可以联合使用。
// 匹配fred、barney、betty之中的一个
;/fred|barney|betty/
选择符会包括它前后的多个字符,比如/ab|cd/
指的是匹配ab
或者cd
,而不是指匹配b
或者c
。如果想修改这个行为,可以使用圆括号。
;/a( |\t)b/.test('a\tb') // true
上面代码指的是,a
和b
之间有一个空格或者一个制表符。
其他的元字符还包括\
、\*
、+
、?
、()
、[]
、{}
等,将在下文解释。
5.2 转义符(\ 反斜杠)
正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+
,就要写成\+
。
/1+1/.test('1+1')
// false
/1\+1/.test('1+1')
// true
上面代码中,第一个正则表达式之所以不匹配,因为加号是元字符,不代表自身。第二个正则表达式使用反斜杠对加号转义,就能匹配成功。
正则表达式中,需要反斜杠转义的,一共有 12 个字符:^
、.
、[
、$
、(
、)
、|
、*
、+
、?
、{
和\
。需要特别注意的是,如果使用RegExp
方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。
new RegExp('1+1')
.test('1+1')(
// false
new RegExp('1\\+1')
)
.test('1+1')
// true
上面代码中,RegExp
作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义。
5.3 特殊字符
正则表达式对一些不能打印的特殊字符,提供了表达方法。
\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 字符。
5.4 字符类
字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如[xyz]
表示x
、y
、z
之中任选一个匹配。
/[abc]/.test('hello world') // false
/[abc]/.test('apple') // true a、b、c其中任意一个
上面代码中,字符串hello world
不包含a
、b
、c
这三个字母中的任一个,所以返回false
;字符串apple
包含字母a
,所以返回true
。
有两个字符在字符类中有特殊含义。
(1)脱字符(^ 排除)
如果方括号内的第一个字符是[^]
,则表示除了字符类之中的字符,其他字符都可以匹配。比如,[^xyz]
表示除了x
、y
、z
之外都可以匹配。
/[^abc]/.test('bbc news') // true 字符串中包含了a、b、c以为的字符
/[^abc]/.test('bbc') // false 字符串中没有除了a、b、c以外的字符
上面代码中,字符串bbc news
包含a
、b
、c
以外的其他字符,所以返回true
;字符串bbc
不包含a
、b
、c
以外的其他字符,所以返回false
。
如果方括号内没有其他字符,即只有[^]
,就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.
)是不包括换行符的。
var s = 'Please yes\nmake my day!'
s.match(/yes.*day/) // null
s.match(/yes[^]*day/) // [ 'yes\nmake my day']
上面代码中,字符串s
含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式[^]
包含一切字符,所以匹配成功。
注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。
(2)连字符(- 字符连续范围)
某些情况下,对于连续序列的字符,连字符(-
)用来提供简写形式,表示字符的连续范围。比如,[abc]
可以写成[a-c]
,[0123456789]
可以写成[0-9]
,同理[A-Z]
表示 26 个大写字母。
/a-z/.test('b') // false
/[a-z]/.test('b') // true
上面代码中,当连字号(dash)不出现在方括号之中,就不具备简写的作用,只代表字面的含义,所以不匹配字符b
。只有当连字号用在方括号之中,才表示连续的字符序列。
以下都是合法的字符类简写形式。
[0-9.,]
[0-9a-fA-F]
[a-zA-Z0-9-]
[1-31] // 注意:不代表1到31,只代表1到3
上面代码中最后一个字符类[1-31]
,不代表1
到31
,只代表1
到3
。
连字符还可以用来指定 Unicode 字符的范围。
var str = '\u0130\u0131\u0132'
;/[\u0128-\uFFFF]/.test(str)
// true
上面代码中,\u0128-\uFFFF
表示匹配码点在0128
到FFFF
之间的所有字符。
另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是[A-z]
,表面上它是选中从大写的A
到小写的z
之间 52 个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。
;/[A-z]/.test('\\') // true
上面代码中,由于反斜杠('\')的 ASCII 码在大写字母与小写字母之间,结果会被选中。
5.5 预定义模式 (常见模式简写\d\D\w\W\s\S\b\B)
预定义模式指的是某些常见模式的简写方式。
\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
匹配非词边界,即在词的内部。(非单词边界)
下面是一些例子。
// \s 的例子
/\s\w*/.exec('hello world') // [" world"]
// \b 的例子
/\bworld/.test('hello world') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('helloworld') // false
// \B 的例子
/\Bworld/.test('hello-world') // false
/\Bworld/.test('helloworld') // true
上面代码中,\s
表示空格,所以匹配结果会包括空格。\b
表示词的边界,所以world
的词首必须独立(词尾是否独立未指定),才会匹配。同理,\B
表示非词的边界,只有world
的词首不独立,才会匹配。
通常,正则表达式遇到换行符(\n
)就会停止匹配。
var html = '<b>Hello</b>\n<i>world!</i>'
;/.*/.exec(html)[0]
// "<b>Hello</b>"
上面代码中,字符串html
包含一个换行符,结果点字符(.
)不匹配换行符,导致匹配结果可能不符合原意。这时使用\s
字符类,就能包括换行符。
var html = '<b>Hello</b>\n<i>world!</i>'
;/[\S\s]*/.exec(html)[0]
// "<b>Hello</b>\n<i>world!</i>"
上面代码中,[\S\s]
指代一切字符。
5.6 重复类 ( {n} {n,} {n,m} )
模式的精确匹配次数,使用大括号({}
)表示。{n}
表示恰好重复n
次,{n,}
表示至少重复n
次,{n,m}
表示重复不少于n
次,不多于m
次。
/lo{2}k/.test('look') // true 2个o
/lo{2,5}k/.test('looook') // true 2到5个o
上面代码中,第一个模式指定o
连续出现 2 次,第二个模式指定o
连续出现 2 次到 5 次之间。
5.7 量词符(?)(*)(+)
量词符用来设定某个模式出现的次数。
?
问号表示某个模式出现0 次或 1 次,等同于{0, 1}
。*
星号表示某个模式出现0 次或多次,等同于{0,}
。+
加号表示某个模式出现1 次或多次,等同于{1,}
。
// t 出现0次或1次
/t?est/.test('test') // true
/t?est/.test('est') // true
// t 出现1次或多次
/t+est/.test('test') // true
/t+est/.test('ttest') // true
/t+est/.test('est') // false
// t 出现0次或多次
/t*est/.test('test') // true
/t*est/.test('ttest') // true
/t*est/.test('tttest') // true
/t*est/.test('est') // true
5.8 贪婪模式(对量词符可使用非贪婪模式)
上一小节的三个量词符,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式。
var s = 'aaa'
s.match(/a+/) // ["aaa"]
上面代码中,模式是/a+/
,表示匹配 1 个a
或多个a
,那么到底会匹配几个a
呢?因为默认是贪婪模式,会一直匹配到字符a
不出现为止,所以匹配结果是 3 个a
。
如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。
var s = 'aaa'
s.match(/a+?/) // ["a"]
上面代码中,模式结尾添加了一个问号/a+?/
,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配。
除了非贪婪模式的加号,还有非贪婪模式的星号(*
)和非贪婪模式的问号(?
)。
+?
:表示某个模式出现 1 次或多次,匹配时采用非贪婪模式。*?
:表示某个模式出现 0 次或多次,匹配时采用非贪婪模式。??
:表格某个模式出现 0 次或 1 次,匹配时采用非贪婪模式。
'abb'.match(/ab*b/) // ["abb"]
'abb'.match(/ab*?b/) // ["ab"]
'abb'.match(/ab?b/) // ["abb"]
'abb'.match(/ab??b/) // ["ab"]
5.9 修饰符
修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。
修饰符可以单个使用,也可以多个一起使用。
// 单个修饰符
var regex = /test/i
// 多个修饰符
var regex = /test/gi
(1)g 修饰符 (全局匹配)
默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g
修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。
var regex = /b/
var str = 'abba'
regex.test(str) // true
regex.test(str) // true
regex.test(str) // true
上面代码中,正则模式不含g
修饰符,每次都是从字符串头部开始匹配。所以,连续做了三次匹配,都返回true
。
var regex = /b/g
var str = 'abba'
regex.test(str) // true
regex.test(str) // true
regex.test(str) // false
上面代码中,正则模式含有g
修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串abba
只有两个b
,所以前两次匹配结果为true
,第三次匹配结果为false
。
(2)i 修饰符 (不区分大小写)
默认情况下,正则对象区分字母的大小写,加上i
修饰符以后表示忽略大小写(ignoreCase)。
;/abc/.test('ABC') / // false
abc /
i.test('ABC') // true
上面代码表示,加了i
修饰符以后,不考虑大小写,所以模式abc
匹配字符串ABC
。
(3)m 修饰符(让^和$识别换行符)
m
修饰符表示多行模式(multiline),会修改^
和$
的行为。默认情况下(即不加m
修饰符时),^
和$
匹配字符串的开始处和结尾处,加上m
修饰符以后,^
和$
还会匹配行首和行尾,即^
和$
会识别换行符(\n
)。
;/world$/.test('hello world\n') / // false
world$ /
m.test('hello world\n') // true
上面的代码中,字符串结尾处有一个换行符。如果不加m
修饰符,匹配不成功,因为字符串的结尾不是world
;加上以后,$
可以匹配行尾。
;/^b/m.test('a\nb') // true
上面代码要求匹配行首的b
,如果不加m
修饰符,就相当于b
只能处在字符串的开始处。加上m
修饰符以后,换行符\n
也会被认为是一行的开始。
5.10 组匹配
(1)概述 (圆括号分组)
正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。
;/fred+/.test('fredd') / // true +加号表示重复字母d
fred +
/.test('fredfred') / / true +
加号表示重复单词fred
上面代码中,第一个模式没有括号,结果+
只表示重复字母d
,第二个模式有括号,结果+
就表示匹配fred
这个词。
下面是另外一个分组捕获的例子。
var m = 'abcabc'.match(/(.)b(.)/)
m
// ['abc', 'a', 'c']
上面代码中,正则表达式/(.)b(.)/
一共使用两个括号,第一个括号捕获a
,第二个括号捕获c
。
注意,使用组匹配时,不宜同时使用g
修饰符,否则match
方法不会捕获分组的内容。
var m = 'abcabc'.match(/(.)b(.)/g)
m // ['abc', 'abc']
上面代码使用带g
修饰符的正则表达式,结果match
方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的exec
方法,配合循环,才能读到每一轮匹配的组捕获。
var str = 'abcabc'
var reg = /(.)b(.)/g
while (true) {
var result = reg.exec(str)
if (!result) break
console.log(result)
}
// ["abc", "a", "c"]
// ["abc", "a", "c"]
正则表达式内部,还可以用\n
引用括号匹配的内容,n
是从 1 开始的自然数,表示对应顺序的括号。
;/(.)b(.)\1b\2/.test('abcabc')
// true
上面的代码中,\1
表示第一个括号匹配的内容(即a
),\2
表示第二个括号匹配的内容(即c
)。
下面是另外一个例子。
;/y(..)(.)\2\1/.test('yabccab') // true
括号还可以嵌套。
;/y((..)\2)\1/.test('yabababab') // true
上面代码中,\1
指向外层括号,\2
指向内层括号。
组匹配非常有用,下面是一个匹配网页标签的例子。
var tagName = /<([^>]+)>[^<]*<\/\1>/
tagName.exec('<b>bold</b>')[1]
// 'b'
上面代码中,圆括号匹配尖括号之中的标签,而\1
就表示对应的闭合标签。
上面代码略加修改,就能捕获带有属性的标签。
var html = '<b class="hello">Hello</b><i>world</i>'
var tag = /<(\w+)([^>]*)>(.*?)<\/\1>/g
var match = tag.exec(html)
match[1] // "b"
match[2] // " class="hello""
match[3] // "Hello"
match = tag.exec(html)
match[1] // "i"
match[2] // ""
match[3] // "world"
(2)非捕获组
(?:x)
称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。
非捕获组的作用请考虑这样一个场景,假定需要匹配foo
或者foofoo
,正则表达式就应该写成/(foo){1, 2}/
,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1, 2}/
,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容。
请看下面的例子。
var m = 'abc'.match(/(?:.)b(.)/)
m // ["abc", "c"]
上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容。
下面是用来分解网址的正则表达式。
// 正常匹配
var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/
url.exec('http://google.com/')
// ["http://google.com/", "http", "google.com", "/"]
// 非捕获组匹配
var url = /(?:http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/
url.exec('http://google.com/')
// ["http://google.com/", "google.com", "/"]
上面的代码中,前一个正则表达式是正常匹配,第一个括号返回网络协议;后一个正则表达式是非捕获匹配,返回结果中不包括网络协议。
(3)先行断言
x(?=y)
称为先行断言(Positive look-ahead),x
只有在y
前面才匹配,y
不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成/\d+(?=%)/
。
“先行断言”中,括号里的部分是不会返回的。
var m = 'abc'.match(/b(?=c)/)
m // ["b"]
上面的代码使用了先行断言,b
在c
前面所以被匹配,但是括号对应的c
不会被返回。
(4)先行否定断言
x(?!y)
称为先行否定断言(Negative look-ahead),x
只有不在y
前面才匹配,y
不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成/\d+(?!%)/
。
;/\d+(?!\.)/.exec('3.14')
// ["14"]
上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14
。
“先行否定断言”中,括号里的部分是不会返回的。
var m = 'abd'.match(/b(?!c)/)
m // ['b']
上面的代码使用了先行否定断言,b
不在c
前面所以被匹配,而且括号对应的d
不会被返回。
十一、JSON 对象
1、JSON 格式
JSON 格式(JavaScript Object Notation 的缩写)是一种用于数据交换的文本格式,2001 年由 Douglas Crockford 提出,目的是取代繁琐笨重的 XML 格式。
相比 XML 格式,JSON 格式有两个显著的优点:书写简单,一目了然;符合 JavaScript 原生语法,可以由解释引擎直接处理,不用另外添加解析代码。所以,JSON 迅速被接受,已经成为各大网站交换数据的标准格式,并被写入标准。
每个 JSON 对象就是一个值,可能是一个数组或对象,也可能是一个原始类型的值。总之,只能是一个值,不能是两个或更多的值。
JSON 对值的类型和格式有严格的规定。
- 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
- 原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和
null
(不能使用NaN
,Infinity
,-Infinity
和undefined
)。- 字符串必须使用双引号表示,不能使用单引号。
- 对象的键名必须放在双引号里面。
- 数组或对象最后一个成员的后面,不能加逗号。
以下都是合法的 JSON。
["one", "two", "three"]
{ "one": 1, "two": 2, "three": 3 }
{"names": ["张三", "李四"] }
[ { "name": "张三"}, {"name": "李四"} ]
以下都是不合法的 JSON。
{ name: "张三", 'age': 32 } // 属性名必须使用双引号
[32, 64, 128, 0xFFF] // 不能使用十六进制值
{ "name": "张三", "age": undefined } // 不能使用 undefined
{ "name": "张三",
"birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'),
"getName": function () {
return this.name;
}
} // 属性值不能使用函数和日期对象
注意,null
、空数组和空对象都是合法的 JSON 值。
2、JSON 对象
JSON
对象是 JavaScript 的原生对象,用来处理 JSON 格式数据。它有两个静态方法:JSON.stringify()
和JSON.parse()
。
3、JSON.stringify() 转为 JSON 字符串
3.1 基本用法
JSON.stringify
方法用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式,并且可以被JSON.parse
方法还原。
// 单个字符串、数值、布尔值、null、空数组、空对象,都是合法json格式。
JSON.stringify('abc') // ""abc"" 注意:字符串会转json字符串会有两对双引号
JSON.stringify(1) // "1"
JSON.stringify(false) // "false"
JSON.stringify(null) // "null"
JSON.stringify([]) // "[]"
JSON.stringify({}) // "{}"
JSON.stringify([1, 'false', false])
// '[1,"false",false]'
JSON.stringify({ name: '张三' })
// '{"name":"张三"}'
上面代码将各种类型的值,转成 JSON 字符串。
注意,对于原始类型的字符串,转换结果会带双引号。
JSON.stringify('foo') === 'foo' // false
JSON.stringify('foo') === '"foo"' // true
上面代码中,字符串foo
,被转成了"\"foo\""
。这是因为将来还原的时候,内层双引号可以让 JavaScript 引擎知道,这是一个字符串,而不是其他类型的值。
JSON.stringify(false) // "false"
JSON.stringify('false') // "\"false\""
上面代码中,如果不是内层的双引号,将来还原的时候,引擎就无法知道原始值是布尔值还是字符串。
如果对象的属性是undefined
、函数或 XML 对象,该属性会被JSON.stringify
过滤。
var obj = {
a: undefined,
b: function () {}
}
JSON.stringify(obj) // "{}" 对象属性是undefined、函数或 XML 对象会被过滤掉
上面代码中,对象obj
的a
属性是undefined
,而b
属性是一个函数,结果都被JSON.stringify
过滤。
如果数组的成员是undefined
、函数或 XML 对象,则这些值被转成null
。
var arr = [undefined, function () {}]
JSON.stringify(arr) // "[null,null]" 数组成员是undefined、函数或 XML 对象会被转成null
上面代码中,数组arr
的成员是undefined
和函数,它们都被转成了null
。
正则对象会被转成空对象。
JSON.stringify(/foo/) // "{}"
JSON.stringify
方法会忽略对象的不可遍历的属性。
var obj = {}
Object.defineProperties(obj, {
foo: {
value: 1,
enumerable: true
},
bar: {
value: 2,
enumerable: false
}
})
JSON.stringify(obj) // "{"foo":1}"
上面代码中,bar
是obj
对象的不可遍历属性,JSON.stringify
方法会忽略这个属性。
3.2 第二个参数 (一个数组,对象属性白名单;或一个函数,改写返回值)
JSON.stringify
方法还可以接受一个数组,作为第二个参数,指定需要转成字符串的属性。
var obj = {
prop1: 'value1',
prop2: 'value2',
prop3: 'value3'
}
var selectedProperties = ['prop1', 'prop2'] // 指定prop1、prop2属性转成字符串
JSON.stringify(obj, selectedProperties)
// "{"prop1":"value1","prop2":"value2"}"
上面代码中,JSON.stringify
方法的第二个参数指定,只转prop1
和prop2
两个属性。
这个类似白名单的数组,只对对象的属性有效,对数组无效。
JSON.stringify(['a', 'b'], ['0'])
// "["a","b"]"
JSON.stringify({ 0: 'a', 1: 'b' }, ['0'])
// "{"0":"a"}"
上面代码中,第二个参数指定 JSON 格式只转0
号属性,实际上对数组是无效的,只对对象有效。
第二个参数还可以是一个函数,用来更改JSON.stringify
的返回值。
function f(key, value) {
// 接收两个参数:键名、键值
if (typeof value === 'number') {
value = 2 * value
}
return value
}
JSON.stringify({ a: 1, b: 2 }, f)
// '{"a": 2,"b": 4}'
上面代码中的f
函数,接受两个参数,分别是被转换的对象的键名和键值。如果键值是数值,就将它乘以2
,否则就原样返回。
注意,这个处理函数是递归处理所有的键。
var o = { a: { b: 1 } }
function f(key, value) {
// 每一次处理的对象,都是前一次返回的值
console.log('[' + key + ']:' + value)
return value
}
JSON.stringify(o, f)
// []:[object Object] 第一次键名 空,键值是o整个对象
// [a]:[object Object] 第二次键名 a,键值是{b:1}
// [b]:1 第三次键名 b,键值是 1
// '{"a":{"b":1}}'
上面代码中,对象o
一共会被f
函数处理三次,最后那行是JSON.stringify
的输出。第一次键名为空,键值是整个对象o
;第二次键名为a
,键值是{b: 1}
;第三次键名为b
,键值为 1。
递归处理中,每一次处理的对象,都是前一次返回的值。
var o = { a: 1 }
function f(key, value) {
if (typeof value === 'object') {
return { b: 2 }
}
return value * 2
}
JSON.stringify(o, f)
// "{"b": 4}"
上面代码中,f
函数修改了对象o
,接着JSON.stringify
方法就递归处理修改后的对象o
。
如果处理函数返回undefined
或没有返回值,则该属性会被忽略。
function f(key, value) {
if (typeof value === 'string') {
return undefined
}
return value
}
JSON.stringify({ a: 'abc', b: 123 }, f)
// '{"b": 123}'
上面代码中,a
属性经过处理后,返回undefined
,于是该属性被忽略了。
3.3 第三个参数 (增加返回 JSON 字符串的可读性;json 格式)
JSON.stringify
还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性。如果是数字,表示每个属性前面添加的空格(最多不超过 10 个);如果是字符串(不超过 10 个字符),则该字符串会添加在每行前面。
JSON.stringify({ p1: 1, p2: 2 }, null, 2)
/*
"{
"p1": 1,
"p2": 2
}"
*/
// 参数三是数值在使用JSON.parse()解析时正常
JSON.stringify({ p1: 1, p2: 2 }, null, '|-')
/*
"{
|-"p1": 1,
|-"p2": 2
}"
*/
// 参数三是字符串在使用JSON.parse()解析时报错
3.4 参数对象的 toJSON 方法(对象自定义 toJSON 方法,转字符串只取该方法返回值)
如果参数对象有自定义的toJSON
方法,那么JSON.stringify
会使用这个方法的返回值作为参数,而忽略原对象的其他属性。
下面是一个普通的对象。
var user = {
firstName: '三',
lastName: '张',
get fullName() {
return this.lastName + this.firstName
}
}
JSON.stringify(user)
// "{"firstName":"三","lastName":"张","fullName":"张三"}"
现在,为这个对象加上toJSON
方法。
var user = {
firstName: '三',
lastName: '张',
get fullName() {
return this.lastName + this.firstName
},
toJSON: function () {
// 只使用toJSON的返回值作为参数,忽略user对象的其他参数
return {
name: this.lastName + this.firstName
}
}
}
JSON.stringify(user)
// "{"name":"张三"}"
上面代码中,JSON.stringify
发现参数对象有toJSON
方法,就直接使用这个方法的返回值作为参数,而忽略原对象的其他参数。
Date
对象就有一个自己的toJSON
方法。
var date = new Date('2015-01-01')
date.toJSON() // "2015-01-01T00:00:00.000Z"
JSON.stringify(date) // ""2015-01-01T00:00:00.000Z""
上面代码中,JSON.stringify
发现处理的是Date
对象实例,就会调用这个实例对象的toJSON
方法,将该方法的返回值作为参数。
toJSON
方法的一个应用是,将正则对象自动转为字符串。因为JSON.stringify
默认不能转换正则对象,但是设置了toJSON
方法以后,就可以转换正则对象了。
var obj = {
reg: /foo/
}
// 不设置 toJSON 方法时
JSON.stringify(obj) // "{"reg":{}}"
// 设置 toJSON 方法时
RegExp.prototype.toJSON = RegExp.prototype.toString
JSON.stringify(/foo/) // ""/foo/""
上面代码在正则对象的原型上面部署了toJSON()
方法,将其指向toString()
方法,因此转换成 JSON 格式时,正则对象就先调用toJSON()
方法转为字符串,然后再被JSON.stringify()
方法处理。
4、JSON.parse()
JSON.parse
方法用于将 JSON 字符串转换成对应的 JSON 值。
JSON.parse('{}') // {}
JSON.parse('true') // true
JSON.parse('"foo"') // "foo"
JSON.parse('[1, 5, "false"]') // [1, 5, "false"]
JSON.parse('null') // null
var o = JSON.parse('{"name": "张三"}')
o.name // 张三
如果传入的字符串不是有效的 JSON 格式,JSON.parse
方法将报错。
JSON.parse("'String'") // illegal single quotes
// SyntaxError: Unexpected token ILLEGAL
上面代码中,双引号字符串中是一个单引号字符串,因为单引号字符串不符合 JSON 格式,所以报错。
为了处理解析错误,可以将JSON.parse
方法放在try...catch
代码块中。
try {
JSON.parse("'String'")
} catch (e) {
console.log('parsing error')
}
JSON.parse
方法可以接受一个处理函数,作为第二个参数,用法与JSON.stringify
方法类似。
function f(key, value) {
// 参数:键名、键值
if (key === 'a') {
return value + 10
}
return value
}
JSON.parse('{"a": 1, "b": 2}', f)
// {a: 11, b: 2}
上面代码中,JSON.parse
的第二个参数是一个函数,如果键名是a
,该函数会将键值加上 10。