與C++、Java等強類型面向?qū)ο蟮木幊陶Z言不同,JavaScript對象是動態(tài)的——可以新增屬性也可以刪除屬性,同一類型的不同對象完全可能具備不同的屬性摹恨。
屬性包括名字和值。屬性名可以是包含空字符串在內(nèi)的任意字符串规惰,值可以是任意JavaScript值睬塌,或者(在ECMAScript 5中)可以是一個getter或setter函數(shù)(或兩者都有)泉蝌。除了名字和值之外歇万,每個屬性還有一些與之相關(guān)的值,稱為“屬性特性”(property attribute):
- 可寫(writable attribute)勋陪,表明是否可以設(shè)置該屬性的值贪磺。
- 可枚舉(enumerable attribute),表明是否可以通過for/in循環(huán)返回該屬性诅愚。
- 可配置(configurable attribute)寒锚,表明是否可以刪除或修改該屬性。
除了包含屬性之外违孝,每個對象還擁有三個相關(guān)的對象特性(object attribute):
- 對象的原型(prototype)指向另外一個對象刹前,本對象的屬性繼承自它的原型對象。
- 對象的類(class)是一個標(biāo)識對象類型的字符串雌桑。
- 對象的擴展標(biāo)記(extensible flag)指明了(在ECMAScript 5中)是否可以向該對象添加新屬性喇喉。
1. 創(chuàng)建對象
可以通過對象直接量、關(guān)鍵字new和(ECMAScript 5中的)Object.create()函數(shù)來創(chuàng)建對象校坑。
1.1. 對象直接量
直接上代碼段比較容易理解 :
var empty = {}; //沒有任何屬性的對象
var point = {x:0, y:0}; //兩個屬性
var point2 = {x:point.x, y:point.y+1}; //更復(fù)雜的值
var book={
"main title": "JavaScript", //屬性名字里有空格,必須用字符串表示
'sub-title': "The Definitive Guide", //屬性名字里有連字符拣技,必須用字符串表示
"for": "all audiences", //"for"是保留字,因此必須用引號
author: { //這個屬性的值是一個對象
firstname: "David", //注意耍目,這里的屬性名都沒有引號
surname: "Flanagan"
}
};
在ECMAScript 5(以及ECMAScript 3的一些實現(xiàn))中膏斤,保留字可以用做不帶引號的屬性名。然而對于ECMAScript 3來說邪驮,使用保留字作為屬性名必須使用引號引起來莫辨。在ECMAScript 5中,對象直接量中的最后一個屬性后的逗號將忽略,且在ECMAScript 3的大部分實現(xiàn)中也可以忽略這個逗號衔掸,但在IE中則報錯烫幕。
個人很不推薦這種方式,除非只是簡單地存儲某些數(shù)據(jù)敞映,并不打算抽象化某些事物也不賦予其任何行為较曼,否則在大規(guī)模屬性或原型函數(shù)的情況下不同的對象之間都有獨立存儲這些屬性和成員函數(shù),導(dǎo)致更多的內(nèi)存占用振愿,也不符合面向?qū)ο蟮某绦蛟O(shè)計思想捷犹。對象直接量生成的對象也不具備明確的類型信息,這在大規(guī)模代碼的項目中代碼將是非常難以維護(hù)的冕末。
1.2. 通過new創(chuàng)建對象
var o = new Object(); //創(chuàng)建一個空對象萍歉,和{}一樣
var a = new Array(); //創(chuàng)建一個空數(shù)組,和[]一樣
var d = new Date(); //創(chuàng)建一個表示當(dāng)前時間的Date對象
var r = new RegExp("js"); //創(chuàng)建一個可以進(jìn)行模式匹配的EegExp對象
1.3. Object.create()
ECMAScript 5定義了一個名為Object.create()的方法档桃,它創(chuàng)建一個新對象枪孩,其中第一個參數(shù)是這個對象的原型。Object.create()提供第二個可選參數(shù)藻肄,用以對對象的屬性進(jìn)行進(jìn)一步描述蔑舞。
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. 繼承
這部分內(nèi)容屬于拓展閱讀,剛好看到這兒就了解了一下類的繼承嘹屯,可以參考阮一峰的文章:
- Javascript繼承機制的設(shè)計思想
- Javascript面向?qū)ο缶幊蹋ㄒ唬悍庋b
- Javascript面向?qū)ο缶幊蹋ǘ簶?gòu)造函數(shù)的繼承
- Javascript面向?qū)ο缶幊蹋ㄈ悍菢?gòu)造函數(shù)的繼承
- call apply bind 區(qū)別
2.1. 通過new設(shè)計類繼承
// 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設(shè)計類繼承
// 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對象可以看做屬性的集合攻询,我們經(jīng)常會檢測集合中成員的所屬關(guān)系——判斷某個屬性是否存在于某個對象中≈莸埽可以通過in運算符钧栖、hasOwnPreperty()和propertyIsEnumerable()方法來完成這個工作,甚至僅通過屬性查詢也可以做到這一點婆翔。
in運算符的左側(cè)是屬性名(字符串)拯杠,右側(cè)是對象。如果對象的自有屬性或繼承屬性中包含這個屬性則返回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。某些內(nèi)置屬性是不可枚舉的纺腊。通常由JavaScript代碼創(chuàng)建的屬性都是可枚舉的畔咧,除非在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:不可枚舉
但是這兒要小心了揖膜,采用我們上一節(jié)的方法定義的繼承類誓沸,是否自有屬性可能會出乎我們的意料之外,先看下面代碼:
// 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壹粟,本以為應(yīng)該是false拜隧?
看起來有點前后矛盾宿百,但是仔細(xì)想想應(yīng)該是這樣的,因為我們在構(gòu)造函數(shù)中調(diào)用 Shape.call(this, width, height);
的時候?qū)⒆陨韨鹘o了父類洪添,那么父類構(gòu)造函數(shù)是在給子類傳遞過去的this設(shè)置屬性垦页,因此Shape中定義的x和y就成了Rectangle的自有屬性了。
4. 枚舉屬性
除了for/in循環(huán)之外干奢,ECMAScript 5定義了兩個用以枚舉屬性名稱的函數(shù)痊焊。第一個是Object.keys(),它返回一個數(shù)組忿峻,這個數(shù)組由對象中可枚舉的自有屬性的名稱組成薄啥。
ECMAScript 5中第二個枚舉屬性的函數(shù)是Object.getOwnPropertyNames(),它和Object.keys()類似逛尚,只是它返回對象的所有自有屬性的名稱垄惧,而不僅僅是可枚舉的屬性。在ECMAScript 3中是無法實現(xiàn)類似的函數(shù)的绰寞,因為ECMAScript 3中沒有提供任何方法來獲取對象不可枚舉的屬性到逊。
5. 屬性getter和setter
var o = {
//普通的數(shù)據(jù)屬性
data_prop:value,
//存取器屬性都是成對定義的函數(shù)
get accessor_prop() {/*這里是函數(shù)體*/},
set accessor_prop(value) {/*這里是函數(shù)體*/}
};
這個很容易理解,記錄一下語法滤钱!
6. 屬性的特性
一個屬性包含一個名字和4個特性觉壶。數(shù)據(jù)屬性的4個特性分別是它的值(value)、可寫性(writable)菩暗、可枚舉性(enumerable)和可配置性(configurable)掰曾。存取器屬性不具有值(value)特性和可寫性旭蠕,它們的可寫性是由setter方法存在與否決定的停团,因此存取器屬性的4個特性是讀取(get)掏熬、寫入(set)佑稠、可枚舉性和可配置性。
為了實現(xiàn)屬性特性的查詢和設(shè)置操作旗芬,ECMAScript 5中定義了一個名為“屬性描述符”(property descriptor)的對象舌胶,這個對象代表那4個特性。描述符對象的屬性和它們所描述的屬性特性是同名的疮丛,因此數(shù)據(jù)屬性的描述符對象的屬性有value幔嫂、writable、enumerable和configurable誊薄。存取器屬性的描述符對象則用get屬性和set屬性代替value和writable履恩。其中writable、enumerable和configurable都是布爾值呢蔫,當(dāng)然切心,get屬性和set屬性是函數(shù)值。
通過調(diào)用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 = {}; //創(chuàng)建一個空對象
//添加一個不可枚舉的數(shù)據(jù)屬性x,并賦值為1
Object.defineProperty(o, "x", {
value: 1,
writable: true,
enumerable: false,
configurable: true
}); //屬性是存在的全谤,但不可枚舉
o.x; //=>1
Object.keys(o); //=>[]
//現(xiàn)在對屬性x做修改肤晓,讓它變?yōu)橹蛔x
Object.defineProperty(o, "x", {writable:false});
//試圖更改這個屬性的值
o.x = 2; //操作失敗但不報錯,而在嚴(yán)格模式中拋出類型錯誤異常
o.x; //=>1
//屬性依然是可配置的认然,因此可以通過這種方式對它進(jìn)行修改:
Object.defineProperty(o, "x", {value: 2});
o.x; //=>2
//現(xiàn)在將x從數(shù)據(jù)屬性修改為存取器屬性
Object.defineProperty(o, "x", {get: function() {return 0;}});
o.x; //=>0
如果要同時修改或創(chuàng)建多個屬性材原,則需要使用Object.defineProperties()。第一個參數(shù)是要修改的對象季眷,第二個參數(shù)是一個映射表余蟹,它包含要新建或修改的屬性的名稱,以及它們的屬性描述符子刮,例如:
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
}
});
對于那些不允許創(chuàng)建或修改的屬性來說威酒,如果用Object.defineProperty()和Object.defineProperties()對其操作(新建或修改)就會拋出類型錯誤異常,比如挺峡,給一個不可擴展的對象新增屬性就會拋出類型錯誤異常葵孤。造成這些方法拋出類型錯誤異常的其他原因則和特性本身相關(guān),可寫性控制著對值特性的修改橱赠,可配置性控制著對其他特性(包括屬性是否可以刪除)的修改尤仍。然而規(guī)則遠(yuǎn)不止這么簡單,例如狭姨,如果屬性是可配置的話宰啦,則可以修改不可寫屬性的值。同樣饼拍,如果屬性是不可配置的赡模,仍然可以將可寫屬性修改為不可寫屬性。下面是完整的規(guī)則师抄,任何對Object.defineProperty()或Object.defineProperties()違反規(guī)則的使用都會拋出類型錯誤異常:
- 如果對象是不可擴展的漓柑,則可以編輯已有的自有屬性,但不能給它添加新屬性叨吮。
- 如果屬性是不可配置的辆布,則不能修改它的可配置性和可枚舉性。
- 如果存取器屬性是不可配置的茶鉴,則不能修改其getter和setter方法锋玲,也不能將它轉(zhuǎn)換為數(shù)據(jù)屬性。
- 如果數(shù)據(jù)屬性是不可配置的蛤铜,則不能將它轉(zhuǎn)換為存取器屬性嫩絮。
- 如果數(shù)據(jù)屬性是不可配置的丛肢,則不能將它的可寫性從false修改為true,但可以從true修改為false剿干。
- 如果數(shù)據(jù)屬性是不可配置且不可寫的蜂怎,則不能修改它的值。然而可配置但不可寫屬性的值是可以修改的(實際上是先將它標(biāo)記為可寫的置尔,然后修改它的值杠步,最后轉(zhuǎn)換為不可寫的)。
7. 對象的三個屬性
每一個對象都有與之相關(guān)的原型(prototype)榜轿、類(class)和可擴展性(extensible attribute)幽歼。
7.1. 原型屬性
原型屬性是在實例對象創(chuàng)建之初就設(shè)置好的,通過對象直接量創(chuàng)建的對象使用Object.prototype作為它們的原型谬盐,通過new創(chuàng)建的對象使用構(gòu)造函數(shù)的prototype屬性作為它們的原型甸私,通過Object.create()創(chuàng)建的對象使用第一個參數(shù)(也可以是null)作為它們的原型。
在ECMAScript 5中飞傀,將對象作為參數(shù)傳入Object.getPrototypeOf()可以查詢它的原型皇型。要想檢測一個對象是否是另一個對象的原型(或處于原型鏈中),可用isPrototypeOf()方法砸烦。例如弃鸦,可以通過p.isPrototypeOf(o)來檢測p是否是o的原型:
var p = {x:1}; //定義一個原型對象
var o=Object.create(p); //使用這個原型創(chuàng)建一個對象
p.isPrototypeOf(o); //=>true:o繼承自p
Object.prototype.isPrototypeOf(o); //=>true:p繼承自O(shè)bject.prototype
isPrototypeOf()函數(shù)實現(xiàn)的功能和instanceof運算符非常類似。
7.2. 類屬性
對象的類屬性(class attribute)是一個字符串幢痘,用以表示對象的類型信息唬格。ECMAScript 3和ECMAScript 5都未提供設(shè)置這個屬性的方法,并只有一種間接的方法可以查詢它颜说。默認(rèn)的toString()方法(繼承自O(shè)bject.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);
}
但是這個方法也不能得到準(zhǔn)確的類名购岗,比如上面第2節(jié)中『繼承』中創(chuàng)建的對象rect:
classof(rect); //=>Object
但是我們可以通過以下的方法獲得其類名:
rect.constructor.name; // => Rectangle
7.3. 可擴展性
對象的可擴展性用以表示是否可以給對象添加新屬性。所有內(nèi)置對象和自定義對象都是顯式可擴展的脑沿,宿主對象的可擴展性是由JavaScript引擎定義的藕畔。在ECMAScript 5中马僻,所有的內(nèi)置對象和自定義對象都是可擴展的庄拇,除非將它們轉(zhuǎn)換為不可擴展的,同樣韭邓,宿主對象的可擴展性也是由實現(xiàn)ECMAScript 5的JavaScript引擎定義的措近。
ECMAScript 5定義了用來查詢和設(shè)置對象可擴展性的函數(shù)。通過將對象傳入Object.isExtensible()女淑,來判斷該對象是否是可擴展的瞭郑。如果想將對象轉(zhuǎn)換為不可擴展的,需要調(diào)用Object.preventExtensions()鸭你,將待轉(zhuǎn)換的對象作為參數(shù)傳進(jìn)去屈张。注意擒权,一旦將對象轉(zhuǎn)換為不可擴展的,就無法再將其轉(zhuǎn)換回可擴展的了阁谆。同樣需要注意的是碳抄,preventExtensions()只影響到對象本身的可擴展性。如果給一個不可擴展的對象的原型添加屬性场绿,這個不可擴展的對象同樣會繼承這些新屬性剖效。
可擴展屬性的目的是將對象“鎖定”,以避免外界的干擾焰盗。對象的可擴展性通常和屬性的可配置性與可寫性配合使用璧尸,ECMAScript 5定義的一些函數(shù)可以更方便地設(shè)置多種屬性。
Object.seal()和Object.preventExtensions()類似熬拒,除了能夠?qū)ο笤O(shè)置為不可擴展的爷光,還可以將對象的所有自有屬性都設(shè)置為不可配置的。也就是說澎粟,不能給這個對象添加新屬性瞎颗,而且它已有的屬性也不能刪除或配置,不過它已有的可寫屬性依然可以設(shè)置捌议。對于那些已經(jīng)封閉(sealed)起來的對象是不能解封的哼拔。可以使用Object.isSealed()來檢測對象是否封閉瓣颅。
Object.freeze()將更嚴(yán)格地鎖定對象——“凍結(jié)”(frozen)倦逐。除了將對象設(shè)置為不可擴展的和將其屬性設(shè)置為不可配置的之外,還可以將它自有的所有數(shù)據(jù)屬性設(shè)置為只讀(如果對象的存取器屬性具有setter方法宫补,存取器屬性將不受影響檬姥,仍可以通過給屬性賦值調(diào)用它們)。使用Object.isFrozen()來檢測對象是否凍結(jié)粉怕。
Object.preventExtensions()健民、Object.seal()和Object.freeze()都返回傳入的對象,也就是說贫贝,可以通過函數(shù)嵌套的方式調(diào)用它們:
//創(chuàng)建一個封閉對象秉犹,包括一個凍結(jié)的原型和一個不可枚舉的屬性
var o=Object.seal(Object.create(Object.freeze({x:1}), {
y: {value: 2, writable: true}
}));
8. 序列化對象
對象序列化(serialization)是指將對象的狀態(tài)轉(zhuǎn)換為字符串,也可將字符串還原為對象稚晚。ECMAScript 5提供了內(nèi)置函數(shù)JSON.stringify()和JSON.parse()用來序列化和還原JavaScript對象崇堵。這些方法都使用JSON作為數(shù)據(jù)交換格式,JSON的全稱是"JavaScript Object Notation"——JavaScript對象表示法客燕,它的語法和JavaScript對象與數(shù)組直接量的語法非常相近:
o = {x: 1, y: {z: [false, null, ""]}}; //定義一個測試對象
s = JSON.stringify(o); //s是'{"x":1,"y":{"z":[false,null,""]}}'
p = JSON.parse(s); //p是o的深拷貝