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).寄生組合式繼承:集寄生式繼承和組合式繼承的有點於一身,是實現基於類型繼承的最有效方式;