Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lessfish opened this issue Jun 21, 2016 · 6 comments
Open

Comments

@lessfish
Copy link
Owner

lessfish commented Jun 21, 2016

这篇文章并不在我的 underscore 源码解读计划中,直到 @pod4g 同学回复了我的 issue(详见 https://github.com/hanzichi/underscore-analysis/issues/2#issuecomment-227361035)。其实之前也有同学提出 isNaN 有 native 的 function,正好借此文辨析下几个常见的概念、方法,她们是 NaN,Number.NaN,isNaN,Number.isNaN,以及 underscore 中的 _.isNaN,顺便揪出了一个 BUG。

顺便安利,完整的 underscore 源码解读系列文章请戳 https://github.com/hanzichi/underscore-analysis

NaN & Number.NaN

ok,首先来了解下 NaN 和 Number.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.isNaN

接着来聊 isNaN 和 Number.isNaN 俩方法。

我们都知道,虽然 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.isNaN 和 isNaN 有何区别呢?和全局函数 isNaN() 相比,该方法不会强制将参数转换成数字,只有在参数是真正的数字类型,且值为 NaN 的时候才会返回 true。

isNaN = function(value) {
    Number.isNaN(Number(value));
}

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

值得注意的是,Number.isNaN 是 ES6 引入的,可以用上面的 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.

文档指出,_.isNaN 和 native 的 isNaN 并不一样,必须是个 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。其实能通过这个函数的,只有两个值,NaN 和 new Number(NaN)(当然还有 Number.NaN,前面说了,NaN 和 Number.NaN 是一样的东西,下同)。

而能通过 Number.isNaN 函数的只有 NaN。(Number.isNaN(new Number(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 https://github.com/jashkenas/underscore/issues/749。该 issue 认为,_.isNaN(new Number(NaN)) 应该返回 true。

我们可以看下再之前的版本对于 _.isNaN 的实现(jashkenas/underscore@6ebb43f9b3ba88cc0cca712383534619b82f7e9b):

_.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 吧?

@lessfish lessfish changed the title 你可能不知道的 NaN 以及 underscore 1.8.3 _.NaN 的一个 BUG 你可能不知道的 NaN 以及 underscore 1.8.3 _.isNaN 的一个 BUG Jun 21, 2016
@pod4g
Copy link

pod4g commented Jun 22, 2016

然后补充一下ES规范中,关于NaN的部分。这个才是最权威的。哈哈。

关于isNaN函数 从规范可看到,调用这个函数先会执行toNumber,这个就是所谓的转型。顾名思义,就是把isNaN的参数先toNumber一下。。

关于toNumber的说明和规定

英语渣,暂没有找到中文版的ES6规范。想看看ES6中关于Number.isNaN的说明

@juemin90
Copy link

juemin90 commented Jul 25, 2016

在lodash3.10.1版本中,.isNaN(new Number(0))会返回false,.isNaN(new Number(NaN))会返回true,这应该是解决了bug的结果吧?

@lessfish
Copy link
Owner Author

@juemin90 返回是正确的,不过 lodash 不熟,以前有 bug?

@wiekern
Copy link

wiekern commented Jan 1, 2017

_.isNaN(new Number(0)) 应该返回 false. 现在修复了,见 issue jashkenas/underscore#2257

@aleen42
Copy link

aleen42 commented Feb 25, 2017

哈哈哈哈,找到原因了。若是使用 obj !== +objnew Number(1) 照样会出现问题:

var a = new Number(1);
a; /** => Number {[[PrimitiveValue]]: 1} */
+a; /** => 1 */
a !== +a; /** => true */

+操作符会把 Number 类型转换成字面值

@nightn
Copy link

nightn commented Jan 23, 2018

ES6 的 Number.isNaN 和目前 underscore 的 _.isNaN 除了对 new Number(NaN) 返回结果不一致外,对于 new Object(NaN) 的返回结果也不一致。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants