解析underscore源码-常用类型判断以及一些有用的工具方法

2016-10-09

2016-10-09 摘录自 - 常用类型判断以及一些有用的工具方法

类型判断

我们先说个老生常谈的问题,javascript 中数组类型的判断方法,事实上,我在 Javascript中判断数组的正确姿势 一文中已经详细分析了各种判断方式的优缺点,并给出了正确的代码:

function isArray(a){
    Array.isArray ? Array.isArray(a) : Object.prototype.toString.call(a) === '[object Array]';
}

underscore 其实也正是这么做的:

// Is a given value an array?
// Delegates to ECMA5`s native Array.isArray
// 判断是否为数组
_.isArray = nativeIsArray || function(obj){
    return toString.call(obj) === '[object Array]';
};

nativeIsArray 正是 ES5Array.isArray 方法,如果支持则优先调用,而 toString 变量就保存了 Object.prototype.toString
如何判断对象? underscore 把类型为 functionobject 的变量都算作对象,当然的去除 null

// Is a given variable an object?
// 判断是否为对象
// 这里对象包括 function  object
_.isObject = function(obj){
    var type = typeof obj;
    return type === 'function' || type === 'object' && !!obj;
};

再看 Arguments,Function,String,Number,Date,RegExp,Error 这些类型的判断,其实都可以用 Object.prototype.toString.call 来判断,所以写在一起了:

// Add some isType methods: isArguments,isFunction,isString,isNumber,isDate,isRegExp,isError.
// 其他类型判断
_.each(['Arguments','Function','String','Number','Date','RegExp','Error'],function(name){
    _['is' + name] = function(obj){
        return toString.call(obj) === '[object ' + name + ']';
    };
});

但是看 isArguments 方法,在 IE<9 下对 arguments 调用 Object.prototype.toString.call ,结果是 [object Object] ,而非我们期望的 [object Arguments] 。咋整?我们可以用该元素是否含有 callee 属性来判断,众所周知, arguments.callee 能返回当前 arguments 所在的函数。

// Define a fallback version of the method in browsers (ahem,IE < 9),where there isn`t any inspectable "Arguments" type.
// _.isArguments 方法在 IE < 9 下的兼容
// IE < 9 下对 arguments 调用 Object.prototype.toString.call 方法结果是 [object Object] 而并非我们期望的 [object Arguments]。
// so 用是否含有 callee 属性来判断
if(!_.isArguments(arguments)){
    _.isArguments = function(obj){
        return _.has(obj,'callee');
    };
}

工具类型判断方法

接下来看下一些常用的工具类型判断方法
判断一个元素是否是 DOM 元素,非常简单,只需要保证他不为空,且 nodeType 属性为1:

// Is a given value a DOM element?
// 判断是否为 DOM 元素
_.isElement = function(obj){
    // 确保 obj 不是 null
    // 并且 obj.nodeType === 1
    return !!(obj && obj.nodeType === 1);
};

如何判断一个元素为 NaN ? NaN 其实是属于 Number 类型,Object.prototype.toString.call(NaN) 返回的是 [object Number] ,而且 NaN 不等于本身,利用这两点即可判断(该实现有BUG,下面有详细分析):

// Is the given value 'NaN' ? (NaN is the only number which does not equal itself).
// 判断是否为 NaN
// NaN 是唯一一个 '自己不等于自己' 的 number 类型
_.isNaN = function(obj){
    return _.isNumber(obj) && obj !=== +obj
};

你可能不知道的 NaN 以及 underscore 1.8.3 _.isNaN 的一个 BUG

NaN & Number.NaN

ok,首先来了解下 NaNNumber.NaN 两个属性。
全局属性 NaN 表示 Not-A-Number 的值,顾名思义,就是表示 不是一个数字
在编码中很少直接使用 NaN 。通常都是在计算失败时,作为 Math 的某个方法的返回值出现的(例如:Math.sqrt(-1) 或者尝试将一个字符串解析成数字但是失败了的时候(例如: parseInt("blabla")))。这样做的好处是,不会抛出错误,只需要在下一步的运算中判断上个步骤的运算结果是否是 NaN 即可 。
接着来看 Number.NaN ,这货和 NaN 完全一样。其实,归根结底这俩货都是属于 Number 类型:

