也許你還沒有理解構造函數和原型對象的時候已經在javascript的路上走了很久,但直到你很好的掌握它們之前你不會真正欣賞這門語言,由於Javascript缺乏類,它使用構造函數和原型對象來給對象帶來與類相似的功能,但是,這些相似的功能並不一定表現的跟類完全一致,在本章中,你會詳細看到javascript如何使用構造函數和原型對象來創建對象。
4.1構造函數
構造函數就是你用new創建對象時調用的函數。到目前爲止,你已經見過好幾次內建javascript構造函數了,例如,Object,Array和Function。使用構造函數的好處在於用同一個構造函數創建的對象都具有同樣的屬性和方法。如果想創建多個相同的對象,你可以創建自己的構造函數以及引用類型
構造函數也是函數。你會用同樣的方法定義它,唯一的區別是構造函數名字應該首字母大寫,以此區分其他函數。下列定義了一個空的Person函數。
function Person(){
//intertionally empty
}
這個函數就是構造函數。構造函數和其他函數並沒有絕對的與發上的區別。唯一的線索就時首字母大寫。
定義好構造函數後,你就可以用它來創建對象,例如下面的Person對象
var person1=new Person();
var person2=new Person();
如果沒有需要傳遞給構造函數的參數,你甚至可以忽略小括號。
var person1=new Person;
var person2=new Person;
即使Person構造函數並沒有顯式返回任何東西,person1和person2都會被認爲時一個新的Person類型的實例。new操作符會自動創建給定類型的對象並返回給它們。這也意味着,你可以用instanceof操作符獲取對象的類型。如下所示。
console.log(person1 instanceof Person);
console.log(person1 instanceof Person);
由於person1和person2被Person構造函數創建,用instanceof檢查它們是否爲Person類型時返回true。
你也可以用構造函數屬性來檢查一個對象的類型。每個對象在創建時都自動擁有一個構造函數屬性,其中包含一個指向其構造函數的引用。那些通過對象字面形式或Object構造函數創建出來的泛用對象,其構造函數指向Object;那些通過自定義構造函數創建出來的對象,其構造函數屬性指向創建它的構造函數。如下例person1和person2的constructor屬性Person。
console.log(person1.constructor ===Person);
console.log(person2.constructo===Person);
console.log函數對兩個對象都輸出true。因爲它們都是用Person構造函數創建的。
雖然對象實例機器構造函數之間存在這樣的關係。但是還是建議你使用instanceof來檢查對象類型。這是因爲構造函數屬性可以被覆蓋,並不一定完全準確。
當然,一個空的構造函數並不是十分游泳的。使用構造函數的目的時爲了輕鬆創建許多擁有相同屬性和方法的對象。爲此,你只需要在構造函數內簡單的給this天界你想要的任何屬性即可。如下例。
function Person(name){
this.name =name;
this.sayName=function(){
console.log(this.name);
}}
該板根的Person構造函數接受一個命名參數name並將賦給this對象的name屬性。同時,構造函數還給對象添加一個sayName()方法。當你調用構造函數時,new會自動創建this對象。且其類型就是構造函數的類型,(在本例中爲Person)構造函數本身不不需要返回一個值,new操作符會幫你返回。
現在你而可以用Person構造函數來創建具有初始name屬性的對象了。
var person1=new Person("Nicholas");
var person2=new Person("Greg");
console.log(person1.name);
console.log(person2.name);
person1.sayName();
person2.sayName();
每個對象都有自己的name屬性,所以sayName()方法可以根據不同對象返回不同的值。
注意:你也可以在構造函數中顯式調用return,如果返回的值是一個對象,它會代替新創建的對象實例返回。如果返回的值時一個原始類型那個,他會被忽略,新創建的對象實例會被返回。
構造函數允許你用一致的方式初始化一個類型的實例。在使用對象錢設置好所有的屬性。如下例。你也可以在構造函數中用Ocject.defineProperty()方法 來幫助我們初始話。
function Person(name){
Object.defineProperty(this,"name"{
get:function(){
return name;},
set:function(newName){
name =newName;
},
enumerable:true,
configurable:true
});
this.sayName=function(){
console.log(this.name)}
}
在這個版本的Person構造函數中,name屬性是一個訪問者屬性,利用name參數來保存實際的值,之所以能這樣做,是因爲命名參數就相當於一個本地變量。
始終確保用new調用構造函數;否則,你就是在冒改全局對象的風險,而不是創建一個新的對象,考慮如下代碼中發生了什麼。
var person1= Person("Nicholas");
console.log(person1.instanceof Person);
console.log(name);
console.log(typeof person1);
當Person不是被new調用時,構造函數中的this對象等於全局this對象,由於person構造函數依靠new提供返回值,person1變量爲undefined。沒有new,Person只不過時一個沒有返回語句的函數。對this.name的賦值實際上時創建來一個全局變量name來保存傳遞給Person的參數。第6章介紹的幾個對象模式中將包括對這一問題的解決辦法
注意;在嚴格模式下,當你不通過new調用Person構造函數時會出現錯誤。這是因爲嚴格模式並沒有爲全局對象設置this。this保持爲undefined,而當你是試圖爲undefined添加屬性時都會出錯
構造函數允許你給對象配置同樣的屬性,但是鉤爪函數並沒有消除冗餘臺嗎,在之前的例子中,每一個對象都有自己的sayName()方法。這意味着如果你有100個對象實例,你就有100個函數做相同的使其,只是使用的數據不同。如果所有的對象實例共享一個方法更加有效,該方法可以使用this.name訪問正確的數據,這就是需要用到原型對象啦。
4.2 原型對象
可以把原型對象看成是對象的基類。幾乎所有的函數(除了一些內建函數)都有一個名爲property的屬性,改屬性時一個原型對象用來創建新的對象實例。所有創建的對象實例共享該原型對象,且這些對象實例可以訪問原型對象的屬性。例如,hasOwnProperty()方法被定義在泛用對象Object的原型對象中,但卻可以被任何對象當作自己的屬性訪問,如下例
var book={
title:"The Principles of Object-Oriented JavaScript"
};
console.log("title" in book);
console.log(book.hasOwnProperty("title"));
console.log("hasOwnProperty" in book);
console.log(book.hasOwnProperty("hasOwnProperty"));
console.log(Object.prototype.hasOwnProperty("hasOwnProperty"));
即時book中並沒有hasOwnProperty()這個方法的定義,仍然可以通過book.hasOwnProperty()訪問該方法。這是因爲該方法的定義存在與Object.prototype中。in操作符對原型屬性和自有屬性都返回true。
鑑別一個原型對象 你可以用這樣一個函數去鑑別一個屬性是否是原型對象。
function hasPrototypeProperty(object,name){
return name in object &&! object.hasOwnProperty(name);
}
console.log(hasPrototypeProperty(book,"title"));
console.log(hasPrototypeProperty(book,"hasOwnProperty"));
4.2.1 [[Prototype]]屬性
一個對象實例通過內部屬性[[Prototype]]跟蹤其原型對象。該屬性時一個指向該實例使用的原型對象的指針。當你使用new創建一個新的對象的時候,構造函數的原型對象會給該對象的[[Prototype]]屬性。[[Prototype]]屬性是讓多個對象實例引用同一個原型對象來減少重複代碼的。
你可以調用對象的Object.getPrototypeOf()方法讀取[[Prototype]]屬性的值。下例代碼檢查一個泛用空對象的[[Prototype]]屬性。
var object={};
var prototype=Object.getPrototypeOf(object);
console.log(prototype=== Object.prototype);// true
如上例,任何一個泛用對象,其[[Prototype]]屬性始終指向Object.prototype。
注意:大部分JavaScript引擎在所有對象上都支持一個名爲_proto_的屬性,該屬性使你可以世界讀寫[[Prototype]]屬性。firefox,safari,chrome和node.js都支持該屬性,且EcmaScript6正在考慮將其加入到標準中。
你也可以用isPrototype()方法檢查某個對象是否時另一個對象的原型對象。該方法被包含在所有對象中。
var object={};
console.log(Object.prototype.isPrototypeOf(object)); //true
因爲object時一個泛用對象,它的原型時Object.prototype。意味着本例中的isPrototypeOf()方法返回true。
當讀取一個對象的屬性時,javascript引擎首先在該對象的自有屬性中查找屬性的名字,如果找到則返回,如果自有屬性中不包含該名字,則javascript會搜索[[Prototype]]中的對象。如果找打則返回。如果找不到,則返回undefined。
下列先創建一個沒有自有屬性的對象。
var object={};
console.log(object.toString());
object.toString=function(){
return "[object Custom]";
};
console.log(object.toString());
delete object.toString;
console.log(object.toString());
delete object.toString;
console.log(object.toString());
本例最初的toString()方法來自於原型對象,默認返回"[object Object]"如果你定義一個名叫toString的自有屬性,那麼每次調用該對象的toString()方法都會調用該自有屬性。自有屬性會覆蓋原型屬性。僅當自有屬性被刪除時,原型屬性纔會再以此被使用。delete操作符僅對自有屬性起作用,你無法闡述一個對象的原型屬性。還有就是無法給一個對的原型屬性賦值。
4.2.2 在構造函數總使用原型對象
原型對象的共享機制使得它們成爲一次性爲所有對象定義方法的理想手段。因爲一個方法對所有的對象實例做相同的使其,美麗有每個實例都要有一份自己的方法。
將方法放在原型對象中並用this訪問當前實例時更有效的做法。下例展現了新Person構造函數。
function Person(name){
this.name=name;
};
Person.prototype.sayName=function(){
console.log(this.name);
};
var person1=new Person("Nicholas");
var person2=new Person("Greg");
console.log(person1.name);
console.log(person2.name);
person1.sayName();
person2.sayName();
在這個版本的Person構造函數中,sayName()被定義在原型對象上而不是構造函數中。創建出的對象和本章之前的例子中創建的對象別無二致,只不過sayName()現在是一個原型屬性而不是自有屬性。在person1和person2調用sayName()時,相對的this的值被分別賦在person1或person2.
也可以在原型對象上存儲其他類型的數據,但在存儲引用值時需要注意。因爲這些引用值會被多個實例共享,可能搭建不希望一個實例能夠改變另一個實例的值。下例顯示當你不注意你的引用值的實際指向那裏時會發生的情況。
function Person(name){
this.name=name;
}
Person.prototype.sayName=function(){
console.log(this.name);
};
Person.prototype.favorites=[];
var person1=new Person("Nicholas");
var person2=new Person("Greg");
person1.favorites.push("pizza");
person2.favorites.push("quinoa");
console.log(person1.favorites);
console.log(person2.favorites);
favorites屬性被定義在原型對象上,意味着person1.favorites和person2.favorites指向同一個數組。你對任一Person對象的favorites插入的值都將成爲原型對象上數據的元素。但這可能不是你期望的行爲,所以在原型對象上定義時你要非常小心。
雖然你可以在原型對象上一一添加屬性,但是很多開發者會使用一種更簡潔的方式:直接用一個對象字面形式替換原型對象。
如下:
function Person(name){
this.name=name;
};
Person.prototype={
sayName:function(){
console.log(this.name);
},
toString:function(){
return “[Person”+this.name+"]";
}
};
上段代碼在原型對象上定義了兩個訪拿廣發,sayName()和toString()。這種定義的方式非常流行,因爲這種方式不需要多次鍵入Person.prototype.但是有一個副作用需要注意。
var person1=new Person("Nicholas");
console.log(person1 instanceof Person);
console.log(person1.constructor ===Person);
console.log(person1.constructor ===Object);
使用對象字面形式改寫原型對象改變了構造函數的屬性,因此現在指向Object而不是Person。這是因爲原型對象具有一個constructor屬性,這是其他對象的實例沒有的。當一個函數被創建時,它的prototype屬性也被創建,且該原型對象的constructor屬性指向該函數。當使用對字面形式改寫原型對象的Peroson.prototype時,其constructor屬性將設置爲泛用對象Object。爲了避免這一點。需要在改寫原型對象時手動重置其constructor屬性,如下例。
function Person(name){
this.name=name;
};
Person.prototype={
constructor:Person,
sayName:function(){
console.log(this.name);
},
toString:function(){
return "[Person"+this.name+" ]";
}
};
var person1=new Person("Nicholas");
var person2=new Person("Greg");
console.log(person1 instanceof Person);
console.log(person1.constructor === Person);
console.log(person1.constructor === Object);
console.log(person. instanceof Person);
console.log(person2.constructor === Person);
console.log(person2.constructor === Object);
本例顯示指定了原型對象的constructor屬性,爲了不忘記賦值,最好把它設置爲原型對象的第一屬性。
構造函數,原型對象和對象實例之間的關係最有趣的一個方面也許就是對象的實例和構造函數之間沒有之間聯繫,不過對象實例和原型對象以及原型對象和構造函數之間都有之間關係。這樣的關係意味着,如果打斷對象的實例和原型對之間的聯繫,那麼也將打斷對象和實例和其構造函數的聯繫。
4.2.3 改變原型對象
給定類定的所有對象實例共享一個原型對象,所以你可以一次性擴充所有對象實例。記住,[[Prototype]]屬性只是包含了一個指向原型對象的指針,任何對原型對象的改變都立即反映到所有引用它的對象實例上。這意味着你給原型對象添加的新成員都可以立即被所有已經存在的對象實例使用,如下例
function Person(name){
this.name =name;
}
Person.prototyp={
constructor:Person,
sayName:function(){
console.log(this.name);
},
toString:function(){
return "[Person"+this.name+"]"};
}
var peson1=new Person("Nicholas");
var person2=new Person("Greg");
console.log("sayHi" in person1);
console.log("sayHi" in person2 );
Person.prototype.sayHi=function(){
console.log("Hi");
};
person1.sayHi();
person2.sayHi();
在這段代碼中,Person類型一開始只有兩個方法,sayName()和toString(),然後在創建兩個Person對象實例後給原型對象添加了一個sayHi(),對命名屬性的查找時在每次訪問屬性的時候發生的。所以可以做到無縫體驗。
可以隨時改變原型對象的能力在封印對象和凍結對象上有一個十分有趣的後果。當你在一個對象上使用Obejct.Seal()或Obejct.freeze()時,完全是在操作對象的自有屬性,你無法添加自有屬性或改變凍結對象的自有屬性,但是仍然可以通過原型對象上添加屬性來擴展這些對象的實例。如下例;
var person1=new Person("Nicholas");
var person2=new Person("Greg");
Object.freeze(person1);
Person.prototype.sayHi=function(){
console.log("Hi");
};
person1.sayHi();
person2.sayHi();
本例中有兩個Person的對象實例。Person1是 凍結對象而person2是普通對象,當你在原型對象上添加sayHi()時,person1和person2都獲得了這一新方法,這似乎不合person1的凍結狀態。其實[[Prototye]]屬性是對象實例的自有屬性,屬性本身被凍結。但其指向的值(原型對象)並沒有凍結。
注意:實際開發javascript時,你不可能會頻繁地使用原型對象,但時理解對象實例以及其原型對象之間的關係是非常重要的,而像這樣的奇怪例子有助於你理解這些概念。
4.2.4 內建對象的原型對象
到這裏,你可能會疑惑原型對象是否也允許你改版javascript引擎的標準內建對象,答案是yes。所有內建對象都有搞糟函數,因此也都有原型對象給你去改變。例如,在所有數組上添加一個新的方法只需要簡單地修改Array.prototype即可。
Array.prototype.sum=function(){
return this.reduce(function(pervious,current){
return previous+current;
});
};
var number =[1,2,3,4,5,6];
var result=number.sum();
console.log(result);
這個例子在Array.prototype上創建了一個名爲sum()的方法,該方法對該數組的所有元素求和並返回。numbers數組通過原型對象自動擁有這個方法,在sum()內部,this指向數組的對象實例numbers,於是該方法也可以自由使用數組的其他方法,比如reduce()。
你可能還記得字符串,數字和布爾類型都有內建的原始封裝類型來幫助我們像使用普通對象一樣來使用它們,如果改變原始封裝類型的原始對象,你就可以給這些原始值添加更多的功能,如下例
String.prototype.capitalize=function(){
return this.charAt(0).toUpperCase()+this.substring(1);
};
var message ="hello world";
console.log(message.capitalize());
這段代碼爲字符轉創建了一個名爲capitalize()的方法,String類型是字符串的原始封裝類型。修改其原型對象意味着所有的字串都自動獲得這些改動。
注意:修改內建對象來實驗各種功能是既有好玩的事情,但在生產環境中這門做不是一個好主意,開發者們都期望一個內建對象具有一定的方法並表現出一定的行爲,故意改變內建對象會破壞這種期望導致其他開發者無法確定這些對象會如何工作。
4.3 總結
構造函數就是用new操作符調用普通函數,你可以隨時定義你自己的構造函數來創建多個具有同樣屬性的對象。可以用instaceof 操作符或直接訪問constructor屬性來鑑別對象被那個構造函數創建的。
每一個函數都具有prototype屬性,它定義了該構造函數的所有對象的共享的屬性,通常,共享的方法和原始值屬性被定義在原型對象裏,而其他屬性都定義在構造函數裏。constructor屬性實際上被定義在原型對象裏供所有對象實例共享。
原型對象被保存在對象的實例內部的[[Prototype]]屬性中,這個屬性時一個引用而不是一個副本。由於javascript查找屬性的機制。你對原型對象的修改都立刻出現在所有對象的實例中。當你試圖訪問一個對象的某個屬性時,javascript首先在自有屬性中查找該屬性的名字,如果在自有屬性中沒有找到則查找原型屬性。這樣的機制意味着原型對象可以隨時改變而引用它的對象實例則立即反映出這些改變。
內建對象也有可以被修改的原型對象。雖然不建議在生產環境中這麼做。但它們可以被用來實驗或驗證新功能。