类和模块--ECMAScript 5中的类

  • 让属性不可枚举

例9-6中的Set类使用了一个小技巧,将对象存储为“集合”的成员:它给添加至这个“集合”的任何对象定义了“对象id”属性。之后如果在for/in循环中对这个对象做遍历,这个新添加的属性也会遍历到。ECMAScript 5可以通过设置属性为“不可枚举”(nonenumerable)来让属性不会遍历到。例9-17展示了如何通过Object.defineProperty()来做到这一点,同时也展示了如何定义一个getter函数以及检测对象是否是可扩展的(extensible)。

例9-17:定义不可枚举的属性

//将代码包装在一个匿名函数中,这样定义的变量就在这个函数作用域内
(function(){//定义一个不可枚举的属性objectId,它可以被所有对象继承
//当读取这个属性时调用getter函数
//它没有定义setter,因此它是只读的
//它是不可配置的,因此它是不能删除的
    Object.defineProperty(Object.prototype,"objectId",{
        get:idGetter,//取值器
        enumerable:false,//不可枚举的
        configurable:false//不可删除的
    });//当读取objectId的时候直接调用这个getter函数
    function idGetter(){//getter函数返回该id
        if(!(idprop in this)){//如果对象中不存在id
            if(!Object.isExtensible(this))//并且可以增加属性
                throw Error("Can't define id for nonextensible objects");
            Object.defineProperty(this,idprop,{//给它一个值
                value:nextid++,//就是这个值
                writable:false,//只读的
                enumerable:false,//不可枚举的
                configurable:false//不可删除的
            });
        }
        return this[idprop];//返回已有的或新的值
    };//idGetter()用到了这些变量,这些都属于私有变量
    var idprop="|**objectId**|";//假设这个属性没有用到
    var nextid=1;//给它设置初始值
}());//立即执行这个包装函数
  • 定义不可变的类

除了可以设置属性为不可枚举的,ECMAScript 5还可以设置属性为只读的,当我们希望类的实例都是不可变的,这个特性非常有帮助。例9-18使用Object.defineProperties()和Object.create()定义不可变的Range类。它同样使用Object.defineProperties()来为类创建原型对象,并将(原型对象的)实例方法设置为不可枚举的,就像内置类的方法一样。不仅如此,它还将这些实例方法设置为“只读”和“不可删除”,这样就可以防止对类做任何修改(monkey-patching)。最后,例9-18展示了一个有趣的技巧,其中实现的构造函数也可以用做工厂函数,这样不论调用函数之前是否带有new关键字,都可以正确地创建实例。

例9-18:创建一个不可变的类,它的属性和方法都是只读的

//这个方法可以使用new调用,也可以省略new,它可以用做构造函数也可以用做工厂函数
function Range(from,to){//这些是对from和to只读属性的描述符
    var props={
        from:{value:from,enumerable:true,writable:false,configurable:false},
        to:{value:to,enumerable:true,writable:false,configurable:false}
    };
    if(this instanceof Range)//如果作为构造函数来调用
        Object.defineProperties(this,props);//定义属性
    else//否则,作为工厂方法来调用
        return Object.create(Range.prototype,//创建并返回这个新Range对象,
            props);//属性由props指定
}
//如果用同样的方法给Range.prototype对象添加属性
//那么我们需要给这些属性设置它们的特性
//因为我们无法识别出它们的可枚举性、可写性或可配置性,这些属性特性默认都是false
Object.defineProperties(Range.prototype,{
    includes:{
        value:function(x){return this.from<=x&&x<=this.to;}
    },
    foreach:{
        value:function(f){
            for(var x=Math.ceil(this.from);x<=this.to;x++)f(x);
        }
    },
    toString:{
        value:function(){return"("+this.from+"..."+this.to+")";}
    }
});

例9-18用到了Object.defineProperties()和Object.create()来定义不可变的和不可枚举的属性。这两个方法非常强大,但属性描述符对象让代码的可读性变得更差。另一种改进的做法是将修改这个已定义属性的特性的操作定义为一个工具函数,例9-19展示了两个这样的工具函数:

例9-19:属性描述符工具函数

