<h2 id="3.1">Object對象</h2>
## 概述
JavaScript原生提供一個`Object`對象(注意起首的`O`是大寫),所有其他對象都繼承自這個對象。`Object`本身也是一個構造函數,可以直接通過它來生成新對象。
```javascript
var o = new Object();
```
`Object`作為構造函數使用時,可以接受一個參數。如果該參數是一個對象,則直接返回這個對象;如果是一個原始類型的值,則返回該值對應的包裝對象。
```javascript
var o1 = {a: 1};
var o2 = new Object(o1);
o1 === o2 // true
new Object(123) instanceof Number
// true
```
> 注意,通過`new Object()`的寫法生成新對象,與字面量的寫法`o = {}`是等價的。
與其他構造函數一樣,如果要在`Object`對象上面部署一個方法,有兩種做法。
**(1)部署在Object對象本身**
比如,在Object對象上面定義一個print方法,顯示其他對象的內容。
```javascript
Object.print = function(o){ console.log(o) };
var o = new Object();
Object.print(o)
// Object
```
**(2)部署在Object.prototype對象**
所有構造函數都有一個prototype屬性,指向一個原型對象。凡是定義在Object.prototype對象上面的屬性和方法,將被所有實例對象共享。(關于prototype屬性的詳細解釋,參見《面向對象編程》一章。)
```javascript
Object.prototype.print = function(){ console.log(this)};
var o = new Object();
o.print() // Object
```
上面代碼在Object.prototype定義了一個print方法,然后生成一個Object的實例o。o直接繼承了Object.prototype的屬性和方法,可以在自身調用它們,也就是說,o對象的print方法實質上是調用Object.prototype.print方法。。
可以看到,盡管上面兩種寫法的print方法功能相同,但是用法是不一樣的,因此必須區分“構造函數的方法”和“實例對象的方法”。
## Object對象的方法
### Object()
Object本身當作工具方法使用時,可以將任意值轉為對象。其中,原始類型的值轉為對應的包裝對象(參見《原始類型的包裝對象》一節)。
```javascript
Object() // 返回一個空對象
Object(undefined) // 返回一個空對象
Object(null) // 返回一個空對象
Object(1) // 等同于 new Number(1)
Object('foo') // 等同于 new String('foo')
Object(true) // 等同于 new Boolean(true)
Object([]) // 返回原數組
Object({}) // 返回原對象
Object(function(){}) // 返回原函數
```
上面代碼表示Object函數將各種值,轉為對應的對象。
如果Object函數的參數是一個對象,它總是返回原對象。利用這一點,可以寫一個判斷變量是否為對象的函數。
```javascript
function isObject(value) {
return value === Object(value);
}
```
### Object.keys(),Object.getOwnPropertyNames()
Object.keys方法和Object.getOwnPropertyNames方法很相似,一般用來遍歷對象的屬性。它們的參數都是一個對象,都返回一個數組,該數組的成員都是對象自身的(而不是繼承的)所有屬性名。它們的區別在于,Object.keys方法只返回可枚舉的屬性(關于可枚舉性的詳細解釋見后文),Object.getOwnPropertyNames方法還返回不可枚舉的屬性名。
```javascript
var o = {
p1: 123,
p2: 456
};
Object.keys(o)
// ["p1", "p2"]
Object.getOwnPropertyNames(o)
// ["p1", "p2"]
```
上面的代碼表示,對于一般的對象來說,這兩個方法返回的結果是一樣的。只有涉及不可枚舉屬性時,才會有不一樣的結果。
```javascript
var a = ["Hello", "World"];
Object.keys(a)
// ["0", "1"]
Object.getOwnPropertyNames(a)
// ["0", "1", "length"]
```
上面代碼中,數組的length屬性是不可枚舉的屬性,所以只出現在Object.getOwnPropertyNames方法的返回結果中。
由于JavaScript沒有提供計算對象屬性個數的方法,所以可以用這兩個方法代替。
```javascript
Object.keys(o).length
Object.getOwnPropertyNames(o).length
```
一般情況下,幾乎總是使用Object.keys方法,遍歷數組的屬性。
### Object.observe()
Object.observe方法用于觀察對象屬性的變化。
```javascript
var o = {};
Object.observe(o, function(changes) {
changes.forEach(function(change) {
console.log(change.type, change.name, change.oldValue);
});
});
o.foo = 1; // add, 'foo', undefined
o.foo = 2; // update, 'foo', 1
delete o.foo; // delete, 'foo', 2
```
上面代碼表示,通過Object.observe函數,對o對象指定回調函數。一旦o對象的屬性出現任何變化,就會調用回調函數,回調函數通過一個參數對象讀取o的屬性變化的信息。
該方法非常新,只有Chrome瀏覽器的最新版本才部署。
### 其他方法
除了上面提到的方法,Object還有不少其他方法,將在后文逐一詳細介紹。
**(1)對象屬性模型的相關方法**
- Object.getOwnPropertyDescriptor():獲取某個屬性的attributes對象。
- Object.defineProperty():通過attributes對象,定義某個屬性。
- Object.defineProperties():通過attributes對象,定義多個屬性。
- Object.getOwnPropertyNames():返回直接定義在某個對象上面的全部屬性的名稱。
**(2)控制對象狀態的方法**
- Object.preventExtensions():防止對象擴展。
- Object.isExtensible():判斷對象是否可擴展。
- Object.seal():禁止對象配置。
- Object.isSealed():判斷一個對象是否可配置。
- Object.freeze():凍結一個對象。
- Object.isFrozen():判斷一個對象是否被凍結。
**(3)原型鏈相關方法**
- Object.create():生成一個新對象,并該對象的原型。
- Object.getPrototypeOf():獲取對象的Prototype對象。
## Object實例對象的方法
除了`Object`對象本身的方法,還有不少方法是部署在`Object.prototype`對象上的,所有`Object`的實例對象都繼承了這些方法。
`Object`實例對象的方法,主要有以下六個。
- `valueOf()`:返回當前對象對應的值。
- `toString()`:返回當前對象對應的字符串形式。
- `toLocaleString()`:返回當前對象對應的本地字符串形式。
- `hasOwnProperty()`:判斷某個屬性是否為當前對象自身的屬性,還是繼承自原型對象的屬性。
- `isPrototypeOf()`:判斷當前對象是否為另一個對象的原型。
- `propertyIsEnumerable()`:判斷某個屬性是否可枚舉。
本節介紹前兩個方法,其他方法將在后文相關章節介紹。
### Object.prototype.valueOf()
`valueOf`方法的作用是返回一個對象的“值”,默認情況下返回對象本身。
```javascript
var o = new Object();
o.valueOf() === o // true
```
上面代碼比較`o.valueOf()`與`o`本身,兩者是一樣的。
`valueOf`方法的主要用途是,JavaScript自動類型轉換時會默認調用這個方法(詳見《數據類型轉換》一節)。
```javascript
var o = new Object();
1 + o // "1[object Object]"
```
上面代碼將對象`o`與數字`1`相加,這時JavaScript就會默認調用`valueOf()`方法。所以,如果自定義`valueOf`方法,就可以得到想要的結果。
```javascript
var o = new Object();
o.valueOf = function (){
return 2;
};
1 + o // 3
```
上面代碼自定義了`o`對象的`valueOf`方法,于是`1 + o`就得到了`3`。這種方法就相當于用`o.valueOf`覆蓋`Object.prototype.valueOf`。
### Object.prototype.toString()
`toString`方法的作用是返回一個對象的字符串形式,默認情況下返回類型字符串。
```javascript
var o1 = new Object();
o1.toString() // "[object Object]"
var o2 = {a:1};
o2.toString() // "[object Object]"
```
上面代碼表示,對于一個對象調用`toString`方法,會返回字符串`[object Object]`,該字符串說明對象的類型。
字符串`[object Object]`本身沒有太大的用處,但是通過自定義`toString`方法,可以讓對象在自動類型轉換時,得到想要的字符串形式。
```javascript
var o = new Object();
o.toString = function () {
return 'hello';
};
o + ' ' + 'world' // "hello world"
```
上面代碼表示,當對象用于字符串加法時,會自動調用`toString`方法。由于自定義了`toString`方法,所以返回字符串`hello world`。
數組、字符串、函數、Date對象都分別部署了自己版本的`toString`方法,覆蓋了`Object.prototype.toString`方法。
```javascript
[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)"
```
### toString()的應用:判斷數據類型
`Object.prototype.toString`方法返回對象的類型字符串,因此可以用來判斷一個值的類型。
```javascript
var o = {};
o.toString() // "[object Object]"
```
上面代碼調用空對象的`toString`方法,結果返回一個字符串`object Object`,其中第二個`Object`表示該值的構造函數。這是一個十分有用的判斷數據類型的方法。
實例對象可能會自定義`toString`方法,覆蓋掉`Object.prototype.toString`方法。通過函數的`call`方法,可以在任意值上調用`Object.prototype.toString`方法,幫助我們判斷這個值的類型。
```javascript
Object.prototype.toString.call(value)
```
不同數據類型的`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.prototype.toString`可以得到一個實例對象的構造函數。
```javascript
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`運算符更準確的類型判斷函數。
```javascript
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`函數的基礎上,還可以加上專門判斷某種類型數據的方法。
```javascript
['Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp',
'NaN',
'Infinite'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
};
});
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true
```
## 對象的屬性模型
ECMAScript 5對于對象的屬性,提出了一個精確的描述模型。
### 屬性的attributes對象,Object.getOwnPropertyDescriptor()
在JavaScript內部,每個屬性都有一個對應的attributes對象,保存該屬性的一些元信息。使用`Object.getOwnPropertyDescriptor`方法,可以讀取attributes對象。
```javascript
var o = { p: 'a' };
Object.getOwnPropertyDescriptor(o, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
```
上面代碼表示,使用`Object.getOwnPropertyDescriptor`方法,讀取`o`對象的`p`屬性的attributes對象。
`attributes`對象包含如下元信息。
- `value`:表示該屬性的值,默認為`undefined`。
- `writable`:表示該屬性的值(value)是否可以改變,默認為`true`。
- `enumerable`: 表示該屬性是否可枚舉,默認為`true`。如果設為`false`,會使得某些操作(比如`for...in`循環、`Object.keys()`)跳過該屬性。
- `configurable`:表示“可配置性”,默認為true。如果設為false,將阻止某些操作改寫該屬性,比如,無法刪除該屬性,也不得改變該屬性的attributes對象(value屬性除外),也就是說,configurable屬性控制了attributes對象的可寫性。
- `get`:表示該屬性的取值函數(getter),默認為`undefined`。
- `set`:表示該屬性的存值函數(setter),默認為`undefined`。
### Object.defineProperty(),Object.defineProperties()
`Object.defineProperty`方法允許通過定義`attributes`對象,來定義或修改一個屬性,然后返回修改后的對象。它的格式如下:
```javascript
Object.defineProperty(object, propertyName, attributesObject)
```
`Object.defineProperty`方法接受三個參數,第一個是屬性所在的對象,第二個是屬性名(它應該是一個字符串),第三個是屬性的描述對象。比如,新建一個`o`對象,并定義它的`p`屬性,寫法如下。
```javascript
var o = Object.defineProperty({}, 'p', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});
o.p
// 123
o.p = 246;
o.p
// 123
// 因為writable為false,所以無法改變該屬性的值
```
需要注意的是,`Object.defineProperty`方法和后面的`Object.defineProperties`方法,都有性能損耗,會拖慢執行速度,不宜大量使用。
`Object.defineProperty`的一個用途,是設置動態屬性名。
```javascript
Object.defineProperty(obj, someFunction(), {value: true});
```
如果一次性定義或修改多個屬性,可以使用`Object.defineProperties`方法。
```javascript
var o = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: { get: function () { return this.p1 + this.p2 },
enumerable:true,
configurable:true
}
});
o.p1 // 123
o.p2 // "abc"
o.p3 // "123abc"
```
上面代碼中的`p3`屬性,定義了取值函數`get`。這時需要注意的是,一旦定義了取值函數`get`(或存值函數`set`),就不能將`writable`設為`true`,或者同時定義`value`屬性,否則會報錯。
```javascript
var o = {};
Object.defineProperty(o, 'p', {
value: 123,
get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value,
```
上面代碼同時定義了`get`屬性和`value`屬性,結果就報錯。
`Object.defineProperty()`和`Object.defineProperties()`的第三個參數,是一個屬性對象。它的`writable`、`configurable`、`enumerable`這三個屬性的默認值都為`false`。
`writable`屬性為`false`,表示對應的屬性的值將不得改寫。
```javascript
var o = {};
Object.defineProperty(o, 'p', {
value: "bar"
});
o.p // bar
o.p = 'foobar';
o.p // bar
Object.defineProperty(o, 'p', {
value: 'foobar',
});
// TypeError: Cannot redefine property: p
```
上面代碼由于`writable`屬性默認為`false`,導致無法對`p`屬性重新賦值,但是不會報錯(嚴格模式下會報錯)。不過,如果再一次使用`Object.defineProperty`方法對`value`屬性賦值,就會報錯。
`configurable`屬性為`false`,將無法刪除該屬性,也無法修改`attributes`對象(`value`屬性除外)。
```javascript
var o = {};
Object.defineProperty(o, 'p', {
value: 'bar',
});
delete o.p
o.p // "bar"
```
上面代碼中,由于`configurable`屬性默認為`false`,導致無法刪除某個屬性。
`enumerable`屬性為`false`,表示對應的屬性不會出現在`for...in`循環和`Object.keys`方法中。
```javascript
var o = {
p1: 10,
p2: 13,
};
Object.defineProperty(o, 'p3', {
value: 3,
});
for (var i in o) {
console.log(i, o[i]);
}
// p1 10
// p2 13
```
上面代碼中,`p3`屬性是用`Object.defineProperty`方法定義的,由于`enumerable`屬性默認為`false`,所以不出現在`for...in`循環中。
### 可枚舉性(enumerable)
可枚舉性(enumerable)用來控制所描述的屬性,是否將被包括在`for...in`循環之中。具體來說,如果一個屬性的`enumerable`為`false`,下面三個操作不會取到該屬性。
- `for..in`循環
- `Object.keys`方法
- `JSON.stringify`方法
因此,`enumerable`可以用來設置“秘密”屬性。
```javascript
var o = {a: 1, b: 2};
o.c = 3;
Object.defineProperty(o, 'd', {
value: 4,
enumerable: false
});
o.d
// 4
for( var key in o ) console.log( o[key] );
// 1
// 2
// 3
Object.keys(o) // ["a", "b", "c"]
JSON.stringify(o // => "{a:1,b:2,c:3}"
```
上面代碼中,`d`屬性的`enumerable`為`false`,所以一般的遍歷操作都無法獲取該屬性,使得它有點像“秘密”屬性,但還是可以直接獲取它的值。
至于`for...in`循環和`Object.keys`方法的區別,在于前者包括對象繼承自原型對象的屬性,而后者只包括對象本身的屬性。如果需要獲取對象自身的所有屬性,不管enumerable的值,可以使用`Object.getOwnPropertyNames`方法,詳見下文。
考慮到`JSON.stringify`方法會排除`enumerable`為`false`的值,有時可以利用這一點,為對象添加注釋信息。
```javascript
var car = {
id: 123,
color: 'red',
ownerId: 12
};
var owner = {
id: 12,
name: 'Jack'
};
Object.defineProperty(car, 'ownerInfo', {value: owner, enumerable: false});
car.ownerInfo // {id: 12, name: "Jack"}
JSON.stringify(car) // "{"id": 123,"color": "red","ownerId": 12}"
```
上面代碼中,`owner`對象作為注釋,加入`car`對象。由于`ownerInfo`屬性不可枚舉,所以`JSON.stringify`方法最后輸出`car`對象時,會忽略`ownerInfo`屬性。
這提示我們,如果你不愿意某些屬性出現在JSON輸出之中,可以把它的`enumerable`屬性設為`false`。
### Object.getOwnPropertyNames()
Object.getOwnPropertyNames方法返回直接定義在某個對象上面的全部屬性的名稱,而不管該屬性是否可枚舉。
```javascript
var o = Object.defineProperties({}, {
p1: { value: 1, enumerable: true },
p2: { value: 2, enumerable: false }
});
Object.getOwnPropertyNames(o)
// ["p1", "p2"]
```
一般來說,系統原生的屬性(即非用戶自定義的屬性)都是不可枚舉的。
```javascript
// 比如,數組實例自帶length屬性是不可枚舉的
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
// Object.prototype對象的自帶屬性也都是不可枚舉的
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
// 'valueOf',
// 'constructor',
// 'toLocaleString',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString']
```
上面代碼可以看到,數組的實例對象(`[]`)沒有可枚舉屬性,不可枚舉屬性有length;Object.prototype對象也沒有可枚舉屬性,但是有不少不可枚舉屬性。
### Object.prototype.propertyIsEnumerable()
對象實例的propertyIsEnumerable方法用來判斷一個屬性是否可枚舉。
```javascript
var o = {};
o.p = 123;
o.propertyIsEnumerable("p") // true
o.propertyIsEnumerable("toString") // false
```
上面代碼中,用戶自定義的p屬性是可枚舉的,而繼承自原型對象的toString屬性是不可枚舉的。
### 可配置性(configurable)
可配置性(configurable)決定了是否可以修改屬性的描述對象。也就是說,當configurable為false的時候,value、writable、enumerable和configurable都不能被修改了。
```javascript
var o = Object.defineProperty({}, 'p', {
value: 1,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(o,'p', {value: 2})
// TypeError: Cannot redefine property: p
Object.defineProperty(o,'p', {writable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(o,'p', {enumerable: true})
// TypeError: Cannot redefine property: p
Object.defineProperties(o,'p',{configurable: true})
// TypeError: Cannot redefine property: p
```
上面代碼首先生成對象o,并且定義屬性p的configurable為false。然后,逐一改動value、writable、enumerable、configurable,結果都報錯。
需要注意的是,writable只有在從false改為true會報錯,從true改為false則是允許的。
```javascript
var o = Object.defineProperty({}, 'p', {
writable: true
});
Object.defineProperty(o,'p', {writable: false})
// 修改成功
```
至于value,只要writable和configurable有一個為true,就可以改動。
```javascript
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})
// 修改成功
```
可配置性決定了一個變量是否可以被刪除(delete)。
{% highlight javascript %}
var o = Object.defineProperties({}, {
p1: { value: 1, configurable: true },
p2: { value: 2, configurable: false }
});
delete o.p1 // true
delete o.p2 // false
o.p1 // undefined
o.p2 // 2
{% endhighlight %}
上面代碼中的對象o有兩個屬性,p1是可配置的,p2是不可配置的。結果,p2就無法刪除。
需要注意的是,當使用var命令聲明變量時,變量的configurable為false。
```javascript
var a1 = 1;
Object.getOwnPropertyDescriptor(this,'a1')
// Object {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: false
// }
```
而不使用var命令聲明變量時(或者使用屬性賦值的方式聲明變量),變量的可配置性為true。
```javascript
a2 = 1;
Object.getOwnPropertyDescriptor(this,'a2')
// Object {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: true
// }
// 或者寫成
this.a3 = 1;
Object.getOwnPropertyDescriptor(this,'a3')
// Object {
// value: 1,
// writable: true,
// enumerable: true,
// configurable: true
// }
```
上面代碼中的`this.a3 = 1`與`a3 = 1`是等價的寫法。this指的是當前的作用域,更多關于this的解釋,參見《面向對象編程》一章。
這種差異意味著,如果一個變量是使用var命令生成的,就無法用delete命令刪除。也就是說,delete只能刪除對象的屬性。
```javascript
var a1 = 1;
a2 = 1;
delete a1 // false
delete a2 // true
a1 // 1
a2 // ReferenceError: a2 is not defined
```
### 可寫性(writable)
可寫性(writable)決定了屬性的值(value)是否可以被改變。
`javascript
var o = {};
Object.defineProperty(o, "a", { value : 37, writable : false });
o.a // 37
o.a = 25;
o.a // 37
```
上面代碼將o對象的a屬性可寫性設為false,然后改變這個屬性的值,就不會有任何效果。
這實際上將某個屬性的值變成了常量。在ES6中,constant命令可以起到這個作用,但在ES5中,只有通過writable達到同樣目的。
這里需要注意的是,當對a屬性重新賦值的時候,并不會拋出錯誤,只是靜靜地失敗。但是,如果在嚴格模式下,這里就會拋出一個錯誤,即使是對a屬性重新賦予一個同樣的值。
關于可寫性,還有一種特殊情況。就是如果原型對象的某個屬性的可寫性為false,那么派生對象將無法自定義這個屬性。
```javascript
var proto = Object.defineProperty({}, 'foo', {
value: 'a',
writable: false
});
var o = Object.create(proto);
o.foo = 'b';
o.foo // 'a'
```
上面代碼中,對象proto的foo屬性不可寫,結果proto的派生對象o,也不可以再自定義這個屬性了。在嚴格模式下,這樣做還會拋出一個錯誤。但是,有一個規避方法,就是通過覆蓋attributes對象,繞過這個限制,原因是這種情況下,原型鏈會被完全忽視。
```javascript
Object.defineProperty(o, 'foo', { value: 'b' });
o.foo // 'b'
```
### 存取器(accessor)
除了直接定義以外,屬性還可以用存取器(accessor)定義。其中,存值函數稱為setter,使用`set`命令;取值函數稱為getter,使用`get`命令。
```javascript
var o = {
get p() {
return 'getter';
},
set p(value) {
console.log('setter: ' + value);
}
};
```
上面代碼中,`o`對象內部的`get`和`set`命令,分別定義了`p`屬性的取值函數和存值函數。定義了這兩個函數之后,對`p`屬性取值時,取值函數會自動調用;對`p`屬性賦值時,存值函數會自動調用。
```javascript
o.p // "getter"
o.p = 123 // "setter: 123"
```
注意,取值函數Getter不能接受參數,存值函數Setter只能接受一個參數(即屬性的值)。另外,對象也不能與取值函數同名的屬性。比如,上面的對象`o`設置了取值函數`p`以后,就不能再另外定義一個`p`屬性。
存取器往往用于,某個屬性的值需要依賴對象內部數據的場合。
```javascript
var o ={
$n : 5,
get next() { return this.$n++ },
set next(n) {
if (n >= this.$n) this.$n = n;
else throw '新的值必須大于當前值';
}
};
o.next // 5
o.next = 10;
o.next // 10
```
上面代碼中,`next`屬性的存值函數和取值函數,都依賴于對內部屬性`$n`的操作。
存取器也可以通過`Object.defineProperty`定義。
```javascript
var d = new Date();
Object.defineProperty(d, 'month', {
get: function () {
return d.getMonth();
},
set: function (v) {
d.setMonth(v);
}
});
```
上面代碼為`Date`的實例對象`d`,定義了一個可讀寫的`month`屬性。
存取器也可以使用`Object.create`方法定義。
```javascript
var o = Object.create(Object.prototype, {
foo: {
get: function () {
return 'getter';
},
set: function (value) {
console.log('setter: '+value);
}
}
});
```
如果使用上面這種寫法,屬性`foo`必須定義一個屬性描述對象。該對象的`get`和`set`屬性,分別是`foo`的取值函數和存值函數。
利用存取器,可以實現數據對象與DOM對象的雙向綁定。
```javascript
Object.defineProperty(user, 'name', {
get: function () {
return document.getElementById('foo').value;
},
set: function (newValue) {
document.getElementById('foo').value = newValue;
},
configurable: true
});
```
上面代碼使用存取函數,將DOM對象`foo`與數據對象`user`的`name`屬性,實現了綁定。兩者之中只要有一個對象發生變化,就能在另一個對象上實時反映出來。
### 對象的拷貝
有時,我們需要將一個對象的所有屬性,拷貝到另一個對象。ES5沒有提供這個方法,必須自己實現。
```javascript
var extend = function (to, from) {
for (var property in from) {
to[property] = from[property];
}
return to;
}
extend({}, {a: 1})
// {a: 1}
```
上面這個方法的問題在于,如果遇到存取器定義的屬性,會只拷貝值。
```javascript
extend({}, { get a(){ return 1 } })
// {a: 1}
```
為了解決這個問題,我們可以通過`Object.defineProperty`方法來拷貝屬性。
```javascript
var extend = function (to, from) {
for (var property in from) {
Object.defineProperty(to, property, Object.getOwnPropertyDescriptor(from, property));
}
return to;
}
extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })
```
這段代碼還是有問題,拷貝某些屬性時會失效。
```javascript
extend(document.body.style, {
backgroundColor: "red"
});
```
上面代碼的目的是,設置`document.body.style.backgroundColor`屬性為`red`,但是實際上網頁的背景色并不會變紅。但是,如果用第一種簡單拷貝的方法,反而能夠達到目的。這提示我們,可以把兩種方法結合起來,對于簡單屬性,就直接拷貝,對于那些通過描述對象設置的屬性,則使用`Object.defineProperty`方法拷貝。
```javascript
var extend = function (to, from) {
for (var property in from) {
var descriptor = Object.getOwnPropertyDescriptor(from, property);
if (descriptor && ( !descriptor.writable
|| !descriptor.configurable
|| !descriptor.enumerable
|| descriptor.get
|| descriptor.set)) {
Object.defineProperty(to, property, descriptor);
} else {
to[property] = from[property];
}
}
}
```
上面的這段代碼,可以很好地拷貝任意屬性。
## 控制對象狀態
JavaScript提供了三種方法,精確控制一個對象的讀寫狀態,防止對象被改變。最弱一層的保護是preventExtensions,其次是seal,最強的freeze。
### Object.preventExtensions方法
Object.preventExtensions方法可以使得一個對象無法再添加新的屬性。
```javascript
var o = new Object();
Object.preventExtensions(o);
Object.defineProperty(o, "p", { value: "hello" });
// TypeError: Cannot define property:p, object is not extensible.
o.p = 1;
o.p // undefined
```
如果是在嚴格模式下,則會拋出一個錯誤。
```javascript
(function () {
'use strict';
o.p = '1'
}());
// TypeError: Can't add property bar, object is not extensible
```
不過,對于使用了preventExtensions方法的對象,可以用delete命令刪除它的現有屬性。
```javascript
var o = new Object();
o.p = 1;
Object.preventExtensions(o);
delete o.p;
o.p // undefined
```
### Object.isExtensible方法
Object.isExtensible方法用于檢查一個對象是否使用了preventExtensions方法。也就是說,該方法可以用來檢查是否可以為一個對象添加屬性。
```javascript
var o = new Object();
Object.isExtensible(o)
// true
Object.preventExtensions(o);
Object.isExtensible(o)
// false
```
上面代碼新生成了一個o對象,對該對象使用Object.isExtensible方法,返回true,表示可以添加新屬性。對該對象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已經不能添加新屬性了。
### Object.seal方法
Object.seal方法使得一個對象既無法添加新屬性,也無法刪除舊屬性。
```javascript
var o = { p:"hello" };
Object.seal(o);
delete o.p;
o.p // "hello"
o.x = 'world';
o.x // undefined
```
Object.seal還把現有屬性的attributes對象的configurable屬性設為false,使得attributes對象不再能改變。
```javascript
var o = { p: 'a' };
// seal方法之前
Object.getOwnPropertyDescriptor(o, 'p')
// Object {value: "a", writable: true, enumerable: true, configurable: true}
Object.seal(o);
// seal方法之后
Object.getOwnPropertyDescriptor(o, 'p')
// Object {value: "a", writable: true, enumerable: true, configurable: false}
Object.defineProperty(o, 'p', { enumerable: false })
// TypeError: Cannot redefine property: p
```
從上面代碼可以看到,使用seal方法之后,attributes對象的configurable就變成了false,然后如果想改變enumerable就會報錯。
可寫性(writable)有點特別。如果writable為false,使用Object.seal方法以后,將無法將其變成true;但是,如果writable為true,依然可以將其變成false。
```javascript
var o1 = Object.defineProperty({}, 'p', {writable: false});
Object.seal(o1);
Object.defineProperty(o1,'p',{writable:true})
// Uncaught TypeError: Cannot redefine property: p
var o2 = Object.defineProperty({}, 'p', {writable: true});
Object.seal(o2);
Object.defineProperty(o2,'p',{writable:false})
Object.getOwnPropertyDescriptor(o2, 'p')
/* { value: '',
writable: false,
enumerable: true,
configurable: false } */
```
上面代碼中,同樣是使用了Object.seal方法,如果writable原為false,改變這個設置將報錯;如果原為true,則不會有問題。
至于屬性對象的value是否可改變,是由writable決定的。
```javascript
var o = { p: 'a' };
Object.seal(o);
o.p = 'b';
o.p // 'b'
```
上面代碼中,Object.seal方法對p屬性的value無效,是因為此時p屬性的writable為true。
### Object.isSealed方法
Object.isSealed方法用于檢查一個對象是否使用了Object.seal方法。
```javascript
var o = { p: 'a' };
Object.seal(o);
Object.isSealed(o) // true
```
另外,這時isExtensible方法也返回false。
```javascript
var o = { p: 'a' };
Object.seal(o);
Object.isExtensible(o) // false
```
### Object.freeze方法
Object.freeze方法可以使得一個對象無法添加新屬性、無法刪除舊屬性、也無法改變屬性的值,使得這個對象實際上變成了常量。
```javascript
var o = {p:"hello"};
Object.freeze(o);
o.p = "world";
o.p // hello
o.t = "hello";
o.t // undefined
```
上面代碼中,對現有屬性重新賦值(o.p = "world")或者添加一個新屬性,并不會報錯,只是默默地失敗。但是,如果是在嚴格模式下,就會報錯。
```javascript
var o = {p:"hello"};
Object.freeze(o);
// 對現有屬性重新賦值
(function () { 'use strict'; o.p = "world";}())
// TypeError: Cannot assign to read only property 'p' of #<Object>
// 添加不存在的屬性
(function () { 'use strict'; o.t = 123;}())
// TypeError: Can't add property t, object is not extensible
```
### Object.isFrozen方法
Object.isFrozen方法用于檢查一個對象是否使用了Object.freeze()方法。
```javascript
var o = {p:"hello"};
Object.freeze(o);
Object.isFrozen(o) // true
```
### 局限性
需要注意的是,使用上面這些方法鎖定對象的可寫性,但是依然可以通過改變該對象的原型對象,來為它增加屬性。
```javascript
var o = new Object();
Object.preventExtensions(o);
var proto = Object.getPrototypeOf(o);
proto.t = "hello";
o.t
// hello
```
一種解決方案是,把原型也凍結住。
```javascript
var o = Object.seal(
Object.create(Object.freeze({x:1}),
{y: {value: 2, writable: true}})
);
Object.getPrototypeOf(o).t = "hello";
o.hello // undefined
```
<h2 id="3.2">Array對象</h2>
## 概述
`Array`是JavaScript的內置對象,同時也是一個構造函數,可以用它生成新的數組。
作為構造函數時,`Array`可以接受參數,但是不同的參數,會使得`Array`產生不同的行為。
```javascript
// 無參數時,返回一個空數組
new Array() // []
// 單個正整數參數,表示返回的新數組的長度
new Array(1) // [undefined × 1]
new Array(2) // [undefined x 2]
// 單個非正整數參數(比如字符串、布爾值、對象等),
// 則該參數是返回的新數組的成員
new Array('abc') // ['abc']
new Array([1]) // [Array[1]]
// 多參數時,所有參數都是返回的新數組的成員
new Array(1, 2) // [1, 2]
```
從上面代碼可以看到,`Array`作為構造函數,行為很不一致。因此,不建議使用它生成新數組,直接使用數組的字面量是更好的方法。
```javascript
// bad
var arr = new Array(1, 2);
// good
var arr = [1, 2];
```
另外,`Array`作為構造函數時,如果參數是一個正整數,返回的空數組雖然可以取到`length`屬性,但是取不到鍵名。
```javascript
Array(3).length // 3
Array(3)[0] // undefined
Array(3)[1] // undefined
Array(3)[2] // undefined
0 in Array(3) // false
1 in Array(3) // false
2 in Array(3) // false
```
上面代碼中,`Array(3)`是一個長度為3的空數組。雖然可以取到每個位置的鍵值,但是所有的鍵名都取不到。
JavaScript語言的設計規格,就是這么規定的,雖然不是一個大問題,但是還是必須小心。這也是不推薦使用`Array`構造函數的一個理由。
## Array對象的靜態方法
### isArray方法
Array.isArray方法用來判斷一個值是否為數組。它可以彌補typeof運算符的不足。
```javascript
var a = [1, 2, 3];
typeof a // "object"
Array.isArray(a) // true
```
上面代碼表示,`typeof`運算符只能顯示數組的類型是Object,而Array.isArray方法可以對數組返回true。
## Array實例的方法
以下這些Array實例對象的方法,都是數組實例才能使用。如果不想創建實例,只是想單純調用這些方法,可以寫成`[].method.call`(調用對象,參數) 的形式,或者`Array.prototype.method.call`(調用對象,參數)的形式。
### valueOf方法,toString方法
`valueOf`方法返回數組本身。
```javascript
var a = [1, 2, 3];
a.valueOf() // [1, 2, 3]
```
toString 方法返回數組的字符串形式。
```javascript
var a = [1, 2, 3];
a.toString() // "1,2,3"
var a = [1, 2, 3, [4, 5, 6]];
a.toString() // "1,2,3,4,5,6"
```
### push(),pop()
`push`方法用于在數組的末端添加一個或多個元素,并返回添加新元素后的數組長度。注意,該方法會改變原數組。
```javascript
var a = [];
a.push(1) // 1
a.push('a') // 2
a.push(true, {}) // 4
a // [1, 'a', true, {}]
```
上面代碼使用`push`方法,先后往數組中添加了四個成員。
如果需要合并兩個數組,可以這樣寫。
```javascript
var a = [1, 2, 3];
var b = [4, 5, 6];
Array.prototype.push.apply(a, b)
// 或者
a.push.apply(a, b)
// 上面兩種寫法等同于
a.push(4, 5, 6)
a // [1, 2, 3, 4, 5, 6]
```
`push`方法還可以用于向對象添加元素,添加后的對象變成類似數組的對象,即新加入元素的鍵對應數組的索引,并且對象有一個`length`屬性。
```javascript
var a = {a: 1};
[].push.call(a, 2);
a // {a:1, 0:2, length: 1}
[].push.call(a, [3]);
a // {a:1, 0:2, 1:[3], length: 2}
```
`pop`方法用于刪除數組的最后一個元素,并返回該元素。注意,該方法會改變原數組。
```javascript
var a = ['a', 'b', 'c'];
a.pop() // 'c'
a // ['a', 'b']
```
對空數組使用`pop`方法,不會報錯,而是返回`undefined`。
```javascript
[].pop() // undefined
```
`push`和`pop`結合使用,就構成了“后進先出”的棧結構(stack)。
### join(),concat()
`join`方法以參數作為分隔符,將所有數組成員組成一個字符串返回。如果不提供參數,默認用逗號分隔。
```javascript
var a = [1, 2, 3, 4];
a.join(' ') // '1 2 3 4'
a.join(' | ') // "1 | 2 | 3 | 4"
a.join() // "1,2,3,4"
```
通過`call`方法,`join`方法(即Array.prototype.join)也可以用于字符串。
```javascript
Array.prototype.join.call('hello', '-')
// "h-e-l-l-o"
```
`concat`方法用于多個數組的合并。它將新數組的成員,添加到原數組的尾部,然后返回一個新數組,原數組不變。
```javascript
['hello'].concat(['world'])
// ["hello", "world"]
['hello'].concat(['world'], ['!'])
// ["hello", "world", "!"]
```
除了接受數組作為參數,`concat`也可以接受其他類型的值作為參數。它們會作為新的元素,添加數組尾部。
```javascript
[1, 2, 3].concat(4, 5, 6)
// [1, 2, 3, 4, 5, 6]
[1, 2, 3].concat(4, [5, 6])
```
如果不提供參數,`concat`方法返回當前數組的一個淺拷貝。所謂“淺拷貝”,指的是如果數組成員包括復合類型的值(比如對象),則新數組拷貝的是該值的引用。
```javascript
var obj = { a:1 };
var oldArray = [obj];
var newArray = oldArray.concat();
obj.a = 2;
newArray[0].a // 2
```
上面代碼中,原數組包含一個對象,concat方法生成的新數組包含這個對象的引用。所以,改變原對象以后,新數組跟著改變。事實上,只要原數組的成員中包含對象,concat方法不管有沒有參數,總是返回該對象的引用。
concat方法也可以用于將對象合并為數組,但是必須借助call方法。
```javascript
[].concat.call({ a: 1 }, { b: 2 })
// [{ a: 1 }, { b: 2 }]
[].concat.call({ a: 1 }, [2])
// [{a:1}, 2]
// 等同于
[2].concat({a:1})
```
### shift(),unshift()
`shift`方法用于刪除數組的第一個元素,并返回該元素。注意,該方法會改變原數組。
```javascript
var a = ['a', 'b', 'c'];
a.shift() // 'a'
a // ['b', 'c']
```
`shift`方法可以遍歷并清空一個數組。
```javascript
var list = [1, 2, 3, 4, 5, 6];
var item;
while (item = list.shift()) {
console.log(item);
}
list // []
```
`push`和`shift`結合使用,就構成了“先進先出”的隊列結構(queue)。
`unshift`方法用于在數組的第一個位置添加元素,并返回添加新元素后的數組長度。注意,該方法會改變原數組。
```javascript
var a = ['a', 'b', 'c'];
a.unshift('x'); // 4
a // ['x', 'a', 'b', 'c']
```
### reverse()
`reverse`方法用于顛倒數組中元素的順序,使用這個方法以后,返回改變后的原數組。
```javascript
var a = ['a', 'b', 'c'];
a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]
```
### slice()
`slice`方法用于提取原數組的一部分,返回一個新數組,原數組不變。
它的第一個參數為起始位置(從0開始),第二個參數為終止位置(但該位置的元素本身不包括在內)。如果省略第二個參數,則一直返回到原數組的最后一個成員。
```javascript
// 格式
arr.slice(start_index, upto_index);
// 用法
var a = ['a', 'b', 'c'];
a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
```
如果`slice`方法的參數是負數,則表示倒數計算的字符串位置。
```javascript
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]
```
如果參數值大于數組成員的個數,或者第二個參數小于第一個參數,則返回空數組。
```javascript
var a = ['a', 'b', 'c'];
a.slice(4) // []
a.slice(2, 1) // []
```
`slice`方法的一個重要應用,是將類似數組的對象轉為真正的數組。
```javascript
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`方法用于刪除原數組的一部分成員,并可以在被刪除的位置添加入新的數組成員,返回值是被刪除的元素。注意,該方法會改變原數組。
`splice`的第一個參數是刪除的起始位置,第二個參數是被刪除的元素個數。如果后面還有更多的參數,則表示這些就是要被插入數組的新元素。
```javascript
// 格式
arr.splice(index, count_to_remove, addElement1, addElement2, ...);
// 用法
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2) // ["e", "f"]
a // ["a", "b", "c", "d"]
```
上面代碼從原數組位置4開始,刪除了兩個數組成員。
```javascript
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
```
上面代碼除了刪除成員,還插入了兩個新成員。
如果只是單純地插入元素,`splice`方法的第二個參數可以設為0。
```javascript
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]
```
如果只提供第一個參數,則實際上等同于將原數組在指定位置拆分成兩個數組。
```javascript
var a = [1, 2, 3, 4];
a.splice(2) // [3, 4]
a // [1, 2]
```
### sort()
`sort`方法對數組成員進行排序,默認是按照字典順序排序。排序后,原數組將被改變。
```javascript
['d', 'c', 'b', 'a'].sort()
// ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort()
// [1, 2, 3, 4]
[11, 101].sort()
// [101, 11]
[10111,1101,111].sort()
// [10111, 1101, 111]
```
上面代碼的最后兩個例子,需要特殊注意。sort方法不是按照大小排序,而是按照對應字符串的字典順序排序,所以101排在11的前面。
如果想讓sort方法按照自定義方式排序,可以傳入一個函數作為參數,表示按照自定義方法進行排序。該函數本身又接受兩個參數,表示進行比較的兩個元素。如果返回值大于0,表示第一個元素排在第二個元素后面;其他情況下,都是第一個元素排在第二個元素前面。
```javascript
[10111,1101,111].sort(function (a,b){
return a - b;
})
// [111, 1101, 10111]
[
{ 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 }
// ]
```
## ECMAScript 5 新加入的數組方法
ECMAScript 5新增了9個數組實例的方法,分別是map、forEach、filter、every、some、reduce、reduceRight、indexOf和lastIndexOf。其中,前7個與函數式(functional)操作有關。
這些方法可以在數組上使用,也可以在字符串和類似數組的對象上使用,這是它們不同于傳統數組方法的一個地方。
在用法上,這些方法的參數是一個函數,這個作為參數的函數本身又接受三個參數:數組的當前元素elem、該元素的位置index和整個數組arr(詳見下面的實例)。另外,上下文對象(context)可以作為第二個參數,傳入forEach(), every(), some(), filter(), map()方法,用來綁定函數運行時的上下文。
對于不支持這些方法的老式瀏覽器(主要是IE 8及以下版本),可以使用函數庫[es5-shim](https://github.com/kriskowal/es5-shim),或者[Underscore](http://underscorejs.org/#filter)和[Lo-Dash](http://lodash.com/docs#filter)。
### Array.prototype.map()
`map`方法對數組的所有成員依次調用一個函數,根據函數結果返回一個新數組。
```javascript
var numbers = [1, 2, 3];
numbers.map(function (n) { return n + 1 });
// [2, 3, 4]
numbers
// [1, 2, 3]
```
上面代碼中,`numbers`數組的所有成員都加上1,組成一個新數組返回,原數組沒有變化。
`map`方法接受一個函數作為參數。該函數調用時,`map`方法會將其傳入三個參數,分別是當前成員、當前位置和數組本身。
```javascript
[1, 2, 3].map(function(elem, index, arr) {
return elem * elem;
});
// [1, 4, 9]
```
上面代碼中,map方法的回調函數的三個參數之中,`elem`為當前成員的值,`index`為當前成員的位置,`arr`為原數組(`[1, 2, 3]`)。
`map`方法不僅可以用于數組,還可以用于字符串,用來遍歷字符串的每個字符。但是,不能直接使用,而要通過函數的`call`方法間接使用,或者先將字符串轉為數組,然后使用。
```javascript
var upper = function (x) { return x.toUpperCase() };
[].map.call('abc', upper)
// [ 'A', 'B', 'C' ]
// 或者
'abc'.split('').map(upper)
// [ 'A', 'B', 'C' ]
```
其他類似數組的對象(比如`document.querySelectorAll`方法返回DOM節點集合),也可以用上面的方法遍歷。
`map`方法還可以接受第二個參數,表示回調函數執行時`this`所指向的對象。
```javascript
var arr = ['a', 'b', 'c'];
[1, 2].map(function(e){
return this[e];
}, arr)
// ['b', 'c']
```
上面代碼通過`map`方法的第二個參數,將回調函數內部的`this`對象,指向`arr`數組。
`map`方法通過鍵名,遍歷數組的所有成員。所以,只要數組的某個成員取不到鍵名,`map`方法就會跳過它。
```javascript
var f = function(n){ return n + 1 };
[1, undefined, 2].map(f) // [2, NaN, 3]
[1, null, 2].map(f) // [2, 1, 3]
[1, , 2].map(f) // [2, undefined, 3]
```
上面代碼中,數組的成員依次包含`undefined`、`null`和空位。前兩種情況,`map`方法都不會跳過它們,因為可以取到`undefined`和`null`的鍵名。第三種情況,`map`方法實際上跳過第二個位置,因為取不到它的鍵名。
```javascript
1 in [1, , 2] // false
```
上面代碼說明,第二個位置的空位是取不到鍵名的,因此`map`方法會跳過它。
下面的例子會更清楚地說明這一點。
```javascript
[undefined, undefined].map(function (){
console.log('enter...');
return 1;
})
// enter...
// enter...
// [1, 1]
Array(2).map(function (){
console.log('enter...');
return 1;
})
// [undefined x 2]
```
上面代碼中,`Array(2)`生成的空數組是取不到鍵名的,因此`map`方法根本沒有執行,直接返回了`Array(2)`生成的空數組。
### Array.prototype.forEach()
數組實例的`forEach`方法與`map`方法很相似,也是遍歷數組的所有成員,執行某種操作,但是`forEach`方法沒有返回值,一般只用來操作數據。如果需要有返回值,一般使用`map`方法。
```javascript
function log(element, index, array) {
console.log('[' + index + '] = ' + element);
}
[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9
```
從上面代碼可以看到,`forEach`方法和`map`方法的參數格式是一樣的,第一個參數都是一個函數。該函數接受三個參數,分別是當前元素、當前元素的位置(從0開始)、整個數組。
`forEach`方法會跳過數組的空位。
```javascript
var log = function(n) {
console.log(n + 1);
};
[1, undefined, 2].forEach(log)
// 2
// NaN
// 3
[1, null, 2].forEach(log)
// 2
// 1
// 3
[1, , 2].forEach(log)
// 2
// 3
```
上面代碼中,`forEach`方法不會跳過`undefined`和`null`,但會跳過空位。
`forEach`方法也可以接受第二個參數,用來綁定回調函數的this關鍵字。
```javascript
var out = [];
[1, 2, 3].forEach(function(elem) {
this.push(elem * elem);
}, out);
out // [1, 4, 9]
```
上面代碼中,空數組`out`是`forEach`方法的第二個參數,結果,回調函數內部的`this`關鍵字就指向`out`。
### filter方法
`filter`方法依次對所有數組成員調用一個測試函數,返回結果為`true`的成員組成一個新數組返回。該方法不會改變原數組。
```javascript
[1, 2, 3, 4, 5].filter(function (elem) {
return (elem > 3);
})
// [4, 5]
```
上面代碼將大于`3`的原數組成員,作為一個新數組返回。
再看一個例子。
```javascript
var arr = [0, 1, 'a', false];
arr.filter(Boolean)
// [1, "a"]
```
上面例子中,通過`filter`方法,返回數組`arr`里面所有布爾值為`true`的成員。
`filter`方法的參數函數可以接受三個參數,第一個參數是當前數組成員的值,這是必需的,后兩個參數是可選的,分別是當前數組成員的位置和整個數組。
```javascript
[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
return index % 2 === 0;
});
// [1, 3, 5]
```
上面代碼返回原數組偶數位置的成員組成的新數組。
`filter`方法還可以接受第二個參數,指定測試函數所在的上下文對象(即`this`對象)。
```javascript
var Obj = function () {
this.MAX = 3;
};
var myFilter = function (item) {
if (item > this.MAX) {
return true;
}
};
var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(myFilter, new Obj())
// [8, 4, 9]
```
上面代碼中,測試函數`myFilter`內部有`this`對象,它可以被`filter`方法的第二個參數綁定。上例中,`myFilter`的`this`綁定了`Obj`對象的實例,返回大于`3`的成員。
### some(),every()
這兩個方法類似“斷言”(assert),用來判斷數組成員是否符合某種條件。
`some`方法對所有元素調用一個測試函數,只要有一個元素通過該測試,就返回`true`,否則返回`false`。
```javascript
var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
return elem >= 3;
});
// true
```
上面代碼表示,如果存在大于等于3的數組成員,就返回`true`。
`every`方法對所有元素調用一個測試函數,只有所有元素通過該測試,才返回`true`,否則返回`false`。
```javascript
var arr = [1, 2, 3, 4, 5];
arr.every(function (elem, index, arr) {
return elem >= 3;
});
// false
```
上面代碼表示,只有所有數組成員大于等于3,才返回true。
從上面的代碼可以看到,`some`和`every`的使用方法與`map`和`forEach`一致,參數完全一模一樣。也就是說,它們也可以使用第二個參數,用來綁定函數中的this關鍵字。
### reduce方法,reduceRight方法
reduce方法和reduceRight方法的作用,是依次處理數組的每個元素,最終累計為一個值。這兩個方法的差別在于,reduce對數組元素的處理順序是從左到右(從第一個成員到最后一個成員),reduceRight則是從右到左(從最后一個成員到第一個成員),其他地方完全一樣。
reduce方法的第一個參數是一個處理函數。該函數接受四個參數,分別是:
1. 初始變量,默認為數組的第一個元素值。函數第一次執行后的返回值作為函數第二次執行的初始變量,依次類推。
2. 當前變量,如果指定了reduce函數或者reduceRight函數的第二個參數,則該變量為數組的第一個元素的值,否則,為第二個元素的值。
3. 當前變量對應的元素在數組中的序號(從0開始)。
4. 原數組。
這四個參數之中,只有前兩個是必須的,后兩個則是可選的。
```javascript
[1, 2, 3, 4, 5].reduce(function(x, y){
console.log(x,y)
return x+y;
});
// 1 2
// 3 3
// 6 4
// 10 5
//最后結果:15
```
上面代碼未指定reduce函數的第二個參數,因此,第一輪中,x為1,y為2。然后,第二輪開始,x為上一輪的返回值,y為3。依次執行,直到遍歷完數組中所有元素。所以最終結果為15。
利用reduce方法,可以寫一個數組求和的sum方法。
```javascript
Array.prototype.sum = function (){
return this.reduce(function (partial, value){
return partial + value;
})
};
[3,4,5,6,10].sum()
// 28
```
如果要對初始變量指定初值,可以把它放在reduce方法的第二個參數。
```javascript
[1, 2, 3, 4, 5].reduce(function(x, y){
return x+y;
}, 10);
// 25
```
上面代碼指定參數x的初值為10,所以數組元素從10開始累加,最終結果為25。
由于reduce方法依次處理每個元素,所以實際上還可以用它來搜索某個元素。比如,下面代碼是找出長度最長的數組元素。
```javascript
function findLongest(entries) {
return entries.reduce(function (longest, entry) {
return entry.length > longest.length ? entry : longest;
}, '');
}
```
### indexOf 和 lastIndexOf
`indexOf`方法返回給定元素在數組中第一次出現的位置,如果沒有出現則返回`-1`。
```javascript
var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1
```
indexOf方法還可以接受第二個參數,表示搜索的開始位置。
```javascript
['a', 'b', 'c'].indexOf('a', 1) // -1
```
上面代碼從位置1開始搜索字符`a`,結果為-1,表示沒有搜索到。
`lastIndexOf`方法返回給定元素在數組中最后一次出現的位置,如果沒有出現則返回-1。
```javascript
var a = [2, 5, 9, 2];
a.lastIndexOf(2)
// 3
a.lastIndexOf(7)
// -1
```
注意,如果數組中包含NaN,這兩個方法不適用。
```javascript
[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1
```
這是因為這兩個方法內部,使用嚴格相等運算符(===)進行比較,而NaN是唯一一個不等于自身的值。
### 鏈式使用
上面這些數組方法之中,有不少返回的還是數組,所以可以鏈式使用。
```javascript
var users = [{name:"tom", email:"tom@example.com"},
{name:"peter", email:"peter@example.com"}];
users
.map(function (user){ return user.email; })
.filter(function (email) { return /^t/.test(email); })
.forEach(alert);
// 彈出tom@example.com
```
<h2 id="3.3">包裝對象和Boolean對象</h2>
## 包裝對象
### 定義
在JavaScript中,“一切皆對象”,數組和函數本質上都是對象,就連三種原始類型的值——數值、字符串、布爾值——在一定條件下,也會自動轉為對象,也就是原始類型的“包裝對象”。
所謂“包裝對象”,就是分別與數值、字符串、布爾值相對應的Number、String、Boolean三個原生對象。這三個原生對象可以把原始類型的值變成(包裝成)對象。
```javascript
var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
```
上面代碼根據原始類型的值,生成了三個對象,與原始值的類型不同。這用`typeof`運算符就可以看出來。
```javascript
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false
```
JavaScript設計包裝對象的最大目的,首先是使得JavaScript的“對象”涵蓋所有的值。其次,使得原始類型的值可以方便地調用特定方法。
### 包裝對象的構造函數
Number、String和Boolean這三個原生對象,既可以當作構造函數使用(即加上new關鍵字,生成包裝對象實例),也可以當作工具方法使用(即不加new關鍵字,直接調用),這相當于生成實例后再調用valueOf方法,常常用于將任意類型的值轉為某種原始類型的值。
```javascript
Number(123) // 123
String("abc") // "abc"
Boolean(true) // true
```
工具方法的詳細介紹參見第二章的《數據類型轉換》一節。
### 包裝對象實例的方法
包裝對象實例可以使用Object對象提供的原生方法,主要是 valueOf 方法和 toString 方法。
**(1)valueOf方法**
valueOf方法返回包裝對象實例對應的原始類型的值。
```javascript
new Number(123).valueOf()
// 123
new String("abc").valueOf()
// "abc"
new Boolean("true").valueOf()
// true
```
**(2)toString方法**
toString方法返回該實例對應的原始類型值的字符串形式。
```javascript
new Number(123).toString()
// "123"
new String("abc").toString()
// "abc"
new Boolean("true").toString()
// "true"
```
### 原始類型的自動轉換
原始類型的值,可以自動當作對象調用,即調用各種對象的方法和參數。這時,JavaScript引擎會自動將原始類型的值轉為包裝對象,在使用后立刻銷毀。
比如,字符串可以調用`length`屬性,返回字符串的長度。
```javascript
'abc'.length // 3
```
上面代碼中,`abc`是一個字符串,本身不是對象,不能調用`length`屬性。JavaScript引擎自動將其轉為包裝對象,在這個對象上調用`length`屬性。調用結束后,這個臨時對象就會被銷毀。這就叫原始類型的自動轉換。
```javascript
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`屬性、還有一個內部屬性`[[PrimitiveValue]]`保存字符串的原始值。這個`[[PrimitiveValue]]`內部屬性,外部是無法調用,僅供`ValueOf`或`toString`這樣的方法內部調用。
這個臨時對象是只讀的,無法修改。所以,字符串無法添加新屬性。
```javascript
var s = 'Hello World';
s.x = 123;
s.x // undefined
```
上面代碼為字符串`s`添加了一個`x`屬性,結果無效,總是返回`undefined`。
另一方面,調用結束后,臨時對象會自動銷毀。這意味著,下一次調用字符串的屬性時,實際是調用一個新生成的對象,而不是上一次調用時生成的那個對象,所以取不到賦值在上一個對象的屬性。如果想要為字符串添加屬性,只有在它的原型對象`String.prototype`上定義(參見《面向對象編程》一章)。
這種原始類型值可以直接調用的方法還有很多(詳見后文對各包裝對象的介紹),除了前面介紹過的valueOf和toString方法,還包括三個包裝對象各自定義在實例上的方法。。
```javascript
'abc'.charAt === String.prototype.charAt
// true
```
上面代碼表示,字符串abc的charAt方法,實際上就是定義在String對象實例上的方法(關于prototype對象的介紹參見《面向對象編程》一章)。
如果包裝對象與原始類型值進行混合運算,包裝對象會轉化為原始類型(實際是調用自身的valueOf方法)。
```javascript
new Number(123) + 123
// 246
new String("abc") + "abc"
// "abcabc"
```
### 自定義方法
三種包裝對象還可以在原型上添加自定義方法和屬性,供原始類型的值直接調用。
比如,我們可以新增一個double方法,使得字符串和數字翻倍。
```javascript
String.prototype.double = function (){
return this.valueOf() + this.valueOf();
};
"abc".double()
// abcabc
Number.prototype.double = function (){
return this.valueOf() + this.valueOf();
};
(123).double()
// 246
```
上面代碼在123外面必須要加上圓括號,否則后面的點運算符(.)會被解釋成小數點。
但是,這種自定義方法和屬性的機制,只能定義在包裝對象的原型上,如果直接對原始類型的變量添加屬性,則無效。
```javascript
var s = "abc";
s.p = 123;
s.p // undefined
```
上面代碼直接對支付串abc添加屬性,結果無效。
## Boolean對象
### 概述
Boolean對象是JavaScript的三個包裝對象之一。作為構造函數,它主要用于生成布爾值的包裝對象的實例。
```javascript
var b = new Boolean(true);
typeof b // "object"
b.valueOf() // true
```
上面代碼的變量b是一個Boolean對象的實例,它的類型是對象,值為布爾值true。這種寫法太繁瑣,幾乎無人使用,直接對變量賦值更簡單清晰。
```javascript
var b = true;
```
### Boolean實例對象的布爾值
特別要注意的是,所有對象的布爾運算結果都是true。因此,false對應的包裝對象實例,布爾運算結果也是true。
```javascript
if (new Boolean(false)) {
console.log("true");
} // true
if (new Boolean(false).valueOf()) {
console.log("true");
} // 無輸出
```
上面代碼的第一個例子之所以得到true,是因為false對應的包裝對象實例是一個對象,進行邏輯運算時,被自動轉化成布爾值true(所有對象對應的布爾值都是true)。而實例的valueOf方法,則返回實例對應的原始類型值,本例為false。
### Boolean函數的類型轉換作用
Boolean對象除了可以作為構造函數,還可以單獨使用,將任意值轉為布爾值。這時Boolean就是一個單純的工具方法。
```javascript
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的情況,都值得認真記住。
使用not運算符(!)也可以達到同樣效果。
```javascript
!!undefined // false
!!null // false
!!0 // false
!!'' // false
!!NaN // false
!!1 // true
!!'false' // true
!![] // true
!!{} // true
!!function(){} // true
!!/foo/ // true
```
綜上所述,如果要獲得一個變量對應的布爾值,有多種寫法。
```javascript
var a = "hello world";
new Boolean(a).valueOf() // true
Boolean(a) // true
!!a // true
```
最后,對于一些特殊值,Boolean對象前面加不加new,會得到完全相反的結果,必須小心。
```javascript
if (Boolean(false))
console.log('true'); // 無輸出
if (new Boolean(false))
console.log('true'); // true
if (Boolean(null))
console.log('true'); // 無輸出
if (new Boolean(null))
console.log('true'); // true
```
<h2 id="3.4">Number對象</h2>
## 概述
Number對象是數值對應的包裝對象,可以作為構造函數使用,也可以作為工具函數使用。
作為構造函數時,它用于生成值為數值的對象。
```javascript
var n = new Number(1);
typeof n // "object"
```
上面代碼中,`Number`對象作為構造函數使用,返回一個值為1的對象。
作為工具函數時,它可以將任何類型的值轉為數值。
```javascript
Number(true) // 1
```
上面代碼將布爾值`true`轉為數值1。Number對象的工具方法,詳細介紹參見上一章的《數據類型轉換》一節。
## Number對象的屬性
Number對象擁有以下一些屬性。
- `Number.POSITIVE_INFINITY`:正的無限,指向`Infinity`。
- `Number.NEGATIVE_INFINITY`:負的無限,指向`-Infinity`。
- `Number.NaN`:表示非數值,指向`NaN`。
- `Number.MAX_VALUE`:表示最大的正數,相應的,最小的負數為`-Number.MAX_VALUE`。
- `Number.MIN_VALUE`:表示最小的正數(即最接近0的正數,在64位浮點數體系中為`5e-324`),相應的,最接近0的負數為`-Number.MIN_VALUE`。
- `Number.MAX_SAFE_INTEGER`:表示能夠精確表示的最大整數,即`9007199254740991`。
- `Number.MIN_SAFE_INTEGER`:表示能夠精確表示的最小整數,即`-9007199254740991`。
```javascript
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
```
## Number對象實例的方法
`Number`對象有4個實例方法,都跟將數值轉換成指定格式有關。
### Number.prototype.toString()
`Number`對象部署了自己的`toString`方法,用來將一個數值轉為字符串形式。
```javascript
(10).toString() // "10"
```
`toString`方法可以接受一個參數,表示輸出的進制。如何省略這個參數,默認將數值先轉為十進制,再輸出字符串;否則,就根據參數指定的進制,將一個數字轉化成某個進制的字符串。
```javascript
(10).toString(2) // "1010"
(10).toString(8) // "12"
(10).toString(16) // "a"
```
上面代碼中,之所以要把10放在括號里,是為了表明10是一個單獨的數值,后面的點表示調用對象屬性。如果不加括號,這個點會被JavaScript引擎解釋成小數點,從而報錯。
```javascript
10.toString(2)
// SyntaxError: Unexpected token ILLEGAL
```
只要能夠讓JavaScript引擎不混淆小數點和對象的點運算符,各種寫法都能用。除了為`10`加上括號,還可以在`10`后面加兩個點,JavaScript會把第一個點理解成小數點(即`10.0`),把第二個點理解成調用對象屬性,從而得到正確結果。
```javascript
10..toString(2)
// "1010"
// 其他方法還包括
10 .toString(2) // "1010"
10.0.toString(2) // "1010"
```
這實際上意味著,可以直接對一個小數使用`toString`方法。
```javascript
10.5.toString() // "10.5"
10.5.toString(2) // "1010.1"
10.5.toString(8) // "12.4"
10.5.toString(16) // "a.8"
```
通過方括號運算符也可以調用`toString`方法。
```javascript
10['toString'](2) // "1010"
```
將其他進制的數,轉回十進制,需要使用`parseInt`方法。
### Number.prototype.toFixed()
`toFixed`方法用于將一個數轉為指定位數的小數,返回這個小數對應的字符串。
```javascript
(10).toFixed(2) // "10.00"
10.005.toFixed(2) // "10.01"
```
上面代碼分別將`10`和`10.005`轉成2位小數的格式。其中,`10`必須放在括號里,否則后面的點運算符會被處理小數點,而不是表示調用對象的方法;而`10.005`就不用放在括號里,因為第一個點被解釋為小數點,第二個點就只能解釋為點運算符。
`toFixed`方法的參數為指定的小數位數,有效范圍為0到20,超出這個范圍將拋出RangeError錯誤。
### Number.prototype.toExponential()
`toExponential`方法用于將一個數轉為科學計數法形式。
```javascript
(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(1) // "1.23e+3"
```
`toExponential`方法的參數表示小數點后有效數字的位數,范圍為0到20,超出這個范圍,會拋出一個RangeError。
### Number.prototype.toPrecision()
`toPrecision`方法用于將一個數轉為指定位數的有效數字。
```javascript
(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"
```
`toPrecision`方法的參數為有效數字的位數,范圍是1到21,超出這個范圍會拋出RangeError錯誤。
`toPrecision`方法用于四舍五入時不太可靠,跟浮點數不是精確儲存有關。
```javascript
(12.35).toPrecision(3) // "12.3"
(12.25).toPrecision(3) // "12.3"
(12.15).toPrecision(3) // "12.2"
(12.45).toPrecision(3) // "12.4"
```
## 自定義方法
與其他對象一樣,`Number.prototype`對象上面可以自定義方法,被`Number`的實例繼承。
```javascript
Number.prototype.add = function (x) {
return this + x;
};
```
上面代碼為`Number`對象實例定義了一個`add`方法。
在數值上調用某個方法,數值會自動轉為`Number`的實例對象,所以就得到了下面的結果。
```javascript
8['add'](2) // 10
```
上面代碼中,調用方法之所以寫成`8['add']`,而不是`8.add`,是因為數值后面的點,會被解釋為小數點,而不是點運算符。將數值放在圓括號中,就可以使用點運算符調用方法了。
```javascript
(8).add(2) // 10
```
由于add方法返回的還是數值,所以可以鏈式運算。
```javascript
Number.prototype.subtract = function (x) {
return this - x;
};
(8).add(2).subtract(4)
// 6
```
上面代碼在`Number`對象的實例上部署了`subtract`方法,它可以與`add`方法鏈式調用。
我們還可以部署更復雜的方法。
```javascript
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`上面,數值本身是無法自定義屬性的。
```javascript
var n = 1;
n.x = 1;
n.x // undefined
```
上面代碼中,`n`是一個原始類型的數值。直接在它上面新增一個屬性x,不會報錯,但毫無作用,總是返回undefined。這是因為一旦被調用屬性,n就自動轉為Number的實例對象,調用結束后,該對象自動銷毀。所以,下一次調用n的屬性時,實際取到的是另一個對象,屬性x當然就讀不出來。
<h2 id="3.5">String對象</h2>
## 概述
String對象是JavaScript原生提供的三個包裝對象之一,用來生成字符串的包裝對象實例。
```javascript
var s = new String('abc');
typeof s // "object"
s.valueOf() // "abc"
```
上面代碼生成的變量`s`,就是String對象的實例,類型為對象,值為原來的字符串。實際上,String對象的實例是一個類似數組的對象。
```javascript
new String("abc")
// String {0: "a", 1: "b", 2: "c"}
```
除了用作構造函數,String還可以當作工具方法使用,將任意類型的值轉為字符串。
```javascript
String(true) // "true"
String(5) // "5"
```
上面代碼將布爾值ture和數值5,分別轉換為字符串。
## String.fromCharCode()
String對象直接提供的方法,主要是fromCharCode()。該方法根據Unicode編碼,生成一個字符串。
```javascript
String.fromCharCode(104, 101, 108, 108, 111)
// "hello"
```
注意,該方法不支持編號大于0xFFFF的字符。
```javascript
String.fromCharCode(0x20BB7)
// "?"
```
上面代碼返回字符的編號是0x0BB7,而不是0x20BB7。這種情況下,只能使用四字節的UTF-16編號,得到正確結果。
```javascript
String.fromCharCode(0xD842, 0xDFB7)
// "??"
```
## 實例對象的屬性和方法
### length屬性
該屬性返回字符串的長度。
```javascript
"abc".length
// 3
```
### charAt(),charCodeAt()
`charAt`方法返回給定位置的字符,參數是從0開始編號的位置。
```javascript
var s = new String('abc');
s.charAt(1) // "b"
s.charAt(s.length-1) // "c"
```
這個方法完全可以用數組下標替代。
```javascript
'abc'[1] // "b"
```
`charCodeAt`方法返回給定位置字符的Unicode編號(十進制表示)。
```javascript
'abc'.charCodeAt(1) // 98
```
上面代碼返回第二個位置的字符`b`的Unicode編號“98”。
如果沒有任何參數,`charCodeAt`返回首字符的Unicode編號。
```javascript
'abc'.charCodeAt() // 97
```
上面代碼中,首字符`a`的Unicode編號是97。
需要注意的是,charCodeAt方法返回的Unicode編碼不大于65536(0xFFFF),也就是說,只返回兩個字節。因此如果遇到Unicode大于65536的字符(根據UTF-16的編碼規則,第一個字節在U+D800到U+DBFF之間),就必需連續使用兩次charCodeAt,不僅讀入charCodeAt(i),還要讀入charCodeAt(i+1),將兩個16字節放在一起,才能得到準確的字符。
如果給定位置為負數,或大于等于字符串的長度,則這兩個方法返回NaN。
### concat方法
字符串的`concat`方法用于連接兩個字符串。
```javascript
var s1 = 'abc';
var s2 = 'def';
s1.concat(s2) // "abcdef"
s1 // "abc"
```
使用該方法后,原字符串不受影響,返回一個新字符串。
該方法可以接受多個參數。
```javascript
'a'.concat('b', 'c') // "abc"
```
如果參數不是字符串,`concat`方法會將其先轉為字符串,然后再連接。
```javascript
var one = 1;
var two = 2;
var three = '3';
''.concat(one, two, three) // "123"
one + two + three // "33"
```
上面代碼中,`concat`方法將參數先轉成字符串再連接,所以返回的是一個三個字符的字符串。而加號運算符在兩個運算數都是數值時,不會轉換類型,所以返回的是一個兩個字符的字符串。
### substring(),substr(),slice()
這三個方法都用來返回一個字符串的子串,而不會改變原字符串。它們都可以接受一個或兩個參數,區別只是參數含義的不同。
**(1)substring方法**
substring方法的第一個參數表示子字符串的開始位置,第二個位置表示結束結果。因此,第二個參數應該大于第一個參數。如果出現第一個參數大于第二個參數的情況,substring方法會自動更換兩個參數的位置。
```javascript
var a = 'The Three Musketeers';
a.substring(4, 9) // 'Three'
a.substring(9, 4) // 'Three'
```
上面代碼中,調換substring方法的兩個參數,都得到同樣的結果。
**(2)substr()**
`substr`方法的第一個參數是子字符串的開始位置,第二個參數是子字符串的長度。
```javascript
var b = 'The Three Musketeers';
b.substr(4, 9) // 'Three Mus'
b.substr(9, 4) // ' Mus'
```
**(3)slice()**
`slice`方法用于取出子字符串。它的第一個參數是子字符串的開始位置,第二個參數是子字符串的結束位置。如果省略第二個參數,則表示一直到字符串結束。
```javascript
'JavaScript'.slice(4) // "Script"
'JavaScript'.slice(0, 4) // "Java"
```
如果參數是負值,表示從結尾開始,倒數計算的位置。
```javascript
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(0, -6) // "Java"
```
與`substring`方法不同的是,如果第一個參數大于第二個參數,slice方法并不會自動調換參數位置,而是返回一個空字符串。這種處理比較符合直覺,推薦使用`slice`替代`substring`。
```javascript
var s = 'The Three Musketeers';
s.slice(4, 9) // 'Three'
s.slice(9, 4) // ''
```
**(4)總結:第一個參數的含義**
對這三個方法來說,第一個參數都是子字符串的開始位置,如果省略第二個參數,則表示子字符串一直持續到原字符串結束。
```javascript
"Hello World".slice(3)
// "lo World"
"Hello World".substr(3)
// "lo World"
"Hello World".substring(3)
// "lo World"
```
**(5)總結:第二個參數的含義**
如果提供第二個參數,對于slice和substring方法,表示子字符串的結束位置;對于substr,表示子字符串的長度。
```javascript
"Hello World".slice(3,7)
// "lo W"
"Hello World".substring(3,7)
// "lo W"
"Hello World".substr(3,7)
// "lo Worl"
```
**(6)總結:負的參數**
如果參數為負,對于slice方法,表示字符位置從尾部開始計算。
```javascript
"Hello World".slice(-3)
// "rld"
"Hello World".slice(4,-3)
// "o Wo"
```
對于substring方法,會自動將負數轉為0。
```javascript
"Hello World".substring(-3)
// "Hello World"
"Hello World".substring(4,-3)
// "Hell"
```
對于substr方法,負數出現在第一個參數,表示從尾部開始計算的字符位置;負數出現在第二個參數,將被轉為0。
```javascript
"Hello World".substr(-3)
// "rld"
"Hello World".substr(4,-3)
// ""
```
### indexOf(),lastIndexOf()
這兩個方法用于確定一個字符串在另一個字符串中的位置,如果返回-1,就表示不匹配。兩者的區別在于,`indexOf`從字符串頭部開始匹配,`lastIndexOf`從尾部開始匹配。
```javascript
'hello world'.indexOf('o') // 4
'JavaScript'.indexOf('script') // -1
'hello world'.lastIndexOf('o') // 7
```
它們還可以接受第二個參數,對于`indexOf`方法,第二個位置表示從該位置開始向后匹配;對于`lastIndexOf`,第二個表示從該位置起向前匹配。
```javascript
'hello world'.indexOf('o', 6) // 7
'hello world'.lastIndexOf('o', 6) // 4
```
### trim()
`trim`方法用于去除字符串兩端的空格。
```javascript
' hello world '.trim()
// "hello world"
```
該方法返回一個新字符串,不改變原字符串。
### toLowerCase(),toUpperCase()
`toLowerCase`用于將一個字符串轉為小寫,`toUpperCase`則是轉為大寫。
```javascript
'Hello World'.toLowerCase()
// "hello world"
'Hello World'.toUpperCase()
// "HELLO WORLD"
```
### localeCompare方法
該方法用于比較兩個字符串。它返回一個數字,如果小于0,表示第一個字符串小于第二個字符串;如果等于0,表示兩者相等;如果大于0,表示第一個字符串大于第二個字符串。
```javascript
'apple'.localeCompare('banana')
// -1
'apple'.localeCompare('apple')
// 0
```
### 搜索和替換
與搜索和替換相關的有4個方法,它們都允許使用正則表達式。
- **match**:用于確定原字符串是否匹配某個子字符串,返回匹配的子字符串數組。
- **search**:等同于match,但是返回值不一樣。
- **replace**:用于替換匹配的字符串。
- **split**:將字符串按照給定規則分割,返回一個由分割出來的各部分組成的新數組。
下面是這4個方法的簡單介紹。它們都可以使用正則對象,涉及正則對象的部分見《Regex對象》一節。
**(1)match方法**
match方法返回一個數組,成員為匹配的第一個字符串。如果沒有找到匹配,則返回null。返回數組還有index屬性和input屬性,分別表示匹配字符串開始的位置(從0開始)和原始字符串。
```javascript
var matches = "cat, bat, sat, fat".match("at");
matches // ["at"]
matches.index // 1
matches.input // "cat, bat, sat, fat"
```
**(2)search方法**
search方法的用法等同于match,但是返回值為匹配的第一個位置。如果沒有找到匹配,則返回-1。
```javascript
"cat, bat, sat, fat".search("at")
// 1
```
**(3)replace方法**
replace方法用于替換匹配的子字符串,一般情況下只替換第一個匹配(除非使用帶有g修飾符的正則表達式)。
```javascript
"aaa".replace("a", "b")
// "baa"
```
**(4)split方法**
split方法按照給定規則分割字符串,返回一個由分割出來的各部分組成的新數組。
```javascript
"a|b|c".split("|")
// ["a", "b", "c"]
```
如果分割規則為空字符串,則返回數組的成員是原字符串的每一個字符。
```javascript
"a|b|c".split("")
// ["a", "|", "b", "|", "c"]
```
如果省略分割規則,則返回數組的唯一成員就是原字符串。
```javascript
"a|b|c".split()
// ["a|b|c"]
```
如果滿足分割規則的兩個部分緊鄰著(即中間沒有其他字符),則返回數組之中會有一個空字符串。
```javascript
"a||c".split("|")
// ["a", "", "c"]
```
如果滿足分割規則的部分處于字符串的開頭或結尾(即它的前面或后面沒有其他字符),則返回數組的第一個或最后一個成員是一個空字符串。
```javascript
"|b|c".split("|")
// ["", "b", "c"]
"a|b|".split("|")
// ["a", "b", ""]
```
split方法還可以接受第二個參數,限定返回數組的最大成員數。
```javascript
"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"]
```
<h2 id="3.6">Math對象</h2>
`Math`是JavaScript的內置對象,提供一系列數學常數和數學方法。該對象不是構造函數,不能生成實例,所有的屬性和方法都必須在Math對象上調用。
```javascript
new Math()
// TypeError: object is not a function
```
上面代碼表示,`Math`不能當作構造函數用。
## 屬性
`Math`對象提供以下一些只讀的數學常數。
- `Math.E`:常數e。
- `Math.LN2`:2的自然對數。
- `Math.LN10`:10的自然對數。
- `Math.LOG2E`:以2為底的e的對數。
- `Math.LOG10E`:以10為底的e的對數。
- `Math.PI`:常數Pi。
- `Math.SQRT1_2`:0.5的平方根。
- `Math.SQRT2`:2的平方根。
這些常數的值如下。
```javascript
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
```
## 方法
`Math`對象提供以下一些數學方法。
- `Math.abs()`:絕對值
- `Math.ceil()`:向上取整
- `Math.floor()`:向下取整
- `Math.max()`:最大值
- `Math.min()`:最小值
- `Math.pow()`:指數運算
- `Math.sqrt()`:平方根
- `Math.log()`:自然對數
- `Math.exp()`:e的指數
- `Math.round()`:四舍五入
- `Math.random()`:隨機數
`Math.abs`方法返回參數值的絕對值。
```javascript
Math.abs(1) // 1
Math.abs(-1) // 1
```
`Math.max`方法和`Math.min`方法都可以接受多個參數,`Math.max`返回其中最大的參數,`Math.min`返回最小的參數。
```javascript
Math.max(2, -1, 5) // 5
Math.min(2, -1, 5) // -1
```
`Math.floor`方法接受一個參數,返回小于該參數的最大整數。
```javascript
Math.floor(3.2) // 3
Math.floor(-3.2) // -4
```
`Math.ceil`方法接受一個參數,返回大于該參數的最小整數。
```javascript
Math.ceil(3.2) // 4
Math.ceil(-3.2) // -3
```
如果需要一個總是返回某個數值整數部分的函數,可以自己實現。
```javascript
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`函數總是返回一個數值的整數部分。
`Math.round`方法用于四舍五入。
```javascript
Math.round(0.1) // 0
Math.round(0.5) // 1
Math.round(0.6) // 1
// 等同于
Math.ceil(x + 0.5)
```
注意,它對負數的處理,主要是對`0.5`的處理。
```javascript
Math.round(-1.1) // -1
Math.round(-1.5) // -1
Math.round(-1.6) // -2
```
`Math.pow`方法返回以第一個參數為底數、第二個參數為冪的指數值。
```javascript
Math.pow(2, 2) // 4
Math.pow(2, 3) // 8
```
下面是計算圓面積的方法。
```javascript
var radius = 20;
var area = Math.PI * Math.pow(radius, 2);
```
`Math.sqrt`方法返回參數值的平方根。如果參數是一個負值,則返回NaN。
```javascript
Math.sqrt(4) // 2
Math.sqrt(-4) // NaN
```
`Math.log`方法返回以e為底的自然對數值。
```javascript
Math.log(Math.E) // 1
Math.log(10) // 2.302585092994046
```
如果要計算以10為底的對數,可以先用`Math.log`求出自然對數,然后除以`Math.LN10`;求以2為底的對數,可以除以`Math.LN2`。
```javascript
Math.log(100)/Math.LN10 // 2
Math.log(8)/Math.LN2 // 3
```
`Math.exp`方法返回常數e的參數次方。
```javascript
Math.exp(1) // 2.718281828459045
Math.exp(3) // 20.085536923187668
```
### Math.random()
`Math.random()`返回0到1之間的一個偽隨機數,可能等于0,但是一定小于1。
```javascript
Math.random() // 0.7151307314634323
```
任意范圍的隨機數生成函數如下。
```javascript
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
getRandomArbitrary(1.5, 6.5)
// 2.4942810038223864
```
任意范圍的隨機整數生成函數如下。
```javascript
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
getRandomInt(1, 6) // 5
```
返回隨機字符的例子如下。
```javascript
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`內的隨機字符所組成的指定長度的字符串。
### 三角函數方法
`Math`對象還提供一系列三角函數方法。
- `Math.sin()`:返回參數的正弦
- `Math.cos()`:返回參數的余弦
- `Math.tan()`:返回參數的正切
- `Math.asin()`:返回參數的反正弦(弧度值)
- `Math.acos()`:返回參數的反余弦(弧度值)
- `Math.atan()`:返回參數的反正切(弧度值)
```javascript
Math.sin(0) // 0
Math.cos(0) // 1
Math.tan(0) // 0
Math.asin(1) // 1.5707963267948966
Math.acos(1) // 0
Math.atan(1) // 0.7853981633974483
```
<h2 id="3.7">Date對象</h2>
## 概述
`Date`對象是JavaScript提供的日期和時間的操作接口。它可以表示的時間范圍是,1970年1月1日00:00:00前后的各1億天(單位為毫秒)。
`Date`對象可以作為普通函數直接調用,返回一個代表當前時間的字符串。
```javascript
Date()
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"
```
注意,即使帶有參數,`Date`作為普通函數使用時,返回的還是當前時間。
```javascript
Date(2000, 1, 1)
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"
```
上面代碼說明,無論有沒有參數,直接調用`Date`總是返回當前時間。
## new Date()
`Date`還可以當作構造函數使用。對它使用`new`命令,會返回一個`Date`對象的實例。如果不加參數,生成的就是代表當前時間的對象。
```javascript
var today = new Date();
```
這個`Date`實例對應的字符串值,就是當前時間。
```javascript
today
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"
// 等同于
today.toString()
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"
```
作為構造函數時,`Date`對象可以接受多種格式的參數。
**(1)new Date(milliseconds)**
Date對象接受從1970年1月1日00:00:00 UTC開始計算的毫秒數作為參數。這意味著如果將Unix時間戳(單位為秒)作為參數,必須將Unix時間戳乘以1000。
```javascript
new Date(1378218728000)
// Tue Sep 03 2013 22:32:08 GMT+0800 (CST)
// 1970年1月2日的零時
var Jan02_1970 = new Date(3600 * 24 * 1000);
// Fri Jan 02 1970 08:00:00 GMT+0800 (CST)
// 1969年12月31日的零時
var Dec31_1969 = new Date(-3600 * 24 * 1000);
// Wed Dec 31 1969 08:00:00 GMT+0800 (CST)
```
上面最后一個例子說明,Date構造函數的參數可以是一個負數,表示1970年1月1日之前的時間。Date對象能夠表示的日期范圍是1970年1月1日前后各一億天。
**(2)new Date(datestring)**
Date對象還接受一個日期字符串作為參數,返回所對應的時間。
```javascript
new Date('January 6, 2013');
// Sun Jan 06 2013 00:00:00 GMT+0800 (CST)
```
日期字符串的完整格式是“month day, year hours:minutes:seconds”,比如“December 25, 1995 13:30:00”。如果省略了小時、分鐘或秒數,這些值會被設為0。
但是,其他格式的日期字符串,也可以被解析。事實上,所有可以被`Date.parse()`方法解析的日期字符串,都可以當作`Date`對象的參數。
```javascript
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('Feberuary, 15, 2013')
new Date('Feberuary 15, 2013')
new Date('15 Feb 2013')
new Date('15, Feberuary, 2013')
// Fri Feb 15 2013 00:00:00 GMT+0800 (CST)
```
上面多種日期字符串的寫法,返回的都是同一個時間。
注意,在ES5之中,如果日期采用連詞線(`-`)格式分隔,且具有前導0,JavaScript會認為這是一個ISO格式的日期字符串,導致返回的時間是以UTC時區計算的。
```javascript
new Date('2014-01-01')
// Wed Jan 01 2014 08:00:00 GMT+0800 (CST)
new Date('2014-1-1')
// Wed Jan 01 2014 00:00:00 GMT+0800 (CST)
```
上面代碼中,日期字符串有沒有前導0,返回的結果是不一樣的。如果沒有前導0,JavaScript引擎假設用戶處于本地時區,所以本例返回0點0分。如果有前導0(即如果你以ISO格式表示日期),就假設用戶處于格林尼治國際標準時的時區,所以返回8點0分。但是,ES6改變了這種做法,規定凡是沒有指定時區的日期字符串,一律認定用戶處于本地時區。
對于其他格式的日期字符串,一律視為非ISO格式,采用本地時區作為計時標準。
```javascript
new Date('2014-12-11')
// Thu Dec 11 2014 08:00:00 GMT+0800 (CST)
new Date('2014/12/11')
// Thu Dec 11 2014 00:00:00 GMT+0800 (CST)
```
上面代碼中,第一個日期字符串是ISO格式,第二個不是。
**(3)new Date(year, month [, day, hours, minutes, seconds, ms])**
Date對象還可以接受多個整數作為參數,依次表示年、月、日、小時、分鐘、秒和毫秒。如果采用這種格式,最少需要提供兩個參數(年和月),其他參數都是可選的,默認等于0。因為如果只使用“年”這一個參數,Date對象會將其解釋為毫秒數。
```javascript
new Date(2013)
// Thu Jan 01 1970 08:00:02 GMT+0800 (CST)
```
上面代碼中,2013被解釋為毫秒數,而不是年份。
其他情況下,被省略的參數默認都是0。
```javascript
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日零點的時間,可以看到月份從0開始計算,因此1月是0,12月是11。但是,月份里面的天數從1開始計算。
這些參數如果超出了正常范圍,會被自動折算。比如,如果月設為15,就折算為下一年的4月。
```javascript
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)
```
參數還可以使用負數,表示扣去的時間。
```javascript
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)
```
上面代碼分別對月和日使用了負數,表示從基準日扣去相應的時間。
年的情況有所不同,如果為0,表示1900年;如果為1,就表示1901年;如果為負數,則表示公元前。
```javascript
new Date(0, 0)
// Mon Jan 01 1900 00:00:00 GMT+0800 (CST)
new Date(1, 0)
// Tue Jan 01 1901 00:00:00 GMT+0800 (CST)
new Date(-1, 0)
// Fri Jan 01 -1 00:00:00 GMT+0800 (CST)
```
### 日期的運算
類型轉換時,Date對象的實例如果轉為數值,則等于對應的毫秒數;如果轉為字符串,則等于對應的日期字符串。所以,兩個日期對象進行減法運算,返回的就是它們間隔的毫秒數;進行加法運算,返回的就是連接后的兩個字符串。
```javascript
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)"
```
## Date對象的靜態方法
### Date.now()
`Date.now`方法返回當前距離1970年1月1日 00:00:00 UTC的毫秒數(Unix時間戳乘以1000)。
```javascript
Date.now() // 1364026285194
```
如果需要比毫秒更精確的時間,可以使用`window.performance.now()`。它提供頁面加載到命令運行時的已經過去的時間,可以精確到千分之一毫秒。
```javascript
window.performance.now() // 21311140.415
```
### Date.parse()
`Date.parse`方法用來解析日期字符串,返回距離1970年1月1日 00:00:00的毫秒數。
標準的日期字符串的格式,應該完全或者部分符合RFC 2822和ISO 8061,即`YYYY-MM-DDTHH:mm:ss.sssZ`格式,其中最后的`Z`表示時區。但是,其他格式也可以被解析,請看下面的例子。
```javascript
Date.parse('Aug 9, 1995')
// 返回807897600000,以下省略返回值
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`。
```javascript
Date.parse('xxx') // NaN
```
### Date.UTC()
默認情況下,Date對象返回的都是當前時區的時間。`Date.UTC`方法可以返回UTC時間(世界標準時間)。該方法接受年、月、日等變量作為參數,返回當前距離1970年1月1日 00:00:00 UTC的毫秒數。
```javascript
// 格式
Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
// 用法
Date.UTC(2011, 0, 1, 2, 3, 4, 567)
// 1293847384567
```
該方法的參數用法與`Date`構造函數完全一致,比如月從0開始計算。
## Date實例對象的方法
Date的實例對象,有幾十個自己的方法,分為以下三類。
- to類:從Date對象返回一個字符串,表示指定的時間。
- get類:獲取Date對象的日期和時間。
- set類:設置Date對象的日期和時間。
### to類方法
**(1)Date.prototype.toString()**
`toString`方法返回一個完整的日期字符串。
```javascript
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()**
`toUTCString`方法返回對應的UTC時間,也就是比北京時間晚8個小時。
```javascript
var d = new Date(2013, 0, 1);
d.toUTCString()
// "Mon, 31 Dec 2012 16:00:00 GMT"
d.toString()
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"
```
**(3)Date.prototype.toISOString()**
`toISOString`方法返回對應時間的ISO8601寫法。
```javascript
var d = new Date(2013, 0, 1);
d.toString()
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"
d.toISOString()
// "2012-12-31T16:00:00.000Z"
```
注意,`toISOString`方法返回的總是UTC時區的時間。
**(4)Date.prototype.toJSON()**
`toJSON`方法返回一個符合JSON格式的ISO格式的日期字符串,與`toISOString`方法的返回結果完全相同。
```javascript
var d = new Date(2013, 0, 1);
d.toJSON()
// "2012-12-31T16:00:00.000Z"
d.toISOString()
// "2012-12-31T16:00:00.000Z"
```
**(5)Date.prototype.toDateString()**
`toDateString`方法返回日期字符串。
```javascript
var d = new Date(2013, 0, 1);
d.toDateString() // "Tue Jan 01 2013"
```
**(6)Date.prototype.toTimeString()**
`toTimeString`方法返回時間字符串。
```javascript
var d = new Date(2013, 0, 1);
d.toTimeString() // "00:00:00 GMT+0800 (CST)"
```
**(7)Date.prototype.toLocalDateString()**
`toLocalDateString`方法返回一個字符串,代表日期的當地寫法。
```javascript
var d = new Date(2013, 0, 1);
d.toLocaleDateString()
// 中文版瀏覽器為"2013年1月1日"
// 英文版瀏覽器為"1/1/2013"
```
**(8)Date.prototype.toLocalTimeString()**
`toLocalTimeString`方法返回一個字符串,代表時間的當地寫法。
```javascript
var d = new Date(2013, 0, 1);
d.toLocaleTimeString()
// 中文版瀏覽器為"上午12:00:00"
// 英文版瀏覽器為"12:00:00 AM"
```
### get類方法
Date對象提供了一系列`get*`方法,用來獲取實例對象某個方面的值。
- `getTime()`:返回距離1970年1月1日00:00:00的毫秒數,等同于`valueOf`方法。
- `getDate()`:返回實例對象對應每個月的幾號(從1開始)。
- `getDay()`:返回星期幾,星期日為0,星期一為1,以此類推。
- `getYear()`:返回距離1900的年數。
- `getFullYear()`:返回四位的年份。
- `getMonth()`:返回月份(0表示1月,11表示12月)。
- `getHours()`:返回小時(0-23)。
- `getMilliseconds()`:返回毫秒(0-999)。
- `getMinutes()`:返回分鐘(0-59)。
- `getSeconds()`:返回秒(0-59)。
- `getTimezoneOffset()`:返回當前時間與UTC的時區差異,以分鐘表示,返回結果考慮到了夏令時因素。
所有這些`get*`方法返回的都是整數,不同方法返回值的范圍不一樣。
- 分鐘和秒:0 到 59
- 小時:0 到 23
- 星期:0(星期天)到 6(星期六)
- 日期:1 到 31
- 月份:0(一月)到 11(十二月)
- 年份:距離1900年的年數
```javascript
var d = new Date('January 6, 2013');
d.getDate() // 6
d.getMonth() // 0
d.getYear() // 113
d.getFullYear() // 2013
d.getTimezoneOffset() // -480
```
上面代碼中,最后一行返回`-480`,表示UTC比當前時間晚480分鐘,即8個小時。
下面是一個例子,計算本年度還剩下多少天。
```javascript
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時間。
- `getUTCDate()`
- `getUTCFullYear()`
- `getUTCMonth()`
- `getUTCDay()`
- `getUTCHours()`
- `getUTCMinutes()`
- `getUTCSeconds()`
- `getUTCMilliseconds()`
```javascript
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。
### set類方法
Date對象提供了一系列`set*`方法,用來設置實例對象的各個方面。
- `setDate(date)`:設置實例對象對應的每個月的幾號(1-31),返回改變后毫秒時間戳。
- `setYear(year)`: 設置距離1900年的年數。
- `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月。
```javascript
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`為例,如果參數超過當月的最大天數,則向下一個月順延,如果參數是負數,表示從上個月的最后一天開始減去的天數。
```javascript
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`類方法,可以結合使用,得到相對時間。
```javascript
var d = new Date();
// 將日期向后推1000天
d.setDate( d.getDate() + 1000 );
// 將時間設為6小時后
d.setHours(d.getHours() + 6);
// 將年份設為去年
d.setFullYear(d.getFullYear() - 1);
```
`set*`系列方法除了`setTime()`和`setYear()`,都有對應的UTC版本,即設置UTC時區的時間。
- `setUTCDate()`
- `setUTCFullYear()`
- `setUTCHours()`
- `setUTCMilliseconds()`
- `setUTCMinutes()`
- `setUTCMonth()`
- `setUTCSeconds()`
```javascript
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點。
### Date.prototype.valueOf()
`valueOf`方法返回實例對象距離1970年1月1日00:00:00 UTC對應的毫秒數,該方法等同于`getTime`方法。
```javascript
var d = new Date();
d.valueOf() // 1362790014817
d.getTime() // 1362790014817
```
該方法可以用于計算精確時間。
```javascript
var start = new Date();
doSomething();
var end = new Date();
var elapsed = end.getTime() - start.getTime();
```
<h2 id="3.8">RegExp對象</h2>
## 概述
正則表達式(regular expression)是一種表達文本模式(即字符串結構)的方法,有點像字符串的模板,常常用作按照“給定模式”匹配文本的工具。比如,正則表達式給出一個Email地址的模式,然后用它來確定一個字符串是否為Email地址。JavaScript的正則表達式體系是參照Perl 5建立的。
新建正則表達式有兩種方法。一種是使用字面量,以斜杠表示開始和結束。
```javascript
var regex = /xyz/;
```
另一種是使用RegExp構造函數。
```javascript
var regex = new RegExp('xyz');
```
上面兩種寫法是等價的,都新建了一個內容為`xyz`的正則表達式對象。它們的主要區別是,第一種方法在編譯時新建正則表達式,第二種方法在運行時新建正則表達式。
RegExp構造函數還可以接受第二個參數,表示修飾符(詳細解釋見下文)。
```javascript
var regex = new RegExp('xyz', "i");
// 等價于
var regex = /xyz/i;
```
上面代碼中,正則表達式`/xyz/`有一個修飾符`i`。
這兩種寫法——字面量和構造函數——在運行時有一個細微的區別。采用字面量的寫法,正則對象在代碼載入時(即編譯時)生成;采用構造函數的方法,正則對象在代碼運行時生成。考慮到書寫的便利和直觀,實際應用中,基本上都采用字面量的寫法。
正則對象生成以后,有兩種使用方式:
- 正則對象的方法:將字符串作為參數,比如`regex.test(string)`。
- 字符串對象的方法:將正則對象作為參數,比如`string.match(regex)`。
這兩種使用方式下面都會介紹。
## 正則對象的屬性和方法
### 屬性
正則對象的屬性分成兩類。
一類是修飾符相關,返回一個布爾值,表示對應的修飾符是否設置。
- **ignoreCase**:返回一個布爾值,表示是否設置了i修飾符,該屬性只讀。
- **global**:返回一個布爾值,表示是否設置了g修飾符,該屬性只讀。
- **multiline**:返回一個布爾值,表示是否設置了m修飾符,該屬性只讀。
```javascript
var r = /abc/igm;
r.ignoreCase // true
r.global // true
r.multiline // true
```
另一類是與修飾符無關的屬性,主要是下面兩個。
- `lastIndex`:返回下一次開始搜索的位置。該屬性可讀寫,但是只在設置了`g`修飾符時有意義。
- `source`:返回正則表達式的字符串形式(不包括反斜杠),該屬性只讀。
```javascript
var r = /abc/igm;
r.lastIndex // 0
r.source // "abc"
```
### test()
正則對象的`test`方法返回一個布爾值,表示當前模式是否能匹配參數字符串。
```javascript
/cat/.test('cats and dogs') // true
```
上面代碼驗證參數字符串之中是否包含`cat`,結果返回`true`。
如果正則表達式帶有`g`修飾符,則每一次`test`方法都從上一次結束的位置開始向后匹配。
```javascript
var r = /x/g;
var s = '_x_x';
r.lastIndex // 0
r.test(s) // true
r.lastIndex // 2
r.test(s) // true
r.lastIndex // 4
r.test(s) // false
```
上面代碼的正則對象使用了`g`修飾符,表示要記錄搜索位置。接著,三次使用`test`方法,每一次開始搜索的位置都是上一次匹配的后一個位置。
帶有`g`修飾符時,可以通過正則對象的`lastIndex`屬性指定開始搜索的位置。
```javascript
var r = /x/g;
var s = '_x_x';
r.lastIndex = 4;
r.test(s) // false
```
上面代碼指定從字符串的第五個位置開始搜索,這個位置是沒有字符的,所以返回`false`。
如果正則模式是一個空字符串,則匹配所有字符串。
```javascript
new RegExp('').test('abc')
// true
```
### exec()
正則對象的`exec`方法,可以返回匹配結果。如果發現匹配,就返回一個數組,成員是每一個匹配成功的子字符串,否則返回`null`。
```javascript
var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
r1.exec(s) // ["x"]
r2.exec(s) // null
```
上面代碼中,正則對象`r1`匹配成功,返回一個數組,成員是匹配結果;正則對象`r2`匹配失敗,返回`null`。
如果正則表示式包含圓括號(即含有“組匹配”),則返回的數組會包括多個成員。第一個成員是整個匹配成功的結果,后面的成員就是圓括號對應的匹配成功的組。也就是說,第二個成員對應第一個括號,第三個成員對應第二個括號,以此類推。整個數組的`length`屬性等于組匹配的數量再加1。
```javascript
var s = '_x_x';
var r = /_(x)/;
r.exec(s) // ["_x", "x"]
```
上面代碼的`exec`方法,返回一個數組。第一個成員是整個匹配的結果,第二個成員是圓括號匹配的結果。
`exec`方法的返回數組還包含以下兩個屬性:
- `input`:整個原字符串。
- `index`:整個模式匹配成功的開始位置(從0開始計數)。
```javascript
var r = /a(b+)a/;
var arr = r.exec('_abbba_aba_');
arr // ["abbba", "bbb"]
arr.index // 1
arr.input // "_abbba_aba_"
```
上面代碼中的`index`屬性等于1,是因為從原字符串的第二個位置開始匹配成功。
如果正則表達式加上`g`修飾符,則可以使用多次`exec`方法,下一次搜索的位置從上一次匹配成功結束的位置開始。
```javascript
var r = /a(b+)a/g;
var a1 = r.exec('_abbba_aba_');
a1 // ['abbba', 'bbb']
a1.index // 1
r.lastIndex // 6
var a2 = r.exec('_abbba_aba_');
a2 // ['aba', 'b']
a2.index // 7
r.lastIndex // 10
var a3 = r.exec('_abbba_aba_');
a3 // null
a3.index // TypeError: Cannot read property 'index' of null
r.lastIndex // 0
var a4 = r.exec('_abbba_aba_');
a4 // ['abbba', 'bbb']
a4.index // 1
r.lastIndex // 6
```
上面代碼連續用了四次`exec`方法,前三次都是從上一次匹配結束的位置向后匹配。當第三次匹配結束以后,整個字符串已經到達尾部,正則對象的`lastIndex`屬性重置為`0`,意味著第四次匹配將從頭開始。
利用`g`修飾符允許多次匹配的特點,可以用一個循環完成全部匹配。
```javascript
var r = /a(b+)a/g;
var s = '_abbba_aba_';
while(true) {
var match = r.exec(s);
if (!match) break;
console.log(match[1]);
}
// bbb
// b
```
正則對象的`lastIndex`屬性不僅可讀,還可寫。一旦手動設置了`lastIndex`的值,就會從指定位置開始匹配。但是,這只在設置了`g`修飾符的情況下,才會有效。
```javascript
var r = /a/;
r.lastIndex = 7; // 無效
var match = r.exec('xaxa');
match.index // 1
r.lastIndex // 7
```
上面代碼設置了`lastIndex`屬性,但是因為正則表達式沒有`g`修飾符,所以是無效的。每次匹配都是從字符串的頭部開始。
如果有`g`修飾符,`lastIndex`屬性就會生效。
```javascript
var r = /a/g;
r.lastIndex = 2;
var match = r.exec('xaxa');
match.index // 3
r.lastIndex // 4
```
上面代碼中,`lastIndex`屬性指定從字符的第三個位置開始匹配。成功后,下一次匹配就是從第五個位置開始。
如果正則對象是一個空字符串,則`exec`方法會匹配成功,但返回的也是空字符串。
```javascript
var r1 = new RegExp('');
var a1 = r1.exec('abc');
a1 // ['']
a1.index // 0
r1.lastIndex // 0
var r2 = new RegExp('()');
var a2 = r2.exec('abc');
a2 // ['', '']
a2.index // 0
r2.lastIndex // 0
```
## 字符串對象的方法
字符串對象的方法之中,有4種與正則對象有關。
- `match()`:返回一個數組,成員是所有匹配的子字符串。
- `search()`:按照給定的正則表達式進行搜索,返回一個整數,表示匹配開始的位置。
- `replace()`:按照給定的正則表達式進行替換,返回替換后的字符串。
- `split()`:按照給定規則進行字符串分割,返回一個數組,包含分割后的各個成員。
下面逐一介紹。
### String.prototype.match()
字符串對象的`match`方法對字符串進行正則匹配,返回匹配結果。
```javascript
var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
s.match(r1) // ["x"]
s.match(r2) // null
```
從上面代碼可以看到,字符串的`match`方法與正則對象的`exec`方法非常類似:匹配成功返回一個數組,匹配失敗返回`null`。
如果正則表達式帶有`g`修飾符,則該方法與正則對象的`exec`方法行為不同,會一次性返回所有匹配成功的結果。
```javascript
var s = "abba";
var r = /a/g;
s.match(r) // ["a", "a"]
r.exec(s) // ["a"]
```
設置正則表達式的`lastIndex`屬性,對`match`方法無效,匹配總是從字符串的第一個字符開始。
```javascript
var r = /a|b/g;
r.lastIndex = 7;
'xaxb'.match(r) // ['a', 'b']
r.lastIndex // 0
```
上面代碼表示,設置`lastIndex`屬性是無效的。
### String.prototype.search()
字符串對象的`search`方法,返回第一個滿足條件的匹配結果在整個字符串中的位置。如果沒有任何匹配,則返回`-1`。
```javascript
'_x_x'.search(/x/)
// 1
```
上面代碼中,第一個匹配結果出現在字符串的第二個字符。
該方法會忽略`g`修飾符。
```javascript
var r = /x/g;
r.lastIndex = 2; // 無效
'_x_x'.search(r) // 1
```
上面代碼中,正則表達式使用`g`修飾符之后,使用`lastIndex`屬性指定開始匹配的位置,結果無效,還是從字符串的第一個字符開始匹配。
### String.prototype.replace()
字符串對象的`replace`方法可以替換匹配的值。它接受兩個參數,第一個是搜索模式,第二個是替換的內容。
```javascript
str.replace(search, replacement)
```
搜索模式如果不加g修飾符,就替換第一個匹配成功的值,否則替換所有匹配成功的值。
```javascript
'aaa'.replace('a', 'b')
// "baa"
'aaa'.replace(/a/, 'b')
// "baa"
'aaa'.replace(/a/g, 'b')
// "bbb"
```
上面代碼中,最后一個正則表達式使用了`g`修飾符,導致所有的`b`都被替換掉了。
`replace`方法的一個應用,就是消除字符串首尾兩端的空格。
```javascript
var str = ' #id div.class ';
str.replace(/^\s+|\s+$/g, '')
// "#id div.class"
```
replace方法的第二個參數可以使用美元符號$,用來指代所替換的內容。
- `$&` 指代匹配的子字符串。
- ``$` `` 指代匹配結果前面的文本。
- `$'` 指代匹配結果后面的文本。
- `$n` 指代匹配成功的第`n`組內容,`n`是從1開始的自然數。
- `$$` 指代美元符號`$`。
```javascript
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1')
// "world hello"
'abc'.replace('b', '[$`-$&-$\']')
// "a[a-b-c]c"
```
`replace`方法的第二個參數還可以是一個函數,將匹配內容替換為函數返回值。
```javascript
'3 and 5'.replace(/[0-9]+/g, function(match){
return 2 * match;
})
// "6 and 10"
var a = 'The quick brown fox jumped over the lazy dog.';
var pattern = /quick|brown|lazy/ig;
a.replace(pattern, function replacer(match) {
return match.toUpperCase();
});
// The QUICK BROWN fox jumped over the LAZY dog.
```
作為`replace`方法第二個參數的替換函數,可以接受多個參數。
它的第一個參數是捕捉到的內容,第二個參數是捕捉到的組匹配(有多少個組匹配,就有多少個對應的參數)。此外,最后還可以添加兩個參數,倒數第二個參數是捕捉到的內容在整個字符串中的位置(比如從第五個位置開始),最后一個參數是原字符串。下面是一個網頁模板替換的例子。
```javascript
var prices = {
'pr_1': '$1.99',
'pr_2': '$9.99',
'pr_3': '$5.00'
};
var template = '/* ... */'; // 這里可以放網頁模塊字符串
template.replace(
/(<span id=")(.*?)(">)(<\/span>)/g,
function(match, $1, $2, $3, $4){
return $1 + $2 + $3 + prices[$2] + $4;
}
);
```
上面代碼的捕捉模式中,有四個括號,所以會產生四個組匹配,在匹配函數中用`$1`到`$4`表示。匹配函數的作用是將價格插入模板中。
### String.prototype.split()
字符串對象的`split`方法按照正則規則分割字符串,返回一個由分割后的各個部分組成的數組。
```javascript
str.split(separator, [limit])
```
該方法接受兩個參數,第一個參數是分隔規則,第二個參數是返回數組的最大成員數。
```javascript
'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' ]
```
上面代碼使用正則表達式,去除了子字符串的逗號后面的空格。
```javascript
"aaa*a*".split(/a*/)
// [ '', '*', '*' ]
"aaa**a*".split(/a*/)
// ["", "*", "*", "*"]
```
上面代碼的分割規則是出現0次或多次的`a`,所以第一個分隔符是“aaa”,第二個分割符是“a”,將兩個字符串分成三個部分和四個部分。出現0次的`a`,意味著只要沒有`a`就可以分割,實際上就是按字符分割。
如果正則表達式帶有括號,則括號匹配的部分也會作為數組成員返回。
```javascript
"aaa*a*".split(/(a*)/)
// [ '', 'aaa', '*', 'a', '*' ]
```
上面代碼的正則表達式使用了括號,第一個組匹配是“aaa”,第二個組匹配是“a”,它們都作為數組成員返回。
## 匹配規則
正則表達式對字符串的匹配有很復雜的規則。下面一一介紹這些規則。
### 字面量字符和元字符
大部分字符在正則表達式中,就是字面的含義,比如`/a/`匹配`a`,`/b/`匹配`b`。如果在正則表達式之中,某個字符只表示它字面的含義(就像前面的`a`和`b`),那么它們就叫做“字面量字符”(literal characters)。
```javascript
/dog/.test("old dog") // true
```
上面代碼中正則表達式的`dog`,就是字面量字符,所以`/dog/`匹配“old dog”,因為它就表示“d”、“o”、“g”三個字母連在一起。
除了字面量字符以外,還有一部分字符有特殊含義,不代表字面的意思。它們叫做“元字符”(metacharacters),主要有以下幾個。
**(1)點字符(.)**
點字符(`.`)匹配除回車(`\r`)、換行(`\n`) 、行分隔符(`\u2028`)和段分隔符(`\u2029`)以外的所有字符。
```javascript
/c.t/
```
上面代碼中,`c.t`匹配`c`和`t`之間包含任意一個字符的情況,只要這三個字符在同一行,比如`cat`、`c2t`、`c-t`等等,但是不匹配`coot`。
**(2)位置字符**
位置字符用來提示字符所處的位置,主要有兩個字符。
- `^` 表示字符串的開始位置
- `$` 表示字符串的結束位置
```javascript
// 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`。
```javascript
/11|22/.test('911') // true
```
上面代碼中,必須匹配`11`或`22`。
多個選擇符可以聯合使用。
```javascript
// 匹配fred、barney、betty之中的一個
/fred|barney|betty/
```
選擇符會包括它前后的多個字符,比如`/ab|cd/`指的是匹配“ab”或者“cd”,而不是指匹配“b”或者“c”。如果想修改這個行為,可以使用圓括號。
```javascript
/a( |\t)b/.test('a\tb') // true
```
上面代碼指的是,“a”和“b”之間有一個空格或者一個制表符。
### 重復類
模式的精確匹配次數,使用大括號(`{}`)表示。`{n}`表示恰好重復n次,`{n,}`表示至少重復n次,`{n,m}`表示重復不少于n次,不多于m次。
```javascript
/lo{2}k/.test('look') // true
/lo{2, 5}k/.test('looook') // true
```
上面代碼中,第一個模式指定`o`連續出現2次,第二個模式指定`o`連續出現2次到5次之間。
### 量詞符
量詞符用來設定某個模式出現的次數。
- `?` 問號表示某個模式出現0次或1次,等同于`{0, 1}`。
- `*` 星號表示某個模式出現0次或多次,等同于`{0,}`。
- `+` 加號表示某個模式出現1次或多次,等同于`{1,}`。
```javascript
// 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
```
### 貪婪模式
上一小節的三個量詞符,默認情況下都是最大可能匹配,即匹配直到下一個字符不滿足匹配規則為止。這被稱為貪婪模式。
```javascript
var s = 'aaa';
s.match(/a+/) // ["aaa"]
```
上面代碼中,模式是`/a+/`,表示匹配1個`a`或多個`a`,那么到底會匹配幾個`a`呢?因為默認是貪婪模式,會一直匹配到字符`a`不出現為止,所以匹配結果是3個`a`。
如果想將貪婪模式改為非貪婪模式,可以在量詞符后面加一個問號。
```javascript
var s = 'aaa';
s.match(/a+?/) // ["a"]
```
上面代碼中,模式結尾添加了一個問號`/a+?/`,這時就改為非貪婪模式,一旦條件滿足,就不再往下匹配。
除了非貪婪模式的加號,還有非貪婪模式的星號(`*`)和加號(`+`)。
- `*?`:表示某個模式出現0次或多次,匹配時采用非貪婪模式。
- `+?`:表示某個模式出現1次或多次,匹配時采用非貪婪模式。
### 字符類
字符類(class)表示有一系列字符可供選擇,只要匹配其中一個就可以了。所有可供選擇的字符都放在方括號內,比如`[xyz]` 表示`x`、`y`、`z`之中任選一個匹配。
```javascript
/[abc]/.test('hello world') // false
/[abc]/.test('apple') // true
```
上面代碼表示,字符串“hello world”不包含`a`、`b`、`c`這三個字母中的任一個,而字符串“apple”包含字母`a`。
有兩個字符在字符類中有特殊含義。
**(1)脫字符(^)**
如果方括號內的第一個字符是`[^]`,則表示除了字符類之中的字符,其他字符都可以匹配。比如,`[^xyz]`表示除了`x`、`y`、`z`之外都可以匹配。
```javascript
/[^abc]/.test('hello world') // true
/[^abc]/.test('bbc') // false
```
上面代碼表示,字符串“hello world”不包含字母`a`、`b`、`c`中的任一個,所以返回`true`;字符串“bbc”不包含`a`、`b`、`c`以外的字母,所以返回`false`。
如果方括號內沒有其他字符,即只有`[^]`,就表示匹配一切字符,其中包括換行符,而點號(`.`)是不包括換行符的。
```javascript
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個大寫字母。
```javascript
/a-z/.test('b') // false
/[a-z]/.test('b') // true
```
上面代碼中,當連字號(dash)不出現在方括號之中,就不具備簡寫的作用,只代表字面的含義,所以不匹配字符`b`。只有當連字號用在方括號之中,才表示連續的字符序列。
以下都是合法的字符類簡寫形式。
```javascript
[0-9.,]
[0-9a-fA-F]
[a-zA-Z0-9-]
[1-31]
```
上面代碼中最后一個字符類`[1-31]`,不代表1到31,只代表1到3。
> 注意,字符類的連字符必須在頭尾兩個字符中間,才有特殊含義,否則就是字面含義。比如,`[-9]`就表示匹配連字符和9,而不是匹配0到9。
連字符還可以用來指定Unicode字符的范圍。
```javascript
var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str)
// true
```
另外,不要過分使用連字符,設定一個很大的范圍,否則很可能選中意料之外的字符。最典型的例子就是`[A-z]`,表面上它是選中從大寫的`A`到小寫的`z`之間52個字母,但是由于在ASCII編碼之中,大寫字母與小寫字母之間還有其他字符,結果就會出現意料之外的結果。
```javascript
/[A-z]/.test('\\') // true
```
上面代碼中,由于反斜杠(`\\`)的ASCII碼在大寫字母與小寫字母之間,結果會被選中。
### 轉義符
正則表達式中那些有特殊含義的字符,如果要匹配它們本身,就需要在它們前面要加上反斜杠。比如要匹配加號,就要寫成`\\+`。
```javascript
/1+1/.test('1+1')
// false
/1\+1/.test('1+1')
// true
```
上面代碼中,第一個正則表達式直接用加號匹配,結果加號解釋成量詞,導致不匹配。第二個正則表達式使用反斜杠對加號轉義,就能匹配成功。
正則模式中,需要用斜杠轉義的,一共有12個字符:`^`、`.`、`[`、`$`、`(`、`)`、`|`、`*`、`+`、`?`、`{`和`\\`。需要特別注意的是,如果使用`RegExp`方法生成正則對象,轉義需要使用兩個斜杠,因為字符串內部會先轉義一次。
```javascript
(new RegExp('1\+1')).test('1+1')
// false
(new RegExp('1\\+1')).test('1+1')
// true
```
上面代碼中,`RegExp`作為構造函數,參數是一個字符串。但是,在字符串內部,反斜杠也是轉義字符,所以它會先被反斜杠轉義一次,然后再被正則表達式轉義一次,因此需要兩個反斜杠轉義。
### 修飾符
修飾符(modifier)表示模式的附加規則,放在正則模式的最尾部。
修飾符可以單個使用,也可以多個一起使用。
```javascript
// 單個修飾符
var regex = /test/i;
// 多個修飾符
var regex = /test/ig;
```
**(1)g修飾符**
默認情況下,第一次匹配成功后,正則對象就停止向下匹配了。`g`修飾符表示全局匹配(global),加上它以后,正則對象將匹配全部符合條件的結果,主要用于搜索和替換。
```javascript
var regex = /b/;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // true
```
上面代碼中,正則模式不含`g`修飾符,每次都是從字符串頭部開始匹配。所以,連續做了三次匹配,都返回`true`。
```javascript
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)。
```javascript
/abc/.test('ABC') // false
/abc/i.test('ABC') // true
```
上面代碼表示,加了`i`修飾符以后,不考慮大小寫,所以模式`abc`匹配字符串`ABC`。
**(3)m修飾符**
`m`修飾符表示多行模式(multiline),會修改`^`和`$`的行為。默認情況下(即不加`m`修飾符時),`^`和`$`匹配字符串的開始處和結尾處,加上`m`修飾符以后,`^`和`$`還會匹配行首和行尾,即`^`和`$`會識別換行符(`\n`)。
```javascript
/world$/.test('hello world\n') // false
/world$/m.test('hello world\n') // true
```
上面的代碼中,字符串結尾處有一個換行符。如果不加`m`修飾符,匹配不成功,因為字符串的結尾不是“world”;加上以后,`$`可以匹配行尾。
```javascript
/^b/m.test('a\nb') // true
```
上面代碼要求匹配行首的`b`,如果不加`m`修飾符,就相當于`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` 匹配非詞邊界,即在詞的內部。
下面是一些例子。
```javascript
// \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`)就會停止匹配。
```javascript
var html = "<b>Hello</b>\n<i>world!</i>";
/.*/.exec(html)[0]
// "<b>Hello</b>"
```
上面代碼中,字符串`html`包含一個換行符,結果點字符(`.`)不匹配換行符,導致匹配結果可能不符合原意。這時使用`\s`字符類,就能包括換行符。
```javascript
var html = "<b>Hello</b>\n<i>world!</i>";
/[\S\s]*/.exec(html)[0]
// "<b>Hello</b>\n<i>world!</i>"
// 另一種寫法(用到了非捕獲組)
/(?:.|\s)*/.exec(html)[0]
// "<b>Hello</b>\n<i>world!</i>"
```
上面代碼中,`[\S\s]`指代一切字符。
### 特殊字符
正則表達式對一些不能打印的特殊字符,提供了表達方法。
- `\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` 匹配一個以兩位十六進制數表示的字符。
- `\uhhhh` 匹配一個以四位十六進制數表示的unicode字符。
### 組匹配
**(1)概述**
正則表達式的括號表示分組匹配,括號中的模式可以用來匹配分組的內容。
```javascript
/fred+/.test('fredd') // true
/(fred)+/.test('fredfred') // true
```
上面代碼中,第一個模式沒有括號,結果`+`只表示重復字母`d`,第二個模式有括號,結果`+`就表示匹配“fred”這個詞。
下面是另外一個分組捕獲的例子。
```javascript
var m = 'abcabc'.match(/(.)b(.)/);
m
// ['abc', 'a', 'c']
```
上面代碼中,正則表達式`/(.)b(.)/`一共使用兩個括號,第一個括號捕獲`a`,第二個括號捕獲`c`。
注意,使用組匹配時,不宜同時使用`g`修飾符,否則`match`方法不會捕獲分組的內容。
```javascript
var m = 'abcabc'.match(/(.)b(.)/g);
m
// ['abc', 'abc']
```
上面代碼使用帶`g`修飾符的正則表達式,結果`match`方法只捕獲了匹配整個表達式的部分。
在正則表達式內部,可以用`\n`引用括號匹配的內容,`n`是從1開始的自然數,表示對應順序的括號。
```javascript
/(.)b(.)\1b\2/.test("abcabc")
// true
```
上面的代碼中,`\1`表示前一個括號匹配的內容(即“a”),`\2`表示第二個括號匹配的內容(即“b”)。
下面是另外一個例子。
```javascript
/y(..)(.)\2\1/.test('yabccab') // true
```
括號還可以嵌套。
```javascript
/y((..)\2)\1/.test('yabababab') // true
```
上面代碼中,`\1`指向外層括號,`\2`指向內層括號。
組匹配非常有用,下面是一個匹配網頁標簽的例子。
```javascript
var tagName = /<([^>]+)>[^<]*<\/\1>/;
tagName.exec("<b>bold</b>")[1]
// 'b'
```
上面代碼中,圓括號匹配尖括號之中的標簽,而`\1`就表示對應的閉合標簽。
上面代碼略加修改,就能捕獲帶有屬性的標簽。
```javascript
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}/`,它的作用與前一個正則是一樣的,但是不會單獨輸出括號內部的內容。
請看下面的例子。
```javascript
var m = 'abc'.match(/(?:.)b(.)/);
m // ["abc", "c"]
```
上面代碼中的模式,一共使用了兩個括號。其中第一個括號是非捕獲組,所以最后返回的結果中沒有第一個括號,只有第二個括號匹配的內容。
下面是用來分解網址的正則表達式。
```javascript
// 正常匹配
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+(?=%)/`。
“先行斷言”中,括號里的部分是不會返回的。
```javascript
var m = 'abc'.match(/b(?=c)/);
m // ["b"]
```
上面的代碼使用了先行斷言,`b`在`c`前面所以被匹配,但是括號對應的`c`不會被返回。
再看一個例子。
```javascript
/Jack (?=Sprat|Frost)/.test('Jack Frost') // true
```
**(4)先行否定斷言**
`x(?!y)`稱為先行否定斷言(Negative look-ahead),`x`只有不在`y`前面才匹配,`y`不會被計入返回結果。比如,要匹配后面跟的不是百分號的數字,就要寫成`/\d+(?!%)/`。
```javascript
/\d+(?!\.)/.exec('3.14')
// ["14"]
```
上面代碼中,正則表達式指定,只有不在小數點前面的數字才會被匹配,因此返回的結果就是`14`。
“先行否定斷言”中,括號里的部分是不會返回的。
```javascript
var m = 'abd'.match(/b(?!c)/);
m // ['b']
```
上面的代碼使用了后行斷言,`b`不在`c`前面所以被匹配,而且括號對應的`d`不會被返回。
<h2 id="3.9">JSON對象</h2>
## JSON格式
JSON格式(JavaScript Object Notation的縮寫)是一種用于數據交換的文本格式,2001年由Douglas Crockford提出,目的是取代繁瑣笨重的XML格式。
相比XML格式,JSON格式有兩個顯著的優點:書寫簡單,一目了然;符合JavaScript原生語法,可以由解釋引擎直接處理,不用另外添加代碼。所以,JSON迅速被接受,已經成為各大網站交換數據的標準格式,并被寫入ECMAScript 5,成為標準的一部分。
簡單說,JSON格式就是一種表示一系列的“值”的方法,這些值包含在數組或對象之中,是它們的成員。對于這一系列的“值”,有如下幾點格式規定:
1. 數組或對象的每個成員的值,可以是簡單值,也可以是復合值。
2. 簡單值分為四種:字符串、數值(必須以十進制表示)、布爾值和null(NaN, Infinity, -Infinity和undefined都會被轉為null)。
3. 復合值分為兩種:符合JSON格式的對象和符合JSON格式的數組。
4. 數組或對象最后一個成員的后面,不能加逗號。
5. 數組或對象之中的字符串必須使用雙引號,不能使用單引號。
6. 對象的成員名稱必須使用雙引號。
以下是合格的JSON值。
```javascript
["one", "two", "three"]
{ "one": 1, "two": 2, "three": 3 }
{"names": ["張三", "李四"] }
[ { "name": "張三"}, {"name": "李四"} ]
```
以下是不合格的JSON值。
```javascript
{ 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;
}
} // 不能使用函數和日期對象
```
> 需要注意的是,空數組和空對象都是合格的JSON值,null本身也是一個合格的JSON值。
## JSON對象
ES5新增了JSON對象,用來處理JSON格式數據。它有兩個方法:JSON.stringify和JSON.parse。
### JSON.stringify()
`JSON.stringify`方法用于將一個值轉為字符串。該字符串符合JSON格式,并且可以被JSON.parse方法還原。
```javascript
JSON.stringify('abc') // ""abc""
JSON.stringify(1) // "1"
JSON.stringify(false) // "false"
JSON.stringify([]) // "[]"
JSON.stringify({}) // "{}"
JSON.stringify([1, "false", false])
// '[1,"false",false]'
JSON.stringify({ name: "張三" })
// '{"name":"張三"}'
```
上面代碼將各種類型的值,轉成JSON字符串。需要注意的是,對于原始類型的字符串,轉換結果會帶雙引號,即字符串`abc`會被轉成`"abc"`,這是因為將來還原的時候,雙引號可以讓JavaScript引擎知道,abc是一個字符串,而不是一個變量名。
如果原始對象中,有一個成員的值是`undefined`、函數或XML對象,這個成員會被省略。如果數組的成員是undefined、函數或XML對象,則這些值被轉成null。
```javascript
JSON.stringify({
f: function(){},
a: [ function(){}, undefined ]
});
// "{"a": [null,null]}"
```
上面代碼中,原始對象的`f`屬性是一個函數,`JSON.stringify`方法返回的字符串會將這個屬性省略。而`a`屬性是一個數組,成員分別為函數和undefined,它們都被轉成了`null`。
正則對象會被轉成空對象。
```javascript
JSON.stringify(/foo/) // "{}"
```
JSON.stringify方法會忽略對象的不可遍歷屬性。
```javascript
var obj = {};
Object.defineProperties(obj, {
'foo': {
value: 1,
enumerable: true
},
'bar': {
value: 2,
enumerable: false
}
});
JSON.stringify(obj); // {"foo":1}
```
上面代碼中,bar是obj對象的不可遍歷屬性,JSON.stringify方法會忽略這個屬性。
JSON.stringify方法還可以接受一個數組參數,指定需要轉成字符串的屬性。
```javascript
var obj = {
'prop1': 'value1',
'prop2': 'value2',
'prop3': 'value3'
};
var selectedProperties = ['prop1', 'prop2'];
JSON.stringify(obj, selectedProperties)
// "{"prop1":"value1","prop2":"value2"}"
```
上面代碼中,`JSON.stringify`方法的第二個參數指定,只轉`prop1`和`prop2`兩個屬性。
`JSON.stringify`方法還可以接受一個函數作為參數,用來更改默認的字符串化的行為。
```javascript
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,否則就原樣返回。
注意,這個處理函數是遞歸處理所有的鍵。
```javascript
var o = {a: {b: 1}};
function f(key, value) {
console.log("["+ key +"]:" + value);
return value;
}
JSON.stringify(o, f)
// []:[object Object]
// [a]:[object Object]
// [b]:1
// '{"a":{"b":1}}'
```
上面代碼中,對象`o`一共會被`f`函數處理三次。第一次鍵名為空,鍵值是整個對象`o`;第二次鍵名為`a`,鍵值是`{b: 1}`;第三次鍵名為`b`,鍵值為1。
遞歸處理中,每一次處理的對象,都是前一次返回的值。
```javascript
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`或沒有返回值,則該屬性會被忽略。
```javascript
function f(key, value) {
if (typeof(value) == "string") {
return undefined;
}
return value;
}
JSON.stringify({ a:"abc", b:123 }, f)
// '{"b": 123}'
```
上面代碼中,`a`屬性經過處理后,返回`undefined`,于是該屬性被忽略了。
`JSON.stringify`還可以接受第三個參數,用于增加返回的JSON字符串的可讀性。如果是數字,表示每個屬性前面添加的空格(最多不超過10個);如果是字符串(不超過10個字符),則該字符串會添加在每行前面。
```javascript
JSON.stringify({ p1: 1, p2: 2 }, null, 2);
/*
"{
"p1": 1,
"p2": 2
}"
*/
JSON.stringify({ p1:1, p2:2 }, null, "|-");
/*
"{
|-"p1": 1,
|-"p2": 2
}"
*/
```
如果`JSON.stringify`方法處理的對象,包含一個`toJSON`方法,則它會使用這個方法得到一個值,然后再將這個值轉成字符串,而忽略其他成員。
```javascript
JSON.stringify({
toJSON: function() {
return "Cool"
}
})
// "Cool""
var o = {
foo: 'foo',
toJSON: function() {
return 'bar';
}
};
var json = JSON.stringify({x: o});
// '{"x":"bar"}'
```
`Date`對象就部署了一個自己的`toJSON`方法。
```javascript
JSON.stringify(new Date("2011-07-29"))
// "2011-07-29T00:00:00.000Z"
```
`toJSON`方法的一個應用是,可以將正則對象自動轉為字符串。
```javascript
RegExp.prototype.toJSON = RegExp.prototype.toString;
JSON.stringify(/foo/)
// "/foo/"
```
上面代碼,在正則對象的原型上面部署了`toJSON`方法,將其指向`toString`方法,因此遇到轉換成`JSON`時,正則對象就先調用`toJSON`方法轉為字符串,然后再被`JSON.stingify`方法處理。
### JSON.parse()
JSON.parse方法用于將JSON字符串轉化成對象。
```javascript
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方法將報錯。
```javascript
JSON.parse("'String'") // illegal single quotes
// SyntaxError: Unexpected token ILLEGAL
```
上面代碼中,雙引號字符串中是一個單引號字符串,因為單引號字符串不符合JSON格式,所以報錯。
為了處理解析錯誤,可以將JSON.parse方法放在try...catch代碼塊中。
JSON.parse方法可以接受一個處理函數,用法與JSON.stringify方法類似。
```javascript
function f(key, value) {
if ( key === ""){
return value;
}
if ( key === "a" ) {
return value + 10;
}
}
var o = JSON.parse('{"a":1,"b":2}', f);
o.a // 11
o.b // undefined
```