砌好牆,下面出場的就是房子了,在ECMAScript中,對象就是我們所說的房子,至於你所寫的整個應用程序,那就是一整套建築群了。在房子里面可以放你想放的任意事物——如果你有足夠的美學造詣,你甚至可以弄一個房中房試試——當然,為了方便管理,我們會給房子里存放的所有事物都會取上一個不重復的名字,比如醫葯房間里的各種葯品名稱。在ECMAScript中,你可以在對象中存放任意你想放的數據,同樣,我們需要給存放的數據取一個名字——也就是對象的屬性名,再存放各種數據。再看看ECMA-262中對象的定義:無序屬性的集合,其屬性可以包含簡單數據類型值、對象或者函數。
進入對象,我開始有些激動了,說實話,讓我想起做這系列學習筆記的最初原因,就是因為該書對對象的深刻論述,讓我對JavaScript的認知從客戶端驗證小工具轉變成一門強大的面向對象腳本語言,但我現在也有點犯難了,因為關於對象,有太多太多的東西需要去細化,一時也不知該從哪個點切入,比如要想深入理解對象,作用域、執行環境、閉包這些概念是肯定離不開的,但如果連對象的概念都沒說就開始執行環境和閉包,又感覺像是空中樓閣。不過又一想,也就釋然了,這畢竟只是自己的個人學習筆記,又不是什么教科書,我大可以使用自己喜歡的方式來做自己的筆記(事實上,在前面的篇章中,我就有意識的重復那些我認為有意思的地方,這就是我喜歡的一種方式),當然,我還是會盡量以一種易於理解的方式來做這些筆記。
對象類型
和5種簡單數據類型(Undefined、Null、Boolean、Number、String)相對應,對象(Object)也是一種數據類型,只是這種數據類型比較特別,它不但可以像簡單數據類型一樣存取通常的數據,而且可以將動作行為作為一種特殊的數據加以存取。
1、對象實例
每種數據類型都有相應的值,比如Undefined類型只有一個值undefined,而數字5是Number類型的一個值。對於對象類型,我們把值稱為對象實例,那么對象類型都可以有哪些(值)實例呢?任意一個對象都是對象類型的值(實例),比如簡單類型包裝對象(Boolean、Number、String)就是對象類型的值(實例)。
2、對象字面量
既然任意一個對象都是對象類型的實例,那么對象實例怎么表示呢?或者說我們在交流過程中怎么書寫出對象實例呢?簡單數據類型的值很好表示,比如用符號“5”表示數字5,符號“true”表示Boolean值true,這些被稱為字面量,那么,有沒有對象字面量呢?答案是肯定的,對象字面量就是通過一對大括號({})來表示的。比如:
{ name:'linjisong', getName:function(){ return this.name; } }
這里最外層的一對大括號({})就表示這是一個對象字面量。另外,還有數組字面量的概念,在ECMAScript中,數組Array是一個繼承了Object的對象實例,通過這個對象實例可以創建數組類型的實例,數組類型的實例也可以直接通過數組字面量來表示,方法如下:
[{ name:'linjisong', age:29 },{ name:'oulinhai', age:23 }]
這里一對中括號([])用於表示數組,這是一個包含了兩個對象的數組。通過對象字面量和數組字面量,形成了難以想象的強大表現力,事實上,流行的JSON數據格式就是基於此。
3、創建對象實例
熟悉一般面向對象的朋友都知道,要創建一個類的實例,首先要定義這個類,然后用new關鍵字來創建這個類的實例(別和我說還可以使用反射,我的Java可學的不好……)。但是在ECMAScript中,根本沒有類的概念,那么,對象實例要怎么創建呢?
在ECMAScript中盡管沒有類,但是也有某種程度上類似的概念,承擔這個角色的就是函數,可以通過new操作符和函數來創建對象實例——每一個對象實例都有一個用於創建這個實例的函數。最基本的函數就是Object(),它是用來創建最一般對象的函數,其它的諸如Number()函數,可以用來創建Number對象的實例,Boolean()函數,可以用來創建Boolean對象的實例:
1 var obj = new Object();//Object()函數,創建最一般的對象實例 2 var num = new Number(1);//Number()函數,創建Number對象的實例 3 var boo = new Boolean(true);//Boolean()函數,創建Boolean對象的實例 4 console.info(typeof num);//object 5 console.info(typeof Number(1));//number 6 console.info(typeof boo);//object 7 console.info(typeof Boolean(true));//boolean
(1)可以看到,要創建一個對象實例,首先需要有一個函數(稱為構造函數),這個函數使用new調用時就是創建對象實例,不使用new時只是通常意義上的函數調用(如果這個函數在內部返回實例了,函數調用也可以創建對象)。
(2)所謂的內置對象實際上也就是內置了一些創建對象實例的函數而已,不同的函數創建不同的內置對象。
(3)關於要不要使用new操作符,我的建議是使用,如果不使用new操作符,有些情況下結果會出乎你的意料之外,像上例中的第5、7行,實際上並沒有創建對象,而只是普通的函數調用,這個調用的作用就是轉換數據類型。
(4)使用new創建對象實例時,如果調用構造函數不需要傳入參數,也可以省略后面的函數調用操作符(()),當然,這種特性也不是什么值得宣揚的事情。
(5)如果需要創建自定義對象的實例,那么首先也需要定義一個構造函數,然后使用new操作符調用創建實例。這里需要注意,如果忘了new的話,可能會污染全局環境:
function Person(){//首先定義一個用於創建對象實例的(構造)函數 this.name = 'linjisong'; this.age = 29; } var person = new Person();//調用(構造)函數創建對象實例 console.info(person.age);//29 try{ console.info(age);//為了演示忘記使用new的情況,這里先輸出全局的age,由於未定義,拋出異常 }catch(e){ console.info(e);//ReferenceError } var person2 = Person();//忘記使用new的情況下,只是普通的函數調用,由於函數沒有返回,這里person2就是undefined了 console.info(person2);//undefined
console.info(age);//29,沒有使用new,內部的this指向了全局作用域,因為可以在全局訪問age了
要避免這種問題,可以修改一下構造函數:
function Person(){ if(this instanceof Person) { this.name = 'linjisong'; this.age = 29; }else{ return new Person(); } } var person2 = Person(); console.info(person2.age);//29,可以訪問person2的age了 console.info(age);//全局環境中沒有age的定義了,拋出異常
這個構造函數首先判斷this值是否為Person類型,如果不是,就在內部使用new調用,以確保返回的值一定是Person類型實例。這種方式使得重構構造函數成為了可能,也許Boolean()、Number()、String()在實現上就是使用了這種方式來區分是構造函數還是轉換函數。如果你在調用Object()時省略new的話,結果也能返回對象,估計也是在后台做了類似處理,同樣的情況還有本文后部分要講的函數類型構造函數Function()。
(5)可能有人會問,既然有對象字面量,何必要用這么復雜的方式來創建對象實例呢,直接寫對象字面量不就完了?用對象字面量創建對象實例,根本沒有使用什么函數,看來,上面的“每一個對象實例都有一個用於創建這個實例的函數”的說法並不正確。
首先第一個問題,的確,可以使用對象字面量來創建函數,而且也非常簡潔,這甚至也是我首先推薦的一種創建方式,但是用這種方式創建對象實例,只能創建單例的實例,對於需要創建多個相同類型的對象實例來說並不適用,然后第二個問題,用對象字面量創建對象,實際上並不是沒有相應的構造函數,只是構造函數為Object(),使用對象字面量,后台可能不會去調用new Object(),但創建出的對象仍然有指向這個函數的屬性,這可以從下面代碼輸出中得到驗證:
var person = {}; console.info(person.constructor===Object);//true
這里的constructor是每個實例對象都有的一個屬性,用於保存創建這個對象實例的函數,這就是下面要講的。
4、對象屬性和方法
每一種數據類型都有各自的共性,比如Number類型值都有可以和另外一個Number類型值相加的特性,同樣,對象類型的實例也有一些相同的特性,這些特性就體現在它們都包含下面的屬性和方法(方法實際上也是一種屬性,只是屬性的值類型是函數的話,我們也稱之為方法):
類別 | 屬性/方法 | 說明 |
屬性 | constructor | 指向用於創建當前對象的函數 |
方法 | hasOwnProperty(propertyName) | 檢查給定的屬性是否在當前對象實例中 |
propertyIsEnumerable(propertyName) | 檢查給定的屬性是否能夠是使用for-in語句來枚舉 | |
isPrototype(object) | 檢查傳入的對象是否是另一個對象的原型 | |
toLocalString() | 返回對象的字符串表示,該字符串與執行環境的地區相對應 | |
toString() | 返回對象的字符串表示 | |
valueOf() | 返回對象的字符串、數值或布爾值表示,通常與toString()方法返回值相同 |
注:在《JavaScript高級程序設計(第3版)》第35頁中的Constructor將首字母大寫了,應該是一個印刷錯誤。
屬性和方法的訪問有兩種方式:
(1)使用點號(.):如person.name。
(2)使用方括號([]):如person[name],使用這種方式,方括號內部可以是一個變量或者表達式,這使得可以訪問名稱包含特殊符號的屬性和方法。
通過結合for-in和這里的hasOwnProperty (propertyName),我們就可以遍歷對象實例自身的屬性而不包括從原型鏈繼承而來的屬性了:
for(var propertyName in object){ if(object.hasOwnPorperty(propertyName)){ //循環處理 } }