子类


在面向对象编程中,类 B 可以继承自另一个类 A,我们将 A 称为父类,将 B 称为子类。B 的实例从 A 继承了所有的实例方法,类 B 也可以定义自己的实例方法,有些方法可以重载类 A 中的同名方法。如果 B 的方法重载了 A 的方法,B 中的重载方法可能会调用 A 中的重载方法,这种做法称为「方法链」,同样,子类的构造函数有时需要调用父类的构造函数,这种做法称之为「构造函数链」。子类还可以有子类,当涉及类的层次结构时,往往需要定义抽象类。抽象类中定义的方法没有实现,需要在抽象类的具体子类中实现。

定义子类

我们可以这样来定义类的继承:

B.prototype = inherit(A.prototype);
B.prototype.constructor = B;

这两行代码是 JavaScript 中创建子类的关键,如果不这样做,原型对象只是继承自 Object.prototype 的普通对象,这意味着你的类和所有类一样都是 Object 的子类。我们将上面两行代码添加到 defineClass() 形成定义子类的方法:

/**
 * 一个用以定义简单类的函数
 * @param superclass 父类构造函数
 * @param constructor 子类构造函数
 * @param methods 实例方法,会被复制到原型对象中
 * @param statics 类属性,会被复制到构造函数中
 * @returns constructor
 */
function defineSubClass(superclass, constructor, methods, statics) {
    constructor.prototype = inherit(superclass.prototype);
    constructor.prototype.constructor = constructor;
    if (methods)
        extend(constructor.prototype, methods);
    if (statics)
        extend(constructor, statics);
    return constructor;
}
    
// 也可以通过父类构造函数的方法来做到这一点
Function.prototype.extend = function (constructor, methods, statics) {
    return defineSubClass(this, constructor, methods, statics);
};

当然,我们还可以不使用构造函数手动来实现子类,下面我们就来定义 Set 类的子类 SingletonSetSingletonSet 是一个特殊的集合,是只读的,而且含有单独的常量成员:

// 定义构造函数
function SingletonSet(member) {
    this.member = member;   // 集合中的唯一成员
}
    
// SingletonSet 的原型对象继承自 Set 的原型对象
SingletonSet.prototype = inherit(Set.prototype);
    
// 给原型添加属性
// 如果属性同名则覆盖 Set.prototype 中的对应属性
extend(SingletonSet.prototype, {
    constructor: SingletonSet,
    add: function () {
        throw "Read-only Set";
    },
    remove: function () {
        throw "Read-only Set";
    },
    // SingletonSet 集合实例中永远只有一个元素
    size: function () {
        return 1;
    },
    // 这个方法只会调用一次,传入集合中的唯一成员
    foreach: function (f, context) {
        f.call(context, this.member);
    },
    // 只需检查传入的值是否匹配集合中的唯一成员即可
    contains: function (x) {
        return x === this.member;
    }
});

我们来测试下上述代码:

除了在子类中重载的5个方法外,SingletonSet 还从父类中继承了 toString()toArray()equals() 方法:

当然,我们也可以对 equals() 方法进行重写:

SingletonSet.prototype.equals = function (that) {
    return that instanceof Set && that.size() == 1 && that.contains(this.member);
};

需要注意的是,SingletonSet 不是将 Set 类中的方法静态地借用过来,而是动态地继承方法。如果给 Set.prototype 添加新方法,SetSingletonSet 的所有实例就会立即拥有这个方法(除非 SingletonSet 已经定义了同名方法)。

构造函数和方法链

当定义子类时,我们更希望对父类的行为进行修改或扩充,而不是完全替换,为了实现这个目的,构造函数和子类的方法需要调用或链接到父类构造函数和父类方法。为此,我们再定义一个 Set 的子类 NoNullSet,它不允许 nullundefined 作为它的成员,所以需要在 add() 方法中对新增元素做校验,但是我们不需要完全定义一个新的 add() 方法,同理,构造函数也是如此:

function NoNullSet() {
    // 调用父类的构造函数对子类进行初始化
    Set.apply(this, arguments);
}
    
// 定义 NoNullSet 继承自 Set
NoNullSet.prototype = inherit(Set.prototype);
NoNullSet.prototype.constructor = NoNullSet;
    
// 重写 add 方法
NoNullSet.prototype.add = function () {
    // 对传入参数做校验,排除 null 和 undefined
    for (var i = 0; i < arguments.length; i++) {
        if (arguments[i] == null)
            throw new Error("Can't add null or undefined to a NoNullSet");
    }
    // 调用父类 add() 方法以执行实际插入操作
    return Set.prototype.add.apply(this, arguments);
};

对上述代码进行测试以检测其是否能正常工作:

对这个示例进一步推而广之,可以定义出各种类型的过滤集合,比如字符串集合、对象集合、函数集合等。为此,我们可以定义一个工厂方法,传入一个过滤函数,返回一个新的 Set 子类,我们将这个方法命名为 filteredSetSubClass

