深入理解Javascript面向對象編程
閱讀目錄
一:理解構造函數原型(prototype)機制
prototype是javascript實現與管理繼承的一種機制,也是面向對象的設計思想.構造函數的原型存儲着引用對象的一個指針,該指針指向與一個原型對象,對象內部存儲着函數的原始屬性和方法;我們可以借助prototype屬性,可以訪問原型內部的屬性和方法。
當構造函數被實列化后,所有的實例對象都可以訪問構造函數的原型成員,如果在原型中聲明一個成員,所有的實列方法都可以共享它,比如如下代碼:
// 構造函數A 它的原型有一個getName方法 function A(name){ this.name = name; } A.prototype.getName = function(){ return this.name; } // 實列化2次后 該2個實列都有原型getName方法;如下代碼 var instance1 = new A("longen1"); var instance2 = new A("longen2"); console.log(instance1.getName()); //longen1 console.log(instance2.getName()); // longen2
原型具有普通對象結構,可以將任何普通對象設置為原型對象; 一般情況下,對象都繼承與Object,也可以理解Object是所有對象的超類,Object是沒有原型的,而構造函數擁有原型,因此實列化的對象也是Object的實列,如下代碼:
// 實列化對象是構造函數的實列 console.log(instance1 instanceof A); //true console.log(instance2 instanceof A); // true // 實列化對象也是Object的實列 console.log(instance1 instanceof Object); //true console.log(instance2 instanceof Object); //true //Object 對象是所有對象的超類,因此構造函數也是Object的實列 console.log(A instanceof Object); // true // 但是實列化對象 不是Function對象的實列 如下代碼 console.log(instance1 instanceof Function); // false console.log(instance2 instanceof Function); // false // 但是Object與Function有關系 如下代碼說明 console.log(Function instanceof Object); // true console.log(Object instanceof Function); // true
如上代碼,Function是Object的實列,也可以是Object也是Function的實列;他們是2個不同的構造器,我們繼續看如下代碼:
var f = new Function(); var o = new Object(); console.log("------------"); console.log(f instanceof Function); //true console.log(o instanceof Function); // false console.log(f instanceof Object); // true console.log(o instanceof Object); // true
我們明白,在原型上增加成員屬性或者方法的話,它被所有的實列化對象所共享屬性和方法,但是如果實列化對象有和原型相同的成員成員名字的話,那么它取到的成員是本實列化對象,如果本實列對象中沒有的話,那么它會到原型中去查找該成員,如果原型找到就返回,否則的會返回undefined,如下代碼測試
function B(){ this.name = "longen2"; } B.prototype.name = "AA"; B.prototype.getName = function(){ return this.name; }; var b1 = new B(); // 在本實列查找,找到就返回,否則到原型查找 console.log(b1.name); // longen2 // 在本實列沒有找到該方法,就到原型去查找 console.log(b1.getName());//longen2 // 如果在本實列沒有找到的話,到原型上查找也沒有找到的話,就返回undefined console.log(b1.a); // undefined // 現在我使用delete運算符刪除本地實列屬性,那么取到的是就是原型屬性了,如下代碼: delete b1.name; console.log(b1.name); // AA
二:理解原型域鏈的概念
原型的優點是能夠以對象結構為載體,創建大量的實列,這些實列能共享原型中的成員(屬性和方法);同時也可以使用原型實現面向對象中的繼承機制~ 如下代碼:下面我們來看這個構造函數AA和構造函數BB,當BB.prototype = new AA(11);執行這個的時候,那么B就繼承與A,B中的原型就有x的屬性值為11
function AA(x){ this.x = x; } function BB(x) { this.x = x; } BB.prototype = new AA(11); console.log(BB.prototype.x); //11 // 我們再來理解原型繼承和原型鏈的概念,代碼如下,都有注釋 function A(x) { this.x = x; } // 在A的原型上定義一個屬性x = 0 A.prototype.x = 0; function B(x) { this.x = x; } B.prototype = new A(1);
實列化A new A(1)的時候 在A函數內this.x =1, B.prototype = new A(1);B.prototype 是A的實列 也就是B繼承於A, 即B.prototype.x = 1; 如下代碼:
console.log(B.prototype.x); // 1 // 定義C的構造函數 function C(x) { this.x = x; } C.prototype = new B(2);
C.prototype = new B(2); 也就是C.prototype 是B的實列,C繼承於B;那么new B(2)的時候 在B的構造函數內 this.x = 2;那么 C的原型上會有一個屬性x =2 即C.prototype.x = 2; 如下代碼:
console.log(C.prototype.x); // 2
下面是實列化 var d = new C(3); 實列化C的構造函數時候,那么在C的構造函數內this.x = 3; 因此如下打印實列化后的d.x = 3;如下代碼:
var d = new C(3); console.log(d.x); // 3
刪除d.x 再訪問d.x的時候 本實列對象被刪掉,只能從原型上去查找;由於C.prototype = new B(2); 也就是C繼承於B,因此C的原型也有x = 2;即C.prototype.x = 2; 如下代碼:
delete d.x; console.log(d.x); //2
刪除C.prototype.x后,我們從上面代碼知道,C是繼承於B的,自身的原型被刪掉后,會去查找父元素的原型鏈,因此在B的原型上找到x =1; 如下代碼:
delete C.prototype.x; console.log(d.x); // 1
當刪除B的原型屬性x后,由於B是繼承於A的,因此會從父元素的原型鏈上查找A原型上是否有x的屬性,如果有的話,就返回,否則看A是否有繼承,沒有繼承的話,繼續往Object上去查找,如果沒有找到就返回undefined 因此當刪除B的原型x后,delete B.prototype.x; 打印出A上的原型x=0; 如下代碼:
delete B.prototype.x; console.log(d.x); // 0 // 繼續刪除A的原型x后 結果沒有找到,就返回undefined了; delete A.prototype.x; console.log(d.x); // undefined
在javascript中,一切都是對象,Function和Object都是函數的實列;構造函數的父原型指向於Function原型,Function.prototype的父原型指向與Object的原型,Object的父原型也指向與Function原型,Object.prototype是所有原型的頂層;
如下代碼:
Function.prototype.a = function(){ console.log("我是父原型Function"); } Object.prototype.a = function(){ console.log("我是 父原型Object"); } function A(){ this.a = "a"; } A.prototype = { B: function(){ console.log("b"); } } // Function 和 Object都是函數的實列 如下: console.log(A instanceof Function); // true console.log(A instanceof Object); // true // A.prototype是一個對象,它是Object的實列,但不是Function的實列 console.log(A.prototype instanceof Function); // false console.log(A.prototype instanceof Object); // true // Function是Object的實列 同是Object也是Function的實列 console.log(Function instanceof Object); // true console.log(Object instanceof Function); // true /* * Function.prototype是Object的實列 但是Object.prototype不是Function的實列 * 說明Object.prototype是所有父原型的頂層 */ console.log(Function.prototype instanceof Object); //true console.log(Object.prototype instanceof Function); // false
三:理解原型繼承機制
構造函數都有一個指針指向原型,Object.prototype是所有原型對象的頂層,比如如下代碼:
var obj = {}; Object.prototype.name = "tugenhua"; console.log(obj.name); // tugenhua
給Object.prototype 定義一個屬性,通過字面量構建的對象的話,都會從父類那邊獲取Object.prototype的屬性;
從上面代碼我們知道,原型繼承的方法是:假如A需要繼承於B,那么A.prototype(A的原型) = new B()(作為B的實列) 即可實現A繼承於B; 因此我們下面可以初始化一個空的構造函數;然后把對象賦值給構造函數的原型,然后返回該構造函數的實列; 即可實現繼承; 如下代碼:
if(typeof Object.create !== 'function') { Object.create = function(o) { var F = new Function(); F.prototype = o; return new F(); } } var a = { name: 'longen', getName: function(){ return this.name; } }; var b = {}; b = Object.create(a); console.log(typeof b); //object console.log(b.name); // longen console.log(b.getName()); // longen
如上代碼:我們先檢測Object是否已經有Object.create該方法;如果沒有的話就創建一個; 該方法內創建一個空的構造器,把參數對象傳遞給構造函數的原型,最后返回該構造函數的實列,就實現了繼承方式;如上測試代碼:先定義一個a對象,有成員屬性name='longen',還有一個getName()方法;最后返回該name屬性; 然后定義一個b空對象,使用Object.create(a);把a對象繼承給b對象,因此b對象也有屬性name和成員方法getName();
理解原型查找原理:對象查找先在該構造函數內查找對應的屬性,如果該對象沒有該屬性的話,
那么javascript會試着從該原型上去查找,如果原型對象中也沒有該屬性的話,那么它們會從原型中的原型去查找,直到查找的Object.prototype也沒有該屬性的話,那么就會返回undefined;因此我們想要僅在該對象內查找的話,為了提高性能,我們可以使用hasOwnProperty()來判斷該對象內有沒有該屬性,如果有的話,就執行代碼(使用for-in循環查找):如下:
var obj = { "name":'tugenhua', "age":'28' }; // 使用for-in循環 for(var i in obj) { if(obj.hasOwnProperty(i)) { console.log(obj[i]); //tugenhua 28 } }
如上使用for-in循環查找對象里面的屬性,但是我們需要明白的是:for-in循環查找對象的屬性,它是不保證順序的,for-in循環和for循環;最本質的區別是:for循環是有順序的,for-in循環遍歷對象是無序的,因此我們如果需要對象保證順序的話,可以把對象轉換為數組來,然后再使用for循環遍歷即可;
下面我們來談談原型繼承的優點和缺點
// 先看下面的代碼: // 定義構造函數A,定義特權屬性和特權方法 function A(x) { this.x1 = x; this.getX1 = function(){ return this.x1; } } // 定義構造函數B,定義特權屬性和特權方法 function B(x) { this.x2 = x; this.getX2 = function(){ return this.x1 + this.x2; } } B.prototype = new A(1);
B.prototype = new A(1);這句代碼執行的時候,B的原型繼承於A,因此B.prototype也有A的屬性和方法,即:B.prototype.x1 = 1; B.prototype.getX1 方法;但是B也有自己的特權屬性x2和特權方法getX2; 如下代碼:
function C(x) { this.x3 = x; this.getX3 = function(){ return this.x3 + this.x2; } } C.prototype = new B(2); C.prototype = new B(2);這句代碼執行的時候,C的原型繼承於B,因此C.prototype.x2 = 2; C.prototype.getX2方法且C也有自己的特權屬性x3和特權方法getX3, var b = new B(2); var c = new C(3); console.log(b.x1); // 1 console.log(c.x1); // 1 console.log(c.getX3()); // 5 console.log(c.getX2()); // 3 var b = new B(2);
實列化B的時候 b.x1 首先會在構造函數內查找x1屬性,沒有找到,由於B的原型繼承於A,因此A有x1屬性,因此B.prototype.x1 = 1找到了;var c = new C(3); 實列化C的時候,從上面的代碼可以看到C繼承於B,B繼承於A,因此在C函數中沒有找到x1屬性,會往原型繼續查找,直到找到父元素A有x1屬性,因此c.x1 = 1;c.getX3()方法; 返回this.x3+this.x2 this.x3 = 3;this.x2 是B的屬性,因此this.x2 = 2;c.getX2(); 查找的方法也一樣,不再解釋
prototype的缺點與優點如下:
優點是:能夠允許多個對象實列共享原型對象的成員及方法,
缺點是:1. 每個構造函數只有一個原型,因此不直接支持多重繼承;
2. 不能很好地支持多參數或動態參數的父類。在原型繼承階段,用戶還不能決定以
什么參數來實列化構造函數。
四:理解使用類繼承(繼承的更好的方案)
類繼承也叫做構造函數繼承,在子類中執行父類的構造函數;實現原理是:可以將一個構造函數A的方法賦值給另一個構造函數B,然后調用該方法,使構造函數A在構造函數B內部被執行,這時候構造函數B就擁有了構造函數A中的屬性和方法,這就是使用類繼承實現B繼承與A的基本原理;
如下代碼實現demo:
function A(x) { this.x = x; this.say = function(){ return this.x; } } function B(x,y) { this.m = A; // 把構造函數A作為一個普通函數引用給臨時方法m this.m(x); // 執行構造函數A; delete this.m; // 清除臨時方法this.m this.y = y; this.method = function(){ return this.y; } } var a = new A(1); var b = new B(2,3); console.log(a.say()); //輸出1, 執行構造函數A中的say方法 console.log(b.say()); //輸出2, 能執行該方法說明被繼承了A中的方法 console.log(b.method()); // 輸出3, 構造函數也擁有自己的方法
上面的代碼實現了簡單的類繼承的基礎,但是在復雜的編程中是不會使用上面的方法的,因為上面的代碼不夠嚴謹;代碼的耦合性高;我們可以使用更好的方法如下:
function A(x) { this.x = x; } A.prototype.getX = function(){ return this.x; } // 實例化A var a = new A(1); console.log(a.x); // 1 console.log(a.getX()); // 輸出1 // 現在我們來創建構造函數B,讓其B繼承與A,如下代碼: function B(x,y) { this.y = y; A.call(this,x); } B.prototype = new A(); // 原型繼承 console.log(B.prototype.constructor); // 輸出構造函數A,指針指向與構造函數A B.prototype.constructor = B; // 重新設置構造函數,使之指向B console.log(B.prototype.constructor); // 指向構造函數B B.prototype.getY = function(){ return this.y; } var b = new B(1,2); console.log(b.x); // 1 console.log(b.getX()); // 1 console.log(b.getY()); // 2 // 下面是演示對構造函數getX進行重寫的方法如下: B.prototype.getX = function(){ return this.x; } var b2 = new B(10,20); console.log(b2.getX()); // 輸出10
下面我們來分析上面的代碼:
在構造函數B內,使用A.call(this,x);這句代碼的含義是:我們都知道使用call或者apply方法可以改變this指針指向,從而可以實現類的繼承,因此在B構造函數內,把x的參數傳遞給A構造函數,並且繼承於構造函數A中的屬性和方法;
使用這句代碼:B.prototype = new A(); 可以實現原型繼承,也就是B可以繼承A中的原型所有的方法;console.log(B.prototype.constructor); 打印出輸出構造函數A,指針指向與構造函數A;我們明白的是,當定義構造函數時候,其原型對象默認是一個Object類型的一個實例,其構造器默認會被設置為構造函數本身,如果改動構造函數prototype屬性值,使其指向於另一個對象的話,那么新對象就不會擁有原來的constructor的值,比如第一次打印console.log(B.prototype.constructor); 指向於被實例化后的構造函數A,重寫設置B的constructor的屬性值的時候,第二次打印就指向於本身B;因此B繼承與構造A及其原型的所有屬性和方法,當然我們也可以對構造函數B重寫構造函數A中的方法,如上面最后幾句代碼是對構造函數A中的getX方法進行重寫,來實現自己的業務~;
五:建議使用封裝類實現繼承
封裝類實現繼承的基本原理:先定義一個封裝函數extend;該函數有2個參數,Sub代表子類,Sup代表超類;在函數內,先定義一個空函數F, 用來實現功能中轉,先設置F的原型為超類的原型,然后把空函數的實例傳遞給子類的原型,使用一個空函數的好處是:避免直接實例化超類可能會帶來系統性能問題,比如超類的實例很大的話,實例化會占用很多內存;
如下代碼:
function extend(Sub,Sup) { //Sub表示子類,Sup表示超類 // 首先定義一個空函數 var F = function(){}; // 設置空函數的原型為超類的原型 F.prototype = Sup.prototype; // 實例化空函數,並把超類原型引用傳遞給子類 Sub.prototype = new F(); // 重置子類原型的構造器為子類自身 Sub.prototype.constructor = Sub; // 在子類中保存超類的原型,避免子類與超類耦合 Sub.sup = Sup.prototype; if(Sup.prototype.constructor === Object.prototype.constructor) { // 檢測超類原型的構造器是否為原型自身 Sup.prototype.constructor = Sup; } } 測試代碼如下: // 下面我們定義2個類A和類B,我們目的是實現B繼承於A function A(x) { this.x = x; this.getX = function(){ return this.x; } } A.prototype.add = function(){ return this.x + this.x; } A.prototype.mul = function(){ return this.x * this.x; } // 構造函數B function B(x){ A.call(this,x); // 繼承構造函數A中的所有屬性及方法 } extend(B,A); // B繼承於A var b = new B(11); console.log(b.getX()); // 11 console.log(b.add()); // 22 console.log(b.mul()); // 121
注意:在封裝函數中,有這么一句代碼:Sub.sup = Sup.prototype; 我們現在可以來理解下它的含義:
比如在B繼承與A后,我給B函數的原型再定義一個與A相同的原型相同的方法add();
如下代碼
extend(B,A); // B繼承於A var b = new B(11); B.prototype.add = function(){ return this.x + "" + this.x; } console.log(b.add()); // 1111
那么B函數中的add方法會覆蓋A函數中的add方法;因此為了不覆蓋A類中的add()方法,且調用A函數中的add方法;可以如下編寫代碼:
B.prototype.add = function(){ //return this.x + "" + this.x; return B.sup.add.call(this); } console.log(b.add()); // 22
B.sup.add.call(this); 中的B.sup就包含了構造函數A函數的指針,因此包含A函數的所有屬性和方法;因此可以調用A函數中的add方法;
如上是實現繼承的幾種方式,類繼承和原型繼承,但是這些繼承無法繼承DOM對象,也不支持繼承系統靜態對象,靜態方法等;比如Date對象如下:
// 使用類繼承Date對象 function D(){ Date.apply(this,arguments); // 調用Date對象,對其引用,實現繼承 } var d = new D(); console.log(d.toLocaleString()); // [object object]
如上代碼運行打印出object,我們可以看到使用類繼承無法實現系統靜態方法date對象的繼承,因為他不是簡單的函數結構,對聲明,賦值和初始化都進行了封裝,因此無法繼承;
下面我們再來看看使用原型繼承date對象;
function D(){} D.prototype = new D(); var d = new D(); console.log(d.toLocaleString());//[object object]
我們從代碼中看到,使用原型繼承也無法繼承Date靜態方法;但是我們可以如下封裝代碼繼承:
function D(){ var d = new Date(); // 實例化Date對象 d.get = function(){ // 定義本地方法,間接調用Date對象的方法 console.log(d.toLocaleString()); } return d; } var d = new D(); d.get(); // 2015/12/21 上午12:08:38
六:理解使用復制繼承
復制繼承的基本原理是:先設計一個空對象,然后使用for-in循環來遍歷對象的成員,將該對象的成員一個一個復制給新的空對象里面;這樣就實現了復制繼承了;如下代碼:
function A(x,y) { this.x = x; this.y = y; this.add = function(){ return this.x + this.y; } } A.prototype.mul = function(){ return this.x * this.y; } var a = new A(2,3); var obj = {}; for(var i in a) { obj[i] = a[i]; } console.log(obj); // object console.log(obj.x); // 2 console.log(obj.y); // 3 console.log(obj.add()); // 5 console.log(obj.mul()); // 6
如上代碼:先定義一個構造函數A,函數里面有2個屬性x,y,還有一個add方法,該構造函數原型有一個mul方法,首先實列化下A后,再創建一個空對象obj,遍歷對象一個個復制給空對象obj,從上面的打印效果來看,我們可以看到已經實現了復制繼承了;對於復制繼承,我們可以封裝成如下方法來調用:
// 為Function擴展復制繼承方法 Function.prototype.extend = function(o) { for(var i in o) { //把參數對象的成員復制給當前對象的構造函數原型對象 this.constructor.prototype[i] = o[i]; } } // 測試代碼如下: var o = function(){}; o.extend(new A(1,2)); console.log(o.x); // 1 console.log(o.y); // 2 console.log(o.add()); // 3 console.log(o.mul()); // 2
上面封裝的擴展繼承方法中的this對象指向於當前實列化后的對象,而不是指向於構造函數本身,因此要使用原型擴展成員的話,就需要使用constructor屬性來指向它的構造器,然后通過prototype屬性指向構造函數的原型;
復制繼承有如下優點:
1. 它不能繼承系統核心對象的只讀方法和屬性
2. 如果對象數據非常多的話,這樣一個個復制的話,性能是非常低的;
3. 只有對象被實列化后,才能給遍歷對象的成員和屬性,相對來說不夠靈活;
4. 復制繼承只是簡單的賦值,所以如果賦值的對象是引用類型的對象的話,可能會存在一些副作用;如上我們看到有如上一些缺點,下面我們可以使用clone(克隆的方式)來優化下:
基本思路是:為Function擴展一個方法,該方法能夠把參數對象賦值賦值一個空構造函數的原型對象,然后實列化構造函數並返回實列對象,這樣該對象就擁有了該對象的所有成員;代碼如下:
Function.prototype.clone = function(o){ function Temp(){}; Temp.prototype = o; return Temp(); } // 測試代碼如下: Function.clone(new A(1,2)); console.log(o.x); // 1 console.log(o.y); // 2 console.log(o.add()); // 3 console.log(o.mul()); // 2