javascript 核心語言筆記 6 - 對象

對象是 JavaScript 的基本數據類型。是一種複合值:將不少值聚合在一塊兒。對象能夠看作是無序集合,每一個屬性都是一個名/值對。這種基本數據結構還有不少叫法,好比「散列」(hash)、「散列表」(hashtable)、「字典」(dictionary)、「關聯數組」(associative array)。JavaScript 還能夠從一個稱爲 原型 的對象繼承屬性git

JavaScript 對象是動態的 —— 能夠新增屬性也能夠刪除屬性,除了字符串、數字、布爾值、null 和 undefined 以外,JavaScript 中的值都是對象github

對象是可變的,經過引用操做對象原對象也會受到影響json

屬性包括名字和值。名字是能夠包含空字符串在內的 任意字符串,值能夠是任意 JavaScript 值,或者(在 ECMAScript 5中)能夠是一個 getter 或者 setter (或都有),每一個屬性還有一些與之相關的值稱爲「屬性特性」(property attribute):api

  • 可寫(writable)
  • 可枚舉(enumerable)
  • 可配置(configurable),代表是否能夠刪除或者修改

ECMAScript 5 以前,經過代碼給對象建立的全部屬性都是可寫、可枚舉和可配置的數組

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

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

下面這些術語用來區分三類 JavaScript 對象和兩類屬性:數據結構

  • 內置對象(native object),由 ECMAScript 規範定義的對象或類。例如 數組、日期
  • 宿主對象(host object),由 JavaScript 解釋器所嵌入的宿主環境(好比瀏覽器)定義的。好比瀏覽器中的 HTMLElement,document
  • 自定義對象(user-defined object),由運行中的 JavaScript 代碼建立的對象
  • 自有屬性(own property),直接在對象中定義的屬性
  • 繼承屬性(inherited property)是在對象的原型對象中定義的屬性

建立對象

對象直接量

 1 var empty = {}
 2 var point = { x:0, y:0 }
 3 var point2 = { x:point.x, y:point.y + 1 }
 4 var book = {
 5     "main title": "JavaScript",
 6     "for": "all audiences",
 7     author: {
 8         firstname: "David",
 9         surname: "Flanagan"
10     }
11 }

在 ECMAScript 5 中,保留字能夠用作不帶引號的屬性名。而後對於 ECMAScript 3 來講,使用保留字做爲屬性名必須使用引號引發來。ECMAScript 5 中屬性最後一個逗號會被忽略,但在 IE 中則報錯dom

經過 new 建立對象

new 運算符建立並初始化一個新對象。new 後跟隨一個函數調用。這裏的函數稱作構造函數(constructor),用來初始化一個新建立的對象。JavaScript 語言核心的原始類型都包含內置構造函數(另外一方面也證明了 JavaScript 中一切皆對象)函數

1 var o = new Object();
2 var a = new Array();
3 var d = new Date();
4 var r = new RegExp('js');

原型

每個 JavaScript 對象(null 除外)都和另外一個對象相關聯,這個對象就是「原型」,每個對象都從原型繼承屬性工具

經過 new 建立的對象原型就是構造函數的 prototype 屬性值,經過 new Object() 建立的對象也繼承自 Obejct.property

沒有原型對象的爲數很少,Obejct.prototype 就是其中之一。它不繼承任何屬性,普通對象都具備原型。全部的內置構造函數都具備一個繼承自 Object.prototype 的原型。例如,Date.prototype 的屬性繼承自 Object.prototype,所以由 new Date() 建立的 Date 對象的屬性現時繼承自 Date.prototype 和 Object.prototype,這一系列連接的原型對象就是所謂的「原型鏈」(prototype chain)

Object.create()

ECMAScript 5 定義了一個名爲 Obejct.create() 的方法,用來建立一個新對象,其中第一個參數是這個對象的原型,第二個可選參數用來對對象的屬性進行進一步描述,Object.create() 是一個 靜態函數,不是提供給對象調用的方法

1 var o1 = Object.create({ x:1, y:2 });       // o1 繼承了屬性 x 和 y
2 var o2 = Obejct.create(null);               // o2 不繼承任何屬性和方法

在 ECMAScript 3 中能夠用相似代碼來模擬原型繼承:

 1 function inherit(p) {
 2     if (p == null) throw TypeError();
 3     if (Object.create) return Object.create(p);
 4 
 5     var t = typeof p;
 6     if (t !== "object" && t !== "undefined") throw TypeError();
 7 
 8     function f() {}
 9     f.prototype = p;
10 
11     return new f();
12 }
13 
14 var o = { x: "test o" }
15 
16 var c = inherit(o);
17 
18 c.x = "test c";
19 
20 console.log(c.x);       // => "test c"
21 console.log(o.x);       // => "test o"

