理解對象
EAMCScript中沒有類的概念,所以它與基于類的語言中的對象有所不同。
ECMAScript-262定義對象為:無序的集合,其屬性可以包含基本值、對象或者函數。
屬性類型
ECMAScript中有兩種屬性:數據屬性和訪問器屬性。
數據屬性
數據屬性有4個描述其行為的特性:
- [[Configurable]]:表示能否通過
delete刪除屬性從而重新定義屬性,能否修改屬性的特性,能否修改屬性為訪問器屬性。默認值為true。 - [[Enumerable]]:表示能否通過for-in循環(huán)返回屬性。默認值為true。
- [[Writable]]:表示能否修改屬性值。默認值為true。
- [[Value]]:包含這個屬性的值。默認值為undefined。
要修改屬性默認的特性,必須使用ECMAScript5的Object.defineProperty()方法。這個方法接受三個參數:屬性所在的對象,屬性名和一個描述對象。
一旦把屬性定義為不可配置,就不能再把它變回可配置了。
在調用Object.defineProperty()方法時,如果不指定,configurable、enumerable和writable特性的默認值都是false。
由于實現(xiàn)的不徹底,所以不建議在IE8中使用
Object.defineProperty()。
訪問器屬性
訪問器屬性不包含數據值,它們包含一對getter和setter函數。讀取時調用getter,寫入時調用setter。
訪問器屬性有4個描述其行為的特性:
- [[Configurable]]:表示能否通過
delete刪除屬性從而重新定義屬性,能否修改屬性的特性,能否修改屬性為數據屬性。默認值為true。 - [[Enumerable]]:表示能否通過for-in循環(huán)返回屬性。默認值為true。
- [[Get]]:在讀取屬性時調用的函數。默認值為undefined。
- [[Set]]:在寫入屬性時調用的函數。默認值為undefined。
訪問器屬性不能直接定義,必須使用Object.defineProperty()來定義。
var book = {
_year:2004,
edition:1
};
Object.defineProperty(book,"year",{
get:function(){
return this._year;
},
set:function(){
if(newValue>2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
_year前面的下劃線是一種常用的記號,用于表示只能通過對象方法訪問的屬性。
支持ECMAScript5的這個方法的瀏覽器有IE9+(IE8只是部分實現(xiàn))、Firefox4+、Safari5+、Opera12+和Chrome。
定義多個屬性
利用ECMAScript5中的Object.definedProperties()方法來通過描述符一次定義多個屬性。
讀取屬性的特性
利用ECMAScript5中的Object.getOwnPropertyDescriptor()方法來取得給定屬性的描述符。方法接受兩個參數:屬性所在的對象和屬性名,返回值是一個對象。
創(chuàng)建對象
工廠模式
這種模式抽象了創(chuàng)建具體對象的過程??紤]到ECMAScript中無法創(chuàng)建類,開發(fā)人員發(fā)明了一種函數,用函數來封裝以特定接口創(chuàng)建對象的細節(jié)。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("a", 29, "s");
var person2 = createPerson("b",27 , "d");
構造函數模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("a", 29, "s");
var person2 = new Person("b",27 , "d");
createPerson()和Person()的不同之處:
- 沒有顯示地創(chuàng)建對象
- 直接將屬性和方法賦予
this對象 - 沒有
return語句
按照慣例,構造函數始終以大寫字母開頭。
調用構造函數會經歷4個步驟:
- 創(chuàng)建一個對象
- 將構造函數的作用域賦給新對象(因此this指向了這個新的對象)
- 執(zhí)行構造函數中的代碼
- 返回新對象
對象都有一個constructor屬性。對象的constructor屬性用來標識對象的,檢測對象還是用instanceof更好一些。
以這種方式定義的構造函數是定義在
Global對象(在瀏覽器中是windows對象)中的。
將構造函數當做函數
任何函數,通過new來調用就作為一個構造函數,不通過new來調用就和普通函數沒什么兩樣。
//當做構造函數來使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();
//當做普通函數來使用
Person("Greg", 27, "Doctor"); //添加到window
windows.sayName():
//在另一個對象的作用域中調用
var o = new Object();
Person.call(o,"Kristen", 25, "Nurse");
o.sayName();
構造函數的問題
使用構造函數的主要問題是每個方法都要在每個實例上重新創(chuàng)建一遍。
可以將函數定義轉移到構造函數外部來解決這個問題:
function Person(name, age, obj){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
上面的問題解決了,可是新的問題又來了:在全局作用域中定義的函數實際上只能被某個對象調用,如果方法很多就要定義很多的全局函數于是就沒有封裝性可言了。好在,可以通過使用原型模式來解決。
原型模式
創(chuàng)建的每個函數都有一個prototype屬性,這個屬性是一個指針,指向對象實例的原型對象。
使用原型的好處就是可以讓所有對象實例共享它所包含的屬性和方法。
function Person(){
}
Person.prototype.name = "a";
Person.prototype.age = 29;
Person.prototype.job = "Software";
Person,prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //a
var person2 = new Person();
person2.sayName(); //a
alert(person1.sayName == person2.sayName); //true
理解原型對象
創(chuàng)建一個新函數就會根據一組特定的規(guī)則為函數創(chuàng)建一個prototype屬性,這個屬性指向函數的原型。在默認情況下,所有的原型對象都會自動獲得一個constructor屬性,這個屬性指向包含prototype屬性所在的函數指針。
創(chuàng)建了自定義的構造函數以后,其原型對象默認只會獲得constructor屬性,其他方法則從Object繼承而來。
通過isPrototypeOf()來判斷對象的原型。
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
ECMAScript5增加了一個新方法,叫Object.getPrototypeOf(),在所有支持的實現(xiàn)中,返回[[Prototype]]的值。
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //a
可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。
如果在實例中添加了一個屬性,且屬性名和原型中的一個屬性同名,那么會在實例中創(chuàng)建該屬性并且屏蔽原型中的同名屬性。不過可以使用delete操作符來刪除實例屬性,從而重新訪問到原型中的屬性。
使用hasOwnProperty()檢測一個屬性是存在與實例中還是存在于原型中。
原型與in操作符
可以單獨使用和在for-in循環(huán)中使用。
單獨使用in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在于實例中還是原型中。
function Person () {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" ------來自實例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" ------來自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" ------來自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
同時使用hasOwnProperty()和in可以確定屬性存在于對象上還是原型上。
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
在使用for-in循環(huán)時,返回的是所有能夠通過對象訪問的、可枚舉(enumerated)屬性,其中即包括存在于實例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumeralbe]]標記的屬性)的實例屬性也會在for-in循環(huán)中返回,因為根據規(guī)定,所有開發(fā)人員定義的屬性都是可枚舉的---只有在IE8及更早的版本例外。
IE早期版本的實現(xiàn)中存在一個bug,即屏蔽不可枚舉屬性的實例屬性不會出現(xiàn)在for-in循環(huán)中。
var o = {
toString : function () {
return "My Object";
},
};
for (var prop in o) {
if (prop == "toString") {
alert("Found toString"); //在IE中不會顯示
}
}
要取得對象上所有可枚舉的實例屬性,可以使用ECMAScript5的Object.keys()方法。這個方法接受一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。
function Person() {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"
如果你想要得到所有實例屬性,無論它是否可枚舉,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person, prototype);
alert(keys); //"constructor,name,age,job,sayName"
更簡單的原型語法
更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象。
function Person(){
}
Person.prototype = {
name : "a",
age : 29,
job : "Software",
sayName : function(){
alert(this.name);
}
};
以這種形式創(chuàng)建的新對象有一個例外:constructor屬性不在指向Person,而指向Object。
如果
constructor屬性很重要,可以將它設置回適當的值。
function Person(){
}
Person.prototype = {
constructor : Person,
name : "a",
age : 29,
job : "Software",
sayName : function(){
alert(this.name);
}
};
但是這種方式會導致constructor屬性的[[Enumerable]]特性被設置為true。默認情況下,原生的constructor不可枚舉。
使用兼容ECMAScript的引擎可以用下面方法解決:
function Person(){
}
Person.prototype = {
name : "a",
age : 29,
job : "Software",
sayName : function(){
alert(this.name);
}
};
Object.defineProperty(Person.prototype,"constructor",{
enumerable : false,
value : Person
});
原型的動態(tài)性
對原型對象所做的任何修改都能夠立刻從實例上反應出來。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("Hi");
};
friend.sayHi(); //Hi
但是如果重寫整個原型對象,情況就不一樣了。
function Person() {
}
var friend = new Person();
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29;
job : "Software";
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
原生對象的原型
所有的原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。
通過原生對象的原型,不僅可以取得所有默認方法的引用,而且也可以定義新的方法。
不推薦在產品化的程序中修改原生對象的原型。
原型對象的問題
原型模式也不是沒有缺點。原型模式最大的問題是由其共享的本性所導致的。
原型中所有屬性是被很多實例共享的,這種共享對于函數非常合適。對于那些包含基本值的屬性倒也說得過去,通過在實例上添加一個同名屬性,可以隱藏原型中的對應屬性。然而,對于包含引用類型值得屬性來說,問題就比較突出了。
function Person() {
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job : "Software",
sayName : function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
組合使用構造函數模式和原型模式
創(chuàng)建自定義類型最常見的方式是組合使用構造函數模式和原型模式。構造函數定義實例屬性,原型模式用于定義方法和共享的屬性。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
syaName : function(){
alert(this.name);
}
}
動態(tài)原型模式
動態(tài)原型模式把所有信息都封裝在了構造函數中,而通過在構造函數中初始化原型(僅在必要情況下),又保持了同時使用構造函數和原型的優(yōu)點。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
寄生構造函數的模式
通常,在前面幾種模式都不適用的情況下,可以使用寄生(parasitic)構造函數模式。這種函數的基本思想是創(chuàng)建一個函數,該函數的作用僅僅是封裝創(chuàng)建對象的代碼,然后再返回新創(chuàng)建的對象。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var friend = new Person("a", 29, "soft");
friend.sayName();
這個模式跟工廠模式很像。構造函數在不返回值得情況下,默認返回新對象實例,而通過在構造函數的末尾添加一個return語句,可以重寫調用構造函數時返回的值。
這個模式可以在特殊的情況下用來為對象創(chuàng)建構造函數。假設我們想創(chuàng)建一個具有額外方法的特殊數組。由于不能直接修改Array構造函數,因此可以使用這個模式。
function SpecialArray () {
//創(chuàng)建數組
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function () {
return this.join("|");
};
//返回數組
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipenString()); //"red|blue|green"
穩(wěn)妥構造函數模式
穩(wěn)妥對象指的是沒有公共屬性,而且其方法也不引用this的對象。穩(wěn)妥對象最適合在一些安全的環(huán)境中(這些環(huán)境會禁止使用this和new),或者在防止數據被其他應用程序(如Mashup程序)改動時使用。
function Person (name, age, job) {
//創(chuàng)建要返回的對象
var o = new Object();
//可以在這里定義私有變量和函數
//添加方法
o.sayName = function () {
alert(name);
};
//返回對象
return o;
}
注意,在以這種模式創(chuàng)建的對象中,除了使用sayName()之外,沒有其他辦法訪問name的值。
var friend = Person("Nicholas", 29, "Software");
friend.sayName(); //"Nicholas"
繼承
許多的OO語言都支持兩種繼承方式:接口繼承和實現(xiàn)繼承。接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法。由于函數沒有簽名,在ECMAScript中無法實現(xiàn)接口繼承。ECMAScript只支持實現(xiàn)繼承,而且其繼承主要是依靠原型鏈來實現(xiàn)。
原型鏈
實現(xiàn)原型鏈有一種基本模式:
function SuperType() {
this.prototype = true;
}
Super.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//繼承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue() = function () {
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
別忘記默認的原型
所有的引用默認都繼承了Object,而這個繼承也是通過原型鏈實現(xiàn)的。所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內部指針,指向Object.prototype。
確定原型和實例的關系
通過兩種方式來確定原型和實例之間的關系:
- 使用
instanceof操作符檢測實例和原型中出現(xiàn)過的構造函數,結果返回true。
alert(instance instanceof Object); //true
- 使用
isPrototypeOf()方法。
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
謹慎的定義方法
子類型有時候需要重寫超類型中的某個方法,或者需要添加超類型中不存在的某個方法。但不管怎么樣,給原型添加方法的代碼一定要放在替換原型的語句之后。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType () {
this.subproperty = false;
}
//繼承了SuperType
SubType.prototype = new SuperType();
//添加新的方法
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
//重寫超類型中的方法
SubType.prototype.getSuperValue = function () {
return false;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false
這里要注意的是,必須在用SuperType的實例替換原型以后,再定義這兩個方法。并且通過原型鏈實現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法,因為這樣會重寫原型鏈。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType () {
this.subproperty = false;
}
//繼承了SuperType
SubType.prototype = new SuperType();
//使用字面量添加新方法,會導致上一行代碼無效
SubType.prototype = {
getSubValue : function () {
return this.subproperty;
},
someOtherMethod : function () {
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); //false
原型鏈的問題
原型鏈雖然很強大,可以用它來實現(xiàn)繼承,但也存在一些問題。最主要的問題來自包含引用類型值的原型。
function SuperType () {
this.colors = ["red", "blue", "green"];
}
function SubType () {
}
//繼承了SuperType
SubType.prototype = new Super();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"
原型鏈的第二個問題是:在創(chuàng)建子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。
借用構造函數
在解決原型中包含引用類型值所帶來問題的過程中,開發(fā)人員開始使用一種叫做借用構造函數(constructor stealing)的技術(有時候也叫做偽造對象或經典繼承)。即在子類型構造函數的內部調用超類型構造函數。
function SuperType () {
this.colors = ["red", "blue", "green"];
}
function SubType () {
//繼承了SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"
傳遞參數
對于原型鏈而言,借用構造函數有一個很大的優(yōu)勢,即可以在子類型構造函數中向超類型構造函數傳遞參數。
function SuperType (name) {
this.name = name;
}
function SubType () {
//繼承了SuperType,同時還傳遞了參數
SuperType.call(this, "Nicholas");
//實例屬性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas"
alert(instance.age); //29
借用構造函數的問題
如果僅僅是借用構造函數,那么也將無法避免構造函數存在的問題----方法都在構造函數中定義,因此函數復用就無從談起了。
組合式繼承
組合繼承(combination inheritance),有時候也叫做偽經典繼承。
function SuperType (name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
alert(this.name);
};
function SubType (name, age) {
//繼承屬性
SuperType.call(this, name);
this.age = age;
}
//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas"
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg"
instance2.sayAge(); // 27
原型式繼承
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
在object()函數內部,先創(chuàng)建了一個臨時性的構造函數,然后傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的一個新實例。從本質上講,objcet()對傳入其中的對象執(zhí)行了一次淺復制。
var person = {
name : "Nicholas",
friends : ["Shelby", "Court", "Van"]
};
var anotherPerson = objcet(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson.name = objcet(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
ECMAScript5通過新增Object.create()方法規(guī)范化了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create()與objcet()方法的行為相同。
var person = {
name : "Nicholas",
friends : ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson.name = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
寄生式繼承
寄生式繼承創(chuàng)建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。
function createAnother (original) {
var clone = object(original); //通過調用函數創(chuàng)建一個新對象
clone.sayHi = function () { //以某種方式來增強這個對象
alert("hi");
};
return clone; //返回這個對象
}