//将o的指定名字(或所有)的属性设置为不可写的和不可配置的
function freezeProps(o){
    var props=(arguments.length==1)//如果只有一个参数
        ?Object.getOwnPropertyNames(o)//使用所有的属性
        :Array.prototype.splice.call(arguments,1);//否则传入了指定名字的属性
    props.forEach(function(n){//将它们都设置为只读的和不可变的
//忽略不可配置的属性
        if(!Object.getOwnPropertyDescriptor(o,n).configurable)return;
        Object.defineProperty(o,n,{writable:false,configurable:false});
    });
    return o;//所以我们可以继续使用它
}
//将o的指定名字(或所有)的属性设置为不可枚举的和可配置的
function hideProps(o){
    var props=(arguments.length==1)//如果只有一个参数
        ?Object.getOwnPropertyNames(o)//使用所有的属性
        :Array.prototype.splice.call(arguments,1);//否则传入了指定名字的属性
    props.forEach(function(n){//将它们设置为不可枚举的
//忽略不可配置的属性
        if(!Object.getOwnPropertyDescriptor(o,n).configurable)return;
        Object.defineProperty(o,n,{enumerable:false});
    });
    return o;
}

Object.defineProperty()和Object.defineProperties()可以用来创建新属性,也可以修改已有属性的特性。当用它们创建新属性时,默认的属性特性的值都是false。但当用它们修改已经存在的属性时,默认的属性特性依然保持不变。比如,在上面的hideProps()函数中,只指定了enumerable特性,因为我们只想修改enumerable特性。

使用这些工具函数,就可以充分利用ECMAScript 5的特性来实现一个不可变的类,而且不用动态地修改这个类。例9-20中不可变的Range类就用到了刚才定义的工具函数。

例9-20:一个简单的不可变的类

function Range(from,to){//不可变的类Range的构造函数
    this.from=from;
    this.to=to;
    freezeProps(this);//将属性设置为不可变的
}
Range.prototype=hideProps({//使用不可枚举的属性来定义原型
    constructor:Range,
    includes:function(x){return this.from<=x&&x<=this.to;},
    foreach:function(f){for(var x=Math.ceil(this.from);x<=this.to;x++)f(x);},
    toString:function(){return"("+this.from+"..."+this.to+")";}
});
  • 封装对象状态

例9-10所示,构造函数中的变量和参数可以用做它创建的对象的私有状态。该方法在ECMAScript 3中的一个缺点是,访问这些私有状态的存取器方法是可以替换的。在ECMAScript 5中可以通过定义属性getter和setter方法将状态变量更健壮地封装起来,这两个方法是无法删除的,如例9-21所示。

例9-21:将Range类的端点严格封装起来

//这个版本的Range类是可变的,但将端点变量进行了良好的封装
//但端点的大小顺序还是固定的:from<=to
function Range(from,to){//如果from大于to
    if(from>to)throw new Error("Range:from must be<=to");//定义存取器方法以维持不变
    function getFrom(){return from;}
    function getTo(){return to;}
    function setFrom(f){//设置from的值时,不允许from大于to
        if(f<=to)from=f;
    else throw new Error("Range:from must be<=to");
    }
    function setTo(t){//设置to的值时,不允许to小于from
        if(t>=from)to=t;
    else throw new Error("Range:to must be>=from");
    }
//将使用取值器的属性设置为可枚举的、不可配置的
    Object.defineProperties(this,{
        from:{get:getFrom,set:setFrom,enumerable:true,configurable:false},
        to:{get:getTo,set:setTo,enumerable:true,configurable:false}
    });
}
//和前面的例子相比,原型对象没有做任何修改
//实例方法可以像读取普通的属性一样读取from和to
Range.prototype=hideProps({
    constructor:Range,
    includes:function(x){return this.from<=x&&x<=this.to;},
    foreach:function(f){for(var x=Math.ceil(this.from);x<=this.to;x++)f(x);},
    toString:function(){return"("+this.from+"..."+this.to+")";}
});
  • 防止类的扩展

