希望将类作为类型对待,这样就可以根据对象所属的类区分它们。
检测对象的类的技术:instanceof运算符、constructor属性、以及构造函数的名字。但每种技术都不甚完美。
鸭式辩型,这种编程哲学更关注对象可以完成什么工作(即检查它包含什么方法)而不是对象属于哪个类。
instanceof运算符
左操作数是待检测其类的对象,右操作数是定义类的构造函数。如果o继承自c.prototype,则
o instanceof c的值为true。这里的继承可以不是直接继承。
虽然构造函数是类的公共标识,但原型是唯一的标识。尽管instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。
能在实例o的原型链中找到该构造函数c的prototype属性所指的对象
检测对象r是否是范围类的成员(实例)range.methods.isPrototypeOf(r);
检测对象的原型链上是否存在某个特定的原型对象,可以不使用构造函数作为中介的方法。
instanceof运算符和isPrototypeOf()方法的缺点:
1. 无法通过对象来获取类名,只能检测对象是否属于指定的类名。
2. 在多窗口和多框架子页面的web应用中兼容性不佳。例如,在两个框架页面中创建两个数组,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算符结果是false。原因,每个窗口和框架子页面都具有单独的执行上下文,每个上下文都包含单独的全局变量和一组构造函数。在两个不同的框架页面中创建的两个数组是继承了两个相同但相互独立的原型对象。
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后的表达式都是函数。
使用constructor属性检查对象是否属于某个类的技术的不足之处和instanceof一样。在多个执行上下文的场景它是无法正常工作的。
而且在JavaScript中也并非所有的对象都包含constructor属性(主要出现在自定义类时会忽略原型上的constructor属性)。
构造函数的名称
使用构造函数名称而不是构造函数本身作为类标识符。
一个窗口里的Array构造函数和另一个窗口的Array构造函数是不相等的,尽管它们的名字、函数体是一样的。
在一些JavaScript的实现中为函数对象提供了一个非标准的属性name,用来表示函数的名称。对于那么没有name属性的JavaScript实现来说,可以将函数转换成字符串,然后从中提取函数名字符串。
type()函数以字符串的形式返回对象的类型:
/** * 以字符串的形式返回o的类型: * -如果o是null,返回“null”;如果o是NaN,返回“NaN” * -如果typeof返回的值不是“object”,则返回这个值 * -如果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” // 这种方式可以识别出大多数的内置对象、宿主对象 // Array、Date、Error、RegExp、Math | window 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]; } // ============= test ==== console.log(type('')); function a() {}; var b = function() {}; console.log(type(a), type(b)); // function function function A() {}; var B = function() {}; function C() {}; // 不存在constructor属性 C.prototype = {}; // C.prototype = {constructor:C}; var b1 = new B(), a1 = new A(), c1 = new C(); console.log(type(a1), type(b1), type(c1)); // A, Object, Object
使用构造函数名字来识别对象的类的做法和constructor属性用一个相同的问题:并不是所有的对象都具有constructor属性。此外,并不是所有的函数都有名字。如果使用不带名字的函数表达式定义一个构造函数,会返回空字符串。
鸭式辩型
像鸭子一样走路、游泳并且嘎嘎叫的鸟就是鸭子。
不关注“对象的类是什么”,而是关注“对象能做什么”。
弱化了对象的类型,强化了对象的功能。
对于程序猿来说,这句话可以理解为:如果一个对象可以像鸭子一样走路、游泳、嘎嘎叫,就认为这个对象是鸭子,哪怕它不是从鸭子类的原型对象继承而来。
鸭式辩型的实现方法让人感觉太“放任自流”:仅仅是假设输入对象实现了必要的方法,根本没有执行进一步的检查。对输入对象进行检查,但不检查它们的类,而是用适当的名字检查它们所实现的方法。这可以将非法输入尽早地拦截在外。
一个强类型的triathlon()函数所需要的参数必须是TriAthlete对象,鸭式辩型的做法是只要对象包含walk()/swim()/bike()这三个方法的对象都是合格的参数。
// 如果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; }
function A() {}; A.prototype = { aa: function() {}, bb: function() {} }; var a = new A(); console.log(quacks(a, 'aa', 'bb')); // string // 继承A function B() {} inherit(B, A); B.prototype.cc = function() {}; var b = new B(); console.log(quacks(b, A)); // function console.log(quacks(b, a)); // object function inherit(subClass, superClass) { var F = function() {}; F.prototype = superClass.prototype; subClass.prototype = new F(); }
鸭式辩型需要注意的地方:首先,只通过特定的名称来检测对象是否含有一个或多个值为函数的属性。我们无法得知这些已经存在的属性的细节信息。
其次,quacks()函数不能应用于内置类。quacks(o, array)内置类的方法都是不可枚举的,函数中for/in循环无法遍历到它们(可通过Object.getOwnPropertyNames()获取)。