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

简介: 然后type方法就轻松了,用toString.call(obj)得出的值作键,直接从映射中取。只有在IE6、IE7、IE8中,我们才费一些周折处理window、document、arguments、nodeList等对象。

本节书摘来自异步社区《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);
}
相关文章
|
Web App开发 机器学习/深度学习 JavaScript
|
JavaScript 前端开发
JavaScript类型判断
JavaScript类型判断
115 0
JavaScript类型判断
|
JavaScript 前端开发
JavaScript专题之类型判断(下)
JavaScript专题系列第五篇,讲解更加复杂的类型判断,比如 plainObject、空对象、类数组对象、Window对象、DOM 元素等
151 0
JavaScript专题之类型判断(下)
|
JSON JavaScript 前端开发
JavaScript专题之类型判断(上)
JavaScript专题系列第四篇,讲解类型判断的各种方法,并且跟着 jQuery 写一个 type 函数。
101 0
JavaScript专题之类型判断(上)
|
JavaScript 前端开发 存储
|
2月前
|
JavaScript
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
27 0
|
2月前
|
消息中间件 Web App开发 JavaScript
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
70 0
|
4天前
|
JavaScript 前端开发 应用服务中间件
node.js之第一天学习
node.js之第一天学习
|
1月前
|
运维 JavaScript 前端开发
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!