JavaScript 語言核心(18)模擬類別的封裝與繼承



JavaScript 是個基於原型(Prototype-based)的語言,不少來自基於類別(Class-based)語言的開發者,會因為不習慣或者是認為以基於類別風格來撰寫或管理程式較易維護等理由,在 JavaScript 中試著模擬出各種類別風格,這沒什麼,純綷就是先前各篇文章的觀念加以綜合運用,端看你想要何種風格罷了。

模擬類別封裝

在〈隱藏諸多細節的建構式〉中可以看到,將 JavaScript 的函式搭配 new 關鍵字作為建構式,風格上其實有點像在定義類別:
function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ',' + this.age + ']';
};

var p = new Person('Justin', 35);
console.log(p.name); // Justin
如果想追求私有(private)成員的概念,可以搭配 Closure 來達成:
function Person(name, age) {
    this.getName = function() {
        return name;
    };
    this.getAge = function() {
        return age;
    };
    this.toString = function() {
        return '[' + name + ',' + age + ']';
    }
}

var p = new Person('Justin', 35);
console.log(p.getName()); // Justin
如果想進一步像〈ECMAScrpt 5 物件與特性操作〉中另一種封裝風格,可以如下:
function privateIt(obj, prop, value) {
    Object.defineProperty(obj, prop, {
        value        : value,
        writable   : true,
        enumerable : false,
    });
}

function getter(obj, prop) {
    Object.defineProperty(obj, prop, {
        get        : function() { return this['__' + prop + '__']; }
    });
}

function Person(name, age) {
    privateIt(this, '__name__', name);
    privateIt(this, '__age__', age);
    getter(this, 'name');
    getter(this, 'age');
}

var p = new Person('Justin', 35);

console.log(p.name);

模擬類別繼承

基於你選擇何種方式來模擬類別,會影響你如何模擬類別的繼承,例如,若以上面看到的第一種風格(也是最常見的風格),那麼繼承的實作可以基於原型鏈。例如:
function Circle(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
}

Circle.PI = 3.14159; // 相當於Java類別的靜態方法

Circle.prototype.area = function() {
    return this.r * this.r * Circle.PI;
};

Circle.prototype.toString = function() {
    var text = [];
    for(var p in this) {
        if(typeof this[p] != 'function') {
            text.push(p + ':' + this[p]);
        }
    }
    return '[' + text.join() + ']';
};

function Cylinder(x, y, r, h) {
    Circle.call(this, x, y, r); // 呼叫父建構式
    this.h = h;
}

// 原型繼承
Cylinder.prototype = new Circle();

// 設定原型物件之constructor為目前建構式
Cylinder.prototype.constructor = Cylinder;

// 以下在 new 時會再建構,不需要留在原型物件上
delete Cylinder.prototype.x;
delete Cylinder.prototype.y;
delete Cylinder.prototype.r;

// 共用的物件方法設定在 prototype 上
Cylinder.prototype.volumn = function() {
    return this.area() * this.h;
};
如果你想讓外觀上看起來更像是類別定義。可以將以上的流程封起來。例如,將建立類別的流程封裝起來:
var Class = {};
Class.create = function(methods) {
    var Clz = methods.initialize;
    for(var mth in methods) {
        if(mth != 'initialize') {
            Clz.prototype[mth] = methods[mth];
        }
    }
    return Clz;
};
那麼你就可以運用以下風格來模擬類別定義:
var Circle = Class.create({
    initialize : function(x, y, r) { // 作為建構式
        this.x = x;
        this.y = y;
        this.r = r;
    },
    area : function() {
        return Math.PI * Math.pow(this.r, 2);
    },
    toString : function() {
        var text = [];
        for(var p in this) {
            if(typeof this[p] != 'function') {
                text.push(p + ':' + this[p]);
            }
        }
        return '[' + text.join() + ']';
    }
});

var circle = new Circle(10, 10, 5);
搭配以上風格,如果想進一步封裝類別的繼承,則可以這麼作:
Class.extend = function(Superclz, methods) {
    var Subclz = this.create(methods);
    var subproto = Subclz.prototype;
    Subclz.prototype = new Superclz();
    for(var p in Subclz.prototype) {
        if(Subclz.prototype.hasOwnProperty(p)) {
            delete Subclz.prototype[p];
        }
    }
    Subclz.prototype.constructor = Subclz;
    for(var p in subproto) {
        Subclz.prototype[p] = subproto[p];
    }
    return Subclz;
};
例如,想繼承先前建立的 Circle,則可以如下:
var Cylinder = Class.extend(Circle, {
    initialize : function(x, y, r, h) {
        Circle.call(this, x, y, r);
        this.h = h;
    },
    volumn : function() {
        return this.area() * this.h;
    }
});

var cylinder = new Cylinder(10, 10, 5, 15);
以上僅是模擬類別的封裝與繼承的概念,至於想要模擬到什麼程度,或者是想達到什麼樣的風格,其實有各種的設計方式。
附帶一提的是,ECMAScript 6 支援類別風格的語法,雖然是語法蜜糖,然而對類別風格的撰寫提供了一種標準作法,如果有考慮到將來遷移至 ECMAScript 6 時採用支援類別的語法,可以先瞭解一下那些語法,大致上對應至現階段何種模擬類別封裝與繼承的風格,有關於 ECMAScript 6 類別風格的語法支援,可參考 初探 ES6(4)極簡的 Classes