深入理解Javascript面向對象編程


深入理解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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM