ECMAScript有兩種開發模式:1.函數式(過程化);2.物件導向(OOP);
一 建立對象
1.普通的建立對象
// 建立一個對象,然後給這個對象新的屬性和方法; var box = new Object(); // 建立一個Object對象; box.name = 'lee'; // 建立一個name屬性並賦值; box.age = 100; box.run = function(){ // 建立一個run()方法並傳回值; return this.name+this.age+'運行中...'; } console.log(box.run()); // 輸入屬性和方法的值;// 缺點:想建立類似的對象,就會產生大量的代碼;
2. 原廠模式建立對象
// 這種方法就是為瞭解決執行個體化對象產生大量代碼重複的問題; function createObject(name,age){ // 集中建立函數體; var obj = new Object; // 函數體內建立Object; obj.name = name; obj.age = age; obj.run = function(){ return this.name+this.age+"運行中..."; }; return obj; } var box1 = createObject("lee",100); // 執行個體化;調用函數並傳參; var box2 = createObject("jack",200); // 執行個體二; console.log(box1.run()+box2.run()); // 執行個體保持相對獨立;// 缺點:對象與執行個體的識別問題;無法搞清楚它們到底是那個對象的執行個體; console.log(typeof box1); // Object;
3.建構函式建立對象
// ECMAScript採用建構函式(構造方法)可用來建立特定的對象; function Box(name,age){ // 建構函式模式; this.name = name; // this代表對象Box; this.age = age; this.run = function(){ return this.name+this.age+"運行中..."; }; } var box1 = new Box("lee",100); // 要建立對象的執行個體必須用new操作符; var box2 = new Box("jack",200); // box1和box2都是Box對象的執行個體; console.log(box1 instanceof Box); // true;很清晰的識別box1從屬於Box;// 使用建構函式,即解決了重複執行個體化的問題,有解決了對象識別的問題;
使用建構函式與原廠模式不同之處:
(1).建構函式方法沒有顯示的建立對象(new Object);
(2).直接將屬性和方法賦值給this對象;
(3).沒有return語句;1 // 建構函式規範:
(1).函數名(function Box)和執行個體化構造名(new Box)相同且大寫;
(2).通過建構函式建立執行個體對象,必須使用new運算子;
// 建構函式和普通函數的區別: var box = new Box('lee',100); // 構造模式調用; Box('lee',200); // 普通模式調用,無效; var o = new Object(); Box.call(o,'jack',200); // 對象冒充調用; // 將Box對象範圍擴充到對象o;Box()方法的運行環境已經變成了對象o裡;
建構函式的問題:
使用建構函式建立每個執行個體的時候,建構函式裡的方法都要在每個執行個體上重新建立一遍;
因為ECMAScript中的函數是對象,因此每定義一個函數,也就是執行個體化了一個對象;
以這種方式建立函數,會導致不同的範圍鏈和標識符解析;
二 原型
// 我們建立的每個函數都有一個prototype(原型)屬性,這個屬性是一個對象;
// 用途:包含可以由特定類型的所有執行個體共用的屬性和方法;
// 理解:prototype是通過調用建構函式建立的那個對象的原型對象;
// 使用原型的好處是可以讓所有對象執行個體共用它所包含的屬性和方法;
// 也就是說,不必在建構函式中定義對象資訊(屬性/方法),而是可以直接將這些資訊添加到原型中;
1.原型模式(prototype添加屬性和方法)
1.原型模式 function Box(){} // 聲明建構函式; Box.prototype.name = 'Lee'; // 在原型裡添加屬性和方法; Box.prototype.age = 100; Box.prototype.run = function() { return this.name+this.age+'運行中...'; }; var box1 = new Box(); var box2 = new Box(); console.log(box1.run==box2.run); // =>true;方法引用的地址保持一致;// 在原型中多了兩個屬性,這兩個原型屬性都是建立對象時自動產生的;// 1.__proto__:建構函式指向原型對象的一個指標;它的作用:指向建構函式的原型的屬性constructor; 14// IE瀏覽器在指令碼訪問__proto__會不能識別; 15 // 判斷一個執行個體對象是否指向了該建構函式的原型對象,可以使用isPrototypeOf()方法來測試; console.log(Box.prototype.isPrototypeOf(box)); // =>true; 只要執行個體化對象,即都會指向;// 原型模式的執行流程:// 1.先尋找建構函式對象的執行個體裡的屬性或方法,若有,立刻返回;// 2.若建構函式對象的執行個體裡沒有,則去它的原型對象裡找,若有,就返回;// 雖然我們可以通過對象執行個體訪問儲存在原型中的值,但卻不能訪問通過對象執行個體重寫原型中的值; var box1 = new Box(); console.log(box1.name); // Lee; 原型裡的值; bo1.name = 'jack'; console.log(box1.name); // Jack;執行個體自己賦的值; var box2 = new Box(); console.log(box2.name); // Lee;原型裡的值;沒有被box1修改; // 如果想要box1繼續訪問原型裡的值,可以把建構函式裡的屬性刪除即可; delete box1.name; // 刪除執行個體自己的屬性; console.log(box1.name); // Lee; 原型裡原來的值;
2.原型與in操作符
如何判斷屬性是在建構函式的執行個體裡,還是在原型裡? 可以用hasOwnProperty()函數來驗證;
console.log(box.hasOwnProperty('name')); // 執行個體裡若有返回true,否則返回false;
in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在與執行個體中還是原型中;
console.log('name' in box); // =>true,存在執行個體中或原型中;3.更簡單的原型文法(原型+字面量模式)
3.更簡單的原型文法(原型+字面量模式)
function Box(){}; Box.prototype = { // 以字面量形式建立包含屬性和方法的新對象; name:'Lee', age:100, run:function(){ return this.name+this.age+'運行中...'; } };// 使用建構函式建立原型對象和使用字面量建立原型對象在使用上基本相同;// 但是,使用字面量建立的原型對象使用constructor屬性不會指向執行個體,而是指向原型對象Object;建構函式的方式則相反; var box = new Box(); console.log(box instanceof Box); console.log(box instanceof Object); console.log(box.constructor == Box); // 字面量方式,返回false; console.log(box.constructor == Object); // 字面量方式,返回true; // 如果想讓字面量方式的constructor指向執行個體對象: Box.prototype = { constructor:Box, // 直接強制指向即可; } // PS:字面量方式為什麼constructor會指向Object? // 因為Box.prototype={}這種字面量寫法就是建立一個新對象; // 而每建立一個函數,就會同時建立它的prototype,這個對象也會自動擷取constructor屬性; // 所以,新對象的constructor重寫了Box原來的constructor,因此指向了新對象, // 那個新對象沒有指定建構函式,那麼就預設為是Object;
4.原型的動態性(重寫會覆蓋之前的內容)
// 原型的聲明是有先後順序的,所以,重寫的原型會切斷之前的原型; function Box(){}; Box.prototype = { constructor:Box, name:'Lee', age:100, run:function(){ return this.age+'運行中...'; } }; Box.prototype = { // 原型重寫了,覆蓋了之前的原型; age:200, run:function(){ return this.age+'運行中...'; } } var box = new Box(); console.log(box.run()); // =>200運行中...; // 重寫原型對象切斷了現有原型與任何之前已經存在的對象執行個體之間的聯絡;對象執行個體引用的仍然是最初的原型;
5.原生對象的原型
// 原型對象不僅僅可以在自訂對象的情況下使用,而是ECMAScript內建的參考型別都可以使用這種方式,
// 並且內建的參考型別本身也是用了原型;
console.log(Array.prototype.sort); // =>function sort() { [native code] };
console.log(String.prototype.substring); // =>function substring() { [native code] };
6.原型對象的問題
// 原型模式建立對象缺點:省略了建構函式傳參初始化這一過程,帶來的缺點就是初始化的值都是一致的;// 而原型最大的有點就是共用,屬性共用;// 但是,如果原型中的屬性包含參考型別(對象),共用就會存在一定問題; function Box(){}; Box.prototype = { constructor:Box, name:'Lee', age:100, family:['father','mother'], run:function(){ return this.name+this.age+this.family; } }; var box1 = new Box(); box1.family.push('sister'); // 為box1的family屬性添加了sister;而這個屬性被共用到原型了; console.log(box1.run()); // =>Lee100father,mother,sister; var box2 = new Box(); console.log(box2.run()); // =>Lee100father,mother,sister; // 資料共用導致執行個體化出的資料不能儲存自己的特性;
7.組合使用建構函式模式(對象不共用的資料)和原型模式(對象共用的資料)
// 為瞭解決構造傳參和共用問題,組合建構函式+原型模式: function Box(name,age){ // 不共用的使用建構函式; this.name = name; this.age = age; this.family = ['father','moter']; }; Box.prototype = { // 共用的使用原型模式; constructor:Box, run:function(){ return this.name+this.age+this.family; } }; // PS:這種混合模式很好的解決了傳參和引用共用的大難題;是建立對象比較好的方法;
8.動態原型模式(將原型封裝到建構函式裡)
// 原型模式,不管是否調用了原型中的共用方法,它都會初始化原型中的方法;// 並且在聲明一個對象時,建構函式+原型讓人感覺怪異;最好把建構函式和原型封裝到一起; function Box(name,age){ // 將所有資訊封裝到建構函式體內; this.name = name; this.age = age; // 當第一次調用建構函式時,run()方法不存在,然後執行初始化原型; // 當第二次調用,就不會初始化,並且第二次建立新對象,原型也不會載初始化; // 這樣既得到了封裝,又實現了原型方法共用,並且屬性都保持獨立; if(typeof this.run != 'function'){ // 僅在第一次調用時初始化; Box.prototype.run = function (){ return this.name+this.age+'運行中...'; }; } }; var box = new Box('lee',10); console.log(box.run());// PS:使用動態原型模式,要注意一點,不可以再使用字面量的方式重寫原型,因為會切斷執行個體和新原型之間的聯絡;
9.寄生建構函式
// 寄生建構函式,其實就是原廠模式+構造模式;這種模式比較通用,但不能確定對象關係; function Box(name,age){ var obj = new Object(); obj.name = name; obj.age = age; obj.run = function (){ return this.name+this.age+'運行中...'; }; return obj; }
三 繼承
1.原型鏈
// 繼承是物件導向中一個比較核心的概念;// 其他正統物件導向語言都會用兩種方式實現繼承:一個是介面實現,一個是繼承;// 而ECMAScript只支援繼承,不支援介面實現,而實現繼承的方式依靠原型鏈完成;// 實質:利用原型讓一個參考型別繼承另一個參考型別的屬性和方法; // 原型繼承鏈:Box ==>> Desk ==>> Table; function Box(){ // Box構造; this.name = 'Lee'; } function Desk(){ // Desk構造; this.age = 100; } Desk.prototype = new Box(); // 通過建立Box執行個體,並賦值給Desk.prototype實現的;通過原型,形成鏈條; // 實質是:重寫了Desk的原型對象,取而代之的是一個新類型Box的執行個體; // 也就是說原來存在於Box執行個體中的屬性和方法,現在也存在與Desk.prototype中了; var desk = new Desk(); console.log(desk.age); // 100; console.log(desk.name); // =>Lee; function Table(){ this.level = 'AAA'; } Table.prototype = new Desk(); // 繼續原型鏈繼承;Table繼承了Desk; var table = new Table(); console.log(table.name); // Lee;
2.原型與執行個體的關係;
// PS:以上原型鏈繼承缺少一環,那就是Object,所有的建構函式都繼承自Object;// 而繼承Object是自動完成的,並不需要手動繼承; console.log(table instanceof Object); // =>true; console.log(desk instanceof Table); // =>false;Desk是Table的超類; console.log(table instanceof Desk); // =>true; console.log(table instanceof Box); // =>true;// 在JS中,被繼承的函數稱為超類型(父類,基類);// 繼承的函數稱為子類型(子類,衍生類別);// 繼承問題:// 字面量重寫原型會中斷關係;// 子類型無法給超類型傳遞參數;
3.借用建構函式(對象冒充)
// 為瞭解決引用共用和給超類型無法傳參問題;
// 在子類型建構函式的內部調用超類型建構函式; function Box(age){ this.name = ['Lee','Jack','Hello']; this.age = age; } function Desk(age){ // 繼承了Box;同時還傳遞了參數; // 這樣一來,就會在新Desk對象上執行Box()函數中定義的所有對象初始化代碼; Box.call(this,age); // 對象冒充,Desk繼承Box,並可以給超類型傳參; // 為了確保Box建構函式不會重寫子類型的屬性,可以在超類型建構函式後,再添加應該在子類型中定義的屬性; this.height = 175; } var desk = new Desk(200); // 向Desk()函數傳參,再通過函數冒用向Box()函數傳參; console.log(desk.age); // =>200; console.log(desk.name); // =>['Lee','Jack','Hello']; desk.name.push('AAA'); // =>添加的新資料,只添加給desk; console.log(desk.name); // =>['Lee','Jack','Hello','AAA'];
4.組合繼承(原型鏈+借用建構函式)
// 借用建構函式雖然解決了引用共用和給超類型無法傳參問題,但是沒有使用原型,複用則無從談起;所以需要組合繼承模式;
// 使用原型鏈實現對原型屬性和方法的繼承;// 通過借用建構函式來實現對執行個體屬性的繼承;// 這樣,既通過在原型上定義方法實現了函數複用,又能保證每個執行個體都有他自己的屬性; function Box(age){ // 建構函式; this.name = ['Lee','Jack','Hello']; this.age = age; } Box.prototype.run = function(){ // 原型; return this.name+this.age; } function Desk(age){ Box.call(this,age); // 繼承屬性; 對象冒充; 將Box對象的範圍擴充到Desk中,Desk就會繼承Box裡的屬性和方法; } Desk.prototype = new Box(); // 繼承方法; 原型鏈繼承; var desk = new Desk(100); console.log(desk.run()); // =>Lee,Jack,Hello100// 最常用的繼承模式;
5.原型式繼承?
// 這種繼承藉助原型並基於已有的對象建立對象,同時還不必因此建立自訂類型; function obj(o){ // 傳遞一個字面量函數; function F(){}; // 建立一個建構函式; F.prototype = o; // 把字面量函數賦值給建構函式的原型; return new F(); // 返回執行個體化的建構函式; } var box = { // 字面量對象; name:'Lee', arr:['brother','sisiter'] }; var box1 = obj(box); console.log(box1.name); // =>Lee; box1.name = 'Jack'; console.log(box1.name); // =>Jack; console.log(box1.arr); // =>brother,sister; box1.arr.push('father'); // console.log(box1.arr); // =>brother,sister,father; var box2 = obj(box); console.log(box2.name); // =>Lee; console.log(box2.arr); // =>brother,sister,father;參考型別共用了;
6.寄生式繼承?
// 把原型式+原廠模式結合而來,目的是為了封裝建立對象的過程;// 建立一個僅用於封裝繼承過程的函數, function create(o){ // 封裝建立過程; var f = obj(o); f.run = function(){ return this.arr; // 同樣會共用引用; }; return f; }
7.寄生組合式繼承?
// 之前說過,組合式繼承是JS最常用的繼承模式;// 但是,組合式繼承也有問題:// 超類型在使用過程中會被調用兩次:一次是建立子類型的時候,另一次是在子類型建構函式的內部; function Box(name){ this.name = name; this.arr = ['brother','sister']; } Box.prototype.run = function(){ return this.name; } function Desk(name,age){ Box.call(this,name); // 第二次調用Box; this.age = age; } Desk.prototype = new Box(); // 第一次調用Box;// 寄生組合式繼承:// 通過借用建構函式來繼承屬性,// 通過原型鏈的混成形式來繼承方法;// 解決了兩次調用的問題; function obj(o){ function F(){}; F.prototype = o; return new F(); } function create(box,desk){ var f = obj(box.prototype); f.constructor = desk; desk.prototype = f; } function Box(name){ this.name = name; this.arr = ['brother','sister']; } Box.prototype.run = function(){ return this.name; } function Desk(name,age){ Box.call(this,name); this.age = age; } inheritPrototype(Box,Desk); // 通過這裡實現繼承; var desk = new Desk('Lee',100); desk.arr.push('father'); console.log(desk.arr); console.log(desk.run()); var desk2 = new Desk('Jack',200); console.log(desk2.arr); // 兩次引用問題解決;
四 小結
1.建立對象
對象可以在代碼執行過程中建立和增強,因此具有動態性而非嚴格定義的實體;
在沒有類的情況下,可以採用下列模式建立對象;
(1).原廠模式:使用簡單的函數建立對象,為對象添加屬性和方法,然後返回對象;
這個模式後來被建構函式模式所取代;
(2).建構函式模式:可以自訂參考型別,可以像建立內建對象執行個體一眼使用new操作符;
缺點:它的每個成員都無法得到複用,包括函數;由於函數可以不局限於任何對象,因此沒有理由不在多個對象間共用函數;
(3).原型模式:使用函數的prototype屬性來指定那些應該共用的屬性和方法;
組合使用建構函式模式和原型模式時,使用建構函式定義執行個體屬性,使用原型定義共用的屬性和方法;
2.原型鏈
原型鏈的構建是通過將一個類型的執行個體賦值給另一個建構函式的原型實現的;
子類型可以訪問到超類型的所有屬性和方法;
原型鏈的問題是對象執行個體共用所有繼承的屬性和方法,因此不適宜單獨使用;
解決方案:借用建構函式,即在子類型建構函式的內部調用超類型建構函式;
這樣就可以做到每個執行個體都具有自己的屬性,同時還能保證只使用建構函式來定義類型;
使用最多的繼承模式是組合繼承;它使用原型鏈繼承共用的屬性和方法,而通過借用建構函式繼承執行個體屬性;
3.繼承模式
(1).原型式繼承:可以在不必預先定義建構函式的情況下實現繼承;其本質是執行對給定對象的淺複製;
而複製得到的副本開可以得到進一步改造;
(2).寄生式繼承:基於某個對象或某些資訊建立一個對象,然後增強對象,最後返回對象;
為瞭解決組合繼承模式由於多次調用超類型建構函式而導致的低效率問題,可以將這個模式與組合繼承一起使用;
(3).寄生組合式繼承:集寄生式繼承和組合式繼承的有點於一身,是實現基於類型繼承的最有效方式;