《JavaScript闖關記》之對象

對象是 JavaScript 的數據類型。它將很多值(原始值或者其他對象)聚合在一起,可通過名字訪問這些值,因此我們可以把它看成是從字符串到值的映射。對象是動態的,可以隨時新增和刪除自有屬性。對象除瞭可以保持自有的屬性,還可以從一個稱為原型的對象繼承屬性,這種「原型式繼承(prototypal inheritance)」是 JavaScript 的核心特征。

對象最常見的用法是創建(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)」:

對象的類(class),是一個標識對象類型的字符串。 對象的原型(prototype),指向另外一個對象,本對象的屬性繼承自它的原型對象。 對象的擴展標記(extensible flag),指明瞭在 ECMAScript 5中是否可以向該對象添加新屬性。

最後,用下面術語來對 JavaScript 的「三類對象」和「兩類屬性」進行區分:

內置對象(native object),是由 JavaScript 規范定義的對象或類。例如,數組、函數、日期和正則表達式都是內置對象。 宿主對象(host object),是由 JavaScript 解釋器所嵌入的宿主環境(比如 Web 瀏覽器)定義的。客戶端 JavaScript 中表示網頁結構的 HTMLElement 對象均是宿主對象。 自定義對象(user-defined object),是由運行中的 JavaScript 代碼創建的對象。 自有屬性(own property),是直接在對象中定義的屬性。 繼承屬性(inherited property),是在對象的原型對象中定義的屬性。

創建對象

可以使用對象字面量、new 關鍵字和 ECMAScript 5中的 Object.create() 函數來創建對象。

使用對象字面量創建對象(推薦)

創建對象最簡單的方式就是在 JavaScript 代碼中使用對象字面量。對象字面量是由若幹名值對組成的映射表,名值對中間用冒號分隔,名值對之間用逗號分隔,整個映射表用花括號括起來。屬性名可以是 JavaScript 標識符也可以是字符串直接量(包括空字符串)。屬性的值可以是任意類型的 JavaScript 表達式,表達式的值(可以是原始值也可以是對象值)就是這個屬性的值。例如:

// 推薦寫法
var person = {
    name : "stone",
    age : 28
};

// 也可以寫成
var person = {};
person.name = "stone";
person.age = 28;

使用 new 關鍵字創建對象

new 關鍵字創建並初始化一個新對象。關鍵字 new 後跟隨一個函數調用。這裡的函數稱做構造函數(constructor),構造函數用以初始化一個新創建的對象。JavaScript 語言核心中的原始類型都包含內置構造函數。例如:

var person = new Object();
person.name = "stone";
person.age = 28;

其中 var person = new Object(); 等價於 var person = {}; 。

使用 Object.create() 函數創建對象

ECMAScript 5定義瞭一個名為 Object.create() 的方法,它創建一個新對象,其中第一個參數是這個對象的原型。Object.create() 提供第二個可選參數,用以對對象的屬性進行進一步描述。Object.create() 是一個靜態函數,而不是提供給某個對象調用的方法。使用它的方法很簡單,隻須傳入所需的原型對象即可。例如:

var person = Object.create(Object.prototype);
person.name = "stone";
person.age = 28;

其中 var person = Object.create(Object.prototype); 也等價於 var person = {}; 。

原型(prototype)

所有通過對象字面量創建的對象都具有同一個原型對象,並可以通過 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)」。

屬性的查詢和設置

前面有提到過,可以通過點 . 或方括號 [] 運算符來獲取屬性的值。對於點 . 來說,左側應當是一個對象,右側必須是一個以屬性名稱命名的簡單標識符。對於方括號來說 [] ,方括號內必須是一個計算結果為字符串的表達式,這個字符串就是屬性的名稱。例如:

// 推薦寫法
console.log(person.name);   // "stone"
console.log(person.age);    // "28"

// 也可以寫成
console.log(person["name"]);    // stone
console.log(person["age"]);     // 28

和獲取屬性的值寫法一樣,通過點和方括號也可以創建屬性或給屬性賦值,但需要將它們放在賦值表達式的左側。例如:

// 推薦寫法
person.name = "sophie"; // 賦值
person.age = 30;        // 賦值
person.weight = 38;     // 創建

// 也可以寫成
person["name"] = "sophie";  // 賦值
person["age"] = 30;         // 賦值
person["weight"] = 38;      // 創建

當使用方括號時,方括號內的表達式必須返回字符串。更嚴格地講,表達式必須返回字符串或返回一個可以轉換為字符串的值。