屬性的查詢和設置

1 var author = book.author;           // 取得 book 的 author 屬性
2 var title = book["main title"];     // 使用 [] 訪問屬性時 [] 內必須是一個計算結果爲字符串的表達式
3 
4 book.edition = 6;                   // 給 book 建立一個名爲 edition 的屬性,「.」號運算符後的標識符不能是保留字

做爲關聯數組的對象

當經過 [] 來訪問對象屬性時,屬性名經過字符串來表示。字符串是 JavaScript 的數據類型,在程序運行時能夠修改建立它們。所以,能夠在 JavaScript 中使用下面這種代碼來動態添加/查找屬性:

1 var addr = "";
2 for (i = 0; i < 4; i++) {
3     addr += customer["address" + i] + '\n';
4 }

繼承

假設要查詢對象 o 的屬性 x,若是 o 中不存在 x,那麼將會繼續在 o 的原型對象中查詢屬性 x。若是原型對象中也沒有 x,但這個原型對象還有原型,那麼繼續在這個原型對象的原型上執行查找,直到找到 x 或者找到一個原型是 null 的對象爲止。能夠看出來,原型的屬性構成了一個「連接」,經過這個「鏈」能夠實現屬性的繼承

 1 var o = {}
 2 o.x = 1;
 3 
 4 var p = inherit(o);
 5 p.y = 2;
 6 
 7 var q = inherit(p);
 8 q.z = 3;
 9 
10 var s = q.toString();   // => "[object Object]"
11 q.x + q.y               // => 3

屬性訪問錯誤

屬性訪問並不老是返回或設置一個值,下頁場景給對象 o 設置 屬性 p 會失敗:

  • o 中的屬性 p 是隻讀的(defineProperty() 方法中有一個例外,能夠對可配置的只讀屬性從新賦值)
  • o 中不存在自有屬性 p:o 沒有使用 setter 方法繼承屬性 p,而且 o 的可擴展性(extensible attribute)是 false。若是 o 中不存在 p,並且沒有 setter 方法可供調用,則 p 必定會添加至 o 中。若是 o 不是可擴展的,那麼在 o 中不能定義新的屬性

刪除屬性

使用 delete 運算符能夠刪除對象的屬性,delete 運算符只能刪除 自有屬性,不能刪除繼承屬性(要刪除繼承屬性必須從定義這個屬性的原型對象上刪除它,並且這會影響到全部繼承自這個原型的對象)

若是刪除成功或者刪除了一個沒有影響的值(不存在的屬性),delete 表達式返回 true。當 delete 運算符的操做數不是一個對象的屬性的時候也返回 true

 1 var o = { x: 1 }
 2 delete o.x;             // => true
 3 delete o.x;             // => true x 並不存在
 4 delete o.toString;      // => true toString 是繼承屬性
 5 delete 1                // => true 不是對象屬性
 6 this.b = 1;
 7 delete b;               // => true 刪除全局對象上的變量 b
 8 
 9 delete Object.property  // => false
10 var x = 1;
11 delete this.x;          // => false 不能刪除這個屬性,由於是經過 var 聲明的
12 function f() {}
13 delete f                // => false 不能刪除全局函數

檢測屬性

能夠經過 in 運算符、hasOwnProperty() 方法和 propertyIsEnumerable() 方法來檢測對象是否存在某屬性,propertyIsEnumerable 只有檢測到是自有屬性且這個屬性的可枚舉性爲 true 時它才返回 true

 1 var o = { x: 1 };
 2 "x" in o;                          // => true
 3 "y" in o;                          // => false
 4 "toString" in o                    // => true
 5 
 6 o.hasOwnProperty("x")              // => true
 7 o.hasOwnProperty("y")              // => false
 8 o.hasOwnProperty("toString")       // => false
 9 
10 var o = inherit({ y: 2});
11 o.x = 1;
12 o.propertyIsEnumerable("x")        // => true
13 o.propertyIsEnumerable("y")        // => false
14 o.propertyIsEnumerable("toString") // => false

還能夠經過判斷屬性是不是 undefined 來模擬 in 運算符

1 o.x !== undefined;                 // => true
2 o.y !== undefined;                 // => false
3 o.toString !== undefined;          // => true