通常认为,通过给原型对象添加方法可以动态地对类进行扩展,这是JavaScript本身的特性。ECMAScript 5可以根据需要对此特性加以限制。Object.preventExtensions()可以将对象设置为不可扩展的,也就是说不能给对象添加任何新属性。Object.seal()则更加强大,它除了能阻止用户给对象添加新属性,还能将当前已有的属性设置为不可配置的,这样就不能删除这些属性了(但不可配置的属性可以是可写的,也可以转换为只读属性)。可以通过这样一句简单的代码来阻止对Object.prorotype的扩展:

Object.seal(Object.prototype);
JavaScript的另外一个动态特性是“对象的方法可以随时替换”(或称为"monkey-patch"):

var original_sort_method=Array.prototype.sort;
Array.prototype.sort=function(){
    var start=new Date();
    original_sort_method.apply(this,arguments);
    var end=new Date();
    console.log("Array sort took"+(end-start)+"milliseconds.");
};

可以通过将实例方法设置为只读来防止这类修改,一种方法就是使用上面代码所定义的freezeProps()工具函数。另外一种方法是使用Object.freeze(),它的功能和Object.seal()完全一样,它同样会把所有属性都设置为只读的和不可配置的。

理解类的只读属性的特性至关重要。如果对象o继承了只读属性p,那么给o.p的赋值操作将会失败,就不会给o创建新属性。如果你想重写一个继承来的只读属性,就必须使用Object.definePropertiy()、Object.defineProperties()或Object.create()来创建这个新属性。也就是说,如果将类的实例方法设置为只读的,那么重写它的子类的这些方法的难度会更大。

这种锁定原型对象的做法往往没有必要,但的确有一些场景是需要阻止对象的扩展的。回想一下例9-7中的enumeration(),这是一个类工厂函数。这个函数将枚举类型的每个实例都保存在构造函数对象的属性里,以及构造函数的values数组中。这些属性和数组是表示枚举类型实例的正式实例列表,是可以执行“冻结”(freezing)操作的,这样就不能给它添加新的实例,已有的实例也无法删除或修改。可以给enumeration()函数添加几行简单的代码:

Object.freeze(enumeration.values);
Object.freeze(enumeration);
需要注意的是,通过在枚举类型中调用Object.freeze(),例9-17中定义的objectId属性之后也无法使用了。这个问题的解决办法是,在枚举类型被“冻结”之前读取一次它的objectId属性(调用潜在的存取器方法并设置内部属性)。

  • 子类和ECMAScript 5

例9-22使用ECMAScript 5的特性实现子类。这里使用例9-16中的AbstractWritableSet类来做进一步说明,来定义这个类的子类StringSet。下面这个例子的最大特点是使用Object.create()创建原型对象,这个原型对象继承自父类的原型,同时给新创建的对象定义属性。这种实现方法的困难之处在于,正如上文所提到的,它需要使用难看的属性描述符。

这个例子中另外一个有趣之处在于,使用Object.create()创建对象时传入了参数null,这个创建的对象没有任何继承任何成员。这个对象用来存储集合的成员,同时,这个对象没有原型,这样我们就能对它直接使用in运算符[14],而不须使用hasOwnProperty()方法。

例9-22:StringSet:利用ECMAScript 5的特性定义的子类

function StringSet(){
    this.set=Object.create(null);//创建一个不包含原型的对象
    this.n=0;
    this.add.apply(this,arguments);
}
//注意,使用Object.create()可以继承父类的原型
//而且可以定义单独调用的方法,因为我们没有指定属性的可写性、可枚举性和可配置性
//因此这些属性特性的默认值都是false
//只读方法让这个类难于子类化(被继承)
StringSet.prototype=Object.create(AbstractWritableSet.prototype,{
    constructor:{value:StringSet},
    contains:{value:function(x){return x in this.set;}},
    size:{value:function(x){return this.n;}},
    foreach:{value:function(f,c){Object.keys(this.set).forEach(f,c);}},
    add:{
        value:function(){
            for(var i=0;i<arguments.length;i++){
                if(!(arguments[i]in this.set)){
                    this.set[arguments[i]]=true;
                    this.n++;
                }
            }
            return this;
        }
    },
    remove:{
        value:function(){
            for(var i=0;i<arguments.length;i++){
                if(arguments[i]in this.set){
                    delete this.set[arguments[i]];
                    this.n--;
                }
            }
            return this;
        }
    }
});
  • 属性描述符

