1.4 类型的判定
JavaScript 存在两套类型系统,一套是基本数据类型,另一套是对象类型系统。基本数据类型包括6种,分别是undefined、string、null、boolean、function、object。基本数据类型是通过typeof来检测的。对象类型系统是以基础类型系统为基础的,通过 instanceof 来检测。然而,JavaScript自带的这两套识别机制非常不靠谱,于是催生了isXXX系列。就拿typeof来说,它只能粗略识别出 string、number、boolean、function、undefined、object 这 6 种数据类型,无法识别 Null、RegExpAragument 等细分对象类型。
让我们看一下这里面究竟有多少陷阱。
typeof null// "object" typeof document.childNodes //safari "function" typeof document.createElement('embed')//ff3-10 "function" typeof document.createElement('object')//ff3-10 "function" typeof document.createElement('applet')//ff3-10 "function" typeof /\d/i //在实现了ecma262v4的浏览器返回 "function" typeof window.alert //IE678 "object"" var iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length - 1].Array; var arr = new xArray(1, 2, 3); // [1,2,3] arr instanceof Array; // false arr.constructor === Array; // false window.onload = function() { alert(window.constructor);// IE67 undefined alert(document.constructor);// IE67 undefined alert(document.body.constructor);// IE67 undefined alert((new ActiveXObject('Microsoft.XMLHTTP')).constructor);// IE6789 undefined } isNaN("aaa") //true
上面分4组,第一组是typeof的坑。第二组是instanceof 的陷阱,只要原型上存在此对象的构造器它就返回true,但如果跨文档比较,iframe里面的数组实例就不是父窗口的Array的实例。第三组有关constructor的陷阱,在旧版本IE下DOM与BOM对象的constructor属性是没有暴露出来的。最后有关NaN,NaN对象与null、undefined一样,在序列化时是原样输出的,但isNaN这方法非常不靠谱,把字符串、对象放进去也返回true,这对我们序列化非常不利。
另外,在IE下typeof还会返回unknow的情况。
if (typeof window.ActiveXObject != "undefined") { var xhr = new ActiveXObject("Msxml2.XMLHTTP"); alert(typeof xhr.abort); }
基于这IE的特性,我们可以用它来判定某个VBscript方法是否存在。
<script type="text/VBScript"> function VBMethod(a,b) VBMethod = a + b end function </script> <script> if(typeof VBMethod === "unknown"){//看这个 alert(VBMethod(10,34)) } </script>
另外,以前人们总是以 document.all是否存在来判定 IE,这其实是很危险的。因为用document.all 来取得页面中的所有元素是不错的主意,这个方法 Firefox、Chrome 觊觎好久了,不过人们都这样判定,于是有了在Chrome下的这出闹剧。
typeof document.all // undefined document.all // HTMLAllCollection[728] (728为元素总数)
在判定undefined、null、string、number、boolean、function这6个还算简单,前面两个可以分别与void(0)、null比较,后面4个直接typeof也可满足90%的情形。这样说是因为string、number、boolean可以包装成“伪对象”,typeof无法按照我们的意愿工作了,虽然它严格执行了 Ecmascript 的标准。
typeof new Boolean(1);//"object" typeof new Number(1);//"object" typeof new String("aa");//"object"
这些还是最简单的,难点在于 RegExp 与 Array。判定 RegExp 类型的情形很少,不多讲了, Array则不一样。有关isArray的实现不下二十种,都是因为JavaScript的鸭子类型被攻破了。直到Prototype.js把Object.prototype.toString发掘出来,此方法是直接输出对象内部的[[Class]],绝对精准。有了它,可以跳过95%的陷阱了。
isArray早些年的探索:
function isArray(arr) { return arr instanceof Array; } function isArray(arr) { return !!arr && arr.constructor == Array; } function isArray(arr) {//Prototype.js1.6.0.3 return arr != null && typeof arr === "object" && 'splice' in arr && 'join' in arr; } function isArray(arr) {//Douglas Crockford return typeof arr.sort == 'function' } function isArray(array) {//kriszyp var result = false; try { new array.constructor(Math.pow(2, 32)) } catch (e) { result = /Array/.test(e.message) } return result; }; function isArray(o) {// kangax try { Array.prototype.toString.call(o); return true; } catch (e) { } return false; }; function isArray(o) {//kangax if (o && typeof o == 'object' && typeof o.length == 'number' && isFinite(o.length)) { var _origLength = o.length; o[o.length] = '__test__'; var _newLength = o.length; o.length = _origLength; return _newLength == _origLength + 1; } return false; }
至于null、undefined、NaN直接这样:
function isNaN(obj) { return obj !== obj } function isNull(obj) { return obj === null; } function isUndefined(obj) { return obj === void 0; }
最后要判定的对象是window,由于ECMA是不规范 Host 对象,window 对象属于 Host ,所以也没有被约定,就算Object.prototype.toString也对它无可奈何。
· [object Object]IE6
· [object Object]IE7
· [object Object]IE8
· [object Window]IE9
· [object Window]firefox3.6
· [object Window]opera10
· [object DOMWindow]safai4.04
· [object global]chrome5.0.3.22
不过根据window.window和window.setInterval去判定更加不够谱,用一个技巧我们可以完美识别IE6、IE7、IE8的window对象,其他还是用toString,这个神奇的hack(技巧)就是,window与document互相比较,如果顺序不一样,其结果是不一样的!
window == document // IE678 true; document == window // IE678 false;
当然,如果细数起来,JavaScript匪夷所思的事比比都是。
存在a !== a的情况;
存在a == b && b != a的情况;
存在a == !a的情况;
存在a === a+100的情况;
1 < 2 < 3为true, 3 > 2 > 1为false;
0/0为NaN;
……
好了,至此,所有重要的 isXXX 问题都解决了,剩下的就把它们表达出来。经典做法就是直接罗列。
在Prototype.js中,拥有isElement、isArray、isHash、isFunction、isString、isNumber、isDate、isUndefined方法。
mootools搞了个typeOf判定基本类型,instanceOf判定自定义“类”。
RightJS有isFunction 、isHash、isString、isNumber、isArray 、isElement 、isNode。
EXT有isEmpty、isArray、isDate、isObject、isSimpleObject、isPrimitive、isPrimitive、isFunction、isNumber、isNumeric、isString、isBoolean、isElement、isTextNode、isDefined、isIterable,应有尽有。最后,还有typeOf判定基本类型。
Underscore.js 有 isElement、isEmpty、isArray、isArguments、isObject、isFunction、isString、isNumber、isFinite、isNaN、isBoolean、isDate、isRegExp、isNull、isUndefined。
isXXX系列就像恶性肿瘤一样不断膨胀,其实你多弄几个isXXX也不能满足用户的全部需求。就像isDate、isRegExp会用到的机率有多高呢?
jQuery 就不与其他框架一样了,在 jQuery 1.4 中只有 isFunction、isArray、isPlainObject、isEmptyObject。IsFunction、isArray 肯定是用户用得最多,isPlainObject 则是用来判定是否为纯净的JavaScript对象,既不是DOM、BOM对象,也不是自定义“类”的实例对象,制造它的最初目的是用于深拷贝,避开像window那样自己引用自己的对象。isEmptyObject是用于数据缓存系统,当此对象为空时,就可以删除它。
//jquery2.0 jQuery.isPlainObject = function(obj) { //首先排除基础类型不为Object的类型,然后是DOM节点与window对象 if (jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow(obj)) { return false; } //然后回溯它的最近的原型对象是否有isPrototypeOf, //旧版本IE的一些原生对象没有暴露constructor、prototype,因此会在这里过滤 try { if (obj.constructor && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) { return false; } } catch (e) { return false; } return true; }
在avalon.mobile中有一个更精简的版本,由于它只支持IE10等非常新的浏览器,就没有干扰因素了,可以大胆使用ecma262v5的新API。
avalon.isPlainObject = function(obj) { return obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype }
isArrayLike也是一个常用的方法,但判定一个类数组太难了,唯一的辨识手段是它应该有一个大于或等于零的整型length属性。此外还有一些“共识”,如window与函数和元素节点(如form元素)不算类数组,虽然它们都满足前面的条件。因此至今jQuery没有把它暴露出来。
//jquery2.0 function isArraylike(obj) { var length = obj.length, type = jQuery.type(obj); if (jQuery.isWindow(obj)) { return false; } if (obj.nodeType === 1 && length) { return true; } return type === "array" || type !== "function" && (length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj); } //avalon 0.9 function isArrayLike(obj) { if (obj && typeof obj === "object") { var n = obj.length if (+n === n && !(n % 1) && n >= 0) { //检测length属性是否为非负整数 try {//像Argument、Array、NodeList等原生对象的length属性是不可遍历的 if ({}.propertyIsEnumerable.call(obj, 'length') === false) { return Array.isArray(obj) || /^\s?function/.test(obj.item || obj. callee) } return true; } catch (e) { //IE的NodeList直接抛错 return true } } } return false } //avalon.mobile更倚重Object.prototoype.toString来判定 function isArrayLike(obj) { if (obj && typeof obj === "object") { var n = obj.length, str = Object.prototype.toString.call(obj) if (/Array|NodeList|Arguments|CSSRuleList/.test(str)) { return true } else if (str === "[object Object]" && (+n === n && !(n % 1) && n >= 0)) { return true //由于 ecma262v5 能修改对象属性的 enumerable,因此不能用 propertyIs //Enumerable来判定了 } } return false }
补充一句,1.3版本中,Prototype.js的研究成果(Object.prototype.toString.call)就应用于jQuery了。在1.2版本中,jQuery判定一个变量是否为函数非常复杂。
isFunction: function( fn ) { return !!fn&&typeoffn != "string" && !fn.nodeName&& fn.constructor != Array && /^[\s[]?function/.test( fn + "" ); }
jQuery1.43引入isWindow来处理makeArray中对window的判定,引入isNaN用于确保样式赋值的安全。同时引入type代替typeof关键字,用于获取数据的基本类型。
class2type = {} jQuery.each("Boolean Number String Function Array Date RegExpObject".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); jQuery.type = function(obj) { return obj == null ? String(obj) : class2type[toString.call(obj) ] || "object"; })
jQuery1.7中添加isNumeric代替isNaN。这是个不同于其他框架的isNumber,它可以是字符串,只要外观上像数字就行了。但 jQuery1.7 还做了一件违背之前提到稳定性的事情,贸然去掉jQuery.isNaN ,因此导致基于旧版本 jQuery 的一大批插件失效。
//jquery1.43~1.64 jQuery.isNaN = function(obj) { return obj == null || !rdigit.test(obj) || isNaN(obj); }) //jquery1.7 就是isNaN的取反版 jQuery.isNumeric = function(obj) { return obj != null && rdigit.test(obj) && !isNaN(obj); }) //jquery1.71~1.72 jQuery.isNumeric = function(obj) { return !isNaN(parseFloat(obj)) && isFinite(obj); } //jquery2.1 jQuery.isNumeric = function(obj) { return obj - parseFloat(obj) >= 0; }
mass Framework的思路与jQuery一致,尽量减少isXXX系列的数量,把isWindow、isNaN、nodeName等方法都整进去了。这是个野心勃勃的方法,代码比较长,它既可以获取类型,也可以传入第二参数进行类型比较。
var class2type = { "[objectHTMLDocument]": "Document", "[objectHTMLCollection]": "NodeList", "[objectStaticNodeList]": "NodeList", "[objectIXMLDOMNodeList]": "NodeList", "[objectDOMWindow]": "Window", "[object global]": "Window", "null": "Null", "NaN": "NaN", "undefined": "Undefined" }, toString = class2type.toString; "Boolean,Number,String,Function,Array,Date,RegExp,Window,Document,Arguments,NodeList" .replace($.rword, function(name) { class2type[ "[object " + name + "]" ] = name; }); //class2type这个映射几乎把所有常用判定对象“一网打尽”了 mass.type = function(obj, str) { var result = class2type[ (obj == null || obj !== obj) ? obj : toString.call(obj) ] || obj.nodeName || "#"; if (result.charAt(0) === "#") { //兼容旧版本浏览器与处理个别情况,如window.opera //利用IE6、IE7、IE8 window == document为true,document == window竟然为false的神奇特性 if (obj == obj.document && obj.document != obj) { result = 'Window'; //返回构造器名字 } else if (obj.nodeType === 9) { result = 'Document'; //返回构造器名字 } else if (obj.callee) { result = 'Arguments'; //返回构造器名字 } else if (isFinite(obj.length) && obj.item) { result = 'NodeList'; //处理节点集合 } else { result = toString.call(obj).slice(8, -1); } } if (str) { return str === result; } return result; }
然后type方法就轻松了,用toString.call(obj)得出的值作键,直接从映射中取。只有在IE6、IE7、IE8中,我们才费一些周折处理window、document、arguments、nodeList等对象。当然,这只是在种子模块的情形,在语言模块,mass Framework还是会添加isArray、isFunction这两个著名API,此外还有isPlainObject、isNative、isEmptyObject、isArrayLike这4个方法,在选择器模块,还追加isXML方法。
基于实用主义,我们有时不得不妥协。百度的tangram就是典型, 与EXT一样,能想到的都写上,而且判定非常严谨。
baidu.isDate = function(o) { return {}.toString.call(o) === "[object Date]" && o.toString() !== 'Invalid Date' && !isNaN(o); } baidu.isNumber = function(o) { return '[object Number]' == {}.toString.call(o) && isFinite(o); }