與C++、Java等強類型面向對象的編程語言不同,JavaScript對象是動態的——可以新增屬性也可以刪除屬性,同一類型的不同對象完全可能具備不同的屬性。
屬性包括名字和值。屬性名可以是包含空字符串在內的任意字符串,值可以是任意JavaScript值,或者(在ECMAScript 5中)可以是一個getter或setter函數(或兩者都有)。除了名字和值之外,每個屬性還有一些與之相關的值,稱為“屬性特性”(property attribute):
- 可寫(writable attribute),表明是否可以設置該屬性的值。
- 可枚舉(enumerable attribute),表明是否可以通過for/in循環返回該屬性。
- 可配置(configurable attribute),表明是否可以刪除或修改該屬性。
除了包含屬性之外,每個對象還擁有三個相關的對象特性(object attribute):
- 對象的原型(prototype)指向另外一個對象,本對象的屬性繼承自它的原型對象。
- 對象的類(class)是一個標識對象類型的字符串。
- 對象的擴展標記(extensible flag)指明了(在ECMAScript 5中)是否可以向該對象添加新屬性。
1. 創建對象
可以通過對象直接量、關鍵字new和(ECMAScript 5中的)Object.create()函數來創建對象。
1.1. 對象直接量
直接上代碼段比較容易理解 :
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中則報錯。
個人很不推薦這種方式,除非只是簡單地存儲某些數據,并不打算抽象化某些事物也不賦予其任何行為,否則在大規模屬性或原型函數的情況下不同的對象之間都有獨立存儲這些屬性和成員函數,導致更多的內存占用,也不符合面向對象的程序設計思想。對象直接量生成的對象也不具備明確的類型信息,這在大規模代碼的項目中代碼將是非常難以維護的。
1.2. 通過new創建對象
var o = new Object(); //創建一個空對象,和{}一樣
var a = new Array(); //創建一個空數組,和[]一樣
var d = new Date(); //創建一個表示當前時間的Date對象
var r = new RegExp("js"); //創建一個可以進行模式匹配的EegExp對象
1.3. Object.create()
ECMAScript 5定義了一個名為Object.create()的方法,它創建一個新對象,其中第一個參數是這個對象的原型。Object.create()提供第二個可選參數,用以對對象的屬性進行進一步描述。
var o1 = Object.create({x:1, y:2}); //o1繼承了屬性x和y
var o2 = Object.create(null); //o2不繼承任何屬性和方法
var o3 = Object.create(Object.prototype); //o3和{}和new Object()一樣
2. 繼承
這部分內容屬于拓展閱讀,剛好看到這兒就了解了一下類的繼承,可以參考阮一峰的文章:
- Javascript繼承機制的設計思想
- Javascript面向對象編程(一):封裝
- Javascript面向對象編程(二):構造函數的繼承
- Javascript面向對象編程(三):非構造函數的繼承
- call apply bind 區別
2.1. 通過new設計類繼承
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle(width, height) {
Shape.call(this, width, height); // Shape.apply(this, arguments);
this.width = width;
this.height = height;
}
// subclass extends superclass
Rectangle.prototype = new Shape();
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle(100, 100);
console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
2.2. 通過Object.create設計類繼承
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle(width, height) {
Shape.call(this, width, height); // Shape.apply(this, arguments);
this.width = width;
this.height = height;
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle(100, 100);
console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
3. 檢測屬性
JavaScript對象可以看做屬性的集合,我們經常會檢測集合中成員的所屬關系——判斷某個屬性是否存在于某個對象中。可以通過in運算符、hasOwnPreperty()和propertyIsEnumerable()方法來完成這個工作,甚至僅通過屬性查詢也可以做到這一點。
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有一個自有屬性x
o.hasOwnProperty("y"); //false:o中不存在屬性y
o.hasOwnProperty("toString"); //false:toString是繼承屬性
propertyIsEnumerable()是hasOwnProperty()的增強版,只有檢測到是自有屬性且這個屬性的可枚舉性(enumerable attribute)為true時它才返回true。某些內置屬性是不可枚舉的。通常由JavaScript代碼創建的屬性都是可枚舉的,除非在ECMAScript 5中使用一個特殊的方法來改變屬性的可枚舉性,隨后會提到:
var o=Object.create({y:2});
o.x=1;
o.propertyIsEnumerable("x"); //true:o有一個可枚舉的自有屬性x
o.propertyIsEnumerable("y"); //false:y是繼承來的
Object.prototype.propertyIsEnumerable("toString"); //false:不可枚舉
但是這兒要小心了,采用我們上一節的方法定義的繼承類,是否自有屬性可能會出乎我們的意料之外,先看下面代碼:
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle(width, height) {
Shape.call(this, width, height); // Shape.apply(this, arguments);
this.width = width;
this.height = height;
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle(100, 100);
rect.propertyIsEnumerable("x"); // true,本以為應該是false?
看起來有點前后矛盾,但是仔細想想應該是這樣的,因為我們在構造函數中調用 Shape.call(this, width, height);
的時候將自身傳給了父類,那么父類構造函數是在給子類傳遞過去的this設置屬性,因此Shape中定義的x和y就成了Rectangle的自有屬性了。
4. 枚舉屬性
除了for/in循環之外,ECMAScript 5定義了兩個用以枚舉屬性名稱的函數。第一個是Object.keys(),它返回一個數組,這個數組由對象中可枚舉的自有屬性的名稱組成。
ECMAScript 5中第二個枚舉屬性的函數是Object.getOwnPropertyNames(),它和Object.keys()類似,只是它返回對象的所有自有屬性的名稱,而不僅僅是可枚舉的屬性。在ECMAScript 3中是無法實現類似的函數的,因為ECMAScript 3中沒有提供任何方法來獲取對象不可枚舉的屬性。
5. 屬性getter和setter
var o = {
//普通的數據屬性
data_prop:value,
//存取器屬性都是成對定義的函數
get accessor_prop() {/*這里是函數體*/},
set accessor_prop(value) {/*這里是函數體*/}
};
這個很容易理解,記錄一下語法!
6. 屬性的特性
一個屬性包含一個名字和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.defineProperty 的用法:
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, "x", {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 0;}});
o.x; //=>0
如果要同時修改或創建多個屬性,則需要使用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.defineProperty()和Object.defineProperties()對其操作(新建或修改)就會拋出類型錯誤異常,比如,給一個不可擴展的對象新增屬性就會拋出類型錯誤異常。造成這些方法拋出類型錯誤異常的其他原因則和特性本身相關,可寫性控制著對值特性的修改,可配置性控制著對其他特性(包括屬性是否可以刪除)的修改。然而規則遠不止這么簡單,例如,如果屬性是可配置的話,則可以修改不可寫屬性的值。同樣,如果屬性是不可配置的,仍然可以將可寫屬性修改為不可寫屬性。下面是完整的規則,任何對Object.defineProperty()或Object.defineProperties()違反規則的使用都會拋出類型錯誤異常:
- 如果對象是不可擴展的,則可以編輯已有的自有屬性,但不能給它添加新屬性。
- 如果屬性是不可配置的,則不能修改它的可配置性和可枚舉性。
- 如果存取器屬性是不可配置的,則不能修改其getter和setter方法,也不能將它轉換為數據屬性。
- 如果數據屬性是不可配置的,則不能將它轉換為存取器屬性。
- 如果數據屬性是不可配置的,則不能將它的可寫性從false修改為true,但可以從true修改為false。
- 如果數據屬性是不可配置且不可寫的,則不能修改它的值。然而可配置但不可寫屬性的值是可以修改的(實際上是先將它標記為可寫的,然后修改它的值,最后轉換為不可寫的)。
7. 對象的三個屬性
每一個對象都有與之相關的原型(prototype)、類(class)和可擴展性(extensible attribute)。
7.1. 原型屬性
原型屬性是在實例對象創建之初就設置好的,通過對象直接量創建的對象使用Object.prototype作為它們的原型,通過new創建的對象使用構造函數的prototype屬性作為它們的原型,通過Object.create()創建的對象使用第一個參數(也可以是null)作為它們的原型。
在ECMAScript 5中,將對象作為參數傳入Object.getPrototypeOf()可以查詢它的原型。要想檢測一個對象是否是另一個對象的原型(或處于原型鏈中),可用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運算符非常類似。
7.2. 類屬性
對象的類屬性(class attribute)是一個字符串,用以表示對象的類型信息。ECMAScript 3和ECMAScript 5都未提供設置這個屬性的方法,并只有一種間接的方法可以查詢它。默認的toString()方法(繼承自Object.prototype)返回了如下這種格式的字符串:[object class]
function classof(o) {
if (o === null) return "Null";
if (o === undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8, -1);
}
但是這個方法也不能得到準確的類名,比如上面第2節中『繼承』中創建的對象rect:
classof(rect); //=>Object
但是我們可以通過以下的方法獲得其類名:
rect.constructor.name; // => Rectangle
7.3. 可擴展性
對象的可擴展性用以表示是否可以給對象添加新屬性。所有內置對象和自定義對象都是顯式可擴展的,宿主對象的可擴展性是由JavaScript引擎定義的。在ECMAScript 5中,所有的內置對象和自定義對象都是可擴展的,除非將它們轉換為不可擴展的,同樣,宿主對象的可擴展性也是由實現ECMAScript 5的JavaScript引擎定義的。
ECMAScript 5定義了用來查詢和設置對象可擴展性的函數。通過將對象傳入Object.isExtensible(),來判斷該對象是否是可擴展的。如果想將對象轉換為不可擴展的,需要調用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}
}));
8. 序列化對象
對象序列化(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的深拷貝