然而有一種場景只能使用 in 運算符而不能經過只判斷 undefined 的方式。in 能夠區分不存在的屬性和存在但值爲 undefined 的屬性

1 var o = { x: undefined }
2 o.x !== undefined           // => false 存在 x,只是值爲 undefined
3 o.y !== undefined           // => false
4 "x" in o                    // => true
5 "y" in o                    // => false
6 delete o.x                  // => true
7 "x" in o                    // => false delete 後 o 徹底不存在了

枚舉屬性

許多工具庫給 Object.prototype 添加了新的方法或者屬性(一般不建議這麼作),這些方法和屬性能夠被全部對象繼承並使用。然而在 ECMAScript 5 標籤以前,這些添加的方法是 不能定義爲不可枚舉的,所以它們均可以在 for/in 循環枚舉出來。爲了不這和踐狀況,須要過濾 for/in 循環返回的屬性,下面兩種方法是最多見的:

1 Object.prototype.test = 1;
2 var o = { a: 1, b:2, c: function() {} };
3 for (p in o) {
4     if (!o.hasOwnProperty(p)) continue;
5     console.log(p);
6 }
7 for (p in o) {
8     if (typeof o[p] === "function") continue;
9 }

除了 for/in 循環以外,ECMAScript 5 定義了兩個用以枚舉屬性名稱的函數。第一個是 Object.keys(),它返回一個數組,由對象中的 可枚舉的自有屬性名稱 組成,第二個是 Object.getOwnPropertyNames(),它和上面的方法相似,只是它返回對象的 全部自有屬性名稱,不只僅是可枚舉的屬性

屬性 getter 和 setter

在 ECMAScript 5 中,屬性的值能夠用一個或兩個方法替代,這兩個方法就是 getter 和 setter。由它們定義的屬性稱作「存取器屬性」(accessor property),不一樣於「數據屬性」(data property),數據屬性只有一個簡單的值

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

使用存取器屬性寫入的屬性不具備可寫性(writable)。若是屬性同時具備 getter 和 setter 方法,那麼它是一個讀/寫屬性。哪果它只有 getter 方法,那麼它是一個只讀屬性。若是隻有 setter 方法,那麼它是一個只寫屬性,讀取只寫屬性老是返回 undefined

 1 var p = {
 2   x: 1.0,
 3   y: 1.0,
 4 
 5   get r() {
 6     return Math.sqrt(this.x*this.x + this.y*this.y);
 7   },
 8   set r(newValue) {
 9     var oldValue = Math.sqrt(this.x*this.x + this.y*this.y);
10     var ratio = newValue/oldValue;
11 
12     this.x *= ratio;
13     this.y *= ratio;
14   },
15   get theta() {
16     return Math.atan2(this.y, this.x)
17   }
18 };
19 p.r             // => 1.4142135623730951

屬性的特性

除了包含名字和值以外,屬性還包含一些標識它們可寫、可枚舉和可配置的特性。ECMAScript 3 程序建立的屬性都是可寫、可枚舉、可配置的,且沒法對這些特性作出修改。ECMAScript 5 中卻提供了查詢和設置這些屬性鵝的 API,這些 API 對於庫的開發者來講很是重要,由於:

  • 能夠經過這些 API 給原型對象添加方法,並將它們設置成不可枚舉的,讓它們看起來更像內置方法
  • 能夠經過這些 API 給對象定義不修改或刪除的屬性藉此「鎖定」這個對象

數據屬性 的 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() 能夠得到某個對象特定屬性的屬性描述符

 1 // => {value: 1, writable: true, enumerable: true, configurable: true}
 2 Object.getOwnPropertyDescriptor({ x: 1}, "x")
 3 var random = {
 4     get octet() {
 5         return Math.floor(Math.random() * 256)
 6     },
 7     get uint16() {
 8         return Math.floor(Math.random() * 65536)
 9     },
10     get int16() {
11         return Math.floor(Math.random() * 65536 - 32768)
12     }
13 }
14 // => {set: undefined, get: function, enumerable: true, configurable: true}
15 Object.getOwnPropertyDescriptor(random, "octet")
16 // => undefined
17 Object.getOwnPropertyDescriptor({}, "x")

從函數名字就能夠看出來 Object.getOwnPropertyDescriptor() 只能獲得自有屬性的描述符。繼承屬性的特性須要遍歷原型鏈

