【类和模块】类和类型

类和类型

JavaScript定义了少量的数据类型:null、undefined、布尔值、数字、字符串、函数和对象。typeof运算符可以得出值的类型。然而,我们往往更希望将类作为类型来对待,这样就可以根据对象所属的类来区分它们。JavaScript语言核心中的内置对象(通常是指客户端JavaScript的宿主对象)可以根据它们的class属性来区分彼此。

instanceof 运算符

instanceof运算符,左操作数是待检测其类的对象,右操作数是定义类的构造函数。如果o继承自c.prototype,则表达式o instanceof c值为true。这里的继承可以不是直接继承,如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype,这个表达式的运算结果也是true。

构造函数是类的公共标识,但原型是唯一的标识。尽管instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。

如果你想检测对象的原型链上是否存在某个特定的原型对象,有没有不使用构造函数作为中介的方法呢?答案是肯定的,可以使用isPrototypeOf()方法。比如,可以通过如下代码来检测对象r是否是范围类的成员:

range.methods.isPrototypeOf(r);     // range.method是原型对象

instanceof运算符和isPrototypeOf()方法的缺点是,我们无法通过对象来获得类名,只能检测对象是否属于指定的类名。在客户端JavaScript中还有一个比较严重的不足,就是在多窗口和多框架子页面的Web应用中兼容性不佳。每个窗口和框架子页面都具有单独的执行上下文,每个上下文都包含独有的全局变量和一组构造函数。在两个不同框架页面中创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算结果是false。

constructor属性

另一种识别对象是否属于某个类的方法是使用constructor属性。因为构造函数是类的公共标识,所以最直接的方法就是使用constructor属性,比如:

function typeAndValue(x) {
    if (x == null) return "";                          // NUll和undefined没有构造函数
    switch(x.constructor) {
        case Number: return "Number: " + x;            // 处理原始类型
        case String: return "String: '" + x + "'";      
        case Date: return "Date: " + x;                // 处理内置函数
        case RegExp: return "Regexp: " + x; 
        case Complex: return "Complex: " + x;          // 处理自定义类型
    }
}

需要注意的是,在代码中关键字case后的表达式都是函数,如果改用typeof运算符或获取到对象的class属性的话,它们应当改为字符串。

使用constructor属性检测对象属于某个类的技术的不足之处和instanceof一样。在多个执行上下文的场景中它是无法正常工作的(比如在浏览器窗口的多个框架子页面中)。在这种情况下,每个框架页面各自拥有独立的构造函数集合,一个框架页面中的Array构造函数和另一个框架页面的Array构造函数不是同一个构造函数。

同样,在JavaScript中也并非所有的对象都包含constructor属性。在每个新创建的函数原型上默认会有constructor属性,但我们常常会忽觉原型上的constructor属性。

构造函数的名称

使用instanceof运算符和constructor属性来检测对象所属的类有一个主要的问题,在多个执行上下文中存在构造函数的多个副本的时候,这两种方法的检测结果会出错。多个执行上下文中的函数看起来是一模一样的,但它们是相互独立的对象,因此彼此也不相等。

一种可能的解决方案是使用构造函数的名字而不是构造函数本身作为类标识符。一个窗口里的Array构造函数和另一个窗口的Array构造函数是不相等的,但是它们的名字是一样的。在一些JavaScript的实现中为函数对象提供了一个非标准的属性name,用来表示函数的名称。对于那些没有name属性的JavaScript实现来说,可以将函数转换为字符串,然后从中提取出函数名。

下例定义的type()函数以字符串的形式返回对象的类型。它用typeof运算符来处理原始值和函数。对于对象来说,它要么返回class属性的值要么返回构造函数的名字。

例:可以判断值的类型的type()函数
/**
 * 以字符串形式返回o的类型:
 *  -如果o是null,返回"null";如果o是NaN,返回"nan"
 *  -如果typeof返回的值不是"object",则返回这个值
 *  (注意,有一些JavaScript的实现将正则表达式识别为函数)
 *  - 如果o的类不是"Object",则返回这个值
 *  - 如果o包含构造函数并且这个构造函数具有名称,则返回这个名称
 *  - 否则,一律返回“Object”
 **/
function type(o) {
    var t, c, n; // type, class, name
    // 处理null值的特殊情形
    if (o === null) return "null";
    // 另外一种特殊情形:NaN和它自身不相等
    if (o !== o) return "nan";

    // 如果typeof的值不是"object",则使用这个值
    // 这可以识别出原始值的类型和函数
    if ((t = typeof o) !== "object") return t;

    // 返回对象的类名,除非值为“Object”
    // 这种方式可以识别出大多数的内置对象
    if ((c = classof(o)) !== "Object") return c;

    // 如果对象构造函数的名字存在的话,则返回它
    if (o.constructor && typeof o.constructor === "function" &&
       (n = o.constructor.getName())) return n;

    // 其他的类型都无法判别,一律返回"Object" 
    return "Object";
}