在例9-23中给Object.prototype添加了properties()方法(这个方法是不可枚举的)。这个方法的返回值是一个对象,用以表示属性的列表,并定义了有用的方法用来输出属性和属性特性(对于调试非常有用),用来获得属性描述符(当复制属性同时复制属性特性时非常有用)以及用来设置属性的特性(是上文定义的hideProps()和freezeProps()函数不错的替代方案)。

例9-23:ECMAScript 5属性操作

/*
*给Object.prototype定义properties()方法,
*这个方法返回一个表示调用它的对象上的属性名列表的对象
*(如果不带参数调用它,就表示该对象的所有属性)
*返回的对象定义了4个有用的方法:toString()、descriptors()、hide()和show()
*/
(function namespace(){//将所有逻辑闭包在一个私有函数作用域中
//这个函数成为所有对象的方法
    function properties(){
        var names;//属性名组成的数组
        if(arguments.length==0)//所有的自有属性
            names=Object.getOwnPropertyNames(this);
        else if(arguments.length==1&&Array.isArray(arguments[0]))
        names=arguments[0];//名字组成的数组
    else//参数列表本身就是名字
        names=Array.prototype.splice.call(arguments,0);//返回一个新的Properties对象,用以表示属性名字
        return new Properties(this,names);
    }
//将它设置为Object.prototpye的新的不可枚举的属性
//这是从私有函数作用域导出的唯一一个值
    Object.defineProperty(Object.prototype,"properties",{
        value:properties,
        enumerable:false,writable:true,configurable:true
    });//这个构造函数是由上面的properties()函数所调用的
//Properties类表示一个对象的属性集合
    function Properties(o,names){
        this.o=o;//属性所属的对象
        this.names=names;//属性的名字
    }
//将代表这些属性的对象设置为不可枚举的
    Properties.prototype.hide=function(){
        var o=this.o,hidden={enumerable:false};
        this.names.forEach(function(n){
            if(o.hasOwnProperty(n))
                Object.defineProperty(o,n,hidden);
        });
        return this;
    };//将这些属性设置为只读的和不可配置的
    Properties.prototype.freeze=function(){
        var o=this.o,frozen={writable:false,configurable:false};
        this.names.forEach(function(n){
            if(o.hasOwnProperty(n))
                Object.defineProperty(o,n,frozen);
        });
        return this;
    };//返回一个对象,这个对象是名字到属性描述符的映射表
//使用它来复制属性,连同属性特性一起复制
//Object.defineProperties(dest,src.properties().descriptors());
    Properties.prototype.descriptors=function(){
        var o=this.o,desc={};
        this.names.forEach(function(n){
            if(!o.hasOwnProperty(n))return;
            desc[n]=Object.getOwnPropertyDescriptor(o,n);
        });
        return desc;
    };//返回一个格式化良好的属性列表
//列表中包含名字、值和属性特性,使用"permanent"表示不可配置
//使用"readonly"表示不可写,使用"hidden"表示不可枚举
//普通的可枚举、可写和可配置属性不包含特性列表
    Properties.prototype.toString=function(){
        var o=this.o;//在下面嵌套的函数中使用
        var lines=this.names.map(nameToString);
        return"{\n"+lines.join(",\n")+"\n}";
        function nameToString(n){
            var s="",desc=Object.getOwnPropertyDescriptor(o,n);
            if(!desc)return"nonexistent"+n+":undefined";
            if(!desc.configurable)s+="permanent";
            if((desc.get&&!desc.set)||!desc.writable)s+="readonly";
            if(!desc.enumerable)s+="hidden";
            if(desc.get||desc.set)s+="accessor"+n
            else s+=n+":"+((typeof desc.value==="function")?"function":desc.value);
            return s;
        }
    };//最后,将原型对象中的实例方法设置为不可枚举的
//这里用到了刚定义的方法
    Properties.prototype.properties().hide();
}());//立即执行这个匿名函数

猜你喜欢

转载自blog.csdn.net/wuyufa1994/article/details/86034889
今日推荐