要想設置屬性的特性,或者讓新建屬性具備某種特性,則須要調用 Object.defineProperty(),傳入要修改的對象、要建立或者修改的屬性的名稱之前屬性描述符對象:

 1 var o = {};
 2 Object.defineProperty(o, "x", {
 3     value: 1,
 4     writable: true,
 5     enumerable: false,
 6     configurable: true
 7 });
 8 // x 屬性存在但不可枚舉
 9 Object.keys()       // => []
10 
11 Object.defineProperty(o, "x", { writable: false })
12 o.x = 2             // 試圖更改這個屬性的值,會操做失敗不報錯,嚴格模式中則拋出類型錯誤異常
13 o.x                 // => 1
14 
15 // 將 x 從數據屬性修改成存取器屬性
16 Object.defineProperty(o, "x", { value: 2 })
17 Object.defineProperty(o, "x", { get: function() { return 0} }
18 o.x                 // => 0

傳入 Object.defineProperty() 的屬性描述符對象 沒必要 包含全部 4 個特性。對於建立屬性來講,默認的特性值是 false 或 undefined。對於修改的已有屬性來講,默認的特性值沒有作任何修改。注意,這個方法要麼修改已有屬性要麼新建自胡屬性,但 不能修改繼承屬性,想要同時修改或者建立多個屬性則須要使用 Object.defineProperties(),使用方法能夠參考 MDN 相關 api

對於那些不容許建立或者修改的屬性來講,若是用 Object.defineProperty() 對其操做就會拋出類型錯誤異常,好比給一個不可擴展的對象新增屬性就會拋出類型錯誤異常。可寫性控制着對特定值特性的修改,可配置性控制着對其它特性的修改,使用的時候如下狀況會拋出類型錯誤異常:

  • 若是對象是不可擴展的,則能夠編輯已有的自有屬性,但不能給它添加新屬性
  • 若是屬性是不可配置的,則不能修改它的可配置性和可枚舉性
  • 若是存取器屬性是不可配置的,則不能修改其 getter 和 setter 方法,也不能將它轉換爲數據屬性
  • 若是數據屬性是不可配置的,則不能將它轉換爲存取器屬性
  • 若是數據屬性是不可配置的,則不能將它的可寫性從 false 修改成 true,但能夠從 true 修改成 false
  • 若是數據屬性是不可配置且不可寫的,則不能修改它的值,然而 可配置但不可寫的屬性值是能夠修改的
     1 // 給 Object.prototype 添加一個不可枚舉的 extend() 方法
     2 // 這個方法繼承自調用它的對象,將做爲參數什入的對象屬性都複製
     3 Object.defineProperty(Object.prototype, "extend", {
     4     writable: true,
     5     enumerable: false,
     6     configurable: true,
     7     value: function(o) {
     8         var names = Object.getOwnPropertyNames(0);
     9 
    10         for (var i = 0, l = names.length; i < l; i++) {
    11             if (names[i] in this) continue;
    12 
    13             var desc = Object.getOwnPropertyDescriptor(o, name[i]);
    14             Object.defineProperty(this, names[i], desc)
    15         }
    16     }
    17 });

    getter 和 setter 的老式 API

    在ECMAScript 5標準被採納以前,大多數 JavaScript 的實現(IE 除外)已經能夠支持對象直接量語法中的 get 和 set 寫法。這些實現提供了非標準的老式 API 用來查詢和設置 getter 和 setter。這些 API 由 4 個方法組成,全部對象都擁有這些方法。__lookupGetter__() 和 __lookupSetter__() 用以返回一個命名屬性的 getter 和 setter 方法,__defineSetter__() 和 __defineGetter__() 用以定義 getter 和 setter

    對象的三個屬性

    每一個對象都胡與之相關的 原型(prototype)、(class)和 可擴展性(extensible attribute)

    原型屬性

    原型屬性是在實例對象建立之初就設置好的,ECMAScript 5 中,對象做爲參數傳入 Object.getPrototypeOf() 能夠查看它的原型,在 ECMAScript 3 中,則沒有與之等價的函數,但常用表達式 o.constructor.prototype 來檢測一個對象的原型。經過 new 表達式建立的對象,一般繼承一個 constructor 屬性,這個屬性指代建立這個對象的構造函數

    要想檢測一個對象是不是另外一個對象的原型(或者處於原型鏈中),請使用 isPrototypeOf() 方法,這個方法和 instanceof 運算符很是相似,例如:

    1 var p = { x:1 };
    2 var o = Object.create(p);
    3 p.isPrototypeOf(o)                  // => true
    4 Object.prototype.isPrototypeOf(o)   // => true

    類屬性

    對象的類屬性是一個字符串,用以表示對象的類型信息。ECMAScript 3⁄5 都未提供設置這個屬性的方法,並只有一種間接的方法能夠查詢它。默認的 toString() 方法(繼承自 Object.prototype),返回了以下這種格式的字符串:

    [object class]

    因此能夠經過 toString() 方法返回的字符串截取處理取到 class 名,不過不少對象繼承的 toString() 方法被重寫了,爲了能調用正確的 toString() 版本,必須間接地調用 Function.call() 方法

     1 function classof(o) {
     2     if (o === null) return "Null";
     3     if (o === undefined) return "Undefined";
     4     return Object.prototype.toString.call(o).slice(8, -2);
     5 }
     6 classof(null)     // => "Null"
     7 classof(1)        // => "Number"
     8 classof("")       // => "String"
     9 classof(true)     // => "Boolean"
    10 classof({})       // => "Object"
    11 classof([])       // => "Array"
    12 classof(/./)      // => "Regexp"
    13 classof(new Date) // => "Date"
    14 function f() {}
    15 classof(new f())  // => "Object"

    可擴展屬性

    可擴展性用以表示是否能夠給對象是添加新屬性。全部內置對象和自定義對象都是顯式可擴展的,宿主對象的可擴展屬性是由 JavaScript 引擎定義的,ECMAScript 5 中,全部的內置對象和自定義對象都是可擴展的,除非將它們轉換爲不可擴展的,宿主對象的可擴展性也是由實現 ECMAScript 5 的 JavaScript 引擎定義的

    ECMAScript 5 定義了用來查詢和設置對象可擴展性的函數:Object.isExtensible(),若是將對象轉換爲不可擴展的,須要調用 Object.preventExtensions(),不過一量旦將對象轉換爲不可擴展的,就沒法再轉換回去了。

    Object.seal() 和 Object.preventExtensions() 相似,除了能將對象設置爲不可擴展的,還能夠將對象的全部自有屬性都設置爲不可配置的,也就是說不能給對象添加新的屬性,已有的屬性也不能刪除或配置,已封閉(sealed)的對象是不能解封的,可使用 Object.isSealed() 來檢測對象是否封閉

    Object.freeze() 將更嚴格地鎖定對象 —— 「凍結」,它還能夠將它自有的全部數據屬性設置爲只讀,可使用 Object.isFrozen() 來檢測對象是否凍結

    序列化對象

    對象序列化(serialization)是指將對象的狀態轉換爲字符串,也可將字符串還原爲對象。ECMAScript 5 提供了內置函數 JSON.stringify 和 JSON.parse() 用來序列化和還原 JavaScript 對象。這些方法都使用 JSON 做爲數據交換格式,JSON的全稱是「JavaScript Object Notation」—— JavaScript 對象表示法,正如其名,它的語法和 JavaScript 對象與數組直接量的語法很是相近

    ECMAScript 3 環境中能夠引用 json2 類庫來支持這兩個序列化函數

    JSON 語法是 JavaScript 語法的子集,它並不能表示 JavaScript 裏的全部值,函數、RegExp、Error 對象和 undefined 值不能序列化和不肯。JSON.stringify() 只能序列化對象可枚舉的自有屬性,關於 JSON 對象更多 API 能夠參考 JSON.stringify

    對象方法

    toString() 方法

    toString() 方法沒有參數,在須要將對象轉換爲字符串的時候,JavaScript 都調用這個方法

    1 var s = { x: 1, y: 1 }
    2 s.toString();       // => "[object Ojbect]"

    toLocaleString() 方法

    返回一個對象的本地化字符串。Object 中默認的 toLocaleString() 方法並不作任何本地化自身操做,它僅調用 toString() 方法並返回值。Date 和 Number 類對 toString() 方法作了定製,能夠用它對數字、日期和時間作本地化的轉換

    toJSON() 方法

    Object.prototype 實際上不有定義 toJSON() 方法,但對於須要執行序列化的對象來講,JSON.stringify() 方法會調用 toJSON() 方法,若是存在則調用它,返回值便是序列化的結果,而不是原始對象,參見 Date.toJSON

    valueOf() 方法

    valueOf() 和 toString() 方法很是相似,但每每當 JavaScript 須要 將對象轉換爲某種原始值而非字符串 的時候纔會用到它,尤爲是轉換爲數字的時候。若是在須要使用原始值的上下文中使用了對象,JavaScript 就會自動調用這個方法,一樣有些內置類自定義了 valueOf() 方法,好比 Date.valueOf

相關文章
相關標籤/搜索