對象
對象是JavaScript的基本數據類型。對象是一種復合值:它將很多值(原始值或者其他對象)聚合在一起,可通過名字訪問這些值。對象也可看做是屬性的無序集合,每個屬性都是一個名/值對。屬性名是字符串,因此我們可以把對象看成是從字符串到值的映射。 這種基本數據結構還有很多種叫法,有些我們已然非常熟悉,比如“散列” (hash)、“散列表”(hashtable)、“字典”(dictionary)、“關聯數組”(associative array)。然而對象不僅僅是字符串到值的映射,除了可以保持自有的屬性,JavaScript對象還可以從一個稱為原型的對象繼承屬性。對象的方法通常是繼承的屬性。這種“原型式繼承”(prototypal inheritance)是JavaScript的核心特征。
JavaScript對象是動態的——可以新增屬性也可以刪除屬性——但它們常用來模擬靜態對象以及靜態類型語言中的“結構體”(struct)。有時它們也用做字符串的集合(忽略名/值對中的值)。
除了字符串、數字、true, false, null和undefined之外,JavaScript中的值都是對象。盡管字符串、數字和布爾值不是對象,但它們的行為和不可變對象非常類似。
對象是可變的,我們通過引用而非值來操作對象。如果變量x是指向一個對象的引用,那么執行代碼
var y = x;
變量y也是指向同一個對象的引用,而非這個對象的副本。通過變量y修改這個對象亦會對變量x造成影響。對象最常見的用法是創建(create)、設置(set)、査找(query)、刪除(delete)、檢測(test)和枚舉(enumerate )它的屬性。
屬性包括名字和值。屬性名可以是包含空字符串在內的任意字符串,但對象中不能存在兩個同名的屬性。值可以是任意JavaScript值,或者(在ECMAScript 5中)可以是一個getter或setter函數(或兩者都有)。除了名字和值之外,每個屬性還有一些與之相關的值,稱為“屬性特性” (property attribute):
- 可寫(writable attribute),表明是否可以設置該屬性的值。
- 可枚舉(enumerable attribute),表明是否可以通過for/in循環返回該屬性。
- 可配置(configurable attribute),表明是否可以刪除或修改該屬性。
在ECMAScript 5之前,通過代碼給對象創建的所有屬性都是可寫的、可枚舉的和可配置的。在ECMAScript 5中則可以對這些特性加以配置。
除了包含屬性之外,每個對象還擁有三個相關的對象特性(object attribute):
- 對象的原型(prototype)指向另外一個對象,本對象的屬性繼承自它的原型對象。
- 對象的類(class)是一個標識對象類型的字符串。
- 對象的擴展標記(extensible flag)指明了(在ECMAScript 5中)是否可以向該對象添加新屬性。
注
property和attribute都可以單獨理解為“屬性”,在這里property attribute是一個詞組,意
思是“屬性的特性”,隨后提到的“可寫”、"可枚舉”和“可配置”即是屬性的三種特性。
最后,我們用下面這些術語來對三類JavaScript對象和兩類屬性作區分:
- 內置對象(native object)是由ECMAScript規范定義的對象或類。例如,數組、函數、日期和正則表達式都是內置對象。
- 宿主對象(host object)是由JavaScript解釋器所嵌入的宿主環境(比如Web瀏覽器)定義的。客戶端JavaScript中表示網頁結構的HTMLElement對象均是宿主對象。既然宿主環境定義的方法可以當成普通的JavaScript函數對象,那么宿主對象也可以當成內置對象。
- 自定義對象(user-defined object)是由運行中的JavaScript代碼創建的對象。
- 自有屬性(own property)是直接在對象中定義的屬性。
- 繼承屬性(inherited property)是在對象的原型對象中定義的屬性。
創建對象
可以通過對象直接量、關鍵字new和(ECMAScript 5中的)Object .create( )函數來創建對象。
對象直接量
創建對象最簡單的方式就是在JavaScript代碼中使用對象直接量。對象直接量是由若干名/值對組成的映射表,名/值對中間用冒號分隔,名/值對之間用逗號分隔,整個映射表用花括號括起來。屬性名可以是JavaScript標識符也可以是字符串直接量(包括空字符串)。屬性的值可以是任意類型的JavaScript表達式,表達式的值(可以是原始值也可以是對象值)就是這個屬性的值。下面有一些例子: ```javascript var empty = {}; // 沒有任何屬性的對象 var point = { x:0, y:0 }; // 兩個屬性 var point2 = { x:point.x, y:point.y+1 }; // 更復雜的值 var book = { "main title": "JavaScript", // 屬性名字里有空格,必須用字符串表示 'sub-title': "The Definitive Guide", // 屬性名字里有連字符,必須用字符串表示 "for": "all audiences" // "for"是保留字,因此必須用引號 author: { // 這個屬性的值是一個對象 firstname: "David", // 注意,這里的屬性名都沒有引號 surname: "Flanagan" } } ``` 在ECMAScript 5 (以及ECMAScript 3的一些實現)中,保留字可以用做不帶引號的屬性名。然而對於ECMAScript 3來說,使用保留字作為屬性名必須使用引號引起來。在ECMAScript 5中,對象直接量中的最后一個屬性后的逗號將忽略,且在ECMAScript 3的大部分實現中也可以忽略這個逗號,但在IE中則報錯。 對象直接量是一個表達式,這個表達式的每次運算都創建並初始化一個新的對象。每次計算對象直接量的時候,也都會計算它的每個屬性的值。也就是說,如果在一個重復調用的函數中的循環體內使用了對象直接量,它將創建很多新對象,並且每次創建的對象的屬性值也有可能不同。
通過new創建對象
new運算符創建並初始化一個新對象。關鍵字new后跟隨一個函數調用。這里的函數稱做構造函數(constructor),構造函數用以初始化一個新創建的對象。JavaScript語言核心中的原始類型都包含內置構造函數。例如: ```javascript var o = new Object(); // 創建一個空對象,和{}一樣 var a = new Array(); // 創建一個數組,和 [] 一樣 var d = new Date(); // 創建一個表示當前時間的Date對象 var r = new RegExp("js"); // 創建一個可以進行模式匹配的EegExp對象 ```
原型
每一個JavaScript對象(null除外)都和另一個對象相關聯。“另一個”對象就是我們熟知的原型,每一個對象都從原型繼承屬性。
所有通過對象直接量創建的對象都具有同一個原型對象,並可以通過JavaScript代碼Object.prototype獲得對原型對象的引用。通過關鍵字new和構造函數調用創建的對象的原型就是構造函數的prototype屬性的值。因此,同使用{ }創建對象一樣,通過new Object( )創建的對象也繼承自Object.prototype( )同樣,通過new Array( )創建的對象的原型就是Array.prototype,通過new Date( )創建的對象的原型就是Date.prototype。
沒有原型的對象為數不多,Object.prototype就是其中之一。它不繼承任何屬性。其他原型對象都是普通對象,普通對象都具有原型。所有的內置構造函數(以及大部分自定義的構造函數)都具有一個繼承自Object.prototype的原型。例如,Date.prototype的屬性繼承自Object.prototype,因此由new Date。創建的Date對象的屬性同時繼承自Date.prototype和Object.prototype。這一系列鏈接的原型對象就是所謂的“原型鏈”(prototype chain)。
Object.create( )
ECMAScript 5定義了一個名為Object.create( )的方法,它創建一個新對象,其中第一個參數是這個對象的原型。Object.create( )提供第二個可選參數,用以對對象的屬性進行進一步描述。
Object.create( )是一個靜態函數,而不是提供給某個對象調用的方法。使用它的方法很簡單,只須傳入所需的原型對象即可:
var o1 = Object.create({x:1, y:2}); // o1繼承了屬性x和y
可以通過傳入參數null來創建一個沒有原型的新對象,但通過這種方式創建的對象不會繼承任何東西,甚至不包括基礎方法,比如toString( ),也就是說,它將不能和“+” 運算符一起正常工作:
var o2 = Object.create(null); // o2不繼承任何屬性和方法
如果想創建一個普通的空對象(比如通過{}或作可new Object( )創建的對象),需要傳入Object.prototype:
var o3 = Object.create(Object.prototype); // o3和{}和new Object()一樣
可以通過任意原型創建新對象(換句話說,可以使任意對象可繼承),這是一個強大的特性。在ECMAScript 3中可以用類似下例中的代碼來模擬原型繼承注:
例:通過原型繼承創建一個新對象 // inherit()返回了一個繼承自原型對象p的屬性的新對象 // 這里使用ECMAScript 5中的Object.create()函數(如果存在的話) // 如果不存在Object.create(),則退化使用其他另法 function inherit(p) { if (p == null) throw TypeError(); // p是一個對象,但不能是null if (Object.create) // 如果object.create()存在 return Object.create(p); // 直接使用它 var t = typeof p; // 否則進行進一步檢測 if (t !== "object" && t !== "function") throw TypeError(); function f() {}; // 定義一個空構造函數 f.prototype = p; // 將其原型屬性設置為p return new f(); // 使用f()創建p的繼承對象 }
上面中的inherit( )函數會更容易理解。現在只要知道它返回的新對象繼承了參數對象的屬性就可以了。注意,inherit( )並不能完全代替Object.create( ),它不能通過傳入null原型來創建對象,而且不能接收可選的第二個參 數。
inherit( )函數的其中一個用途就是防止庫函數無意間(非惡意地)修改那些不受你控制的對象。不是將對象直接作為參數傳入函數,而是將它的繼承對象傳入函數。當函數讀取繼承對象的屬性時,實際上讀取的是繼承來的值。如果給繼承對象的屬性賦值,則這些屬性只會影響這個繼承對象自身,而不是原始對象:
var o = { x: "don't change this value" }; library_function (inherit (o)); // 防止對o的意外修改
了解其工作原理,需要首先了解JavaScript中屬性的査詢和設置機制。
屬性的查詢和設置
可以通過點(.)或方括號([ ])運算符來獲取屬性的值。運算符左側應當是一個表達式,它返回一個對象。對於點(.)來說,右側必須是一個以屬性名稱命名的簡單標識符。對於方括號來說([ ]),方括號內必須是一個計算結果為字符串的表達式,這個字符串就是屬性的名字:
var author = book.author; // 得到book的"author"屬性 var name = author.surname // 得到獲得author的"surname"屬性 var title = book["main title"] // 得到book的"main title"屬性
和查詢屬性值的寫法一樣,通過點和方括號也可以創建屬性或給屬性賦值,但需要將它們放在賦值表達式的左側:
book.edition = 6; // 給book創建一個名為"edition"的屬性 book["main title"] = "ECMAScript"; // 給"main title"屬性賦值
在ECMAScript 3中,點運算符后的標識符不能是保留字,比如,o.for或o.class是非法的,因為for是JavaScript的關鍵字,class是保留字。如果一個對象的屬性名是保留字, 則必須使用方括號的形式訪問它們,比如o["for"]和o["class”]。ECMAScript 5對此放寬了限制(包括ECMAScript 3的某些實現),可以在點運算符后直接使用保留字。
當使用方括號時,我們說方括號內的表達式必須返回字符串。其實更嚴格地講,表達式必須返回字符串或返回一個可以轉換為字符串的值。
作為關聯數組的對象
上文提到,下面兩個JavaScript表達式的值相同:
object.property object["property"]
第一種語法使用點運算符和一個標識符,這和C和Java中訪問一個結構體或對象的靜態字段非常類似。第二種語法使用方括號和一個字符串,看起來更像數組,只是這個數組元素是通過字符串索引而不是數字索引。這種數組就是我們所說的關聯數組(associative array),也稱做散列、映射或字典(dictionary)。
在C、C++和Java和一些強類型(strong typed)語言中,對象只能擁有固定數目的屬性,並且這些屬性名稱必須提前定義好。由於JavaScript是弱類型語言,因此不必遵循這條規定,在任何對象中程序都可以創建任意數量的屬性。但當通過點運算符(.)訪問對象的屬性時,屬性名用一個標識符來表示。標識符必須直接出現在JavaScript程序中, 它們不是數據類型,因此程序無法修改它們。
注
強類型,為所有變量指定數據類型稱為“強類型”。強/弱類型是指類型檢查的嚴格程度。語言有無類型、弱類型和強類型三種。無類型的不檢查,甚至不區分指令和數據。弱類型的檢查很弱,僅能嚴格地區分指令和數據。強類型的則嚴格的在編譯期間進行檢查。
這里的意思是可以動態地給對象添加屬性。嚴格講,JavaScript對象的屬性個數是有上限的
“程序不能修改標識符”的意思是說,在程序運行時無法動態指定一個標識符,當然eval除外。
反過來講,當通過[ ]來訪問對象的屬性時,屬性名通過字符串來表示。字符串是JavaScript的數據類型,在程序運行時可以修改和創建它們。因此,可以在JavaScript中使用下面這種代碼:
var addr = ""; for(i =0; i < 4; i++) { addr += customer["address" + i] + '\n';}
這段代碼讀取customer對象的address0、address1、address2和address3屬性,並將它們連接起來。
這個例子主要說明了使用數組寫法和用字符串表達式來訪問對象屬性的靈活性。這段代碼也可以通過點運算符來重寫,但是很多場景只能使用數組寫法來完成。假設你正在寫一個程序,這個程序利用網絡資源計算當前用戶股票市場投資的金額。程序允許用戶輸 入每只股票的名稱和購股份額。該程序使用名為portfolio的對象來存儲這些信息。每只股票在這個對象中都有對應的屬性,屬性名稱就是股票名稱,屬性值就是購股數量, 例如,如果用戶持有IBM的50股,那么portfolio.ibm屬性的值就為50。
下面是程序的部分代碼,這個函數用來給portifolio添加新的股票:
function addstock(portfolio, stockname, shares) { portfolio[stockname] = shares; }
由於用戶是在程序運行時輸入股票名稱,因此在之前無法得知這些股票的名稱是什么。而由於在寫程序的時候不知道屬性名稱,因此無法通過點運算符(.)來訪問對象portfolio的屬性。但可以使用口運算符,因為它使用字符串值(字符串值是動態的,可以在運行時更改)而不是標識符(標識符是靜態的,必須寫死在程序中)作為索引對屬性進行訪問。
當使用for/in循環遍歷關聯數組時,就可以清晰地體會到for/in的強大之處。下面的例子就是利用for/in計算portfolio 的總計值:
function getvalue(portfolio) { var total = 0.0; for(stock in portfolio) { // 遍歷portfolio中的每只股票 var shares = portfolio[stock]; // 得到每只股票的份額 var price = getquote(stock); // 查找股票價格 total += shares * price; // 將結果累加至total中 } return total; // 返回total的值 }
繼承
JavaScript對象具有“自有屬性"(own property),也有一些屬性是從原型對象繼承而來的。為了更好地理解這種繼承,必須更深入地了解屬性訪問的細節。
假設要査詢對象o的屬性x,如果o中不存在x,那么將會繼續在o的原型對象中査詢屬性x。如果原型對象中也沒有x,但這個原型對象也有原型,那么繼續在這個原型對象的原型上執行査詢,直到找到x或者査找到一個原型是null的對象為止。可以看到,對象的原型屬性構成了一個“鏈”,通過這個“鏈”可以實現屬性的繼承。
var o = {} // 從 Object.prototype 繼承對象的方法 o.x = 1; // 給o定義一個屬性x var p = inherit(o); // p繼承o和Object.prototype p.y = 2; // 給p定義一個屬性y var q = inherit(p); // q繼承p、o和Object.prototype q.z = 3; // 給q定義一個屬性z var s = q.toString(); // toString繼承自Object.prototype q.x + q.y // => 3: x和y分別繼承自o和p
現在假設給對象o的屬性X賦值,如果o中已經有屬性x (這個屬性不是繼承來的),那么這個賦值操作只改變這個已有屬性x的值。如果o中不存在屬性x,那么賦值操作給。添加一個新屬性x。如果之前o繼承自屬性x,那么這個繼承的屬性就被新創建的同名屬性覆蓋了。
屬性賦值操作首先檢査原型鏈,以此判定是否允許賦值操作。例如,如果o繼承自一個只讀屬性x,那么賦值操作是不允許的。如果允許屬性賦值操作,它也總是在原始對象上創建屬性或對已有的屬性賦值,而不會去修改原型鏈。 在JavaScript中,只有在査詢屬性時才會體會到繼承的存在,而設置屬性則和繼承無關, 這是JavaScript的一個重要特性,該特性讓程序員可以有選擇地覆蓋(override)繼承的屬性。
var unitcircle={ r:1 }; // 一個用來繼承的對象 var c = inherit(unitcircle); // c繼承屬性r c.x = 1; c.y=1; // c定義兩個屬性 c.r = 2; // c覆蓋繼承來的屬性 unitcircle.r; // => 1, 原型對象沒有修改
屬性賦值要么失敗,要么創建一個屬性,要么在原始對象中設置屬性,但有一個例外, 如果。繼承自屬性x,而這個屬性是一個具有setter方法的accessor屬性, 那么這時將調用setter方法而不是給o創建一個屬性x。需要注意的是,setter方法是由對象o調用的,而不是定義這個屬性的原型對象調用的。因此如果setter方法定義任意屬 性,這個操作只是針對o本身,並不會修改原型鏈。
屬性訪問錯誤
屬性訪問並不總是返回或設置一個值。
査詢一個不存在的屬性並不會報錯,如果在對象o自身的屬性或繼承的屬性中均未找到屬性x,屬性訪問表達式o.x返回undefined。回想一下我們的book對象有屬性“sub-title",而沒有屬性“subtitle”:
book.subtitle; // => undefined:屬性不存在
但是,如果對象不存在,那么試圖査詢這個不存在的對象的屬性就會報錯。null和undefined值都沒有屬性,因此査詢這些值的屬性會報錯,接上例:
// 拋出一個類型錯誤異常,undefined沒有length屬性 var len = book.subtitle.length;
除非確定book和book.subtitle都是(或在行為上)對象,否則不能這樣寫表達式book.subtitle.length,因為這樣會報錯,下面提供了兩種避免出錯的方法:
// 一種冗余但很易懂的方法 var lerr = undefined; if (book) { if (book.subtitle) len = book.subtitle.length; } // 一種更簡練的常用方法,獲取subtitle的length屬性或undefined var len = book && book.subtitle && book.subtitle.length;
為了理解為什么這里的第二種方法可以避免類型錯誤異常。
當然,給null和undefined設置屬性也會報類型錯誤。給其他值設置屬性也不總是成功,有一些屬性是只讀的,不能重新賦值,有一些對象不允許新增屬性,但讓人頗感意外的是,這些設置屬性的失敗操作不會報錯:
// 內置構造函數的原型是只讀的 Object.prototype = 0; // 賦值失敗,但沒報錯,Object.prototype沒有修改
這是一個歷史遺留問題,這個bug在ECMAScript 5的嚴格模式中已經修復。在嚴格模式 中,任何失敗的屬性設置操作都會拋出一個類型錯誤異常。
盡管屬性賦值成功或失敗的規律看起來很簡單,但要描述清楚並不容易。在這些場景下給對象o設置屬性p會失敗:
- o中的屬性p是只讀的:不能給只讀屬性重新賦值(defineProperty( )方法中有一個例外,可以對可配置的只讀屬性重新賦值)。
- o中的屬性p是繼承屬性,且它是只讀的:不能通過同名自有屬性覆蓋只讀的繼承屬性。
- o中不存在自有屬性p:o沒有使用setter方法繼承屬性p,並且o的可擴展性 (extensible attribute)是false 。如果o中不存在p,而且沒有setter方法可供調用,則p一定會添加至o中。但如果o不是可擴展的,那么在o中不能定義新屬性。
刪除屬性
delete運算符可以刪除對象的屬性。它的操作數應當是一個屬性訪問表達式。讓人感到意外的是,delete只是斷開屬性和宿主對象的聯系,而不會去操作屬性中的屬性:
delete book.author; // book不再有屬性author delete book["main title"]; // book也不再有屬性"main title"
注
a = {p:{x:1}}; b = a.p; delete a.p;執行這段代碼之后b.x的值依然是1。由於已經刪除的屬性的引用依然存在,因此在JavaScript的某些實現中,可能因為這種不嚴謹的代碼而造成內存泄漏。所以在銷毀對象的時候,要遍歷屬性中的屬性,依次刪除。
delete運算符只能刪除自有屬性,不能刪除繼承屬性(要刪除繼承屬性必須從定義這個屬性的原型對象上刪除它,而且這會影響到所有繼承自這個原型的對象)。
當delete表達式刪除成功或沒有任何副作用(比如刪除不存在的屬性)時,它返回 true。如果delete后不是一個屬性訪問表達式,delete同樣返回true:
o = {x:1}; // o有一個屬性x,並繼承屬性toString delete o.x; // 刪除x,返回true delete o.x; // 什么都沒做(x已經不存在了),返回true delete o.toString; // 什么也沒做(toString是繼承來的),返回true delete 1; // 無意義,返回true
delete不能刪除那些可配置性為false的屬性(盡管可以刪除不可擴展對象的可配置屬性)。某些內置對象的屬性是不可配置的,比如通過變量聲明和函數聲明創建的全局對象的屬性。在嚴格模式中,刪除一個不可配置屬性會報一個類型錯誤。在非嚴格模式中(以及ECMAScript 3中),在這些情況下的delete操作會返回false:
delete Object.prototype; // 不能刪除,屬性是不可配置 var x = 1; // 聲明一個全局變量 delete this.x; // 不能刪除這個屬性 function f() {} // 聲明一個全局函數 delete this.f; // 也不能刪除全局函數
當在非嚴格模式中刪除全局對象的可配值屬性時,可以省略對全局對象的引用,直接在delete操作符后跟隨要刪除的屬性名即可:
this.x = 1; // 創建一個可配置的全局屬性(沒有用var) delete x; // 將它刪除
然而在嚴格模式中,delete后跟隨一個非法的操作數(比如x),則會報一個語法錯誤, 因此必須顯式指定對象及其屬性:
delete x; // 在嚴格模式下報語法錯誤 delete this.x; // 正常工作
檢測屬性
JavaScript對象可以看做屬性的集合,我們經常會檢測集合中成員的所屬關系—— 判斷某個屬性是否存在於某個對象中。可以通過in運算符、hasOwnPreperty( )和 propertylsEnumerable( )方法來完成這個工作,甚至僅通過屬性査詢也可以做到這一點。
in運算符的左側是屬性名(字符串),右側是對象。如果對象的自有屬性或繼承屬性中包含這個屬性則返回true:
var o = { x:1 } "x" in o; // true: "x"是o的屬性 "y" in o; // false: "y"不是o的屬性 "toString" in o; // true:o繼承toString 屬性
對象的hasOwnProperty ( )方法用來檢測給定的名字是否是對象的自有屬性。對於繼承屬性它將返回false:
var o = { x: 1 ) o.hasOwnProperty("x"); // true:o有一個自有屬性 o.hasOwnProperty ("y"); // false:o中不存在屬性y o.hasOwnProperty("toString"); // false:toString是繼承屬性
propertylsEnumerable( )是hasOwnProperty( )的增強版,只有檢測到是自有屬性且這個屬性的可枚舉性(enumerable attribute)為true時它才返回true。某些內置屬性是不可枚舉的。通常由JavaScript代碼創建的屬性都是可枚舉的,除非在ECMAScript 5中使用一個特殊的方法來改變屬性的可枚舉性,隨后會提到:
var o = inherit({ y: 2 }); o.x = 1; o.propertyIsEnumerable("x"); // true:o有一個可枚舉的自有屬性x o.propertyIsEnumerable("y"); // false: y是繼承來的 Object.prototype.propertyIsEnumerable("toString"); // false: 不可枚舉
除了使用in運算符之外,另一種更簡便的方法是使用判斷一個屬性是否是undefined:
var o = { x:1 } o.x !== undefined; // true:o中有屬性x o.y !== undefined; // false: o中沒有屬性y o.toString !== undefined; // true:o繼承了toString屬性
然而有一種場景只能使用in運算符而不能使用上述屬性訪問的方式。in可以區分不存在的屬性和存在但值為undefined的屬性。例如下面的代碼:
var o = { x: undefined } // 屬性被顯式賦值為undefined o.x !== undefined // false:屬性存在,但值為undefined o.y !== undefined // false:屬性不存在 "x" in o // true:屬性存在 "y" in o // false:屬性不存在 delete o.x // 刪除了屬性x "x" in o // false:屬性不再存在
注意,上述代碼中使用的是
“!==”
運算符,而不是“!=”
。“!==”
可以區分undefined和null。有時則不必作這種區分:// 如果o中含有屬性x,且x的值不是null或undefined, o.x乘以2. if (o.x != null) o.x *= 2; // 如果o中含有屬性x,且x的值不能轉換為false,o.x乘以2. // 如果x是undefined,null、false、" "、o或NaN,則它保持不變 if (o.x) o.x *= 2;
枚舉屬性
除了檢測對象的屬性是否存在,我們還會經常遍歷對象的屬性。通常使用for/in循環遍歷,ECMAScript 5提供了兩個更好用的替代方案。
for/in循環可以在循環體中遍歷對象中所有可枚舉的屬性(包括自有屬性和繼承的屬性),把屬性名稱賦值給循環變量。對象繼承的內置方法不可枚舉的,但在代碼中給對象添加的屬性都是可枚舉的(除非用下文中提到的一個方法將它們轉換為不可枚舉的)。例如:
var o = {x:1, y:2, z:3}; // 三個可枚舉的屬性 o.propertyIsEnumberable("toString") // =>false,不可枚舉 for(p in o) // 遍歷屬性 console.log(p) // 輸出x、y和z,不會輸出toString
有許多實用工具庫給Object.prototype添加了新的方法或屬性,這些方法和屬性可以被所有對象繼承並使用。然而在ECMAScript 5標准之前,這些新添加的方法是不能定義為不可枚舉的,因此它們都可以在for/in循環中枚舉出來。為了避免這種情況,需要過濾for/in循環返回的屬性,下面兩種方式是最常見的:
for(p in o) { if (!o.hasOwnProperty(p)) continue; // 跳過繼承的屬性 } for(p in o) { if (typeof o[p] === "function") continue; // 跳過方法 }
下面定義了一些有用的工具函數來操控對象的屬性,這些函數用到了for/in循環。實際上extend( )函數經常出現在JavaScript實用工具庫中。
用來枚舉屬性的對象工具函數 /* * 把p中的可枚舉屬性復制到o中,並返回。 * 如果o和p中含有同名屬性,則覆蓋o中的屬性 * 這個函數並不處理getter和setter以及復制屬性 */ function extend(o, p) { for (prop in p) { // 遍歷p中的所有屬性 o[prop] = p[prop]; // 將屬性添加至o中 } return o; } /* * 將P中的可枚舉屬性復制至o中,並返回。 * 如果o和P中有同名的屬性,o中的屬性將不受影響 * 這個函數並不處理getter和setter以及復制屬性 */ function merge(o, p) { for (prop in p) { // 遍歷p中的所有屬性 if (o.hasOwnProperty[prop]) continue; // 過濾掉已經在o存在的屬性 o[prop] = p[prop]; // 將屬性添加至o中 } return o; } / * 如果o中的屬性在p中沒有同名屬性,則從o中刪除這個屬性 * 返回o */ function restrict(o, p) { for (prop in o) { // 遍歷o中的所有屬性 if (! (prop in p)) delete o[prop]; // 如果在p中不存在,則刪除之 } return o; } /* * 如果o中的屬性在p中存在同名屬性,則從o中刪除這個屬性 * 返回o */ function subtract(o, p) { for (prop in p) { // 遍歷p中的所有屬性 delete o[prop]; // 從o中刪除(刪除一個不存在的屬性不會報錯) } return o; } /* * 返回一個新對象,這個對象同時擁有o的屬性和p的屬性 * 如果o和p中有重名屬性,使用p中的屬性值 */ function union(o, p) { return extend(extend({},o), p);} /* * 返回一個新對象,這個對象擁有同時在o和p中出現的屬性 * 很像求o和p的交集,但p中屬性的值被忽略 */ function intersection(o, p) { return restrict(extend((),o), p);} /* * 返回一個數組,這個數組包含的是o中可枚舉的自有屬性的名字 */ function keys(o) { if (typeof o !== "object") throw TypeError(); // 參數必須是對象 var result = []; // 將要返回的數組 for (var prop in o) { // 遍歷所有可枚舉的屬性 if (o.hasOwnProperty(prop)) // 判斷是否是自有屬性 result.push(prop); // 將屬性名添加至數組 } return result; // 返回這個數組 }
除了for/in循環之外,ECMAScript 5定義了兩個用以枚舉屬性名稱的函數。第一個是Object.keys( ),它返回一個數組,這個數組由對象中可枚舉的自有屬性的名稱組成,它的工作原理和的工具函數keys( )類似。
ECMAScript 5中第二個枚舉屬性的函數是Object.getOwnPropertyNames( ),它和Ojbect.keys( )類似,只是它返回對象的所有自有屬性的名稱,而不僅僅是可枚舉的屬性。在ECMAScript 3中是無法實現的類似的函數的,因為ECMAScript 3中沒有提供任何方法來獲取對象不可枚舉的屬性。
屬性getter和setter
我們知道,對象屬性是由名字、值和一組特性(attribute)構成的。在ECMAScript 5中,屬性值可以用一個或兩個方法替代,這兩個方法就是getter和setter。由getter和setter定義的屬性稱做"存取器屬性” (accessor property),它不同於“數據屬性”(data property),數據屬性只有一個簡單的值。
當程序査詢存取器屬性的值時,JavaScript調用getter方法(無參數)。這個方法的返回值就是屬性存取表達式的值。當程序設置一個存取器屬性的值時,JavaScript調用setter方法,將賦值表達式右側的值當做參數傳入setter。從某種意義上講,這個方法負責“設置”屬性值。可以忽略setter方法的返回值。
和數據屬性不同,存取器屬性不具有可寫性(writable attribute)。如果屬性同時具有getter和setter方法,那么它是一個讀/寫屬性。如果它只有getter方法,那么它是一個只讀屬性。如果它只有setter方法,那么它是一個只寫屬性(數據屬性中有一些例外),讀取只寫屬性總是返回undefined。
定義存取器屬性最簡單的方法是使用對象直接量語法的一種擴展寫法:
// 普通的數據屬性 data_prop: value, // 存取器屬性都是成對定義的函數 get accessor_prop() { /*這里是函數體 */ }, set accessor_prop(value) { /* 這里是函數體*/ } };
存取器屬性定義為一個或兩個和屬性同名的函數,這個函數定義沒有使用function關鍵字,而是使用get和(或)set。注意,這里沒有使用冒號將屬性名和函數體分隔開,但在函數體的結束和下一個方法或數據屬性之間有逗號分隔。例如,思考下面這個表示2D笛卡爾點坐標的對象。它有兩個普通的屬性x和y分別表示對應點的X坐標和Y坐標, 它還有兩個等價的存取器屬性用來表示點的極坐標:
// x和y是普通的可讀寫的數據屬性 x: 1.0, y: 1.o, // r是可讀寫的存取器屬性,它有getter和setter. // 函數體結束后不要忘記帶上逗號 get r() { return Math.sqrt(this.x*this.x + this.y*this.y); }, set r(newvalue) { var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y); var ratio = newvalue/oldvalue; this.x *= ratio; this.y *= ratio; }, // theta是只讀存取器屬性,它只有getter方法 get theta() { return Math.atan2(this.y, this.x); } };
注
笛卡爾坐標系就是直角坐標系和斜角坐標系的統稱。相交於原點的兩條數軸,構成了平面放射坐標系。
注意在這段代碼中getter和setter里this關鍵字的用法。JavaScript把這些函數當做對象的方法來調用,也就是說,在函數體內的this指向表示這個點的對象,因此,r屬性的getter方法可以通過this.x和this.y引用x和y屬性。
和數據屬性一樣,存取器屬性是可以繼承的,因此可以將上述代碼中的對象p當做另一個“點”的原型。可以給新對象定義它的x和y屬性,但r和theta屬性是繼承來的:
var q = inherit(p); // 創建一個繼承getter和setter的新對象 q.x = 1, q.y = 1; // 給q添加兩個屬性 console.log(q.r); // 可以使用繼承的存取器屬性 console.log(q.theta);
這段代碼使用存取器屬性定義API,API提供了表示同一組數據的兩種方法(笛卡爾坐標系表示法和極坐標系表示法)。還有很多場景可以用到存取器屬性,比如智能檢測屬性的寫入值以及在每次屬性讀取時返回不同值:
// 這個對象產生嚴格自增的序列號 var serialnum = { // 這個數據屬性包含下一個序列號 // $符號暗示這個屬性是一個私有屬性 $n: 0, // 返回當前值,然后自增 // get next() { return this.$n++; }, // 給n設置新的值,但只有當它比當前值大時才設置成功 set next(n) { if (n >= this.$n) this.$n = n; else throw "序列號的值不能比當前值小"; } };
最后我們再來看一個例子,這個例子使用getter方法實現一種“神奇”的屬性:
// 這個對象有一個可以返回隨機數的存取器屬性 // 例如,表達式"random.octet"產生一個隨機數 // 每次產生的隨機數都在0〜255之間 var random = { get octet() { return Math.floor(Math.random()*256); }, get uint16() { return Math.floor(Math.random()*65536); }, get int16() { return Math.floor(Math.random()*65536)-32768; } };
屬性的特性
除了包含名字和值之外,屬性還包含一些標識它們可寫、可枚舉和可配置的特性。在ECMAScript 3中無法設置這些特性,所有通過ECMAScript 3的程序創建的屬性都是可寫的、可枚舉的和可配置的,且無法對這些特性做修改。這些API對於庫的開發者來說非常重要,因為:
- 可以通過這些API給原型對象添加方法,並將它們設置成不可枚舉的,這讓它們看起來更像內置方法。
- 可以通過這些API給對象定義不能修改或刪除的屬性,借此“鎖定”這個對象。
我們將存取器屬性的getter和setter方法看成是屬性的特性。按照這個邏輯, 我們也可以把數據屬性的值同樣看做屬性的特性。因此,可以認為一個屬性包含一個名字和4個特性。數據屬性的4個特性分別是它的值(value)、可寫性(writable)、可枚舉性(enumerable)和可配置性(configurable)。存取器屬性不具有值(value)特性和可寫性,它們的可寫性是由setter方法存在與否決定的。因此存取器屬性的4個特性是讀取(get)、寫入(set)、可枚舉性和可配置性。
為了實現屬性特性的査詢和設置操作,ECMAScript 5中定義了一個名為“屬性描述符”(property descriptor)的對象,這個對象代表那4個特性。描述符對象的屬性和它們所描述的屬性特性是同名的。因此,數據屬性的描述符對象的屬性有value、writable、enumerable和configurable。存取器屬性的描述符對象則用get屬性和set屬性代替value和writable。其中writable、enumerable和configurable都是布爾值,當然,get屬性和set屬性是函數值。
通過調用Object.getOwnPropertyDescriptor( )可以獲得某個對象特定屬性的屬性描述符:
// 返回 { value: 1, writable:true, enumerable:true, configurable:true} Object.getOwnPropertyDescriptor({x:1}, "x"); // 査詢上文中定義的randam對象的octet屬性 // 返回 { get: /*func*/, set:undefined, enumerable:true, configurable:true} Object.getOwnPropertyDescriptor(random, "octet"); // 對於繼承屬性和不存在的屬性,返回undefined Object.getOwnPropertyDescriptor({}, "x"); // undefined,沒有這個屬性 Object.getOwnPropertyDescriptor({}, "toString"); // undefined,繼承屬性
從函數名字就可以看出,Object.getOwnPropertyDescriptor。只能得到自有屬性的描述符。要想獲得繼承屬性的特性,需要遍歷原型鏈。
要想設置屬性的特性,或者想讓新建屬性具有某種特性,則需要調用Object.definePeoperty( ),傳入要修改的對象、要創建或修改的屬性的名稱以及屬性描述符對象:
var o = {}; // 創建一個空對象 // 添加一個不可枚舉的數據屬性x,並賦值為1 Object.defineProperty(o,"x", { value : 1, writable: true, enumerable: false, configurable: true}); // 屬性是存在的,但不可枚舉 o.x; // => 1 Object.keys(o) // => [] // 現在對屬性x做修改,讓它變為只讀 Object.defineProperty(o, { writable: false }); // 試圖更改這個屬性的值 o.X = 2; // 操作失敗但不報錯,而在嚴格模式中拋出類型錯誤異常 o.X // => 1 // 屬性依然是可配置的,因此可以通過這種方式對它進行修改: Object.defineProperty(o, "x", { value: 2 }); o.x // => 2 // 現在將x從數據屬性修改為存取器屬性 Object.defineProperty(o, "x", { get: function() { return o; } }); o.x // => 0
傳AObject.defineProperty( )的屬性描述符對象不必包含所有4個特性。對於新創建的屬性來說,默認的特性值是false或undefined。對於修改的已有屬性來說,默認的特性值沒有做任何修改。注意,這個方法要么修改已有屬性要么新建自有屬性,但不能修改繼承屬性。
如果要同時修改或創建多個屬性,則需要使用Object.defineProperties( )。第一個參數是要修改的對象,第二個參數是一個映射表,它包含要新建或修改的屬性的名稱,以及它們的屬性描述符,例如:
var p = Object.defineProperties({}, { x: { value: 1, writable: true, enumerable:true, configurable:true }, y: { value: 1, writable: true, enumerable:true, configurable:true ), r:{ get: function() { return Math.sqrt(this.x*this.x + this.y*this.y) }, enumerable:true, configurable:true } });
這段代碼從一個空對象開始,然后給它添加兩個數據屬性和一個只讀存取器屬性。最終Object.defineProperties( )返回修改后的對象(和Object.defineProperty( )一樣)。
對於那些不允許創建或修改的屬性來說,如果用Object.defineProperty( )和Object.defineProperties( )對其操作(新建或修改)就會拋出類型錯誤異常,比如,給一個不可擴展的對象新增屬性就會拋出類型錯誤異常。造成這些方法拋出類型錯誤異常的其他原因則和特性本身相關。可寫性控制着對值特性的修改。可配置性控制着對其他特性(包括屬性是否可以刪除)的修改。然而規則遠不止這么簡單,例如,如果屬性是可配置的話,則可以修改不可寫屬性的值。同樣,如果屬性是不可配置的,仍然可以將可寫屬性修改為不可寫屬性。下面是完整的規則,任何對0bject.defineProperty( )或Object.defineProperties( )違反規則的使用都會拋出類型錯誤異常:
- 如果對象是不可擴展的,則可以編輯已有的自有屬性,但不能給它添加新屬性。
- 如果屬性是不可配置的,則不能修改它的可配置性和可枚舉性。
- 如果存取器屬性是不可配置的,則不能修改其getter和setter方法,也不能將它轉換為數據屬性。
- 如果數據屬性是不可配置的,則不能將它轉換為存取器屬性。
- 如果數據屬性是不可配置的,則不能將它的可寫性從false修改為true,但可以從true修改為false。
- 如果數據屬性是不可配置且不可寫的,則不能修改它的值。然而可配置但不可寫屬性的值是可以修改的(實際上是先將它標記為可寫的,然后修改它的值,最后轉換為不可寫的)。
上面實現了extend( )函數,這個函數把一個對象的屬性復制到另一個對象中。這個函數只是簡單地復制屬性名和值,沒有復制屬性的特性,而且也沒有復制存取器屬性的getter和setter方法,只是將它們簡單地轉換為靜態的數據屬性。下面給出了改進的extend( ),它使用Object.getOwnPropertyDescriptor( )和Object.defineProperty( )對屬 性的所有特性進行復制。新的extend( )作為不可枚舉屬性添加到Object.prototype中, 因此它是Object上定義的新方法,而不是一個獨立的函數。
復制屬性的特性 /* * * 給Object.prototype添加一個不可枚舉的extend()方法 * 這個方法繼承自調用它的對象,將作為參數傳入的對象的屬性一一復制 * 除了值之外,也復制屬性的所有特性,除非在目標對象中存在同名的屬性, * 參數對象的所有自有對象(包括不可枚舉的屬性)也會一一復制。 * */ Object.defineProperty(Object.prototype, "extend", // 定義Object.prototype.extend { writable: true; enumerable: false, // 將其定義為不可枚舉 configurable: true, value: function(o) { // 得到所有的自有屬性,包括不可枚舉屬性 var names = Object.getOwnPropertyNames(o); // 遍歷它們 for(var i = 0; i < names.length; i++) { // 如果屬性已經存在,則跳過 if (names[i] in this) continue; // 獲得o中的屬性的描述符 var desc = Object.getOwnPropertyDescriptor(o,names[i]); // 用它給this創建一個屬性 Object.defineProperty(this, names[i], desc); } } });
getter和setter的老式API
可以通過對象直接量語法給新對象定義存取器屬性,但不能査詢屬性的getter和setter方法或給已有的對象添加新的存取器屬性。在ECMAScript 5中,可以通過Object.getOwnPropertyDescriptor( )和Object.defineProperty( )來完成這些工作。
在ECMAScript 5標准被采納之前,大多數JavaScript的實現(IE瀏覽器除外)已經可以支持對象直接量語法中的get和set寫法。這些實現提供了非標准的老式API用來査詢和設置getter和setter。這些API由4個方法組成,所有對象都擁有這些方法。
_lookupGetter( )
和_lookupsetter( )
用以返回一個命名屬性的getter和setter方法。_defineGetter( )
和_defineSetter( )
用以定義getter和setter,這兩個函數的第一個參數是屬性名字,第二個參數是getter和setter方法。這4個方法都是以兩條下划線作前綴,兩條下划線作后綴,以表明它們是非標准的方法。
對象的三個屬性
每一個對象都有與之相關的原型(prototype)、類(class)和可擴展性(extensible attribute)。
原型屬性
對象的原型屬性是用來繼承屬性的,這個屬性如此重要,以至於我們經常把“0的原型屬性”直接叫做“o的原型”。
原型屬性是在實例對象創建之初就設置好的,通過對象直接量創建的對象使用Object.prototype作為它們的原型。通過new創建的對象使用構造函數的prototype屬性作為它們的原型。通過Object.create( )創建的對象使用第一個參數(也可以是null)作為它們的原型。
在ECMAScript 5中,將對象作為參數傳入Object.getPrototypeOf( )可以查詢它的原型。在ECMAScript 3中,則沒有與之等價的函數,但經常使用表達式o.constructor.prototype來檢測一個對象的原型。通過new表達式創建的對象,通常繼承一個constructor屬性,這個屬性指代創建這個對象的構造函數。注意,通過對象直接量或Object.create( )創建的對象包含一個名為constructor的屬性, 這個屬性指代Object( )構造函數。因此,constructor.prototype才是對象直接量的真正的原型,但對於通過Object.create( )創建的對象則往往不是這樣。
要想檢測一個對象是否是另一個對象的原型(或處於原型鏈中),請使用isPrototypeOf( )方法。例如,可以通過p.isPrototypeOf(o)來檢測p是否是o的原型:
var p = {x:1}; // 定義一個原型對象 var o = Object.create(p); // 使用這個原型創建一個對象 p.isPrototypeOf(o) // => true: o繼承自p Object.Prototype.isPrototypeOf(o) // => true: p繼承自Object.prototype
需要注意的是,isPrototypeOf( )函數實現的功能和instanceof運算符非常類似。
Mozilla實現的JavaScript(包括早些牽的Netscape)對外暴露了一個專門命名為
_proto_
的屬性,用以直接査詢/設置對象的原型。但並不推薦使用_proto_
,因為盡管Safari和Chrome的當前版本都支持它,但IE和Opera還未實現它(可能以后也不會實現)。實現了ECMAScript 5的Firefox版本依然支持_proto_
,但對修改不可擴展對象的原型做了限制。
類屬性
對象的類屬性(class attribute)是一個字符串,用以表示對象的類型信息。ECMAScript 3和ECMAScript 5都未提供設置這個屬性的方法,並只有一種間接的方法可以査詢它。 默認的toString( )方法(繼承自Object.prototype)返回了如下這種格式的字符串:
[object class]
因此,要想獲得對象的類,可以調用對象的toString( )方法,然后提取已返回字符串的第8個到倒數第二個位置之間的字符。不過讓人感覺棘手的是,很多對象繼承的toString( )方法重寫了,為了能調用正確的toString( )版本,必須間接地調用Function。call( )方法。下例中的classof( )函數可以返回傳遞給它的任意對象的類:
classof()函數 function classof(o) { if (o === null) return "Null"; if (o === undefined) return "Undefined"; return Object.prototype.toString.call(o).slice(8,-l); }
classof( )函數可以傳入任何類型的參數。數字、字符串和布爾值可以直接調用toString( )方法,就和對象調用toString()方法一樣(實際上是這些類型的變量調用toString( )方法,而不是通過它們的直接量調用toString( )比如1.toString( )是不對的,而是要先聲明變量var a = 1;然后調用a.toString( )。),並且這個函數包含了對null和undefined的特殊處理(在ECMAScript 5中不需要對這些特殊情況做處理)。通過內置構造函數(比如Array和Date)創建的對象包含“類屬性”(class attribute),它與構造函數名稱相匹配。宿主對象也包含有意義的“類屬性”,但這和具體的JavaScript實現有關。通過對象直接量和Object.create創建的對象的類屬性是“Object”,那些自定義構造函數創建的對象也是一樣,類屬性也是“Object”,因此對於自定義的類來說,沒辦法通過類屬性來區分對象的類:
classof(null) // => "Null" classof(1) // => "Number" classof("") // => "String" classof(false) // => "Boolean" classof({}) // => "Object" classof([]) // => "Array" classof(/./) // => "Regexp" classof(new Date()) // => "Date" classof(Widow) // => "Window"(這是客戶端宿主對象) function f() {}; // 定義一個自定義構造函數 classof(new f()); // => "Object"
可擴展性
對象的可擴展性用以表示是否可以給對象添加新屬性。所有內置對象和自定義對象都是顯式可擴展的,宿主對象的可擴展性是由JavaScript引擎定義的。在ECMAScript 5中, 所有的內置對象和自定義對象都是可擴展的,除非將它們轉換為不可擴展的,同樣,宿主對象的可擴展性也是由實現ECMAScript 5的JavaScript引擎定義的。
ECMAScript 5定義了用來査詢和設置對象可擴展性的函數。通過將對象傳入Object.esExtensible( ),來判斷該對象是否是可擴展的。如果想將對象轉換為不可擴展的, 需要調用Object.preventExtensions( ),將待轉換的對象作為參數傳進去。注意,一旦將對象轉換為不可擴展的,就無法再將其轉換回可擴展的了。同樣需要注意的是, preventExtensions( )只影響到對象本身的可擴展性。如果給一個不可擴展的對象的原型添加屬性,這個不可擴展的對象同樣會繼承這些新屬性。
可擴展屬性的目的是將對象“鎖定”,以避免外界的干擾。對象的可擴展性通常和屬性的可配值性與可寫性配合使用,ECMAScript 5定義的一些函數可以更方便地設置多種屬性。
Object.seal( )和Object. preventExtensions( )類似,除了能夠將對象設置為不可擴展的,還可以將對象的所有自有屬性都設置為不可配置的。也就是說,不能給這個對象添加新屬性,而且它已有的屬性也不能刪除或配置,不過它已有的可寫屬性依然可以設置。對於那些已經封閉(sealed)起來的對象是不能解封的。可以使用Object. isSealed( )來檢測對象是否封閉。Object.freeze( )將更嚴格地鎖定對象 “凍結”(frozen)。除了將對象設置為不可 擴展的和將其屬性設置為不可配置的之外,還可以將它自有的所有數據屬性設置為只讀(如果對象的存取器屬性具有setter方法,存取器屬性將不受影響,仍可以通過給屬性賦值調用它們)。使用Object.isFrozen( )來檢測對象是否凍結。
Object.preventExtensions( ). Object.seal( )和Object.freeze( )都返回傳入的對象, 也就是說,可以通過函數嵌套的方式調用它們:
// 創建一個封閉對象,包括一個凍結的原型和一個不可枚舉的屬性 var o = Object.seal(Object.create(Object.freeze({x:1}),{y: {value: 2, writable: true}}));
序列化對象
對象序列化(serialization)是指將對象的狀態轉換為字符串,也可將字符串還原為對象。ECMAScript 5提供了內置函數JSON.stringify( )和JSON.parse( )用來序列化和還原JavaScript對象。這些方法都使用JSON作為數據交換格式,JSON的全稱是"JavaScript Object Notation"——JavaScript對象表示法,它的語法和JavaScript對象與數組直接量的語法非常相近:
o = {x:1, y:{z:[false,null,""]}}; // 定義一個測試對象 s = JSON.stringify(o); // s是 '{"x":1,"y":{"z":[false,null,""]}}' p = JSON.parse(s); // p是o的深拷貝
ECMAScript 5中的這些函數的本地實現和 http://json.org/json2.js 中的公共域ECMAScript 3 版本的實現非常類似,或者說完全一樣,因此可以通過引入json2.js模塊在ECMAScript 3 的環境中使用ECMAScript 5中的這些函數。
JSON的語法是JavaScript語法的子集,它並不能表示JavaScriptfi的所有值。支持對象、數組、字符串、無窮大數字、true、false和null,並且它們可以序列化和還原。NaN、Infinity和-Infinity序列化的結果是null,日期對象序列化的結果是ISO格式的日期字符串(參照Date.toJSON( )函數),但JSON.parse()依然保留它們的字符串形態,而不會將它們還原為原始日期對象。函數、RegExp、Error對象和undefined值不能序列化和還原。JSON.stringify( )只能序列化對象可枚舉的自有屬性。對於一個不能序列化的屬性來說,在序列化后的輸出字符串中會將這個屬性省略掉。JSON.stringify( )和JSON.parse()都可以接收第二個可選參數,通過傳入需要序列化或還原的屬性列表來定制自定義的序列化或還原操作。
對象方法
上文已經討論過,所有的JavaScript對象都從Object.prototype繼承屬性(除了那些不通過原型顯式創建的對象)。這些繼承屬性主要是方法,因為JavaScript程序員普遍對繼承方法更感興趣。我們已經討論過hasOwnProperty()、propertyIsEnumerable( )和isPrototypeOf( )這三個方法,以及在Object構造函數里定義的靜態函數Object.create( )和Object.getPrototypeOf( )等。
toString( )方法
toString( )方法沒有參數,它將返回一個表示調用這個方法的對象值的字符串。在需要將對象轉換為字符串的時候,JavaScript都會調用這個方法。比如,當使用“+”運算符連接一個字符串和一個對象時或者在希望使用字符串的方法中使用了對象時都會調用 toString( )。
默認的toString( )方法的返回值帶有的信息量很少(盡管它在檢測對象的類型時非常有用),例如,下面這行代碼的計算結果為字符串"[object Object]”:
var s = { x:1, y:1 }.toString();
由於默認的toString( )方法並不會輸出很多有用的信息,因此很多類都帶有自定義的toString( )。例如,當數組轉換為字符串的時候,結果是一個數組元素列表,只是每個元素都轉換成了字符串,再比如,當函數轉換為字符串的時候,得到函數的源代碼。
toLocaleString( )方法
除了基本的toString( )方法之外,對象都包含toLocaleString( )方法,這個方法返回一個表示這個對象的本地化字符串。Object中默認的toLocaleString( )方法並不做任何本地化自身的操作,它僅調用toString( )方法並返回對應值。Date和Number類對toLocaleString( )方法做了定制,可以用它對數字、日期和時間做本地化的轉換。Array 類的toLocaleString( )方法和toString( )方法很像,唯一的不同是每個數組元素會調用toLocaleString( )方法轉換為字符串,而不是調用各自的toString()方法。
toJSON( )方法
Object.prototype實際上沒有定義toJSON( )方法,但對於需要執行序列化的對象來說,JSON.stringify( )方法會調用toJSON( )方法。如果在待序列化的對象中存在這個方法,則調用它,返回值即是序列化的結果,而不是原始的對象。具體示例參見Date.toJSON( )。
valueOf( )方法
valueOf( )方法和toString( )方法非常類似,但往往當JavaScript需要將對象轉換為某種原始值而非字符串的時候才會調用它,尤其是轉換為數字的時候。如果在需要使用原始值的上下文中使用了對象,JavaScript就會自動調用這個方法。默認的value0f( )方法不足為奇,但有些內置類自定義了value0f( )方法(比如Date.valueOf())。