在Javascript中,类的实现是基于原型继承来实现的,类的一个重要特征是“动态扩展”(dynamically extendable)的能力。这种动态扩展自省的能力也是动态脚本语言的强大之处。

原型与工厂函数

工厂函数也是创建对象的一种方式,借助与inherit函数可以简单的实现工厂函数。

function range(from, to) {
    var r = inherit(range.methods);
    r.from = from;
    r.to = to;
    return r;
}

range.methods = {
    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);
    }
};

// test
var r = range(1,3);
r.includes(2); // true
r.foreach( function(x){console.log(x)} ); // 1 2 3

构造函数

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

Range.prototype = {
    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); 
    }
};

// test
var r = new Range(1,3);
r.includes(2); // true
r.foreach( function(x){console.log(x)} ); // 1 2 3

prototype属性

还可以使用isPrototypeOf()来检测对象是否是实例对象的原型,如第一个例子中:range.methods.isPrototypeOf(r) 为true

constructor属性

每个javascript函数都自动拥有一个prototype属性,这个属性的值是一个对象,它包含一个不可枚举属性constructor。
constructor属性的值是一个函数对象:

var F = function(){};
var p = F.prototype;
var c = p.constructor;
c === F // -> true: 对于任意函数 F.prototype.constructor == F

var o = new F();
o.constructor === F // -> true,constructor属性指代这个类

由于constructor是原型对象预定义的属性,上面的例子可以保留预定义属性并依次给原型对象添加方法: Range.prototype.includes = function(x) { return this.from <=x && x <= this.to; }

面向对象技术

像C++中的面向对象类,具有实例字段,实例方法,类字段,类方法等概念。
javascript中,方法都是以值的形式出现的,方法和字段没有太大的区别。

javascript中类相关的几个概念

javascript中定义类的三步走

  1. 定义构造函数,并初始化新对象的实例属性
  2. 给构造函数的prototype对象定义实例方法
  3. 给构造函数定义类字段和类属性
// 逻辑上通用的定义类的工具函数
function defineClass( constructor, methods, statics ) {
    if( methods ) extend(constructor.prototype, methods);
    if(statics) extend(constructor, statics);
    return constructor;
}

类的动态扩展

javascript中基于原型的继承机制是动态的:对象从原型继承属性,如果创建对象之后,原型的属性发生彼岸花,也会影响到继承自这个原型的所有实例对象。因此可以通过给原型对象添加新方法来扩充javascript类。

甚至javascript的内置类的原型对象也可以扩展,如:

// 多次调用f,传入迭代数
Number.prototype.times = function(f, context) {
    var n = Number(this);
    for(var i=0; i<n; i++)
        f.call( context, i );
};

// test
var n = 3;
n.times( function(n) { console.log(n); } ); // 0 1 2

实例:实现枚举类型

function enumeration( namesToValues ) {
    var enumeration = function() {
        throw "can't instantiate enumeration"; 
    };
    
    var proto = enumeration.prototype = {
        constructor : enumeration,
        toString : function() { return this.name; },
        valueOf : function() { return this.value; },
        toJSON : function() { return this.name; }
    };
    
    // 类字段
    enumeration.values = [];
    
    // 创建实例
    for (name in namesToValues) {
        // 创建基于原型的对象,使得 e instanceof enumeration
        var e = inherit(proto); 
        e.name = name;
        e.value = namesToValues[name];
        enumeration[name] = e;
        enumeration.values.push(e);
    }
    
    // 类方法
    enumeration.foreach = function(f, c) {
        for ( var i=0; i<this.values.length; i++ )
            f.call(c, this.values[i]);
    };

    // 返回构造函数
    return enumeration;
}


// test
var Coin = enumeration( {penny : 1, Nickel : 5, Dime : 10} );
var c = Coin.Dime;
c instanceof Coin; // true
c.constructor == Coin; // true
Coin.Dime == 10; // true

标准方法

有些方法是javascript需要类型转换的时候自动调用的:

  1. toString:返回一个可以标识该对象的字符串,比如在’+’运算符连接字符串时会自动调用该方法。
  2. toJSON:如果定义了,该方法将由JSON.stringify()自动调用。
  3. 可以定义”准标准“方法:’equals’, ‘compareTo’来实现对象的比较。

关于私有状态

经典面向对象语言中一般都有关键字private,表示字段或方法时私有的,外部无法访问。

javascript中可以通过闭包来模拟私有字段,并用方法来访问这些字段;这个封装会让类实例看起来时不可修改的:

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

Range.prototype = {
    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); 
    }
};

// test
var r = new Range(1,5);
r.includes(3); // true

构造函数重载

构造函数重载(overload)在javascript中需要根据传入参数的不同来执行不同的初始化方法

比如:集合Set类型的初始化:

function Set() {
    this.values = {}; // 保存集合
    this.n = 0;       // 保存个数
    
    // 如果转入数组,则其元素添加到集合中
    // 否则,将所有参数都添加到集合中
    if ( arguments.length == 1 && isArrayLike(arguments[0]) )
        this.add.apply( this, arguments[0] );
    else if( arguments.length > 0 )
        this.add.apply( this, arguments );
}

子类

类B继承自类A,则A称为父类(superclass),B称为子类(subclass):

实现子类的关键:原型继承

B.prototype = inherit(A.prototype);
B.prototype.constructor = B;

构造函数与方法链

NonNullSet继承自Set,Set实现了构造函数和add方法:

function NonNullSet() {
    // 构造函数链
    Set.apply( this, arguments );
}

// 子类
NonNullSet.prototype = inherit( Set.prototype );
NonNullSet.prototype.constructor = NonNullSet;

// 重载add方法,用以过滤null
NonNullSet.prototype.add = function() {
    for ( var i=0; i<arguments.length; i++ )
        if( arguments[i] == null )
            throw new Error("can't add null");
    
    // 方法链
    Set.prototype.add.apply(this, arguments);
}

组合 vs. 继承

组合:持有成员,并重写相关的方法,并且可能会使用持有成员的方法:

function FilteredSet( set, filter ) {
    this.set = set;
    this.filter = filter;
}

FilteredSet.prototype = {
    constructor : FilteredSet,
    add : function() {
        if( this.filter ) {
            // filter elements
        }
        
        this.set.add.apply( this.set, arguments );
        return this;
    }
};

关于属性描述

Object.defineProperty( o, prop, 
    {writable : false, configurable : false})
var descriptor = Object.getOwnPropertyDescriptor(o, prop);

defineProperty的可配置的属性描述参考:

模块化

模块创建时,避免污染全局变量的一种方法时使用一个对象作为命名空间,它将函数和属性作为命名空间对象的属性存储起来。

例如:命名空间为collections.sets

var collections; // 声明(或重新声明)全局对象
if( !collections )
    collections = {};
collections.sets = {}    

匿名函数执行模块惯用法

var Set = ( function namespace(){
    
    // 构造函数
    function Set() {
        this.values = {};
        this.n = 0;
        this.add.apply( this, arguments );
    }
    
    // 实例方法
    Set.prototype.contains = function(value) {
        return this.values.hasOwnProperty( v2s(value) );
    }
    
    
    // 内部辅助函数和变量
    function v2s(val) { /* ... */ }
    var nextId = 1;
    
    
    return Set;
}() );