模拟类的封装与继承


在 ECMAScript 6 出现之前,JavaScript 在语法层面上,是个基于原型(Prototype-based)的语言,不少来自基于类(Class-based)语言的开发者,会因为不习惯或者是认为以基于类风格来编写或管理程序较易维护等理由,在 JavaScript 中试着模拟出各种类风格,这没什么,纯綷就是先前各篇文章的观念加以综合运用,端看你想要何种风格罢了。

模拟类封装

在〈隐藏诸多细节的构造函数〉中可以看到,将 JavaScript 的函数搭配new关键字作为构造函数,风格上其实有点像在定义类:

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

如果想进一步像〈对象特性 API〉中另一种封装风格,可以如下:

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 支持类风格的语法,虽然是语法蜜糖,然而对类风格的编写提供了一种标准作法,这在之后的文件中会谈到。


展开阅读全文