JavaScript核心-對象
物件導向(Object-Oriented,OO)的語言有一個標識,那就是有類的概念,通過類可以建立任意多個具有相同屬性和方法的對象。
ECMAScript也是高度抽象的物件導向的語言,但它沒有類的概念,所以它的對象也與基於類的語言中的對象有所不同。
ECMA-262把對象定義為:“無序屬性的集合,其屬性可以包含基本值、對象或者函數”。
我們可以把對象想象成散列表:無非就是一組名值對,其中值可以是資料或函數。
建立自訂對象的兩種簡單方式:
1.使用new操作符建立一個Object參考型別的新執行個體。它其實是調用了Object打的原生建構函式。
var person = new Object();person.name = "zhu";person.age = 24;person.sayHello = function(){ alert("hello");};
2.使用對象字面量標記法。這種方法要求的代碼量較少,而且給人一種封裝資料的感覺。
var person = {name : "zhu";age : 24;sayHello : function(){ alert("hello");};};
使用以上兩種方式建立對象時,如果要建立很多個有相同屬性的對象時,會產生大量重複代碼。為避免這個問題,我們也可以用建構函式模式來建立對象。
Function Person(name,age){ this.name = name; this.age = age; this.sayHello = function(){ alert("hello"); };}var person = new Person("zhu",24);
注意:按照慣例,建構函式的函數名要使用大寫字母開頭。
建構函式與其他函數的唯一區別就是調用它們的方式不同。建構函式只是要通過new操作符來調用,如果不用new操作符來調用,那建構函式和普通函數也沒什麼區別。
如果把上面定義的Person()函數當做普通函數來調用:
Person("zhu","24");window.sayHello(); //彈出框顯示"hello"
在全域範圍中調用了Person()函數,那麼函數中的this對象就指向了Global對象,而window對象就是瀏覽器的全域對象。
當用上面的構造器函數來建立對象時,我們觀察代碼的紅色部分,它定義了一個函數,由於在ECMAScript中函數也是對象,所以紅色代碼部分相當於執行個體化了一個新的函數對象。當我們建立了多個Person對象時,也就建立了多個sayHello函數對象,而這些函數都完成的是相同的任務,所以建立多個實在沒有必要。我們可以把代碼最佳化一下:
function Person(name,age){ this.name = name; this.age = age; this.sayHello =sayHello; //因為在ECMAScript中函數名本身就是一個變數,所以可以把這個函數作為這個person對象的一個屬性值來使用,這裡還沒調用這個函數}function sayHello(){ alert("hello");}var person = new Person("zhu",24);person.sayHello(); //彈出框顯示hello這樣,無論我們建立多少個person對象,它們都共用了一個了在全域範圍中定義的同一個sayHello()函數。
但是這樣也有新的問題,如果對象定義了很多方法,那麼就要定義很多全域函數,導致我們自訂的這個參考型別就沒有了封裝性可言。由此可以引出對象中最重要的一個屬性,原型-prototype。
我們建立的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個對象,這個對象的用途是包含可以由特定類型的所有執行個體共用的屬性和方法。
利用Person建構函式的原型屬性,我們也可以把上面的例子改寫成如下:
function Person(name,age){this.name = name;this.age = age;}Person.prototype.sayHello = function(){alert("hello");};var person = new Person("zhu",24);person.sayHello();
無論什麼時候,只要我們用function關鍵字建立了一個函數,就會為該函數建立一個prototype屬性,這個屬性就指向了函數的原型對象。預設情況下,所有的原型對象都自動獲得一個constructor(建構函式)屬性,這個屬性包含了一個指向prototype屬性所在的函數的指標。針對上面的代碼,Person函數有一個prototype屬性,指向一個Person函數的原型對象Person Prototype,這個原型對象Person Prototype自動獲得一個constructor屬性,指回這個Person函數。
當我們用Person建構函式建立了很多執行個體person1,person2....時,每一個執行個體都有一個內部屬性[[Prototype]],這個內部屬性指向了建構函式的原型對象Person Prototype。注意,這個內部屬性對指令碼來說是隱藏的。但是FireFox、Safari、Chrome在每個對象上都支援一個屬性__proto__(注意每邊是雙底線,表示內部屬性),但為了保證瀏覽器安全色性,一般也不直接使用這個屬性,我是這麼理解的。
如果我們在一個對象執行個體中添加了一個屬性,該屬性與原型對象中的一個屬性同名,那麼屬性就是這個執行個體的屬性,該屬性會屏蔽原型對象中儲存的同名屬性,但不會修改原型對象,換句話說,它只是阻止了我們訪問原型對象中的那個屬性。例如:
function Person(name,age){this.name = name;this.age = age;}Person.prototype.sayHello = function(){alert("hello");};var person1 = new Person("zhu",24);//屏蔽原型對象中的儲存的同名屬性person1.sayHello = function(){alert("byebye");};person1.sayHello(); //彈出框顯示“byebye”var person2 = new Person("li",24);//訪問的是原型對象中的屬性person2.sayHello(); //彈出框顯示“hello”原型對象中的所有屬性都是被很多執行個體共用的,這種共用對於函數來說十分合適,但對於一些包含參考型別值的屬性來說,就不太適合放置在原型對象中了。我們上面例子中的Person對象,實際是組合使用了建構函式模式和原型模式來定義的,對於OO的編程來說,沒有體現出封裝的特點,所以我們也可以用動態原型模式來解決這個問題,例如上面的例子又可以改寫成:
function Person(name,age){this.name = name;this.age = age;if (typeof this.sayName != "function") {Person.prototype.sayHello = function(){alert("hello");}; }}var person1 = new Person("zhu",24);//屏蔽原型對象中的儲存的同名屬性person1.sayHello = function(){alert("byebye");};person1.sayHello(); //彈出框顯示“byebye”var person2 = new Person("li",24);//訪問的是原型對象中的屬性person2.sayHello(); //彈出框顯示“hello”我們把所有資訊都封裝在了建構函式中,在需要的情況下在建構函式中初始化了原型對象,保持了同時使用建構函式和原型的優點。
原型的另一個關鍵作用是在原型鏈中來使用,而原型鏈是ECMAScript中實現繼承的關鍵依靠。這個下一篇再寫。