《JavaScript 闖關記》之對象

對象是 JavaScript 的數據類型。它將不少值(原始值或者其餘對象)聚合在一塊兒,可經過名字訪問這些值,所以咱們能夠把它當作是從字符串到值的映射。對象是動態的,能夠隨時新增和刪除自有屬性。對象除了能夠保持自有的屬性,還能夠從一個稱爲原型的對象繼承屬性,這種「原型式繼承(prototypal inheritance)」是 JavaScript 的核心特徵。javascript

對象最多見的用法是建立(create)、設置(set)、查找(query)、刪除(delete)、檢測(test)和枚舉(enumerate)它的屬性。java

屬性包括名字和值。屬性名能夠是包含空字符串在內的任意字符串,但對象中不能存在兩個同名的屬性。值能夠是任意 JavaScript 值,或者在 ECMAScript 5中能夠是 gettersetter 函數。git

除了名字和值以外,每一個屬性還有一些與之相關的值,稱爲「屬性特性(property attribute)」:github

  • 可寫(writable attribute),代表是否能夠設置該屬性的值。
  • 可枚舉(enumerable attribute),代表是否能夠經過 for-in 循環返回該屬性。
  • 可配置(configurable attribute),代表是否能夠刪除或修改該屬性。

在 ECMAScript 5以前,經過代碼給對象建立的全部屬性都是可寫的、可枚舉的和可配置的。在 ECMAScript 5中則能夠對這些特性加以配置。正則表達式

除了包含屬性特性以外,每一個對象還擁有三個相關的「對象特性(object attribute)」:json

  • 對象的類(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 語言核心中的原始類型都包含內置構造函數。例如:less

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.prototypeObject.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複製代碼

可是,若是對象不存在,那麼試圖查詢這個不存在的對象的屬性就會報錯。nullundefined 值都沒有屬性,所以查詢這些值的屬性會報錯。例如:

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

除非肯定 personperson.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,屬性再也不存在複製代碼

擴展閱讀「JavaScript 檢測原始值、引用值、屬性」
shijiajie.com/2016/06/20/…

擴展閱讀「JavaScript 檢測之 basevalidate.js」
shijiajie.com/2016/06/25/…

枚舉屬性

除了檢測對象的屬性是否存在,咱們還會常常遍歷對象的屬性。一般使用 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中沒有提供任何方法來獲取對象不可枚舉的屬性。

屬性的 gettersetter

咱們知道,對象屬性是由名字、值和一組特性(attribute)構成的。在ECMAScript 5中,屬性值能夠用一個或兩個方法替代,這兩個方法就是 gettersetter。由 gettersetter 定義的屬性稱作「存取器屬性(accessor property)」,它不一樣於「數據屬性(data property)」,數據屬性只有一個簡單的值。

當程序查詢存取器屬性的值時,JavaScript 調用 getter 方法。這個方法的返回值就是屬性存取表達式的值。當程序設置一個存取器屬性的值時,JavaScript 調用 setter 方法,將賦值表達式右側的值當作參數傳入 setter。從某種意義上講,這個方法負責「設置」屬性值。能夠忽略 setter 方法的返回值。

和數據屬性不一樣,存取器屬性不具備可寫性(writable attribute)。若是屬性同時具備 gettersetter 方法,那麼它是一個讀/寫屬性。若是它只有 getter 方法,那麼它是一個只讀屬性。若是它只有 setter 方法,那麼它是一個只寫屬性,讀取只寫屬性老是返回 undefined。定義存取器屬性最簡單的方法是使用對象直接量語法的一種擴展寫法。例如:

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

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

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

序列化對象(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中的這些函數的本地實現和 github.com/douglascroc… 中的公共域ECMAScript 3版本的實現很是相似,或者說徹底同樣,所以能夠經過引入 json2.js 模塊在ECMAScript 3的環境中使用ECMAScript 5中的這些函數。

JSON 的語法是 JavaScript 語法的子集,它並不能表示 JavaScript 裏的全部值。它支持對象、數組、字符串、無窮大數字、truefalsenull,能夠序列化和還原它們。NaNInfinity-Infinity 序列化的結果是 null,日期對象序列化的結果是 ISO 格式的日期字符串(參照 Date.toJSON() 函數),但 JSON.parse() 依然保留它們的字符串形態,而不會將它們還原爲原始日期對象。函數、RegExpError 對象和 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) {
    // 請實現函數體
}複製代碼

更多

關注微信公衆號「劼哥舍」回覆「答案」,獲取關卡詳解。
關注 github.com/stone0090/j…,獲取最新動態。

相關文章
相關標籤/搜索