序
從最近的js入門系列的閱讀量逐步遞減,觀眾老爺?shù)呐d趣也不再能夠接受一些細(xì)節(jié)性的地方深度挖掘,讓我有了一些思考。是應(yīng)該繼續(xù)看書挖掘細(xì)節(jié),還是從易接受性來寫一些文章。但是我覺得這個系列本身就是讓大家體會一下js的深度與廣度,不想與許多快餐式的入門教程來重合。對于想學(xué)習(xí)的人來說,挖掘知識的深度,比知識的廣度更為重要,而且對于今后的學(xué)習(xí)都是莫大的提升。本身在許多知識點(diǎn)上面已經(jīng)省略了許多,將一些重要的知識呈現(xiàn)在在大家面前,可能需要觀眾老爺?shù)囊恍┠托?,畢竟文章、代碼都已經(jīng)超出了10分鐘閱讀的量,許多知識細(xì)節(jié),需要一整天來消化吸收。我希望在我沒有放棄之前,讀者們也不要放棄,覺得食之無味。細(xì)細(xì)的去體會里面的每一段話,相信你會收獲頗豐!
對象
對象(object)是js的基本數(shù)據(jù)類型。是一種復(fù)合值:將很多值(原始值或者其它對象)聚合在一起,可以通過名字訪問這些值。
每個屬性都是一個名/值對(key/value),屬性名是字符串,因此我們可以把對象看成從字符串到值的映射。
然而對象不僅僅是字符串到值的映射,除了保持自有的屬性,js對象還可以從一個稱為原型的對象繼承屬性。
對象的方法通常是繼承的屬性,這種原型式繼承是js的核心特征。
js對象是動態(tài)的,可以新增屬性,也可以刪除屬性。
除了字符串、數(shù)字、true、false、null和undefined之外,js中的值都是對象。
對象是可變的,我們通過引用而非值來操作對象。如果變量x是指向一個對象的引用,那么執(zhí)行代碼
var y = x;
變量y也是指向同一個對象的引用,而非這個對象的副本。通過y來修改這個對象也會對變量x造成影響
js的屬性,除了名字和值之外,每個屬性還有一些與之相關(guān)的值,稱為 屬性特性:
- 可寫,表明是否可以設(shè)置該屬性的值
- 可枚舉,表明是否可以通過for/in循環(huán)返回該屬性
- 可配置,表明是否可以刪除或修改該屬性
除了包含屬性之外,每個對象還擁有三個相關(guān)的對象特性:
- 對象的原型(prototype)指向另一個對象,本對象的屬性繼承自它的原型對象
- 對象的類是一個標(biāo)識對象類型的字符串
- 對象的擴(kuò)展標(biāo)記(extensible flag)指明了在ECMAScript5中是否可以向該對象添加新的屬性
三類js對象和兩類屬性的區(qū)分:
- 內(nèi)置對象(native object)是由ECMAScript規(guī)范定義的對象或類。例如,數(shù)組、函數(shù)、日期和正則表達(dá)式
- 宿主對象(host object)是由js解釋器所嵌入的宿主環(huán)境(比如WEB瀏覽器)定義的??蛻舳薺s中表示網(wǎng)頁結(jié)構(gòu)的HTMLElement對象均是宿主對象。既然宿主環(huán)境定義的方法可以當(dāng)成普通的js函數(shù)對象,那么宿主對象也可以當(dāng)成內(nèi)置對象
- 自定義對象(user-defined Object)是由運(yùn)行中的js代碼創(chuàng)建的對象
- 自有屬性(own property)是直接在對象中定義的屬性
- 繼承屬性(inherited property)是在對象的原型對象中定義的屬性
創(chuàng)建對象
創(chuàng)建對象主要有三種方法:
- 對象直接量
- 關(guān)鍵字new創(chuàng)建
- Object.create()函數(shù)
對象直接量
對象直接量是由若干名值對組成的映射表,名值對中間用冒號分割,名值對之間用逗號分割,整個映射表用花括號括起來
屬性名可以是js標(biāo)識符也可以是字符串直接量
屬性的值可以是任意類型js表達(dá)式、表達(dá)式的值就是這個屬性的值
來個例子:
var empty = {}; // 沒有任何屬性的對象
var point = { x:0, y:1 }; // 兩個屬性
var point2 = { x: point.x; y: point.y }; // 更復(fù)雜的屬性值
var book = {
"main title": "javascript", // 屬性名字有空格,必須用字符串表示
"sub-titile": "book", // 屬性名字有連字符,必須用字符串表示
"for": "all audience", // 屬性名字是保留字,必須用引號
author: { // 允許的名字可以沒有引號
firstname: "zhao",
subname: "lion"
}
}
對象直接量是一個表達(dá)式,這個表達(dá)式的每次運(yùn)算都創(chuàng)建并初始化一個新的對象。每次計算對象直接量時候,也都會計算它的每一個屬性的值。
通過new創(chuàng)建對象
new 運(yùn)算符創(chuàng)建并初始化一個新對象。關(guān)鍵字new后跟隨一個函數(shù)調(diào)用。這個函數(shù)被稱為 構(gòu)造函數(shù)(constructor)
構(gòu)造函數(shù)用來初始化一個新創(chuàng)建的對象。js語言核心中原始類型都包含內(nèi)置構(gòu)造函數(shù).比如:
var o = new Object(); // 創(chuàng)建一個空對象
var a = new Array(); // 創(chuàng)建一個空數(shù)組
var d = new Date(); // 創(chuàng)建一個當(dāng)前時間對象
var r = new RegExp("^js"); // 創(chuàng)建一個可以進(jìn)行模式匹配的RegExp對象
原型
每一個js對象(null除外)都和另一個對象相關(guān)聯(lián)。“另一個對象”就是我們經(jīng)常聽到的原型,每一個對象都從原型繼承屬性。
所有通過對象直接量創(chuàng)建的對象都具有同一個原型對象,并且可以通過Object.prototype來獲得原型對象的引用。
通過new創(chuàng)建和構(gòu)造函數(shù)調(diào)用創(chuàng)建的對象原型就是構(gòu)造函數(shù)的prototype屬性的值。因此,同使用{}創(chuàng)建對象一樣,通過new Object()創(chuàng)建的對象也繼承自O(shè)bject.prototype。
同樣,通過new Array()創(chuàng)建的對象原型就是Array.prototype,通過new Date()創(chuàng)建的對象的原型就是Date.prototype。
沒有原型的對象為數(shù)不多,Object.prototype就是一個。它不繼承任何屬性。
其它原型對象都是普通對象,普通對象都有原型。所有的內(nèi)置構(gòu)造函數(shù)(以及大部分自定義構(gòu)造函數(shù))都具有一個繼承自O(shè)bject.prototype的原型。而這一系列的原型對象就是所謂的“原型鏈”
Object.create()
ECMAScript5定義了一個Object.create()的方法,創(chuàng)建一個新對象,其中一個參數(shù)就是這個對象的原型。Object.create()提供第二個可選參數(shù),用以對新對象的屬性進(jìn)行進(jìn)一步描述.
語法如下:
Object.create(proto[, propertiesObject])
Object.create()是一個靜態(tài)函數(shù),不是提供給某個對象調(diào)用的方法。使用很簡單,傳入所需的原型對象即可:
var o1 = Object.create({ x:1, y:2}); // o1繼承屬性x和y
可以通過傳入?yún)?shù)null來創(chuàng)建一個沒有原型的新對象。通過這種方式創(chuàng)建的對象不會繼承任何東西,包括toString()
var o2 = Object.create(null); // o2不繼承任何屬性和方法
如果想創(chuàng)建一個普通的空對象(類似通過{}或new Object()創(chuàng)建的對象),需要 Object.prototype:
var o3 = Object.create(Object.prototype); // o3和{}和new Object()創(chuàng)建對象一樣
可以通過任何原型創(chuàng)建新對象。但是如果遇到j(luò)s實(shí)現(xiàn)不支持Object.create()時,如何模擬原型繼承呢?看下代碼:
function inheritPrototype(p){
if(p==null) throw TypeError(); // p是一個對象,不能是null
if(Object.create) // 如果存在Object.create方法,使用
return Object.create(p);
var t = typeof p; // 否則進(jìn)一步檢測
if(t !== "object" && t !== "function") throw TypeError(); // 如果既不是對象,也不是函數(shù),拋出異常
function f() {}; // 定義一個空構(gòu)造函數(shù)
f.prototype = p; // 將原型指向p
return new f(); // 使用f()創(chuàng)建p的繼承對象
}
上面這個函數(shù)模擬了一部分Object.create一部分,但還是非常有用的。比如防止庫函數(shù)無意間修改不受你控制的對象。
不是將對象直接作為參數(shù)傳入函數(shù),而是將它的繼承對象傳入函數(shù)。當(dāng)函數(shù)讀取繼承對象的屬性時,實(shí)際上讀取的事繼承來的值。
如果給繼承對象的屬性賦值,則這些屬性之后影響這個繼承對象自身,不會影響原始對象:
var o = {x: "don't change this value"}; // 創(chuàng)建一個對象
var inherit_o = inheritPrototype(o); // 創(chuàng)建一個原型是o的繼承對象
inherit_o.x = "change it"; // 修改繼承對象本身的屬性x
o.x; // => "don't change this value" 原型o的屬性并沒改變
屬性的查詢和設(shè)置
查詢屬性可以通過. 和[]來獲取屬性的值。
-
.的右側(cè)必須是一個以屬性名稱命名的簡單標(biāo)示符 -
[]的方括號內(nèi)必須是一個計算結(jié)果為字符串的表達(dá)式
var author = book.author;
var name = author.subname;
var title = book["main title"]
和查詢屬性一樣,可以通過. 和[]來獲取創(chuàng)建屬性或給屬性賦值。
book.edition = 1;
book["main title"] = "Js";
當(dāng)通過[]訪問對象屬性時,可以在程序運(yùn)行時修改和創(chuàng)建。舉個例子:
通過讀取customer對象的address0/address1/address2的屬性,并且連接起來
var addr = "";
for(var i=0; i<3; i++){
addr += customer["address"+i] + '\n';
}
繼承
js對象具有自己的屬性,也有一些屬性從原型對象繼承而來。
假如要查詢對象o的屬性x,如果o中不存在x,那么將會在o的原型中查詢屬性x。如果原型對象也沒有x,但是這個原型對象也有原型,
那么繼續(xù)在這個原型對象的原型上查詢,直到找到x或者查找到一個對象的原型是null為止。
這樣對象的原型屬性構(gòu)成一個“鏈”,通過這個”鏈實(shí)現(xiàn)屬性的繼承。
看看下面這個栗子,加強(qiáng)一下理解:
var o = {}; // 創(chuàng)建一個空對象
o.x = 1; // o對象新增一個屬性x
var p = inheritPrototype(o); // 上面提到的繼承函數(shù),p繼承自o
p.y = 2; // p新增一個屬性y
var q = inheritPrototype(p); // q繼承自p
q.z = 3; // q新增屬性z
var s = q.toString(); // toString繼承自O(shè)bject.prototype
q.x + q.y; // x繼承自o,y繼承自p
在屬性賦值上,總是在原始對象上去創(chuàng)建屬性或?qū)σ延械膶傩再x值,不會修改原型鏈
只有查詢屬性,才會體會繼承的存在,而設(shè)置屬性與繼承無關(guān),這是js的重要特性,務(wù)必記住
這種特性,讓程序員有選擇的覆蓋繼承的屬性:
var raw_circle = { r:1 };
var new_circle = inheritPrototype(raw_circle); // new_circle繼承自raw_circle
new_circle.r = 2; // 修改new_circle繼承來的屬性的值,只修改自身
raw_circle.r; // => 1 ,原型的值并沒有改變
屬性訪問錯誤
查詢一個不存在的屬性并不會報錯,如果對象自身的屬性和繼承的屬性中均沒有,則會返回undefined。
但是如果對象不存在,試圖查詢這個不存在的對象的屬性就會報錯。null和undefined都沒有屬性,因此查詢這些值的屬性會報錯
下面提供2種避免出錯的方法:
// 簡單的方法
var len = undefined;
if(book){
if(book.title) len = book.title.length;
}
// 更為靈活的方法
var len = book && book.title && book.title.length; // &&短路行為
給null和undefined設(shè)置屬性會報類型錯誤
給其他值設(shè)置屬性不會全部成功,有一部分屬性是只讀的,不能重新賦值,有一些對象不能新增屬性,但是這些設(shè)置屬性的失敗操作不會報錯。但在ECMAScript5的嚴(yán)格模式下會報異常
Object.prototype = o; // 失敗,Object.prototype沒有被修改
總之,下面場景中給對象o設(shè)置屬性p會失?。?/p>
- o的屬性是只讀的,不能給只讀的屬性重新賦值
- o中的屬性p是繼承屬性,且它是只讀的,不能通過同名自有屬性覆蓋只讀的繼承屬性
- o中不存在自有屬性p:o沒有使用setter方法繼承屬性p,并且o的可擴(kuò)展性是false。如果o中不存在p,且沒有setter方法可以調(diào)用,則p一定會添加到o中,但如果o不是可擴(kuò)展的,那么在o中不能定義新屬性
刪除屬性
delete運(yùn)算符可以刪除對象的屬性。但是,delete只是斷開屬性和宿主對象的聯(lián)系,不是刪除這個屬性。因此,在銷毀對象的時候,要遍歷屬性中的屬性,依次刪除
a = { p: {x:1}}; //
b = a.p; // b是對a的屬性p的一個引用
delete a.p; // 刪除a的屬性p
b.x; // => 1,已經(jīng)刪除的屬性p的引用仍然存在
delete運(yùn)算符只能刪除自有屬性,不能刪除繼承屬性。
當(dāng)delete表達(dá)式刪除成功后并且沒有副作用,返回true。如果delete后面不是一個屬性訪問表達(dá)式,同樣返回true
o = {x:1};
delete o.x; // 刪除x,true
delete o.x; // 刪除失敗,什么都不做,返回true
delete o.toString(); // 無法刪除,返回true
delete 1; // 無意義,返回true
delete 可以刪除不可擴(kuò)展對象的可配置屬性,但不能刪除可配置性為false的屬性。在嚴(yán)格模式中,刪除一個不可配置的屬性會報一個類型錯誤。
在非嚴(yán)格模式下,這些情況delete操作會失敗并返回false:
delete Object.prototype; // 不能刪除,屬性不可配置
var x=1; // 聲明一個全局變量
delete this.x; // 不能刪除這個屬性
function f(){}; // 聲明一個全局函數(shù)
delete this.f; // 也不能刪除全局函數(shù)
當(dāng)在非嚴(yán)格模式中刪除全局對象的可配置屬性時,可以省略對全局對象的引用,直接delete后面跟要刪除的屬性名即可:
this.x = 1;
delete x;
在嚴(yán)格模式中,delete后跟隨一個非法的操作數(shù),將會報一個語法錯誤。必須顯式的指定對象及其屬性。
delete x; // 嚴(yán)格模式下報語法錯誤
delete this.x; // 正常
檢測屬性
我們經(jīng)常需要判斷一個屬性是否存在某個對象中。
- in運(yùn)算符
- hasOwnPreperty()
- propertyIsEnumerable()
in運(yùn)算符左側(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屬性
對象的hasOwnPreperty()方法用來檢測給的的名字是否是對象的自有屬性。對于繼承屬性返回false
var o = {x:1};
o.hasOwnPreperty("x"); // true,o有一個自有屬性x
o.hasOwnPreperty("y"); // false,o沒有屬性y
o.hasOwnPreperty("toString"); // false,toString是繼承的屬性
propertyIsEnumerable()是hasOwnPreperty()的增強(qiáng)版,只有檢測到是自有屬性且這個屬性的可枚舉性為true時,才會返回true。
某些內(nèi)置屬性是不可枚舉的。通常js代碼創(chuàng)建的屬性都是可枚舉的,除非在ECMAScript5中使用一個特殊的方法來改變屬性的可枚舉性。
var o =inheritPrototype({y:2});
o.x = 1;
o.propertyIsEnumerable("x"); // true,o有一個可枚舉的屬性x
o.propertyIsEnumerable("y"); // false,y屬性是繼承來的
object.prototype.propertyIsEnumerable("toString"); // false, 不可枚舉的
除了使用in運(yùn)算符之外,另一種更簡便的方法是使用!==判斷一個屬性是否是undefined:
var o = {x:1};
o.x !== undefined; // true,o中有屬性x
o.y !== undefined; // false,y屬性沒有
o.toString !== undefined; // true,o繼承了toString
然后有一種情況,只能用in運(yùn)算符,不能使用上述屬性訪問表達(dá)式。in運(yùn)算符可以區(qū)分不存在的屬性和存在但值為undefined的屬性。
var o = {x:undefined};
o.x !== undefined; // false,屬性存在,但是值為undefined
o.y !== undefined; // false,屬性不存在
"x" in o; // true,屬性存在
"y" in o; // false,屬性不存在
delete o.x; // 刪除屬性x
"x" in o; // 屬性x不存在
注意,上面使用的是!==而不是!=,!==可以區(qū)分undefined和null。有時不需要進(jìn)行區(qū)分:
// 如果o中含有屬性x,且x值不是null或undefined
if (o.x != null) o.x += 2;
// 如果o中含有屬性x,且x的值不能轉(zhuǎn)換成false,乘以2
// 如果x是undefined、null、false、" "、0 或者NaN,則保持不變
if (o.x) o.x *= 2;
枚舉屬性
除了檢測對象的屬性是否存在,還經(jīng)常需要遍歷對象的屬性。通常使用for/in循環(huán)遍歷
for/in循環(huán)可以遍歷對象中所有可枚舉的屬性(包括自身和繼承的屬性)
一般來說,對象繼承的內(nèi)置方法是不可枚舉的,還有通過Object.defineProperty(obj,property,enumerable:false)來定義不可枚舉的屬性
var o = { x:1, y:2}; // 2個可枚舉的屬性
o.propertyIsEnumerable("toString"); // false 繼承的方法,不可枚舉
for(p in o){
console.log(p) // 輸出x,y,不會輸出toString
}
很多時候給對象添加新的方法或?qū)傩?,但是會被for/in枚舉,因此需要跳過繼承的屬性和跳過方法,來避免在for/in中被循環(huán)枚舉出來
for(p in o){
if(!o.hasOwnPreperty(p)) continue;
}
for(p in o){
if(typeof o[p] === "function") continue;
}
除了for/in循環(huán)之外,ECMAScript5定義了2個用來枚舉屬性名稱的函數(shù)
- Object.keys(),返回一個數(shù)組,數(shù)組由對象中可枚舉的自有屬性的名稱組成
- Object.getOwnPropertyNames(),與keys相似,但是它返回所有自有屬性的名稱,而不僅僅是可枚舉的屬性
屬性getter和setter
對象屬性是由名字、值和一組特性(attribute) 構(gòu)成的。
在ECMAScript5中,屬性可以用1-2個方法替代,這兩個方法就是getter和setter。
由getter和setter定義的屬性稱作“存取器屬性”(access property),與“數(shù)據(jù)屬性”不同,數(shù)據(jù)屬性只有一個簡單的值。
當(dāng)程序查詢存取器屬性的值時,js調(diào)用getter方法(無參數(shù)),這個方法返回值就是屬性存取表達(dá)式的值。
當(dāng)程序設(shè)置一個存取器屬性的值時,js調(diào)用setter方法,將賦值表達(dá)式右側(cè)的值當(dāng)作參數(shù)傳入setter??梢院雎詓etter方法的返回值。
和數(shù)據(jù)屬性不同,存取器屬性不具有可寫性(writable attribute)。如果屬性同時具有g(shù)etter和setter方法,那么是一個讀/寫屬性,
如果它只有g(shù)etter方法,那么它是一個只讀屬性;如果它只有setter方法,那么它是一個只寫屬性,讀取只寫屬性,只會返回undefined
定義存取器最簡單的方法如下:
var o = {
// 普通的數(shù)據(jù)屬性
data_prop: value;
// 存取器都是成對定義的函數(shù)
get accessor_prop() { /* function body */ }
set accessor_prop(value) { /* function body */ }
}
注意,存取器屬性定義為1個或者2個和屬性同名的函數(shù),但是沒有使用function,而是使用get和set。
來看個例子:表示2D笛卡爾坐標(biāo)系的對象
var p = {
// 2個普通的讀寫屬性
x: 1.0,
y: 1.0,
// r是可讀寫的存取器屬性,有g(shù)etter和setter
// 函數(shù)體結(jié)束后,不要忘記加逗號
get r() { return Math.sqrt(this.x*this.x + this.y*this.y); }
set r(newvalue) {
var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y);
var ratio = newvalue/oldvalue;
this.x *= ratio;
this.y *= ratio;
}
// theta是只讀存取器,只有g(shù)etter
get theta() { return Math.atan2(this.y, this.x); }
}
注意,在這段代碼中,getter和setter中this關(guān)鍵字的用法。js把這些函數(shù)當(dāng)作對象的方法來調(diào)用,也就說,在函數(shù)體內(nèi)的this指向表示這個點(diǎn)的對象,
因此,r屬性的getter方法可以通過this.x和this.y引用x和y的屬性。
和數(shù)據(jù)屬性一樣,存取起屬性是可以繼承的,因此可以將上述代碼的對象p當(dāng)在另一個點(diǎn)的原型。可以給新對象定義它的x和y屬性,但r和theta屬性是繼承的:
var q = inheritPrototype(p); // 創(chuàng)建一個繼承g(shù)etter和setter的新對象
q.x = 1, q.y = 2; // 給q添加2個屬性
console.log(q.r); // 可以使用繼承的存取器屬性
console.log(q.theta);
屬性的特性
屬性除了名字和值之外,屬性還包含一些標(biāo)識:可寫、可枚舉和可配置的特性。
ECMAScript5查詢和設(shè)置屬性的API:
- 原型對象添加方法,設(shè)置成不可枚舉的,更像內(nèi)置方法
- 給對象定義不能修改或刪除的屬性,鎖定這個對象
一個屬性包含一個名字和四個特性,分別是:
- 它的值(value)
- 可寫性(writable)
- 可舉性(enumerable)
- 可配置性(configurable)
存取器屬性不具有值(value)特性和可寫性,它們的可寫性是由setter方法是否存在決定。因此存取器屬性的4個特性是讀取(get)、寫入(set)、可舉性和可配置性。
為了實(shí)現(xiàn)屬性特性的查詢和設(shè)置操作,ECMAScript5定義了一個“屬性描述符”的對象,這個對象代表了4個屬性。
數(shù)據(jù)屬性的描述符對象的屬性有:
- value
- writable
- enumerable
- configurable
存取器屬性的描述符對象用set和get屬性代替value和writable。
通過調(diào)用Object,getOwnPropertyDescriptor()可以獲得某個對象特定屬性的屬性描述符:
// 返回 { value:1, writable:true, enumerable:true, configurable:true }
Object.getOwnPropertyDescriptor({ x:1 },"x");
// 查詢上文中定義的random對象的octet屬性
// 返回 { get: /*func*/, set:undefined, enumerable:true, configurable:true }
Object.getOwnPropertyDescriptor(random, 'octet');
// 對于繼承屬性和不存在的屬性,返回undefined
Object.getOwnPropertyDescriptor({}, 'x');
Object.getOwnPropertyDescriptor({}, 'toString');
從名字就可以看出,Object,getOwnPropertyDescriptor()只能得到自有屬性的描述符。
要想獲得繼承屬性的特性,需要遍歷原型鏈(使用Object.getPrototypeOf())
要想設(shè)置屬性的特性,或者想讓新建屬性具有某種特性,可以調(diào)用Object.defineProperty(),傳入修改的對象、要修改或創(chuàng)建的屬性名稱以及屬性描述符對象:
var o={};
// 添加一個不可枚舉的數(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修改,變成制度屬性
Object.defineProperty(o, "x",
{
writable: false
});
// 試圖更改這個屬性的值
o.x = 2; // 操作失敗,但是不報錯,但是在嚴(yán)格模式中拋出類型錯誤異常
o.x; // => 1
// 屬性依然可以配置,因此可以通過這種方式對它修改:
Object.defineProperty(o, "x",
{
value: 2
});
// 返回的值變?yōu)?
o.x // =>2
Object.defineProperty(o, "x",
{
get:function() { return 0;}
});
o.x; // => 0 現(xiàn)在將x從數(shù)據(jù)屬性修改為存取器屬性
傳入Object.defineProperty()的屬性描述符對象不必包含所有4個特性。
對于新創(chuàng)建的屬性,默認(rèn)的特性值是false或undefined。對于修改的已有屬性來說,默認(rèn)特性值沒有任何修改。另外,這個方法要么修改已有屬性,要么新建自有屬性,但不會修改繼承屬性
如果同時修改或創(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
}
});
這段代碼從一個空對象開始,給它添加兩個數(shù)據(jù)屬性和一個只讀存取器屬性。最終Object.defineProperties()返回修改的對象
和Object.defineProperty()一樣
對于那些不允許創(chuàng)建或修改的屬性,如果用Object.defineProperty()和Object.defineProperties()對其操作(新建或修改)
就會拋出類型錯誤異常。比如,給一個不可擴(kuò)展的對象新增屬性就會拋出類型錯誤異常。
可寫性控制著對值特性的修改,可配置性控制著對其它值特性的修改。
如果屬性是可配置的,則可以修改不可寫屬性的值。同樣的,如果屬性是不可配置的,仍然可以將可寫屬性修改為不可寫屬性。
下面是修改的規(guī)則,違反規(guī)則的使用都會拋出類型錯誤異常:
- 如果對象是不可擴(kuò)展的,則可以編輯已有屬性,但不能添加新屬性(preventExtensions()阻止擴(kuò)展)
- 如果屬性是不可配置的,則不能修改它的可配置性和可枚舉性
- 如果存取器屬性是不可配置,則不能修改getter和setter的方法,也不能轉(zhuǎn)換成數(shù)據(jù)屬性
- 如果數(shù)據(jù)屬性是不可配置的,則不能將它轉(zhuǎn)換成存取器屬性
- 如果數(shù)據(jù)屬性是不可配置的,則不能將它的可寫性從false修改為true,但可以從true修改成false
- 如果數(shù)據(jù)屬性是不可配置且不可寫的,則不能修改它的值,然而可配置但不可寫屬性是可以修改的(實(shí)際上先將它標(biāo)記為可寫的,然后修改它的值,最后轉(zhuǎn)換為不可寫)
下面這個方法,不僅將一個對象的屬性復(fù)制到另一個對象中,而且復(fù)制了屬性的特性。
/* 給Object.prototype添加一個不可枚舉的extend()方法
* 這個方法繼承自調(diào)用它的對象
* 將作為參數(shù)傳入的對象的屬性一一復(fù)制
* 除了值之外,也復(fù)制屬性的所有特性,除非在目標(biāo)對象中存在同名屬性
* 參數(shù)對象的所有自有對象(包括不可枚舉的屬性)也會一一復(fù)制
Object.defineProperty(Object.prototype,
"extend", // 定義 Object.prototype.extend
{
writable: true,
enumerable: false, // false,定義為不可枚舉的
configurable: true,
value: function(o){ // 值就是這個函數(shù)
// 得到所有的自有屬性,包括不可枚舉屬性
var names = Object.getOwnPropertyNames(o);
// 遍歷它們
for(var i=0; i<names.length; i++){
// 如果屬性已經(jīng)存在,則跳過
if(names[i] in this) continue;
// 獲得o中屬性的描述符
var desc = Object.getOwnPropertyDescriptor(o, names[i]]);
// 用它給this創(chuàng)建一個屬性
Object.defineProperty(this, names[i], desc)
}
}
});
對象的三個屬性
每一個對象都有與之相關(guān)的原型(prototype)、類(class)和可擴(kuò)展性(extensible)
原型屬性
對象的原型屬性是用來繼承屬性。
原型屬性是在實(shí)例對象創(chuàng)建之前就設(shè)置好的,通過對象直接量創(chuàng)建的對象使用Object.prototype作為它們的原型。
通過new創(chuàng)建的對象使用構(gòu)造函數(shù)的prototype屬性作為它的原型。通過Object.create()創(chuàng)建的對象使用第一個參數(shù)(也可以是null)作為它們的原型。
在ECMAScript5中,將對象作為參數(shù)傳入Object.getPrototypeOf()可以查詢它的原型。
通過new表達(dá)式創(chuàng)建的對象,通常繼承一個constructor屬性,這個屬性指代創(chuàng)建這個對象的構(gòu)造函數(shù)。
通過對象直接量或Object.create()創(chuàng)建的對象包含一個名為constructor的屬性,這個屬性指代Object()構(gòu)造函數(shù)。
因此,constructor.prototype才是對象直接量的真正的原型,但是通過Object.create()創(chuàng)建的對象往往不是這樣
要想檢測一個對象是否是另一個對象的原型(或處于原型鏈中),可以使用isPrototypeOf()方法。
var p = { x:1 }; // 定義一個原型對象
var o = Object.create(p); // 使用這個原型創(chuàng)建一個對象
p.isPrototypeOf(o); // true,o繼承自p
Object.prototype.isPrototypeOf(p); // true,p繼承自O(shè)bject.prototype
類屬性
對象的類屬性是一個字符串,用以標(biāo)識對象的類型信息,有一個間接的toString()方法,返回如下這種格式的字符串[object class]
要想獲得對象的類,可以調(diào)用對的toString()方法,然后提取返回字符串串的第8個到倒數(shù)第2個位置之間的字符。為了能夠調(diào)用正確的toString()
版本,必須間接的調(diào)用Function.call()方法。
下面栗子返回了傳遞給他的任意對象的類
function classof(o){
if(o === null) return "NULL";
if(o === undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8,-1)
}
classof()函數(shù)可以傳入任何類型的參數(shù)。數(shù)字、字符串和布爾值可以直接調(diào)用toString(),就和對象toString()方法一樣。
通過對象直接量和Object.create創(chuàng)建的對象的類屬性是“Object”,那些自定義的構(gòu)造函數(shù)創(chuàng)建的對象也是一樣,類屬性也是"Object",
因此沒辦法通過類屬性來區(qū)分對象的類
classof(null) // => "NULL"
classof(1) // => "Number"
classof("") // => "String"
classof(false) // => "Boolean"
classof({}) // => "Object"
classof([]) // => "Array"
classof(/./) // => "Regexp"
classof(new Date()) // => "Date"
classof(window) // => "Window"
function f() {}; // => "定義一個自定義構(gòu)造函數(shù)"
classof(new f()); // => "Object"
可擴(kuò)展性
對象的可擴(kuò)展性用以表示是否給剋有給對象添加新的屬性。
所有的內(nèi)置對象和自定義對象都是顯式可擴(kuò)展的,宿主對象的可擴(kuò)展性是由js引擎定義的。
在ECMAScript5中,所有的內(nèi)置對象和自定義對象都是可擴(kuò)展的,除非唄轉(zhuǎn)換成不可擴(kuò)展的。
可擴(kuò)展性的目的是將對象“鎖定”,以避免外界的干擾。對象的可擴(kuò)展性通車和屬性的可配值性和可寫性配合使用
ECMAScript5定義了用來查詢和設(shè)置對象可擴(kuò)展性的函數(shù)。
- 通過將對象傳入Object.esExtensible(),來判斷對象是否是可擴(kuò)展的。
- 通過將preventExtensions()只影響對象本身的可擴(kuò)展性。將對象作為參數(shù)傳進(jìn)去,一旦將對象轉(zhuǎn)換為不可擴(kuò)展的,就無法轉(zhuǎn)換成可擴(kuò)展的。如果給一個不可擴(kuò)展的對象的原型添加屬性,這個不可擴(kuò)展的對象同樣會繼承這些新屬性
- Object.seal()和Object.preventExtensions()類似,除了能夠?qū)ο笤O(shè)置為不可擴(kuò)展性,還可以將對象的所有自有屬性都設(shè)置為不可配置的,也就是說,不能給這個對象添加新屬性,而且它已有的屬性也不能刪除或配置,不過已有的可寫屬性依然可以是設(shè)置。對于那些已經(jīng)封閉的(sealed)起來的對象是不能解封的??梢允褂肙bject.isSealed()來檢測對象是否封閉
- Object.freeze()將更嚴(yán)格的鎖定對象,將對象設(shè)置成不可擴(kuò)展的和屬性設(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}}));
序列化對象
對象序列化是指將對象的狀態(tài)轉(zhuǎn)換為字符串,也可以將字符串還原為對象。
ECMAScript5提供內(nèi)置函數(shù)JSON.stringfy()和JSON.parese()用來序列化和還原js對象。
這些方法使用JSON作為數(shù)據(jù)交換格式,JSON的語法和js對象和數(shù)組直接量的語法非常接近
o = { x:1, y: { z: [false,null,""]}}; // 定義一個測試對象
s = JSON.stringfy(o); // s是 '{"x":1, "y":{"z":[false,null,""]}}'
p = JSON.parse(s); // p 是 o 的深拷貝
JSON的語法是js語法的子集,并不能表示js里的所有值。
支持對象、數(shù)組、數(shù)組、字符串、無窮大數(shù)字、true、false和null,并且他們可以序列化和還原。
NaN、Infinity和-Infinity序列化的結(jié)果是null,日期對象序列化的結(jié)果是ISO格式的日期字符串,但JSON.parse()依然保留它們的字符串形態(tài),
而不會將它們還原成原始日期對象。函數(shù)、RegExp、Error對象和undefined值不能序列化和歡原
JSON.stringfy()只能序列化對象可枚舉的自有屬性,對于一個不能序列化的屬性來說,在序列化后的輸出字符串中會將這個屬性省略掉。
對象方法
所有的js對象都從Object.prototype繼承屬性。
下面說幾個最常用的方法
toString()方法
toString()方法沒有參數(shù),將返回一個調(diào)用這個方法的對象對象值的字符串。在需要將對象轉(zhuǎn)換為字符串的時候,js都會調(diào)用這個方法。
默認(rèn)的toString()方法的返回值帶有的信息量很少,例如下面這行代碼的計算結(jié)果為字符串:
var s = { x:1, y:1}.toString(); // => [object object]
由于默認(rèn)的toString()方法并不會輸出很多有用的信息,因此很多類都帶有自定義的toString()。
toLocaleString()方法
除了基本的toString()方法之外,對象都包含toLocaleString()方法,這個方法返回一個表示這個對象的本地化字符串。
Object中默認(rèn)的toLocaleString()方法并不做任何本地化自身的操作,僅調(diào)用toString()方法并返回對應(yīng)值。
Date和Number類對toLocaleString()方法做了定制,可以用它對數(shù)字、日期和實(shí)際本地化的轉(zhuǎn)換
Array類的toLocaleString方法和toString方法很像,唯一的不同是每個數(shù)組元素會調(diào)用toLocaleString()方法轉(zhuǎn)換為字符串,而不是調(diào)用給各自的toString
toJSON()方法
Object.prototype實(shí)際上沒有定義toJSON()方法,但對于需要執(zhí)行序列化的對象來說,JSON.stringfy()方法會調(diào)用toJSON()方法,
如果在待序列化的對象中存在這個方法,則調(diào)用它,返回值是序列化的結(jié)果,而不是原始對象。
valueOf()方法
valueOf()方法和toString()方法非常類似,但往往當(dāng)js需要將對象轉(zhuǎn)換成某種原始值而非字符串對象的時候才會調(diào)用,尤其是轉(zhuǎn)換成數(shù)字的時候。
如果在需要使用原始值的上下文中使用了對象,js會自動調(diào)用這個方法