underscore 源码解读:JS 中如何判断两个元素是否“相同”

Why underscorephp

最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了个人 2016 计划中。git

阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到不少。为何是 underscore?最主要的缘由是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,很是适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不只能够学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也能够学到变量类型判断、函数节流&函数去抖等经常使用的方法,还能够学到不少浏览器兼容的 hack,更能够学到做者的总体设计思路以及 API 设计的原理(向后兼容)。github

以后楼主会写一系列的文章跟你们分享在源码阅读中学习到的知识。canvas

欢迎围观~ (若是有兴趣,欢迎 star & watch~)您的关注是楼主继续写做的动力框架

_.isEqualecmascript

本文跟你们聊聊 JavaScript 中如何判断两个参数 “相同”,即 underscore 源码中的 _.isEqual 方法。这个方法能够说是 underscore 源码中实现最复杂的方法(用了百来行),几乎没有之一。ide

那么,我说的 “相同” 究竟是什么意思?举个栗子,1 和 new Number(1) 被认为是 equal,[1] 和 [1] 被认为是 equal(尽管它们的引用并不相同),固然,两个引用相同的对象确定是 equal 的了。函数

那么,如何设计这个 _.isEqual 函数呢?咱们跟着 underscore 源码,一步步来看它的实现。后文中均假设比较的两个参数为 a 和 b。

首先咱们判断 a === b,为 true 的状况有两种,其一是 a 和 b 都是基本类型,那么就是两个基本类型的值相同,其二就是两个引用类型,那么就是引用类型的引用相同。那么若是 a === b 为 true,是否就是说 a 和 b 是 equal 的呢?事实上,99% 的状况是这样的,还得考虑 0 和 -0 这个 special case,0 === -0 为 true,而 0 和 -0 被认为是 unequal,至于缘由,能够参考 http://wiki.ecmascript.org/doku.php?id=harmony:egal

这部分代码能够这样表示:

// Identical objects are equal. `0 === -0`, but they aren't identical.

// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).

// a === b 时

// 须要注意 `0 === -0` 这个 special case

// 0 和 -0 不相同

// 至于缘由能够参考上面的连接

if (a === b) return a !== 0 || 1 / a === 1 / b;

接下去的状况,也就是 a !== b 的状况了。

若是 a 和 b 中有一个是 null 或者 undefined,那么能够特判下,不用继续比较了。源码实现:

// A strict comparison is necessary because `null == undefined`.

// 若是 a 和 b 有一个为 null(或者 undefined)

// 判断 a === b

if (a == null || b == null) return a === b;

我的以为这里写的有点多余,由于根据上面的判断过滤,a === b 确定是返回 false 的。

ok,咱们继续,接下来咱们能够先根据 a 和 b 的类型来判断,若是类型不同,那么就不必继续判断了。如何获取变量类型?没错,就是神奇的 Object.prototype.toString.call!

若是类型是 RegExp 和 String,咱们能够将 a 和 b 分别转为字符串进行比较(若是是 String 就已是字符串了),举个栗子:

var a = /a/;

var b = new RegExp("a");

console.log(_.isEqual(a, b));  // => true

其实它在 underscore 内部是这样判断的:

var a = /a/;

var b = new RegExp("a");

var _a = '' + a; // => /a/

var _b = '' + b; // => /a/

console.log(_a === _b); // => true

若是是 Number 类型呢?这里又有个 special case,就是 NaN!这里规定,NaN 仅和 NaN 相同,与别的 Number 类型均 unequal。这里咱们将引用类型均转为基本类型,看以下代码:

var a = new Number(1);

console.log(+a); // 1

没错,加个 + 就解决了,其余的不难理解,都在注释里了。

// `NaN`s are equivalent, but non-reflexive.

// Object(NaN) is equivalent to NaN

// 若是 +a !== +a

// 那么 a 就是 NaN

// 判断 b 是否也是 NaN 便可

if (+a !== +a) return +b !== +b;

// An `egal` comparison is performed for other numeric values.

// 排除了 NaN 干扰

// 还要考虑 0 的干扰

// 用 +a 将 Number() 形式转为基本类型

// 若是 a 为 0,判断 1 / +a === 1 / b

// 不然判断 +a === +b

return +a === 0 ? 1 / +a === 1 / b : +a === +b;

// 若是 a 为 Number 类型

// 要注意 NaN 这个 special number

// NaN 和 NaN 被认为 equal

接下来咱们看 Date 和 Boolean 两个类型。跟 Number 类型类似,它们也能够用 + 转化为基本类型的数字!看下面代码:

var a = new Date();

var b = true;

var c = new Boolean(false);

console.log(+a); // 1464180857222

console.log(+b); // 1

console.log(+c); // 0

很是简单,其实 +new Date() (或者也能够写成 +new Date)获取的正是当前时间和 1970 年 1 月 1 日 0 点的毫秒数(millisecond),可能你据说过期间戳,其实时间戳就是这个数据除以 1000,也就是秒数。在用 canvas 作动画时,我常常用 +new Date 来当时间戳。

so,若是 a 和 b 均是 Date 类型或者 Boolean 类型,咱们能够用 +a === +b 来判断是否 equal。

程序接着走,咱们接着看,彷佛还有两类重要的类型没有判断?没错,Array 和 Object!underscore 对此采用递归方法展开来比较。

仍是举个栗子吧,举例比较直观。

假设 a,b 以下:

var a = {name: "hanzichi", loveCity: [{cityName: "hangzhou", province: "zhenjiang"}], age: 30};

var b = {name: "hanzichi", loveCity: [{cityName: "hangzhou", province: "zhenjiang"}], age: 25};

首先 a,b 是对象,咱们能够分别比较其键值对,若是有一个键值对不一样(或者说一个键值对 a 和 b 有一个没有),则 a 和 b unequal。若是是数组呢?那就一个一个元素比较喽。由于数组可能嵌套对象,对象的 value 又多是数组,因此这里用了递归。

仍是以上面的例子,咱们能够把它拆成三次比较,分别比较三个 key 的 value 值是否相同。对于 loveCity 这个 key 的 value,由于其 value 又是个数组,因此咱们将这个 value 传入比较函数,经过这个比较的结果,来判断最后的比较结果。递归就是这样,能够将大的东西,拆成一个个小的,根据小的结果,来汇总获得大的结果。

最后,给出代码位置。关于 _.isEqual 方法的源码,你们能够参考这里。

相关文章
相关标签/搜索