《JavaScript框架设计》——1.4 类型的判定

简介:

本节书摘来自异步社区《JavaScript框架设计》一书中的第1章,第1.4节,作者:司徒正美 更多章节内容可以访问云栖社区“异步社区”公众号查看。

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);
}
相关文章
|
1月前
|
JavaScript 前端开发 开发者
如何在 JavaScript 中处理不同类型的错误?
【10月更文挑战第29天】通过对不同类型错误的准确识别和恰当处理,可以提高JavaScript程序的可靠性和稳定性,减少错误对程序运行的影响。
|
2月前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
56 0
|
1月前
|
JavaScript 前端开发 Java
除了 JavaScript,还有哪些编程语言支持 Set 类型
【10月更文挑战第30天】这些编程语言中的 `Set` 类型虽然在语法和具体实现细节上有所不同,但都提供了类似的集合操作功能,方便开发者在不同的编程场景中处理集合相关的数据和逻辑。
|
1月前
|
存储 JavaScript 前端开发
js的基础类型和引用类型
【10月更文挑战第29天】理解 JavaScript 中的基础类型和引用类型的区别对于正确地编写代码和理解程序的行为非常重要。在实际开发中,需要根据具体的需求合理地选择和使用不同的数据类型,以避免出现一些意想不到的错误和问题。同时,在处理引用类型数据时,要特别注意对象的引用关系,避免因共享引用而导致的数据不一致等问题。
|
2月前
|
JavaScript 前端开发
JavaScript返回判断类型有哪些?
JavaScript返回判断类型有哪些?
33 0
|
4月前
|
缓存 JavaScript 前端开发
|
4月前
|
JavaScript 前端开发
在JavaScript如何确认数据的类型?
# `typeof` 与 `instanceof` 数据类型判断 `typeof` 操作符用于确定变量的基本数据类型,例如: - "string" - "number" - "boolean" - "undefined" 但对于引用类型如对象和数组,包括 `null`,它返回 "object"。 `instanceof` 用于检查对象是否为特定构造函数的实例,返回布尔值。它能准确识别数组等复杂类型,通过检查对象的原型链来确定其是否属于某个构造函数的实例。 两者结合使用可全面判断数据类型。
36 2
|
4月前
|
JavaScript 前端开发 UED
探秘 JavaScript 错误背后的真相——揭开异常类型的神秘面纱,让你的代码从此无懈可击!
【8月更文挑战第23天】本文深入探讨了JavaScript中常见的异常类型,包括`ReferenceError`(未定义的引用)、`TypeError`(类型错误)、`SyntaxError`(语法错误)、`RangeError`(范围错误)、`EvalError`(评估错误)以及`URIError`(URI错误),并通过示例展示了如何有效地诊断与处理这些异常。此外,还介绍了如何自定义错误类以适应特定场景的需求。掌握这些异常处理技巧对于构建稳定可靠的Web应用程序至关重要。
43 0
|
4月前
|
JavaScript 前端开发 安全
TypeScript:解锁JavaScript的超级英雄模式!类型系统如何化身守护神,拯救你的代码免于崩溃与混乱,戏剧性变革开发体验!
【8月更文挑战第22天】TypeScript作为JavaScript的超集,引入了强大的类型系统,提升了编程的安全性和效率。本文通过案例展示TypeScript如何增强JavaScript:1) 显式类型声明确保函数参数与返回值的准确性;2) 接口和类加强类型检查,保证对象结构符合预期;3) 泛型编程提高代码复用性和灵活性。这些特性共同推动了前端开发的标准化和规模化。
62 0
|
4月前
|
JavaScript 前端开发
javascript 异常问题之JavaScript中的异常有哪些类型,可以举例说明吗
javascript 异常问题之JavaScript中的异常有哪些类型,可以举例说明吗