在JavaScript中如何定义不可变的类

作者:admin     字体:[增加 减小]    类型:原创
除了设置属性为不可枚举的,ECMAScript5还可以设置属性为只读的,当我们希望类的实例都是不可变的,这个特性非常有帮助。在本文中给出了两个非常有用的方法freezeProps与hideProps,希望对大家有所帮助。

除了设置属性为不可枚举的,ECMAScript5还可以设置属性为只读的,当我们希望类的实例都是不可变的,这个特性非常有帮助。使用Object.defineProperties()Object.create()可以定义不可变的类。同样使用Object.defineProperties()来为类创建原型对象,并将(原型对象的)实例方法设置为不可枚举的,就像内置类的方法一样。不仅如此,它还将这些实例方法设置为“只读”和“不可删除”,这样就可以防止对类做任何修改。

Object.defineProperties(Object.prototype,{
    'freezeProps': {    //将o的指定名字(或所有)的属性设置为不可写的和不可配置的
        value: function(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;   //所以我们可以继续使用它
        }
    },
    'hideProps': {  //将o的指定名字(或所有)的属性设置为不可枚举的和可配置的
        value: function(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;   //所以我们可以继续使用它
        }
    },
})

//类中通用方法为Klass.method
function Klass(){throw new Error("Can't instantiate class Klass.");}

//这个方法可以使用new调用,也可以省略new,它可以用做构造函数也可以用做工厂函数
Object.defineProperty(Klass,'defineClass',{
    value: function(_this,klass,properties) {
                for ( var prop in properties){
                    if ( typeof properties[prop] !== "object"){
                        properties[prop] = {value: properties[prop]};
                    }
                }
                if (_this instanceof klass) {
                    Object.defineProperties(_this,properties);
                } else {
                    return Object.create(klass.prototype,properties);
                }
            },
    enumerable: false,
    writable: false,
    configurable: false
});

//创建一个不可变的类,它的属性和方法都是只读的
function Range(from,to){
    var props = {
        from: from,
        to: to
    };
    return Klass.defineClass(this,Range,props);
}

var r = new Range(1,3);
Klass.freezeProps(r);
r.from = 10;        //并不能改变属性的值
console.log(r.from);    //1

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

使用这些工具函数,就可以充分利用ECMAScript5的特性来实现一个不可变的类,而且不用动态修改这个类。

function Range(from,to){
    this.from = from;
    this.to = to;
    Klass.freezeProps(this);
}

Range.prototype = Klass.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 + ")";}
});

var r = new Range(1,5);
r.foreach(function(x){
    console.log(x);
});