工厂函数和构造函数在定义类时的比较

作者:admin     字体:[增加 减小]    类型:原创
在定义类时,可以通过工厂函数与构造函数两种方法。一般来说,我们使用构造函数来定义类,并通过new关键字来调用类。但是工厂函数也需要了解一下,有时也会用到它的设计思路。

在JavaScript中,类的实现是基于其原型继承机制的,如果两个实例都从同一个原型对象上继承了属性,我们说它们是同一个类的实例。可见,原型对象是类的核心。

通过工厂函数来定义类

//通过原型继承创建一个新对象
//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的继承对象
}

//这个工厂方法返回一个新的“范围对象”
function range(from,to){
    //使用inherit()函数来创建对象,这个对象继承自下面定义的原型对象
    //原型对象作为函数的一个属性存储,并定义所有“范围对象”所共享的方法(行为)
    var r = inherit(range.methods);

    //存储新的“范围对象”的起始位置和结束位置(状态)
    //这两个属性是不可继承的,每个对象都拥有唯一的属性
    r.from = from;
    r.to = to;

    //返回这个新创建的对象,用到了闭包
    return r;
}

//原型对象定义方法,这些方法为每个范围对象继承
//这里的巧妙之处在于,将原型对象定义为函数的属性
range.methods = {
    //如果x在范围内,则返回true,否则返回false
    //这个方法可以比较数字范围,也可以比较字符串和日期范围
    includes: function(x){
        return this.from<=x && x<=this.to;
    },

    //对于范围内的每个整数都调用一次f
    //这个方法只可用做数字范围
    foreach: function(f){
        for(var x=this.from;x<=this.to;x++){
            f.call(undefined,x);
        }
    },

    //返回表示这个范围的字符串
    toString: function(){ return this.from + "..." + this.to;}
};

var r = range(1,5);
console.log(r.includes(3));
r.foreach(function(x){
    console.log(x);
});
//r.foreach(console.log);   这样写是会出错的,因为log方法只能作用在console对象上

上面这段代码定义了一个工厂方法range(),用来创建新的范围对象。这里给range()函数定义了一个属性range.methods,用以快捷地存放定义类的原型对象。把原型对象挂在函数上没什么大不了,但也不是惯用做法。再者,注意range()函数给每个范围对象都定义了from和to属性,用以定义范围的起始位置和结束位置,这两个属性是非共享的,当然也是不可继承的。最后,注意在range.methods中定义的那些可共享,可继承的方法都用到了from和to属性,而且使用了this关键字,为了指代它们,二者使用this关键字来指代调用这个方法的对象。任何类的方法都可以通过this的这种基本用法来读取对象的属性。

通过构造函数来定义类

通过构造函数来初始新创建对象,使用关键字new来调用构造函数,构造函数的prototype属性被用做新对象的原型。这样通过同一个构造函数创建的所有对象都继承自一个相同的对象,它们都是同一个类的成员。

//这是一个构造函数,用以初始化新创建的“范围对象”
//注意,这里并没有创建并返回一个对象,仅仅是初始化
function Range(from,to){
    //存储新的“范围对象”的起始位置和结束位置(状态)
    //这两个属性是不可继承的,每个对象都拥有唯一的属性
    this.from = from;
    this.to = to;
}

//所有的“范围对象”都继承自这个对象
//注意,属性的名字必须是"prototype"
Range.prototype = {
    //如果x在范围内,则返回true,否则返回false
    //这个方法可以比较数字范围,也可以比较字符串和日期范围
    includes: function(x){
        return this.from<=x && x<=this.to;
    },

    //对于范围内的每个整数都调用一次f
    //这个方法只可用做数字范围
    foreach: function(f){
        for(var x=this.from;x<=this.to;x++){
            f.call(undefined,x);
        }
    },

    //返回表示这个范围的字符串
    toString: function(){ return this.from + "..." + this.to;}  
}

var r = new Range(1,5);         //能过new关键字来初始化对象
console.log(r.includes(3));     //true,3在这个范围内
r.foreach(function(x){      //输出 1 2 3 4 5
    console.log(x);
});
//r.foreach(console.log);   这样写是会出错的,因为log方法只能作用在console对象上

仔细对比这两种方法,定义构造函数既是定义类,并且类名首字母要大写,而普通函数和方法都是首字母小写。Range()构造函数是通过new关键字调用的,而range()工厂函数则不必使用new。由于Range()构造函数是通过new关键字调用的,因此不必调用inherit()或其他什么逻辑来创建新对象。Range()构造函数只不是初始化this而已。构造函数不必返回这个新创建的对象,构造函数会自动创建对象,并返回这个新对象。构造函数必须通过关键字new调用,如果将构造函数用做普通函数的话,往往不会正常工作。不过,两者的范围方法定义和调用方式是完全一样的。