每天敲点犀牛书:子类

作者: 刘尐六 | 来源:发表于2016-10-25 22:44 被阅读0次

    一、起因

    最近听一个前端的朋友说红宝书上的代码敲一遍,前端的工作随便挑(当然是针对应届毕业生来说的,社招不会这么简单)。不管这句话正确与否,但是我相信敲代码肯定是一个不错的学习方式。由于目前正在看犀牛书,所以决定开始敲犀牛书上的代码。
    今天就从9.7节的子类开始(因为我刚好看到这里了),犀牛书前几章的代码等以后有时间再补,犀牛书看完还要再回头敲一遍红宝书(任重而道远啊啊啊)。

    二、子类

    JavaScript中创建子类的关键在于,采用合适的方法对原型对象进行初始化。

    定义子类

    //inherit函数的定义参看前面的章节
    B.prototype = inherit(A.prototype);  //子类派生自父类
    B.prototype.constructor = B;  //重载继承来的constructor属性
    

    这两行代码是在JavaScript中创建子类的关键。如果不这样做,原型对象仅仅是一个普通对象,它只继承自Object.prototype,这意味着你的类和所有的类一样是Object的子类。如下代码使用了这两行代码:

    //用一个简单的函数创建简单的子类
    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 defineSunclass(this, constructor, methods, statics);
    }
    

    如果不使用defineSubclass()函数,如何“手工”实现子类呢?以前面章节的Set类为例,这里定义它的一个简单子类SingletonSet。

    //构造函数
    function SingletonSet(member) {
        this.member = member;  //记住集合中这个唯一的成员
    }
    
    //创建一个原型对象,这个原型对象继承自Set的原型
    SingletonSet.prototype = inherit(Set.prototype);
    
    //给原型添加属性
    //如果有同名的属性就覆盖Set.prototype中的同名属性
    extend(SingletonSet.prototype, { 
        constructor: SingletonSet,  //设置合适的constructor属性      
        //这个集合是只读的:调用add()和remove()都会报错
        add: function() {throw "read-only set";},  
        remove: function() {throw "read-only set";},
        size: function() {return 1 ;} //singletonSet的实例中只有一个元素
        //这个方法只调用一次,传入这个集合的唯一成员
        foreach: function(f, context) {f.call(context, this.member);},
        //contains()方法非常简单,只需检查传入的值是否匹配这个集合唯一的成员即可
        contains(): function(x) {return x === this.member;}
    });
    

    这是一个简单实现,包含5个方法定义,也从它的父类Set中继承了toString(), toArray()和equals()方法。当然也可以给SingletonSet定义自己的equals()方法:

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

    SingletonSet动态地从Set类继承方法,如果给Set.prototype添加新方法,Set和SingletonSet的所有实例就会立即拥有这个方法。

    构造函数和方法链

    定义子类时,我们希望对父类的行为进行修改或扩充,而不是完全替换它们,为此,构造函数和子类的方法需要调用或链接到父类构造函数和父类方法。
    下面是一个在子类中调用父类的构造函数和方法的例子:

    //NonNullSet是Set的子类,它的成员不能是null和undefined
    function NonNullSet() {
        //仅链接到父类
        //作为普通函数调用父类的构造函数来初始化通过该构造函数调用创建的对象
        Set.apply(this, arguments);
    }
    
    //将NonNullSet设置为Set的子类
    NonNullSet.prototype = inherit(Set.prototype);
    NonNullSet.prototype.constructor = NonNullSet;
    
    //为了将null和undefined排除在外, 只需重写add()方法
    NonNullSet.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 undefind to a NonNullSet");
           
        //调用父类的add()方法以执行实际插入操作
        return Set.prototype.add.apply(this, arguments);
    };
    

    这个NonNullSet其实就是一个“过滤后的集合”,这个集合中的成员必须先传入一个过滤函数然后再执行添加操作。为此,定义一个类工厂函数如下:

    //定义一个只能保存字符串的“集合”类
    var StringSet = filteredSetSubclass(Set, function(x) {
        return typeof x === "string"; });
    
    //这个集合类的成员不能是null, undefined或函数
    var MySet = filteredSubSetclass(NonNullSet, function(x) {
        return typeof x !== "function";});
    

    这个类工厂函数filteredSubSetclass()的实现代码如下:

    /* 
     * 这个函数返回具体Set类的子类
     * 并重写该类的add()方法用以对添加的元素做特殊的过滤
     */
    function filteredSetSubclass(sperclass, filter) {
        var constructor = function() {    //子类构造函数
            superclass.apply(this, arguments);    //调用父类构造函数
        }
        var proto = contructor.prototype = inherit(superclass.prototype);
        proto.constructor = constructor;
        proto.add = function() {
            //在添加任何成员之前先使用过滤器过滤所有参数
            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;
    }
    

    如上,用一个函数将创建子类的代码包装起来,这样就可以在构造函数和方法链中使用父类的参数。如果想修改父类,只需修改一处代码即可。
    通过包装函数和extend()方法重写NonNullSet:

    var NonNullSet = (function() {  //定义并立即调用这个函数
        var superclass = Set;  //仅指定父类
        return superclass.extend(
            function() {superclass.apply(this, arguments);}, //构造函数
            {                                               //方法
                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");
                    
                     //调用父类的add()方法
                    return superclass.prototype.add.apply(this, arguments);
                }
            });
    }());
    

    最后,值得强调的是,这种创建类工厂的能力是JavaScript语言动态特性的一个体现,类工厂是一种非常强大和有用的特性,这在Java和C++等中是没有的。


    今天就先敲这么多吧,子类还有一部分内容明天再写。

    相关文章

      网友评论

        本文标题:每天敲点犀牛书:子类

        本文链接:https://www.haomeiwen.com/subject/wftduttx.html