跳到主要內容

JavaScript 物件繼承機制

這篇文章延續Javascript物件導向一文
更新後的內容較多就獨立出一篇文章

JavaScript的物件繼承,在現在前端的開發日趨複雜的影響下有了解的必要
實際上在ECMA6規範裡也出現了 "Class" 的使用,以後也會成為JavaScript重要得一部分吧

最簡單的繼承實現方式是透過函式的 call() 或 apply() 將子類別作為this綁在父類別上
子類別就能擁有父類別的內部成員
function Parent(name) {
    this.name = name;
    this.speak = function () {
        console.log(this.name);
    }
}

function Child(name) {
    Parent.call(this, name);
    this.act = "cry";
    this.speak2 = function () {
        console.log(this.act);
    }
}

var child = new Child("test");

這個方法的缺點是沒辦法繼承父類別的prototype


直接將子類別的prototype指向父類別是最常在一般教學中看到的方法
作法1是指向一個父類別建立的新物件
function Parent(name) {
    this.name = name;
}
Parent.prototype.speak = function () {
    console.log(this.name);
}

function Child() {
    this.act = "cry";
    this.speak2 = function () {
        console.log(this.act);
    }
}

Child.prototype = new Parent("James");
Child.prototype.constructor = Child;
var c = new Child();

作法2為略做修改
Child.prototype = Parent.prototype;  替換掉 Child.prototype = new Parent();

作法2因為不用再建立新物件,所以效率會比作法1來的好些
但也因為兩者的prototype相同,所以對子類別的prototype做的變更也會影響父類別

這種直接指定prototype的方法的缺點就是子類別原本的prototyp會被清空
而且要注意重新指定prototype後,Child.prototype.constructor也會指向Parent
要重新指定constructor為原本建構式才能維持正確的類別結構


綜合上述做法,做出以下的改進
function extend(Child, Parent) {    
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;  
}

function Parent(name) {
    this.name = name;
}
Parent.prototype.speak = function () {
    console.log(this.name);
}

function Child(name) {
    Parent.call(this, name);
    this.act = "cry";
}

extend(Child, Parent);

Child.prototype.speak2 = function () {
    console.log(this.act);
}

extend()為上面的指定prototype的作法2的封裝改進版
因為F()本身是空類別,建立起來也不耗什麼資源
因為多了一個中介類別,所以修改prototype就不會影響父類別的prototype
而父類別的prototype上的方法也能藉由原型鏈的方式取得
實際上現在JavaScript ECMA5的Object.create()的實現方式也是如此,最後的示範會再提到它

而子類別也透過父類別的 call() 繼承了父類別的內部成員
如此就完整的繼承了內部成員及prototype


另一種繼承的思維,所謂的繼承其實就是完整複製整個父類別
function extend2(p, c) { 
    for (var i in p) {    
        if (typeof p[i] === 'object') {        
            c[i] = (p[i].constructor === Array) ? [] : {};        
            extend2(p[i], c[i]);      
        } else {         
            c[i] = p[i];      
        }
    }  
}

function Parent(name) {
    this.name = name;
}
Parent.prototype.speak = function () {
    console.log(this.name);
};

function Child(name) {
    Parent.call(this, name);
    this.act = "cry";
}
Child.prototype.speak2 = function () {
    console.log(this.act);
};

extend2(Parent.prototype, Child.prototype);


我滿喜歡這種做法,利用遞迴的方式複製完整屬性
而且也因為這種作法不是重新指定prototype,所以能做到多重繼承
jQuery的作法也是如此

最後示範一個我使用的結構,實作Design Pattern的Observer Pattern
if (!Object.create) {
    Object.create = function (o) {
        if (arguments.length > 1) {
            throw new Error('Object.create implementation only accepts the first parameter.');
        }
        function F() {}
        F.prototype = o;
        return new F();
    };
}

function publisher(obj) {
    this.obj = obj;
    this.subscribers = [];
}
publisher.prototype.publish = function (data) {
    for (var i = 0; i < this.subscribers.length; i++) {
        this.subscribers[i].update(data);
    }
};

function tracker(obj) {
    this.obj = obj;
}
tracker.prototype.subscribe = function (publisher) {
    publisher.subscribers.push(this);
};
tracker.prototype.unsubscribe = function (publisher) {
    for (var i = 0; i < publisher.subscribers.length; i++) {
        if (publisher.subscribers[i] === this) {
            publisher.subscribers.splice(i, 1);
            break;
        }
    }
};

//Inheritance implement
function ltPulisher(obj) {
    publisher.call(this, obj);
}
ltPulisher.prototype = Object.create(publisher.prototype);
ltPulisher.prototype.init = function () {
    var t = this;
    this.obj.onchange = function (e) { ... };
};

function ltTracker(obj) {
    tracker.call(this, obj);
}
ltTracker.prototype = Object.create(tracker.prototype);
ltTracker.prototype.update = function (data) { ... };

在較老舊的瀏覽器會套用MDN建議的Object.create實作
從內容可以知道它類似前面提到的extend()
不過這個示範選擇它的關係是因為這個例子不需要多重繼承
且使用瀏覽器的內建函式效率一定會比自訂函式來的快

留言

  1. 您好!
    「而且要注意重新指定prototype後,Child.prototype.constructor也會指向Parent」
    請問為什麼 Child.prototype.constructor 會指向 Parent 呢?
    謝謝您!

    回覆刪除
    回覆
    1. 因為 prototype.constructor 會指到原本類別的 constructor

      刪除

張貼留言