屬性的訪問錯誤

查詢一個不存在的屬性並不會報錯,如果在對象 o 自身的屬性或繼承的屬性中均未找到屬性 x,屬性訪問表達式 o.x 返回 undefined。例如:

var person = {};
person.wife;    // undefined

但是,如果對象不存在,那麼試圖查詢這個不存在的對象的屬性就會報錯。null 和 undefined 值都沒有屬性,因此查詢這些值的屬性會報錯。例如:

var person = {};
person.wife.name;   // Uncaught TypeError: Cannot read property 'name' of undefined.

除非確定 person 和 person.wife 都是對象,否則不能這樣寫表達式 person.wife.name,因為會報「未捕獲的錯誤類型」,下面提供瞭兩種避免出錯的方法:

// 冗餘但易懂的寫法
var name;
if (person) {
    if (person.wife) 
        name = person.wife.name;
}

// 簡練又常用的寫法(推薦寫法)
var name = person && person.wife && person.wife.name;

刪除屬性

delete 運算符用來刪除對象屬性,事實上 delete 隻是斷開屬性和宿主對象的聯系,並沒有真正的刪除它。delete 運算符隻能刪除自有屬性,不能刪除繼承屬性(要刪除繼承屬性必須從定義這個屬性的原型對象上刪除它,而且這會影響到所有繼承自這個原型的對象)。

代碼范例,請參見「變量和數據類型」-「數據類型」-「delete 運算符」。

檢測屬性

JavaScript 對象可以看做屬性的集合,我們經常會檢測集合中成員的所屬關系(判斷某個屬性是否存在於某個對象中)。可以通過 in 運算符、hasOwnPreperty() 和 propertyIsEnumerable() 來完成這個工作,甚至僅通過屬性查詢也可以做到這一點。

in 運算符的左側是屬性名(字符串),右側是對象。如果對象的自有屬性或繼承屬性中包含這個屬性則返回 true。例如:

var o = { x: 1 }
console.log("x" in o);          // true,x是o的屬性
console.log("y" in o);          // false,y不是o的屬性
console.log("toString" in o);   // true,toString是繼承屬性

對象的 hasOwnProperty() 方法用來檢測給定的名字是否是對象的自有屬性。對於繼承屬性它將返回 false。例如:

var o = { x: 1 }
console.log(o.hasOwnProperty("x"));          // true,x是o的自有屬性
console.log(o.hasOwnProperty("y"));          // false,y不是o的屬性
console.log(o.hasOwnProperty("toString"));   // false,toString是繼承屬性

propertyIsEnumerable() 是 hasOwnProperty() 的增強版,隻有檢測到是自有屬性且這個屬性的可枚舉性(enumerable attribute)為 true 時它才返回 true。某些內置屬性是不可枚舉的。通常由 JavaScript 代碼創建的屬性都是可枚舉的,除非在 ECMAScript 5中使用一個特殊的方法來改變屬性的可枚舉性。例如:

var o = inherit({ y: 2 });
o.x = 1;
o.propertyIsEnumerable("x");    // true:,x是o的自有屬性,可枚舉
o.propertyIsEnumerable("y");    // false,y是繼承屬性
Object.prototype.propertyIsEnumerable("toString");  // false,不可枚舉

除瞭使用 in 運算符之外,另一種更簡便的方法是使用 !== 判斷一個屬性是否是 undefined。例如:

var o = { x: 1 }
console.log(o.x !== undefined);              // true,x是o的屬性
console.log(o.y !== undefined);              // false,y不是o的屬性
console.log(o.toString !== undefined);       // true,toString是繼承屬性

然而有一種場景隻能使用 in 運算符而不能使用上述屬性訪問的方式。in 可以區分不存在的屬性和存在但值為 undefined 的屬性。例如:

var o = { x: undefined }        // 屬性被顯式賦值為undefined
console.log(o.x !== undefined); // false,屬性存在,但值為undefined
console.log(o.y !== undefined); // false,屬性不存在
console.log("x" in o);          // true,屬性存在
console.log("y" in o);          // false,屬性不存在
console.log(delete o.x);        // true,刪除瞭屬性x
console.log("x" in o);          // false,屬性不再存在

枚舉屬性

除瞭檢測對象的屬性是否存在,我們還會經常遍歷對象的屬性。通常使用 for-in 循環遍歷,ECMAScript 5提供瞭兩個更好用的替代方案。

for-in 循環可以在循環體中遍歷對象中所有可枚舉的屬性(包括自有屬性和繼承的屬性),把屬性名稱賦值給循環變量。對象繼承的內置方法不可枚舉的,但在代碼中給對象添加的屬性都是可枚舉的。例如:

var o = {x:1, y:2, z:3};            // 三個可枚舉的自有屬性
o.propertyIsEnumerable("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;          // 跳過繼承的屬性
   if (typeof o[p] === "function") continue;    // 跳過方法
}

除瞭 for-in 循環之外,ECMAScript 5定義瞭兩個用以枚舉屬性名稱的函數。第一個是 Object.keys(),它返回一個數組,這個數組由對象中可枚舉的自有屬性的名稱組成。第二個是 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。定義存取器屬性最簡單的方法是使用對象直接量語法的一種擴展寫法。例如:

var o = {
    // 普通的數據屬性
    data_prop: value,

    // 存取器屬性都是成對定義的函數
    get accessor_prop() { /*這裡是函數體 */ },
    set accessor_prop(value) { /* 這裡是函數體*/ }
};

存取器屬性定義為一個或兩個和屬性同名的函數,這個函數定義沒有使用 function 關鍵字,而是使用 get 或 set。註意,這裡沒有使用冒號將屬性名和函數體分隔開,但在函數體的結束和下一個方法或數據屬性之間有逗號分隔。

序列化對象(JSON)

對象序列化(serialization)是指將對象的狀態轉換為字符串,也可將字符串還原為對象。ECMAScript 5提供瞭內置函數 JSON.stringify() 和 JSON.parse() 用來序列化和還原 JavaScript 對象。這些方法都使用 JSON 作為數據交換格式,JSON 的全稱是「JavaScript 對象表示法(JavaScript Object Notation)」,它的語法和 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中的這些函數的本地實現和 https://github.com/douglascrockford/JSON-js 中的公共域ECMAScript 3版本的實現非常類似,或者說完全一樣,因此可以通過引入 json2.js 模塊在ECMAScript 3的環境中使用ECMAScript 5中的這些函數。

JSON 的語法是 JavaScript 語法的子集,它並不能表示 JavaScript 裡的所有值。它支持對象、數組、字符串、無窮大數字、true、false 和 null,可以序列化和還原它們。NaN、Infinity 和 -Infinity 序列化的結果是 null,日期對象序列化的結果是 ISO 格式的日期字符串(參照 Date.toJSON() 函數),但 JSON.parse() 依然保留它們的字符串形態,而不會將它們還原為原始日期對象。函數、RegExp、Error 對象和 undefined 值不能序列化和還原。JSON.stringify() 隻能序列化對象可枚舉的自有屬性。對於一個不能序列化的屬性來說,在序列化後的輸出字符串中會將這個屬性省略掉。JSON.stringify() 和 JSON.parse() 都可以接收第二個可選參數,通過傳入需要序列化或還原的屬性列表來定制自定義的序列化或還原操作。

關卡

請實現下面用來枚舉屬性的對象工具函數:

/*
 * 把 p 中的可枚舉屬性復制到 o 中,並返回 o
 * 如果 o 和 p 中含有同名屬性,則覆蓋 o 中的屬性
 */
function extend(o, p) {
    // 請實現函數體
}
/*
 * 將 p 中的可枚舉屬性復制至 o 中,並返回 o
 * 如果 o 和 p 中有同名的屬性,o 中的屬性將不受影響
 */
function merge(o, p) {
    // 請實現函數體
}
/*
 * 如果 o 中的屬性在 p 中沒有同名屬性,則從 o 中刪除這個屬性
 * 返回 o
 */
function restrict(o, p) {
    // 請實現函數體
}
/*
 * 如果 o 中的屬性在 p 中存在同名屬性,則從 o 中刪除這個屬性
 * 返回 o
 */
function subtract(o, p) {
    // 請實現函數體
}
/*
 * 返回一個新對象,這個對象同時擁有 o 的屬性和 p 的屬性
 * 如果 o 和 p 中有重名屬性,使用 p 中的屬性值
 */
function union(o, p) { 
    // 請實現函數體
}
/*
 * 返回一個新對象,這個對象擁有同時在 o 和 p 中出現的屬性
 * 很像求 o 和 p 的交集,但 p 中屬性的值被忽略
 */
function intersection(o, p) { 
    // 請實現函數體
}
/*
 * 返回一個數組,這個數組包含的是 o 中可枚舉的自有屬性的名字
 */
function keys(o) {
    // 請實現函數體
}

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *