举例说明如何在JavaScript中使用类工厂和方法链

作者:admin     字体:[增加 减小]    类型:原创
用一个函数将创建子类的代码包装起来,这样就可以在构造函数和方法链中使用父类的参数,而不是通过写死某个父类的名字来使用它的参数。也就是说如果想修改父类,只须修改一处代码即可,而不必对每个用到父类名的地方都做修改。这就是使用类工厂的好处。

在文章《JavaScript中如何在子类中调用父类的构造函数和方法》中我们知道了如何使用构造函数和方法来对实例集合进行过滤,在这个集合中的成员必须首先传入一个过滤函数再执行添加操作。为此,定义一个类工厂函数(类似JavaScript的自定义函数生成枚举数据类型中的enumeration函数),传入一个过滤函数,返回一个新的Set子类。实际上,可以对此做进一步的通用化的处理,定义一个可以接收两个参数的类工厂:子类和用于add()方法的过滤函数。这个工厂方法称为filteredSubClass(),并通过这样的代码来使用它:

//通过原型继承创建一个新对象
//inherit() 返回一个继承自原型对象p的属性的新对象
//这里使用ECMASscript 5中的object.create()函数(如果存在的话)
//如果不存在,则使用其他方法
function inherit(p) {
    if ( p==null ) throw TypeError();   //p是一个对象,但不能是null
    if ( Object.create )        //如果Object.create()存在
        return Object.create(p);    //直接使用它
    var t = typeof p;       //否则进一步检测
    if ( t!=='object' && t!=='function' ) throw TypeError();
    function f(){};         //定义一个空构造函数
    f.prototype = p;        //将其原型属性设置为p
    return new f();         //使用f()创建p的继承对象
}

//集合类
//集合(set)是一种数据结构,用以表示非重复值的无序集合
function Set( ){
    this.values = {};
    this.n = 0;
    this.add.apply(this,arguments);
}

//将每个参数都添加至集合中
Set.prototype.add = function() {
    for ( var i=0;i<arguments.length;i++ ) {    //遍历每个参数
        var val = arguments[i];     //待添加到集合的值
        var str = Set._v2s(val);    //把它转换为字符串
        if ( !this.values.hasOwnProperty(str) ) {   //如果不在集合中
            this.values[str] = val; //将字符串和值对应起来
            this.n++;   //集合中值的计数加一
        }
    }
    return this;    //支持链式方法调用
}

//从集合删除元素,这些元素由参数指定
Set.prototype.remove = function() {
    var self = this;
    Array.prototype.map.call(arguments,function(e){ //遍历每个参数
        var str = Set._v2s(e);  //将字符串和值对应起来    
        if( self.values.hasOwnProperty(str) ) { //如果它在集合中
            delete self.values[str];        //删除它
            this.n--;   //集合中值的计数减一
        }
    });
    return this;    //支持链式方法调用
}


//如果集合包含这个值,则返回true,否则,返回false
Set.prototype.contains = function(value) {
    return this.values.hasOwnProperty(Set._v2s(value));
}

//返回集合的大小
Set.prototype.size = function() {
    return this.n;
}

//遍历集合中的所有幸免于难,在指定的上下文中调用f
Set.prototype.foreach = function(f,context) {
    for( var s in this.values ) {   //遍历集合中的所有键值
        if (this.values.hasOwnProperty(s))  //忽略继承的属性
            f.call(context,this.values[s]); //调用f,传入value
    }
}

//这是一个内部函数,用以将任意JavaScript值和唯一的字符串对应起来
Set._v2s = function( val ) {
    var self = this;
    switch( val ) {
        case undefined : return 'u';    //特殊的原始值
        case null:  return 'n';         //值只有一个字母
        case true:  return 't';
        case false: return 'f';
        default: switch( typeof val ) {
            case 'number': return '#' + val;    //数字都带有#前缀
            case 'string': return '"' + val;    //字符串都带有"前缀
            default: return '@' + objectId(val);    //Objs and funcs get @
        }   
    }

    function objectId(o) {
        var prop = "|**objectid**|";    //私有属性,用以存放id
        if (!o.hasOwnProperty(prop) ) { //如果对象没有id
            o[prop] = self._v2s.next++; //将下一个值赋给它
        }
        return o[prop];     //返回这个id
    }
}

/*
这个函数返回具体 Set 类的子类
并重写该类的 add() 方法用以对添加的元素做特殊的过滤
*/
function filteredSetSubClass(superClass,filter){
    var constructor = function(){   //子类构造函数
        superClass.apply(this,arguments);   //调用父类构造函数
    }

    var proto = constructor.prototype = inherit(superClass);
    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");
        }
        superClass.prototype.add.apply(this,arguments);
    }
    return constructor;
}

var StringSet = filteredSetSubClass(Set,function(x){return typeof x === 'string'});

//只包含字符串时运行正常
var s = new StringSet('a', 'b','c');
console.log(s);

//如果包含非字符串,则出错
var t = new StringSet(1,2,3,'a');   //Uncaught value 1 rejected by filter
//console.log(t);

上面的代码有一个有趣的事情是,用一个函数将创建子类的代码包装起来,这样就可以在构造函数和方法链中使用父类的参数,而不是通过写死某个父类的名字来使用它的参数。也就是说如果想修改父类,只须修改一处代码即可,而不必对每个用到父类名的地方都做修改。