<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                <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)脫字符(&#94;)** 如果方括號內的第一個字符是`[^]`,則表示除了字符類之中的字符,其他字符都可以匹配。比如,`[^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 ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看