/**
 * 这个工厂方法返回具体 Set 类的子类
 * 需要重写 add() 方法对添加的元素做特殊的过滤
 * @param superclass
 * @param filter
 * @returns {constructor}
 */
function filteredSetSubClass(superclass, filter) {
    var constructor = function () {   // 子类构造函数
        superclass.apply(this, arguments);  // 调用父类构造函数
    };
    var proto = constructor.prototype = inherit(superclass.prototype);
    proto.constructor = constructor;
    proto.add = function () {
        // 对所有新增元素调用 filter() 函数进行过滤
        for (var i = 0; i < arguments.length; i++) {
            var v = arguments[i];
            if (!filter(v))
                throw ("value " + v + " rejected by filter");
        }
        // 调用父类的 add() 方法
        superclass.prototype.add.apply(this, arguments);
    };
    return constructor;
}

我们可以调用这个新定义的工厂方法来创建字符串集合:

这种类工厂的能力是 JavaScript 语言动态特性的一个体现,类工厂是一种非常强大和有用的特性。

组合 vs 子类

在前一节中,定义的集合可以根据特定的标准对集合成员做限制,而且使用了子类的技术来实现。父类和过滤函数的每个组合都要创建一个新的类来实现。

除了子类之外,还有一种更好的方法来完成这种需求,即面向对象中一种广为人知的设计原则:「组合优于继承」(这句话出自《设计模式》一书)。我们可以通过组合的原理定义一个新的集合实现,还是以一个具体的例子来演示:

var FilteredSet = Set.extend(
    function filteredSet(set, filter) {  // 构造函数
        this.set = set;
        this.filter = filter;
    },
    {  // 实例方法
        add: function () {
            // 如果有过滤器的话
            if (this.filter) {
                for (var i = 0; i < arguments.length; i++) {
                    var v = arguments[i];
                    if (!this.filter(v)) {
                        throw new Error("FilteredSet: value " + v + " rejected by filter");
                    }
                }
            }
            // 调用 set 中的 add() 方法
            this.set.add.apply(this.set, arguments);
            return this;
        },
        // 剩下的方法都保持不变
        remove: function () {
            this.set.remove.apply(this.set, arguments);
            return this;
        },
        contains: function (v) {
            return this.set.contains(v);
        },
        size: function () {
            return this.set.size();
        },
        foreach: function (f, c) {
            this.set.foreach(f, c);
        }
    }
);

在这个例子中,使用组合的好处是只需要定义一个子类 FilteredSet 即可,可以通过这个子类来创建任何带有成员限制的集合实例。比如 NoNullSet 对象实例可以这样创建:

var noNullSet = new FilteredSet(new Set(), function (x) {
    return x != null;
});

`StringSet` 对象实例可以这样创建:

var stringSet = new FilteredSet(new Set(), function (x) {
    return typeof x == 'string';
});

新的组合类 FilteredSet 中包装了集合类 Set,对组合实例方法的调用实际上是对包装的集合对象方法的调用。

类的层次结构和抽象类

本节我们将引入抽象类,抽象类和接口一样,都是从实现中抽离出接口,从而使类的层次结构更加清晰,同时提高系统的可扩展性和可维护性。

下面我们将以一个具体实例的方式来演示抽象类的使用,AbstractSet 类只定义了一个抽象方法 contains(),然后定义 AbstractSet 的子类 AbstractEnumerableSet,这个类增加了抽象的 size()foreach() 方法,但并没有定义 add()remove() 方法,代表只读集合,SingletonSet 是继承自它的非抽象子类,最后定义了 AbstractEnumerableSet 的子类 AbstractWritableSet,这个抽象类定义了 add()remove() 方法,我们定义了 ArraySet 子类继承自它:

/**
 * 一个用以定义简单子类的函数
 * @param superclass 父类构造函数
 * @param constructor 子类构造函数
 * @param methods 实例方法,会被复制到原型对象中
 * @param statics 类属性,会被复制到构造函数中
 * @returns constructor
 */
function defineSubClass(superclass, constructor, methods, statics) {
    constructor.prototype = inherit(superclass.prototype);
    constructor.prototype.constructor = constructor;
    if (methods)
        extend(constructor.prototype, methods);
    if (statics)
        extend(constructor, statics);
    return constructor;
}
    
// 把 p 中的可枚举属性复制到 o 中,并返回 o
function extend(o, p) {
    for (prop in p) {
        o[prop] = p[prop];
    }
    return o;
}
    
// 通过该方法定义继承自原型p的新对象
function inherit(p) {
    if (p == null) throw TypeError();
    if (Object.create)
        return Object.create(p);
    var t = typeof p;
    if (t !== 'function' && t !== 'object') throw TypeError();
    function f() {};
    f.prototype = p;
    return new f();
}
    
// 在父类上调用 extend() 方法来定义子类,下面会用到这种方式来定义子类
Function.prototype.extend = function (constructor, methods, statics) {
    return defineSubClass(this, constructor, methods, statics);
};
    
