JavaScript之面向對象(ECMAScript5)
- 理解對象屬性
- 創建對象
- 繼承
理解對象屬性
ECMA-262稱對象為:無序屬性的集合,其屬性可以包含基本值,對象或者函數。
由此在ECMAScript中可以把對象想象成散列表,無非就是鍵值對,值可以為數據或者函數。
//兩種定義對象的方式 var person = new Object(); person.name="xiaoming"; person.age=29; person.showName = function(){ alert(this.name); }
var person = { name:"xiaoming", age:29, showName:function(){ alert(this.name); } }
屬性類型
有些特性定義是為了實現JavaScript引擎用的。為了表示特性時內部值,ECMA-262規範把他們放在兩對方括號中,例如:[ [ Enumerable ] ]。
ECMAScript中有兩種屬性:數據屬性和訪問器屬性。
數據屬性 有以下描述行為的特性(多數情況下很少用到這些高級功能)
- [ [ Configurable ] ]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性特性,或者能否把屬性修改為訪問器屬性。默認值都為true
- [ [ Enumerable ] ]:表示能否通過for-in循環返回屬性。默認true
- [ [ Writable ] ]:表示能否修改屬性的值。默認為true
- [ [ Value ] ]:包含屬性的數據值,用作讀寫。
修改以上屬性的默認特性使用Object.defineProperty()方法
var person={} //接收三個參數:屬性所在的對象,屬性的名字和配置特性的對象 Object.defineProperty(person,"name",{ writable:false, value:"xiaoming" })
訪問器屬性 包含一對getter和setter函數。有如下4個特性:
- [ [ Configurable ] ]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性特性,或者能否把屬性修改為訪問器屬性。默認值都為true
- [ [ Enumerable ] ]:表示能否通過for-in循環返回屬性。默認true
- [ [ Get ] ]:在讀取屬性時調用的函數。默認為undefined
- [ [ Set ] ]:在寫入屬性時調用的函數。默認為undefined
1 var book = { 2 _year:2017, 3 edition:1 4 } 5 6 Object.defineProperty(book,"year", 7 { 8 get:function() 9 { 10 return this._year; 11 } 12 set:function(newValue) 13 { 14 if(newVaule>2017) 15 { 16 this._year = newValue; 17 this.edition+=newVlaue-2017 18 } 19 } 20 }) 21 22 //定義多個屬性 23 var book2 ={}; 24 Object.defineProperties(book2, 25 { 26 _year: 27 { 28 writable:true, 29 value:2004 30 }, 31 32 edition: 33 { 34 writable:true, 35 value:1 36 }, 37 38 year: 39 { 40 get:function() 41 { 42 return this._year; 43 } 44 set:function(newValue) 45 { 46 if(newVaule>2017) 47 { 48 this._year = newValue; 49 this.edition+=newVlaue-2017; 50 } 51 } 52 } 53 })View Code
若想要讀取屬性的某些特性,可以利用Object.getOwnPropertyDescriptor()方法,接收兩個參數:屬性所在的對象和要讀取其描述的屬性名稱。
創建對象
問題一: 前面通過Object構造函數或對象字面量的形式創建單個對象,但是這種方式使用同一接口創建很多對象,產生大量重復代碼。
解決方法:工廠模式,用函數來封裝以特定接口創建對象的細節。
具體實現:函數createSchool()能夠根據接收的參數來構建一個包含所有必要信息的School對象。
function createSchool(NO,name,address) { var obj = new Object(); obj.No = NO; obj.name = name; obje.address = address; return obj; }
問題二:工廠模式解決了創建多個相似對象的問題,但是沒有解決對象識別問題,不知道對象是什麽類型,如上面的createSchool方法,得不到創建的類型是School類型。
解決方法:構造函數模式。創建自定義的構造函數,從而自定義對象類型的屬性和方法。
具體實現:與工廠模式區別在於沒有顯示的創建對象,直接將屬性和方法賦給了this對象,沒有return語句。
function School(No,name,address) { this.No = No; this.name = name; this.address = address; this.showName = function(){ console.log(this.name); } }
要創建新實例要使用new操作符,會經歷四個步驟
- 創建一個新對象
- 將構造函數的作用域賦給新對象,this就指向了這個新對象
- 執行構造函數中代碼,為對象添加屬性
- 返回新對象
創建後的對象都會有一個constructor屬性,該屬性指向School。通過利用創建的實例對象school1.constructor == School來檢測對象類型。比較可靠的方法還是使用
school1 instanceof School方式。另外構造函數其實也就是JS中簡單的函數,當通過new操作符調用時,作為構造函數來使用,否則它也就是簡單的函數。
問題三:構造函數雖然解決了工廠模式的缺陷,但是對於每個方法都需要在每個實例上重新創建一遍。也就是說對於上面School中的showName方法,其實等價於this.showName = new Function("console.log(..)");
也就是又實例化了一個對象。以這種方式會導致不同作用域鏈和標識符解析,不同實例上的同名函數是不相等的,不能實現共享。例如:school1.showName==school2.showName 返回的是false.
解決方法:將函數定義放在構造函數的外部。
function School(No,name,address) { this.No = No; this.name = name; this.address = address; this.showName = showName; } function showName(){ console.log(this.name); }
問題四:將方法定義外面,確保了實例對象調用了相同的事件,但是在全局作用域中定義函數使得在任意地方均可調用,沒有封裝性可言。
解決方法:原型模式。每個函數都有一個prototype(原型)屬性,它是一個指針,指向一個對象(原型對象),用途可以將想要實例間共享的屬性和方法定義在這個對象上。
function School(){} School.prototype.No = 1001; School.prototype.name = "xxx"; School.prototype.showName = function(){ console.log(this.name); } var school = new School(); school.showName(); var school2 = new School(); school2.showName(); console.log(school.showName==school2.showName);//true
知識點:
- 原型對象:只要創建一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。默認情況下,所有原型對象都會自動獲得一個constructor屬性,這個屬性是一個指向prototype屬性所在函數的指針。
- 每當代碼讀取某個屬性時,根據給定名字的屬性,搜索首先會從對象的實例本身開始,如果找到則返回該屬性的值;如果沒有找到,繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性,找到返回值,否則未定義,報錯。
- 更簡單的原型語法:
School.prototype = { //為了使自動獲取的constructor指向School, //因為通過這種原型語法constructor等於Object,需要手動設置如下 constructor:School, name:"", showName:function(){ console.log(this.name); } }
- 原生對象的原型。所有的原生引用類型(Object,Array,String等)都在其構造函數的原型上定義了方法。如在Array.prototype中可以找到sort()方法。我們也可以通過這種方法給某個原生類型添加方法和屬性。例如:String.prototype.showText = function(){ alert("Text"); }
問題四:通過原型模式中原型對象的方式,會導致所有實例在默認情況下都將取得相同的屬性值,造成了不便。同時由於他的共享性本身,對於引用類型的值來說問題較大,因為引用類型保存的是一個指針,共享導致某個實例改變改屬性會同步到其他實例中。
解決方法:將構造函數模式和原型模式組合使用:構造函數模式用於定義實例屬性,而原型模式用於定義方法和需要共享的屬性。 使用最廣泛的一種方法。
function School(No,name) { this.No = No; this.name = name; this.Classes=["一年級","二年級"]; } School.prototype = { constructor:School, showName:function(){ console.log(this.name); } }
繼承
說明:ECMAScript支持實現繼承,主要依靠原型鏈
原型鏈方法:利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
function SuperType() { this.supername="super"; } function SubType() { this.subname = "sub"; } SubType.prototype = new SuperType(); var instance = new SubType(); alert(instance.subname);
解析:在上述代碼中,沒有使用SubType的默認提供的原型,而是替換為SuperType的實例。新原型不僅作為基類(SuperType)的實例而具有全部的屬性和方法,並且其內部還有一個指針,指向了SuperType的原型。從而instance指向SubType的原型,SubType的原型又指向SuperType的原型。通過之前講過的,原型搜索機制,沿著原型鏈繼續向上,直到找到指定的方法或屬性。
補充:
- 由於所有的引用類型默認都繼承Object,所以所有函數的默認原型都是Object的實例,默認原型包含一個內部指針,指向Object.prototype,因此自定義的類型都會有toString(),valueOf()等方法。
- 如何驗證實例兼容哪種原型。可以使用instanceof (instance instanceof Object)或 isPrototypeOf()方法(Object.prototype.isPrototypeOf())。
- 給原型添加方法要放在替換原型的語句之後。即SubType.prototype.getSubValue=function(){};要放在SubType.prototype = new SubperType();之後
- 不要使用字面量添加新方法,因為會把原型鏈切斷從而使得SubType和SuperType沒有關系。
原型鏈的問題:前面講過,通過原型鏈的形式會使得引用類型值的原型屬性被所有實例共享,從而造成不便。另外創建子類的實例時,不能向超類型的構造函數傳遞參數。看如下代碼
function SuperType() { this.colors = ["red","blue"]; } function SubType() { } SubType.prototype = new SuperType(); var instance = new SubType(); instance.colors.push("green"); console.log(instance.colors);//red,bule,greed var instance2 = new SubType(); console.log(instance2.colors);//red,bule,greed
解決方法:借用構造函數,解決原型中包含引用類型值所帶來的問題
實現思路:在子類型構造函數的內部調用超類型構造函數。使用apply()或call()方法,只要將上面的代碼中SubType函數裏加入SuperType.call(this);即可。同時也可以傳遞參數,如下
function SuperType(name) { this.name = name; } function SubType() { SuperType.call(this,"jack"); } var instance = new SubType(); console.log(instance.name);//jack
借用構造函數方法的問題:僅僅使用這一模式無法使函數復用。而且在基類中定義的方法,對於子類型而言是不可見的。
解決方法:組合繼承。
實現思路:使用原型鏈實現對原型屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承。
function SuperType(name) { this.name = name; this.colors = ["red","blue"]; } SuperType.prototype.showName = function() { console.log(this.name); } function SubType(name,age) { SuperType.call(this,name); this.age = age; } //繼承形式 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; //給子類添加方法 SubType.prototype.showAge = function() { console.log(this.age); }
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了優點。
組合繼承問題:其無論在什麽情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候(SubType.prototype = new SuperType())。另一次是在子類型構造函數內部(SuperType.call(this,name))。這種情況會造成兩組name和colors屬性,實例上和SubType原型中。
解決方法:寄生組合式繼承
思路:通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。我們所需要的就是超類型的副本。所以這種模式的基本代碼結構如下
function SuperType(name) { this.name = name; this.colors = ["red","blue"]; } SuperType.prototype.showName = function() { console.log(this.name); } function SubType(name,age) { SuperType.call(this,name); this.age = age; } //將基類與子類聯系在一起 inheritPrototype(subType,superType); //然後給子類添加方法 SubType.prototype.showAge = function() { console.log(this.age); } function inheritPrototype(subType,superType) { var prototype = Object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; }
寄生組合式繼承,只調用一次SuperType構造函數,避免在SubType.prototype上創建不必要,多余的屬性,原型鏈還能保持不變。
總結:ECMAScript支持面向對象編程,使用如下模式創建對象:
- 工廠模式,使用簡單的函數創建對象,為對象添加屬性和方法,然後返回對象。後來被構造函數模式取代
- 構造函數模式,創建自定義引用類型,可以像對象實例一樣使用new操作符。缺點就是每個成員都無法得到復用,包括函數。由於函數與對象具有松散耦合的特點,,因此沒有理由不在多個對象間共享函數。
- 原型模式,使用構造函數的prototype屬性指定那些應該共享的屬性和方法。組合使用構造函數模式和原型模式,從而使用構造函數定義實例屬性,使用原型定義共享的屬性和方法
JavaScript主要通過原型鏈實現繼承。原型鏈的構建是通過將一個類型的實例賦值給另一個構造函數的原型實現的。這樣,子類型就能夠訪問超類型所有屬性和方法。原型鏈的問題是對象實例之間共享所有繼承的屬性和方法,不適宜單獨使用。配和借用構造函數,即在子類型構造函數內部調用超類型構造函數。原型鏈繼承共享的屬性和方法,借用構造函數繼承實例屬性。另外還有寄生組合式繼承,原型繼承,寄生式繼承等。
參考:《JavaScript高級程序設計》
JavaScript之面向對象(ECMAScript5)