Object.prototype.toString.call(NaN); // "[object Number]"
Object.prototype.toString.call(Number.NaN); // "[object Number]"

isNaN & Number.NaN

接着来聊 isNaNNumber.NaN 俩方法。
我们都知道,虽然 NaN 作为 Number 类型,但是他不等于自己, NaN == NaN 或则 NaN === NaN 都会返回 false ,那么怎么检测一个 NaN 值呢?答案大家都知道了,isNaN 方法。

isNaN(NaN); // true
isNaN(undefined); // true
isNaN({}); // true
isNaN("abc"); // true

好多东西传入 isNaN 的结果都是 true ,并不是只是 NaN ,为什么?因为参数会先被强制转换成 Number 类型,然后再进行判断。

Number(NaN); // NaN
Number(undefined); // NaN
Number({}); // NaN
Number("abc"); //NaN

ok,强制转换后其实都变成了 NaN
那么 Number.isNaNisNaN 有何区别呢? 和全局函数 isNaN() 相比,该方法不会强制将参数转换成数字,只有在参数是真正的数字类型,且值为 NaN 的时候才会返回 true

isNaN = function(value){
    Number.isNaN(Number(value));
};
Number.isNaN = Number.isNaN || function(value){
    return typeof value === "number" && isNaN(value);
};

值得注意的是,Number.isNaNES6 引入的,可以用上面的 Polyfill

_.isNaN

最后来看看 underscore 对于 _.isNaN 的实现。
写代码首先的看需求,我们先看看 _.isNaN 的作用,查阅 API 文档 http://underscorejs.org/#isNaN:

this is not the same as the native isNaN function, which will also return true for many other not-number values,such as undefined.
文档指出, _.isNaNnativeisNaN 并不一样,必须是个 Number 类型才可能返回true ,等等,似乎和 Number.isNaN 一样?且慢下结论。
我们来看看 edge 版本对其的实现 https://github.com/jashkenas/underscore/blob/master/underscore.js

// Is the given value 'NaN'?
_.isNaN = function(obj){
    return _.isNumber(obj) && isNaN(obj);
};

obj得是个 Number 类型,并且能通过 isNaN 函数的判断,才能返回 true 。其实能通过这个函数的,只有两个值,NaNnew Numer(NaN) (当然还有 Number.NaN ,前面说了,NaNNumber.NaN 是一样的东西,下同)。
而能通过 Number.isNaN 函数的只有 NaN 。(Number.isNaN(new Numner(NaN)) 会返回 false
但是1.8.3其实是这样实现的:

_.isNaN = function(obj){
    return _.isNumber(obj) && obj !=== +obj;
};

其实这是由BUG的,很显然 new Number(0) 并不应该是 Not-A-Number

_.isNaN(new Number(0)); // true

为什么会这样写?这引发了我的好奇,找了下历史记录,是为了修复这个 issue jashkenas/underscore#749
issue 认为,_.isNaN(new Number(NaN) 应该返回 true
我们可以看下再之前的版本对于 _.isNaN 的实现(jashkenas/underscore@6ebb43f):

_.isNaN = function(obj){
    return obj !=== obj;
};

我又翻了下当时的测试数据(https://github.com/jashkenas/underscore/blob/6ebb43f9b3ba88cc0cca712383534619b82f7e9b/test/objects.js),发现当时没有类似 new Number(0) 的测试数据(现在已经有了)。

总结

对于NaN 的判断,如果只针对Number 类型,用underscore 最新版的_.isNaN判断完全没有问题,或者用ES6的Number.isNaN ,两者的区别就在于一个new Number(NaN),不过话又说胡来,没人会这么蛋疼的去这样 new 一个 NaN吧?