// 这个函数可以用作任何抽象方法
function abstractmethod() {
    // 抽象方法不能直接调用
    throw new Error("abstract method");
}
    
// 抽象类 AbstractSet 只定义了一个抽象方法 contains
function AbstractSet() {
    // 抽象类不能实例化
    throw new Error("Can't instantiate abstract classes");
}
AbstractSet.prototype.contains = abstractmethod;
    
/**
 * NotSet 是 AbstractSet 的非抽象子类
 * 通过 Function.prototype.extend 的方式来定义子类(下同)
 */
var NotSet = AbstractSet.extend(
    function NotSet(set) {
        this.set = set;
    },
    {
        contains: function (x) {
            return !this.set.contains(x);
        },
        toString: function () {
            return "~" + this.set.toString();
        },
        equals: function (that) {
            return that instanceof NotSet && this.set.equals(that.set);
        }
    }
);
    
/**
 * AbstractEnumerableSet 是 AbstractSet 的抽象子类
 * 定义了两个抽象方法 size() 和 foreach()
 * 以及一些非抽象方法
 */
var AbstractEnumerableSet = AbstractSet.extend(
    function () {
        throw new Error("Can't instantiate abstract classes.");
    },
    {
        size: abstractmethod,
        foreach: abstractmethod,
        isEmpty: function () {
            return this.size() === 0;
        },
        toString: function () {
            var s = "{", i = 0;
            this.foreach(function (v) {
                if (i++ > 0)
                    s += ", ";
                s += v;
            });
            return s + "}";
        },
        toLocaleString: function () {
            var s = "{", i = 0;
            this.foreach(function (v) {
                if (i++ > 0)
                    s += ", ";
                if (v == null)
                    s += v;
                else
                    s += v.toLocaleString();
            });
            return s + "}";
        },
        toArray: function () {
            var a = [];
            this.foreach(function (v) {
               a.push(v);
            });
            return a;
        },
        equals: function (that) {
            if (!that instanceof AbstractEnumerableSet)
                return false;
            if (this.size() !== that.size())
                return false;
            try {
                this.foreach(function (v) {
                    if (!that.contains(v))
                        throw false;
                });
            } catch (x) {
                if (x === false)
                    return false;
                throw x;
            }
            return true;
        }
    }
);
    
/**
 * SingletonSet 是 AbstractEnumerableSet 的非抽象子类
 * 它是只读的,只包含一个属性
 */
var SingletonSet = AbstractEnumerableSet.extend(
    function SingletonSet(member) {
        this.member = member;
    },
    {
        contains: function (x) {
            return x === this.member;
        },
        size: function () {
            return 1;
        },
        foreach: function (f, ctx) {
            f.call(ctx, this.member);
        }
    }
);
    
/**
 * AbstractWritableSet 是 AbstractEnumerableSet 的抽象子集
 * 定义了抽象方法 add() 和 remove()
 * 以及计算集合并集、交集、差集的非抽象方法
 */
var AbstractWritableSet = AbstractEnumerableSet.extend(
    function () {
        throw new Error("Can't instantiate abstract classes");
    },
    {
        add: abstractmethod,
        remove: abstractmethod,
        union: function (that) {  // 计算两个集合的并集
            var self = this;
            that.foreach(function (v) {
                self.add(v);
            });
            return this;
        },
        intersection: function (that) {  // 计算两个集合的交集
            var self = this;
            this.foreach(function (v) {
                if (!that.contains(v))
                    self.remove(v);
            });
            return this;
        },
        difference: function (that) {  // 计算两个集合的差集
            var self = this;
            that.foreach(function (v) {
                if (that.contains(v))
                    self.remove(v);
            });
            return this;
        }
    }
);
    
/**
 * ArraySet 是 AbstractWritableSet 的非抽象子类
 * 它以数组的形式表示集合中的元素
 * 我们以数组提供的方法和属性实现 contains、size 和 foreach 方法,这样效率更高
 */
var ArraySet = AbstractWritableSet.extend(
    function ArraySet() {
        this.values = [];
        this.add.apply(this, arguments);
    },
    {
        contains: function (v) {
            return this.values.indexOf(v) !== -1;
        },
        size: function () {
            return this.values.length;
        },
        foreach: function (f, ctx) {
            this.values.forEach(f, ctx);
        },
        add: function () {
            for (var i = 0; i < arguments.length; i++) {
                var arg = arguments[i];
                if (!this.contains(arg))
                    this.values.push(arg);
            }
            return this;
        },
        remove: function () {
            for (var i = 0; i < arguments.length; i++) {
                var p = this.values.indexOf(arguments[i]);
                if (p === -1)
                    continue;
                this.values.splice(p, 1);
            }
            return this;
        }
    }
);

下面我们来测试下上述代码:


点赞 取消点赞 收藏 取消收藏

<< 上一篇: JavaScript 中的面向对象技术

>> 下一篇: ECMAScript 5 中的类