深刻理解Javascript面向對象編程

深刻理解Javascript面向對象編程javascript

閱讀目錄java

一:理解構造函數原型(prototype)機制編程

    prototype是javascript實現與管理繼承的一種機制,也是面向對象的設計思想.構造函數的原型存儲着引用對象的一個指針,該指針指向與一個原型對象,對象內部存儲着函數的原始屬性和方法;咱們能夠藉助prototype屬性,能夠訪問原型內部的屬性和方法。數組

   當構造函數被實列化後,全部的實例對象均可以訪問構造函數的原型成員,若是在原型中聲明一個成員,全部的實列方法均可以共享它,好比以下代碼:app

// 構造函數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的屬性值爲11this

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
相關文章
相關標籤/搜索