// 返回对象的类
function classof(o) {
    return Object.prototype.toString.call(o).slice(8, -1);
};

// 返回函数的名字(可能是空字符串),不是函数的话返回null
Function.prototype.getName = function () {
    if ("name" in this) return this.name;
    return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};

这种使用构造函数名字来识别对象的类的做法和使用constructor属性一样有一个问题:并不是所有的对象都具有constructor属性。此外,并不是所有的函数都有名字。如果使用不带名字的函数定义表达式定义一个构造函数,getName()方法则会返回空字符串:

// 这个构造函数没有名字
var Complex = function(x,y) { this.r = x; this.i = y; }
// 这个构造函数有名字
var Range = function Range(f,t) { this.from = f; this.to = t; }

鸭式辩型

上文所描述的检测对象的类的各种技术多少都会有些问题,至少在客户端JavaScript中是如此。解决办法就是规避掉这些问题:不要关注“对象的类是什么”,而是关注“对象能做什么”。这种思考问题的方式在Python和Ruby中非常普遍,称为“鸭式辩型”(这个表述是由作家James Whitcomb Riley 提出的)。

像鸭子一样走路、游泳并且嘎嘎叫的鸟就是鸭子。

对于JavaScript程序员来说,这句话可以理解为“如果一个对象可以像鸭子一样走路、游泳并且嘎嘎叫,就认为这个对象是鸭子,哪怕它并不是从鸭子类的原型对象继承而来的”。

鸭式辩型的实现方法让人感觉太“放任自流”:仅仅是假设输入对象实现了必要的方法,根本没有执行进一步的检査。如果输入对象没有遵循“假设”,那么当代码试图调用那些不存在的方法时就会报错。另一种实现方法是对输入对象进行检査。但不是检査它们的类,而是用适当的名字来检査它们所实现的方法。这样可以将非法输入尽可能早地拦截在外,并可给出带有更多提示信息的报错。

下例中按照鸭式辩型的理念定义了quacks()函数(函数名叫“implements”会更加合适,但implements是保留字)。quacks()用以检査一个对象(第一个实参)是否实现了剩下的参数所表示的方法。对于除第一个参数外的每个参数,如果是字符串的话则直接检査是否存在以它命名的方法;如果是对象的话则检査第一个对象中的方法是否在这个对象中也具有同名的方法;如果参数是函数,则假定它是构造函数,函数将检査第一个对象实现的方法是否在构造函数的原型对象中也具有同名的方法。

例:利用鸭式辩型实现的函数
//如果o实现了除第一个参数之外的参数所表示的方法,则返回true 
function quacks(o /*, ... */) {
	for (var i = 1; i < arguments.length; i++) {  		
	// 遍历o之后的所有参数
		var arg = arguments[i];
		switch (typeof arg) {	    // 如果参数是:
			case 'string':	        // string: 直接用名字做检査
				if (typeof o[arg] !== "function") return false;
				continue;
			case 'function':	    // function: 检査函数的原型对象上的方法
  // 如果实参是函数,则使用它的原型
  			arg = arg.prototype;        // 进入下一个case
			case 'object':	        // object:检査匹配的方法
				for (var m in arg) {        // 遍历对象而每个属性
					if (typeof arg[m] !== "function") continue; // 跳过不是方法的属性 
					if (typeof o[m] !== "function") return false;
				}
			}
		}

		// 如果程序能执行到这里,说明o实现了所有的方法
		return true;
}

关于这个quacks()函数还有一些地方是需要尤为注意的。首先,这里只是通过特定的名称来检测对象是否含有一个或多个值为函数的属性。我们无法得知这些已经存在的属性的细节信息,比如,函数是干什么用的?它们需要多少参数?参数类型是什么?然而这是鸭式辩型的本质所在,如果使用鸭式辩型而不是强制的类型检测的方式定义API,那么创建的API应当更具灵活性才可以,这样才能确保你提供给用户的API更加安全可靠。关于quacks()函数还有另一问题需要注意,就是它不能应用于内置类。比如,不能通过 quacks(o,Array)来检测o是否实现了Array中所有同名的方法。原因是内置类的方法都是 不可枚举的,quacks()中的for/in循环无法遍历到它们(注意,在ECMAScript 5中有一 个补救办法,就是使用ojbeet.getOwnPropertyNames())。

上一篇:反射的基础使用


下一篇:【手写代码】new 操作符