1.創建自定義對象的最簡單方式就是**創建一個Object()的實例**,然后再為它添加屬性和方法;隨后流行**使用對象字面量創建對象**。
2.ECMAScript的屬性(property)由內部才能用的特性(attribute)來描述。內部特性無法在JavaScript中直接訪問。為了表示特性是內部值,該規范把它們放在了兩對方括號中。
3.ECMAScrip有兩種屬性:數據屬性和訪問器屬性。
4.數據屬性有四個描述其行為的特性:
**[[Configurable]]**:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。這個特性的默認值為true。
**[[Enumearble]]**:表示能否通過for-in循環返回屬性。這個特性默認值為true。
**[[Writable]]**:表示能否修改屬性的值。這個特性的默認值為true。
**[[Value]]**:包含這個屬性的數據值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認值為undefined。
5.要修改屬性默認的特性,必須使用ECMAScript5的Object.defineProperty()方法。這個方法接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中描述符對象的屬性必須是configurable、enumerable、writable、value。例:
~~~
var person = {};
Object.defineProperty(person,’name’,{
writable:false,
value:’Nicholas’
});
~~~
6.一旦把屬性定義為不可配置的,就不能再把它變回可配置了,此時再調用Object.defineProperty()方法修改除writable之外的特性,都會導致錯誤。在調用Obejct.defineProperty()方法時,如果不指定,configurable、writable、enumerable特性的默認值都為false。
7.訪問器不包含數據,包含一對getter和setter函數(不過這兩個函數都不是必須的)。在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter函數并傳入新值,這個函數負責決定如何處理數據。訪問器屬性有如下4個特性:
**[[Configurable]]**:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數據屬性。這個特性的默認值為true。
**[[Enumearble]]**:表示能否通過for-in循環返回屬性。這個特性默認值為true。
**[[Get]]**:在讀取屬性時調用的函數。這個特性的默認值為undefined。
**[[Set]]**:在寫入屬性時調用的函數。這個特性的默認值為undefined。
8.訪問器屬性不能直接定義,必須使用***Object.defineProperty()***來定義。例:
~~~
var book = {
_year:2004,
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
~~~
以上代碼創建了一個book對象,并給它定義了兩個默認屬性。_year和edition。**_year前面的下劃線是一種常用的記號,用于表示只能通過對象方法訪問的屬性。**在這個函數里,把year屬性修改為2005會導致_year變成2005,而edition變為2。**這是使用訪問器屬性的常見方式,即設置一個屬性的值會導致其他屬性發生變化。**
9.***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;
}
}
}
});
~~~
10.***Object.getOwnPropertyDescriptor()***方法,可以取得給定屬性的描述符。這個方法接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,如果是訪問其屬性,這個對象的屬性有configurable、enumerable、get和set;如果是數據屬性,這個對象的屬性有configurable、enumerable、writable和value。這個方法只能用于實例屬性,要取得原型屬性的描述符,必須直接在原型對象上調用這個方法。
~~~
var descriptor = Object.getOwnpropertyDescriptor(book,’_year’);
alert(descriptor.value) //2005
alert(descriptor.configurable) //false
alert(descriptor.get) //undefined
~~~
11.**工廠模式在JavaScript中是指用函數來封裝以特定接口創建對象的細節**。這種模式抽象了創建具體對象的過程。例:
~~~
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;
}
~~~
**工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎么知道一個對象的類型)。**
12.**構造函數模式**
**通過創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。**
~~~
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.setName = function(){
alert(this.name)
};
}
~~~
Person()函數與createPerson()函數之間的不同之處在于:
-沒有顯式地創建對象;
-直接將屬性和方法賦給了this對象;
-沒有return語句;
按照慣例,***構造函數始終都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。***這個做法借鑒自其他OO語言,主要為了區別于ECMAScript中的其他函數;因為構造函數本身也是函數,只不過可以用來創建對象而已。
要創建Person對象的新實例,必須使用new 操作符。以這種方式調用構造函數實際上會經歷以下四個步驟:
* **創建一個新對象;**
* **將構造函數的作用域賦給新對象(因此this就指向了這個新對象)**
* **執行構造函數中的代碼(為這個新對象添加屬性);**
* **返回新對象**。
創建自定義的構造函數意味著將來可以將它的實例標識為一種特定的類型。
任何函數,只要通過new操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過new操作符來調用,那它跟普通函數沒有什么區別。**當一個原意用作構造函數的函數不通過new操作符調用,那函數體內的this對象就指向全局作用域window。**當然,可以通過call()或者apply()在某個特殊對象的作用域中調用。例:
~~~
var o = new Object();
Person.call(0,’Ken’,28,’frontend’);
o.sayName(); //Ken
~~~
構造函數的主要問題在于,每個方法都要在每個實例上重新創建一遍。可以通過把函數定義轉移到構造函數外面來解決這個問題:
~~~
function sayName() {
alert(this.name);
}
~~~
但這樣的做法又帶來了新問題,全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那么就要定義很多個全局函數,于是我們這個自定義的引用類型就絲毫沒有封裝性可言。
13.**原型模式**
每個創建的**函數**都有一個prototype屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。prototype就是通過調用構造函數而創建的那個對象實例的原型對象。使用原型對象的好處是可以就讓所有對象實例共享它所包含的屬性和方法。例:
~~~
function Person(){
}
Person.prototype.name = ‘ken’;
Person.prototype.age = 28;
Person.prototype.job = ‘frontend’;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true (不加括號,否則對比的是運行值)
~~~
關于原型對象的深入理解可以查看P148.
14.無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。
15.在默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性指向prototype屬性所在函數的指針。例如Person.prototype.constructor指向Person。
16.當調用構造函數創建一個新實例后,該實例的內部將包含一個[[prototype]]指針(主流瀏覽器中的_proto_屬性)**。要明確的重要一點是,這個連接存在于實例與原型對象之間,而不是存在于實例與構造函數之間。**
17.**雖然在所有實現中都無法訪問[[prototype]],但可以通過isPrototypeOf()方法來確定對象之間是否存在這種關系。**
~~~
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
~~~
18.**Ojbect.getPrototypeOf()方法返回[[prototype]]的值。**
~~~
alert(Object.getPrototypeOf(person1)) //Person.prototype
alert(Object.getPrototypeOf(person1).name) //ken
~~~
19.雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性,但不會修改那個屬性。不過,使用delete操作符則可以完全刪除實例屬性,從而讓我們能夠重新訪問原型中的屬性。
~~~
var person1 = new Person();
person1.name = 'jason';
delete person1.name;
alert(person1.name); //ken
~~~
20.單獨使用in操作符,會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在于實例中還是原型中。
`alert('name' in person1); //true;`
使用for-in循環時,返回的是所有能夠通過對象訪問的,可枚舉的屬性,其中既包含存在于實例中的屬性,也包括存在原型中的屬性。屏蔽了原型中不可枚舉屬性的實例屬性也會在for-in循環中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的(除了IE8及更早版本)。
要取得對象上所有**可枚舉的實例屬性**,可以使用ECMAScript5中的***Object.keys()***方法。這個方法接收一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。
如果想要得到所有實例屬性,無論它是否可枚舉,都可以使用**Object.getOwnPropertyNames()**。
總結:
for-in 實例+原型、可枚舉;
Object.keys() 實例、可枚舉;
Object.getOwnPropertyNames() 實例、可枚舉+不可枚舉
21.更簡單的原型語法:
~~~
function Person(){
}
Person.prototype = {
name:'Ken',
age:28,
job:'FrontEnd Engineer',
sayName:function(){
alert(this.name);
}
};
~~~
**使用這樣的語法創建原型對象,constructor屬性不再指向Person了。前面曾經介紹過,每創建一個函數,就會同時創建他的prototype對象。在這里使用的語法,本質上完全重寫了默認的prototype對象,因此,constructor屬性也就變成了新對象的constructor屬性(指向Object構造函數),不再指向Person函數。此時,盡管instanceof操作符還能返回正確的結果,但通過constructor已經無法確立對象的類型了。**
**如果constructor的值真的很重要,那么在使用上面的語法重寫原型對象時,可以手動將construtor的屬性值設為Person。注意,以這種方式重設constructor屬性會導致他的[[Enumerable]]特性被置為true。可以通過Object.defineProperty()方法將屬性置為默認的不可枚舉:**
~~~
Object.defineProperty(Person.prototype,'constructor',{
enumerable: false,
value: Person
});
~~~
22.原型的動態性
由于在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來--即使是先創建了實例后修改原型也照樣如此。
盡管可以隨時為原型添加屬性和方法,并且修改能夠立即在所有對象實例中反映出來,但**如果是重寫整個原型對象,那么情況就不一樣了。我們知道,調用構造函數時會為實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改為另一個對象就等于切斷了構造函數與最初原型之間的聯系。**
23.不推薦在產品化的程序中修改原生對象的原型。
24.將構造函數模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性,這是目前在ECMAScript中使用最廣泛、認同度最高的一種創建自定義類型的方法。
~~~
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['wesley','fox'];
}
Person.prototype = {
constructor:Person,
sayName:function(){
alert(this.name)
}
}
~~~
25.**動態原型模式**解決了構造函數與原型分別獨立、沒有封裝在一起的問題。
~~~
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['wesley','fox'];
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function(){
alert(this.name)
}
}
}
~~~
26.**寄生構造函數模式**解決了這樣一個場景下的問題:假設想創建一個具有額外方法的特殊數組,又不能直接修改Array構造函數,就可以使用這個模式。**除了使用new操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣的**。
~~~
function SpecialArray(){
var values = new Array();
values.push.apply(values,arguments); //此處使用apply是因為apply可以方便地把arguments添加到values數組。
values.toPipeString = function(){
return this.join('|');
}
return values;
}
var colors = new SpecialArray('red','blue','green');
alert(colors.toPipeString()); //'red|blue|green'
~~~
關于寄生構造函數模式,有一點需要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關系;也就是說,構造函數返回的對象與構造函數外部創建的對象沒什么不同。為此,**不能依賴instanceof操作符來確定對象類型**。由于存在上述問題,我們建議在在可以使用其他模式的情況下,不要使用這種模式。
~~~
alert(SpecialArray.prototype.isPrototypeOf(colors)); //false
alert(colors instanceof SpecialArray); //false
~~~
27.所謂**穩妥對象**,指的是沒有公共屬性,而且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境會禁止使用this和new),或者防止數據被其他應用程序改動時使用。**穩妥構造函數模式遵循與寄生構造函數模式類似的模式,但有兩點不同:一是新創建對象實例方法不引用this;二是不使用new操作符調用構造函數。**
~~~
function Person(name,age,job){
//var name = name,age = age,job = job;
var o = new Object();
o.sayName = function(){
alert(name);
};
return o;
}
var friend = Person('ken','28','FrontEnd Engineer');
friend.sayName(); //ken
~~~
這樣,變量friend中保存的是一個穩妥對象,而除了調用sayName()方法外,沒有別的方式可以訪問其數據成員。即使有其他代碼會給這個對象添加方法和數據成員,但也不可能有別的辦法訪問傳入到構造函數中的原始數據。與寄生構造函數模式類似,使用穩妥構造函數模式創建的對象與構造函數之間也沒有什么關系型,因此instanceof操作符對這種對象也沒有意義。
28.ECMAScript只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。**原型鏈的基本思想是讓一個引用類型繼承另一個引用類型屬性和方法**。***具體的實現方法就是讓一個引用類型的原型等于另一個類型的實例***。
~~~
function SuperType(){
this.property = true;
}
SuperType.property.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue) //true
~~~
要注意,此時instance.constructor現在指向的是SuperType,這是因為SubType的原型指向了另一個對象--SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType。
29.由于原型鏈的關系,我們可以說instance是Object、SuperType、SubType中任何一個類型的實例。通過兩種方式來確定原型與實例之間的關系。第一種方式是使用instanceof操作符;第二種方式是使用isPrototypeOf()方法。
`alert(SubType.prototype.isPrototypeOf(instance)); //true;`
30.原型鏈的主要問題:第一,包含引用類型值的原型屬性會被所有實例共享。第二,在創建子類型的實例時,沒有辦法在不影響那個所有對象實例的情況下,給超類型的構造函數傳遞參數。
31.**借用構造函數(經典繼承)**的基本思想相當簡單,即在子類型構造函數的內部調用超類型構造函數。
~~~
function SuperType(){
this.colors = ['red','yellow','blue'];
}
function SubType(){
SuperType.call(this);
}
~~~
通過使用call()或者apply()方法,我們實際上是在(未來將要)創建的SubType實例的環境下調用了SuperType構造函數。這樣一來,就會在新SubType()對象上執行SuperType()函數中定義的所有對象初始化代碼。結果,SubType的每個實例就都會具有自己的colors屬性的副本。
相對于原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數。
~~~
function SuperType(name){
this.name = name;
}
function SubType(){
SuperType.call(this,'ken');
this.age = 28;
}
~~~
如果僅僅是借用構造函數,那么也將無法避免構造函數模式存在的問題--方法都在構造函數中定義,因此函數復用就無從談起了。而且,在超類型的原型中定義的方法(使用prototype定義的),對于子類型而言也是不可見的,結果所有類型都只能使用構造函數模式。
32.**組合繼承(偽經典模式)**其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既在原型上定義方法實現了函數復用,又能夠保證每個實例都有自己的屬性。
~~~
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.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType('ken',28);
instance1.colors.push('black);
alert(instance1.colors); //'red,blue,green,black'
instance1.sayName(); //'ken'
instance1.sayAge(); //28
var instance2 = new SubType('Greg',27);
alert(instance2.colors); //'red,blue,green'
instance2.sayName(); //'Greg'
instance2.sayAge(); //27
~~~
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了他們的優點,成為JavaScript中最常用的繼承模式。而且,instanceof和isPrototypeOf()也能夠用于識別鯽魚組合繼承創建的對象。
33.**原型式繼承**沒有嚴格意義上的使用構造函數,它是借助原型可以基于已有的對象創建新對象,同時還不必因此創建自定義類型。
~~~
function object(o){
function F(){}
F.prototype = o;
return new F();
}
~~~
在object()函數內部,先創建了一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回這個臨時類型的新實例。從本質上講,object()對傳入其中的對象執行了一次淺復制。例:
~~~
var person = {
name: 'Nicholas',
friends: ['wesley','fox']
};
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); //'wesley,fox,Rob,Barbie'
~~~
ES5通過新增Object.creat()方法規范了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。
~~~
var person = {
name: 'Nicholas',
friends: ['wesley','fox']
};
var anotherPerson = Object.create(person, {
name:{
value: 'Greg'
}
});
~~~
在沒有必要興師動眾地創建構造函數,而只是想讓一個對象與另個對象保持類似的情況下,原型式繼承是完全可以勝任的。不過別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式一樣。
34.**寄生式繼承**是與原型式繼承密寫相關的一種思路,即創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象。
~~~
function createAnother(original) {
var clone = object(original);
clone.sayHi = function(){
alert('hi');
};
return clone;
}
~~~
這樣創建的新對象不僅具有original的所有屬性和方法,而且還有自己的sayHi()方法。在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。
35.**寄生組合式繼承**
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的思路是**:不必為了指定子類型的原型調用超類型的構造函數,我們所需要的無非就是超類型原型的一個副本而已。**本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型。
~~~
function inheritPrototype(subType,SuperType){
var prototype = Object(subType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
~~~
~~~
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;
}
//繼承方法
inheritPrototype(subType,SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
}
~~~
**寄生組合式繼承與組合繼承的區別在于,前者不將超類型中的實例屬性繼承為子類型的原型屬性。**