1.理解對象
1.1屬性類型
數據屬性:
- [[Configurable]] :表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改爲訪問器屬性。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認值爲 true 。
- [[Enumerable]] :表示能否通過 for-in 循環返回屬性。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認值爲 true 。
- [[Writable]] :表示能否修改屬性的值。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認值爲 true 。
- [[Value]] :包含這個屬性的數據值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認值爲 undefined 。
要修改屬性默認的特性,必須使用 ECMAScript 5 的 Object.defineProperty() 方法。這個方法接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是: configurable 、 enumerable 、 writable 和 value
。設置其中的一或多個值,可以修改對應的特性值。
var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"
訪問器屬性:
- [[Configurable]] :表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改爲數據屬性。對於直接在對象上定義的屬性,這個特性的默認值爲true 。
- [[Enumerable]] :表示能否通過 for-in 循環返回屬性。對於直接在對象上定義的屬性,這個特性的默認值爲 true 。
- [[Get]] :在讀取屬性時調用的函數。默認值爲 undefined 。
- [[Set]] :在寫入屬性時調用的函數。默認值爲 undefined 。
var book = {
_year: 2004,// _year 前面的下劃線是一種常用的記號,用於表示只能通過對象方法訪問的屬性
edition: 1
};
Object.defineProperty(book, "year", {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); //2
1.2定義多個屬性
利用Object.defineProperties()方法,通過描述符一次定義多個屬性,接收兩個對象參數,第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應。
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
1.3讀取屬性的特性
使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以取得給定屬性的描述符。這個方法接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,如果是訪問器屬性,這個對象的屬性有 configurable 、 enumerable 、 get
和 set ;如果是數據屬性,這個對象的屬性有 configurable 、 enumerable 、 writable 和 value 。
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
2.創建對象
Object構造函數或對象字面量創建單個對象的缺點:使用同一個接口創建很多對象,會產生大量的重複代碼。
2.1工廠模式
用函數來封裝以特定接口創建對象的細節。
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 person = createPerson("Grey",27,"doctor");
工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
2.2構造函數模式
創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job'
this.sayName = function(){
alert(this.name);
}
}
var person = new Person("Grey",27,"doctor");
person對象有一個constructor構造函數屬性,該屬性指向Person
與工廠模式的區別:
- 沒有顯式地創建對象;
- 直接將屬性和方法賦給了this對象;
- 沒有return語句。
優點:創建自定義的構造函數意味着將來可以將它的實例標識爲一種特定的類型。
a.將構造函數當作函數
構造函數與其它函數唯一的區別,就在於調用它們的方式不一樣,任何函數,只要通過new操作符來調用,那它就可以作爲構造函數。
// 當作構造函數使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作爲普通函數調用
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"
// 在另一個對象的作用域中調用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
b.構造函數的問題
每個方法都要在每個實例上重新創建一遍,會導致不同的作用域鏈和標識符解析。
可以把構造函數中的函數放在全局作用域裏,然後在構造函數內部調用,可以解決兩個函數做同一件事的問題。但是在全局作用域中定義的函數實際上只能被某個對象調用,如果對象需要定義很多方法,那麼就要定義很多個全局函數。例如:
function Person(name, age, job){
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");
2.3原型模式
每個函數都有一個prototype屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法,即prototype就是通過調用構造函數而創建的那個對象實例的原型對象。
好處是可以讓所有對象實例共享它所包含的屬性和方法,不必在構造函數中定義對象實例的信息,而是將這些信息直接添加到原型對象中。
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();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
a.理解原型對象
只要創建了一個新函數,就會根據一組特定的規則爲該函數創建一個prototype屬性,這個屬性指向函數的原型對象,在默認情況下,所有原型對象都會自動獲得一個constructor構造函數屬性,這個屬性包含一個指向prototype屬性所在函數的指針,通過這個構造函數我們還可以繼續爲原型對象添加其他屬性和方法。
創建了自定義的構造函數之後,其原型對象默認只會取得 constructor 屬性;至於其他方法,則都是從 Object 繼承而來的。當調用構造函數創建一個新實例後,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262
第 5 版中管這個指針叫 [[Prototype]] 。雖然在腳本中沒有標準的方式訪問 [[Prototype]] ,但 Firefox、Safari 和 Chrome 在每個對象上都支持一個屬性__proto__ ;而在其他實現中,這個屬性對腳本則是完全不可見的。不過,要明確的真正重要的一點就是,這個__proto__連接存在於實例與構造函數的原型對象之間,而不是存在於實例與構造函數之間。
ECMAScript 5 增加了一個新方法,叫 Object.getPrototypeOf() ,在所有支持的實現中,這個方法返回 [[Prototype]] 的值。例如:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性。
ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法只能用於實例屬性,要取得原型屬性的描述符,必須直接在原型對象上調用 Object.getOwnPropertyDescriptor() 方法。
b.原型與in操作符
有兩種方式使用 in 操作符:單獨使用和在 for-in 循環中使用。在單獨使用時, in 操作符會在通過對象能夠訪問給定屬性時返回 true ,無論該屬性存在於實例中還是原型中。
由於 in 操作符只要通過對象能夠訪問到屬性就返回 true , hasOwnProperty() 只在屬性存在於實例中時才返回 true ,因此只要 in 操作符返回 true 而 hasOwnProperty() 返回 false ,就可以確定屬性是原型中的屬性。
要取得對象上所有可枚舉的實例屬性,可以使用 ECMAScript 5 的 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"
c.更簡單的原型語法
用一個包含所有屬性和方法的對象字面量來重寫整個原型對象。
function Person(){}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
constructor 屬性不再指向 Person 了,可以通過constructor : Person設置回適當的值
d.原型的動態性
即使是先創建了實例後修改原型能夠立即從實例上反映出來。原因可以歸結爲實例與原型之間的鬆散連接關係。當我們調用 person.sayHi()時,首先會在實例中搜索名爲 sayHi 的屬性,在沒找到的情況下,會繼續搜索原型。因爲實例與原型之間的連接只不過是一個指針,而非一個副本,因此就可以在原型中找到新的
sayHi 屬性並返回保存在那裏的函數。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"
如果是重寫整個原型對象,那麼情況就不一樣了。我們知道,調用構造函數時會爲實例添加一個指向最初原型的[[Prototype]] 指針,而把原型修改爲另外一個對象就等於切斷了構造函數與最初原型之間的聯繫。請記住:實例中的指針僅指向原型,而不指向構造函數。
function Person(){}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
d.原生對象的原型
所有原生引用類型( Object 、 Array 、 String ,等等)都在其構造函數的原型上定義了方法。例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到substring() 方法。
通過原生對象的原型,不僅可以取得所有默認方法的引用,而且也可以定義新方法。可以像修改自定義對象的原型一樣修改原生對象的原型,因此可以隨時添加方法。下面的代碼就給基本包裝類型String
添加了一個名爲 startsWith() 的方法:
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
e.原型對象的問題
原型模式的最大問題是由其共享的本性所導致的,通過在實例上添加一個同名屬性,可以隱藏原型中的對應屬性。然而,對於包含引用類型值的屬性來說,問題就比較突出了。
function Person(){}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
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
2.4組合使用構造函數模式和原型模式
構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。結果,每個實例都會有自己的一份實例屬性的副本,但同時又共享着對方法的引用,最大限度地節省了內存。另外,這種混成模式還支持向構造函數傳遞參數;可謂是集兩種模式之長。
function Person(name, age, job){//構造函數定義實例屬性
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {//原型模式定義方法和共享的屬性
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
2.5動態原型模式
把所有信息都封裝在了構造函數中,而通過在構造函數中初始化原型(僅在必要的情況下),又保持了同時使用構造函數和原型的優點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。
function Person(name, age, job){
//屬性
this.name = name;
this.age = age;
this.job = job;
// 方法
if (typeof this.sayName != "function"){// if 語句檢查的可以是初始化之後應該存在的任何屬性或方法
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
2.6寄生構造函數模式
這種模式的基本思想是創建一個函數,該函數的作用僅僅是封裝創建對象的代碼,然後再返回新創建的對象。
function Person(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("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
構造函數在不返回值的情況下,默認會返回新對象實例。而通過在構造函數的末尾添加一個 return 語句,可以重寫調用構造函數時返回的值。
2.7穩妥構造函數模式
所謂穩妥對象,指的是沒有公共屬性,而且其方法也不引用 this 的對象。穩妥對象最適合在一些安全的環境中(這些環境中會禁止使用 this 和 new ),或者在防止數據被其他應用程序(如
Mashup程序)改動時使用。
function Person(name, age, job){
//創建要返回的對象
var o = new Object();
//可以在這裏定義私有變量和函數
//添加方法
o.sayName = function(){
alert(name);
};
//返回對象
return o;
}
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
3.繼承
ECMAScript只支持實現繼承,主要是依靠原型鏈來實現的。
3.1原型鏈
基本思想:利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
構造函數、原型和實例的關係:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。
假如我們讓原型對象等於另一個類型的實例,結果會怎麼樣呢?顯然,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含着一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。
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;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
實現的本質是重寫原型對象,代之以一個新類型的實例。
1.別忘記默認的原型
所有函數的默認原型都是 Object 的實例,因此默認原型都會包含一個內部指針,指向 Object.prototype 。這也正是所有自定義類型都會繼承 toString() 、valueOf() 等默認方法的根本原因。
2.確定原型和實例的關係
方法1:使用instanceof操作符,測試實例與原型鏈中出現過的構造函數,結果就會返回true
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true, instance 是 Object 、 SuperType 或 SubType 中任何一個類型的實例。
方法2:使用isPrototypeOf()方法,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
3.謹慎地定義方法
給原型添加方法的代碼一定要放在替換原型的語句之後。
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
通過原型鏈實現繼承時,不能使用對象字面量創建原型方法,這樣會重寫原型鏈,導致原型鏈被切斷。
4.原型鏈的問題
在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。於是,原先的實例屬性也就順理成章地變成了現在的原型屬性了。
原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。有鑑於此,再加上前面剛剛討論過的由於原型中包含引用類型值所帶來的問題,實踐中很少會單獨使用原型鏈。
3.2借用構造函數
基本思想:在子類型構造函數的內部調用超類型構造函數,因此通過使用 apply() 和 call() 方法也可以在(將來)新創建的對象上執行構造函數。
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"
1.傳遞參數
借用構造函數可以在子類型構造函數中向超類型構造函數傳遞參數。
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
2.借用構造函數的問題
方法都在構造函數中定義,因此函數複用就無從談起了,而且在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。
3.3組合繼承(僞經典繼承)
思路:使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承,這樣既通過在原型上定義方法實現了函數複用,又能保證每個實例都有它自己的屬性。最大的問題是無論什麼情況下,都會調用兩次超類型構造函數:一次是創建子類型原型的時候,另一次是在子類型構造函數內部。
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);//第二次調用SuperType()
this.age = age;
}
//繼承方法
SubType.prototype = new SuperType();//第一次調用SuperType()
SubType.prototype.constructor = SubType;
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
3.4原型式繼承
藉助原型可以基於已有的對象創建新對象,要求你必須有一個對象可以作爲另一個對象的基礎
function object(o){ //object()對傳入其中的對象執行了一次淺複製
function F(){} //創建臨時性的構造函數
F.prototype = o; //將傳入的對象作爲這個構造函數的原型
return new F(); //返回這個臨時類型的一個新實例
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
ECMAScript 5 通過新增 Object.create() 方法規範化了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create() 與 object() 方法的行爲相同。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create() 方法的第二個參數與 Object.defineProperties() 方法的第二個參數格式相同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。例如:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"
只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。
3.5寄生式繼承
思路:創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,再返回對象。
function createAnother(original){
var clone = object(original); //通過調用函數創建一個新對象
clone.sayHi = function(){ //以某種方式來增強這個對象
alert("hi");
};
return clone; //返回這個對象
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。使用寄生式繼承來爲對象添加函數,會由於不能做到函數複用而降低效率;這一點與構造函數模式類似。
3.6寄生組合式繼承
通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。
基本思路:不必爲了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個副本,本質上就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型。