1、为什么要判断?
可能有些同学看到这个标题就会产生疑惑,为什么我们要判断JavaScript中的两个变量是否相等,JavaScript不是已经提供了双等号“==”以及三等号“===”给我们使用了吗?
其实,JavaScript虽然给我们提供了相等运算符,但是还是存在一些缺陷,这些缺陷不符合我们的思维习惯,有可能在使用的时候得到一些意外的结果。为了避免这种情况的出现,我们需要自己函数来实现JavaScript变量之间的对比。
2、JavaScript等号运算符存在哪些缺陷?
2.1 0与-0
在JavaScript中:
0 === 0
//true
+0 === -0
//true
相等运算符认为+0和-0是相等的,但是我们应当认为两者是不等的,具体原因源码中给出了一个链接:Harmony egal proposal.
2.2 null和undefined
在JavaScript中:
null == undefined
//true
null === undefined
//false
我们应当认为null不等于undefined,所以在比较null和undefined时,应当返回false。
2.3 NaN
前文有说过,NaN是一个特殊的值,它是JavaScript中唯一一个自身不等于自身的值。
NaN == NaN
//false
NaN === NaN
//false
但是我们在对比两个NaN时,我们应当认为它们是相等的。
2.4 数组之间的对比
由于在JavaScript中,数组是一个对象,所以如果两个变量不是引用的同一个数组的话,即使两个数组一模一样也不会返回true。
var a = [];
//undefined
var b = [];
//undefined
a=== b
//false
a==b
//false
但是我们应当认为,两个元素位置、顺序以及值相同的数组是相等的。
2.5 对象之间的对比
凡是涉及到对象的变量,只要不是引用同一个对象,都会被认为不相等。我们需要做出一些改变,两个完全一致的对象应当被认为是相等的。
var a = {};
//undefined
var b = {};
//undefined
a == b
//false
a === b
//false
这种情况在所有JavaScript内置对象中也适用,比如我们应当认为两个一样的RegExp对象是相等的。
2.6 基本数据类型与包装数据类型之间的对比
在JavaScript中,数值2和Number对象2是不严格相等的:
2 == new Number(2);
//true
2 === new Number(2);
//false
但是我们在对比2和new Number(2)时应当认为两者相等。
3 underscore的实现方法
我们实现的方法当然还是依赖于JavaScript相等运算符的,只不过针对特例需要有特定的处理。我们在比较之前,首先应该做的就是处理特殊情况。
underscore的代码中,没有直接将逻辑写在_.isEqual方法中,而是定义了两个私有方法:eq和deepEq。在GitHub用户@hanzichi的repo中,我们可以看到1.8.3版本的underscore中并没有deepEq方法,为什么后来添加了呢?这是因为underscore的作者把一些特例的处理提取了出来,放到了eq方法中,而更加复杂的对象之间的对比被放到了deepEq中(同时使得deepEq方法更加便于递归调用)。这样的做法使得代码逻辑更加鲜明,方法的功能也更加单一明确,维护代码更加简洁快速。
eq方法的源代码:
var eq = function (a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php"htmlcode">var deepEq = function (a, b, aStack, bStack) { // Unwrap any wrapped objects. //如果a,b是_的一个实例的话,需要先把他们解包出来再进行比较。 if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. //先根据a,b的Class字符串进行比较,如果两个对象的Class字符串都不一样, //那么直接可以认为两者不相等。 var className = toString.call(a); if (className !== toString.call(b)) return false; //如果两者的Class字符串相等,再进一步进行比较。 //优先检测内置对象之间的比较,非内置对象再往后检测。 switch (className) { // Strings, numbers, regular expressions, dates, and booleans are compared by value. //如果a,b为正则表达式,那么转化为字符串判断是否相等即可。 case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. //如果a, b是字符串对象,那么转化为字符串进行比较。因为一下两个变量: //var x = new String('12'); //var y = new String('12'); //x === y是false,x === y也是false,但是我们应该认为x与y是相等的。 //所以我们需要将其转化为字符串进行比较。 return '' + a === '' + b; case '[object Number]': //数字对象转化为数字进行比较,并且要考虑new Number(NaN) === new Number(NaN)应该要成立的情况。 // `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN. if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. //排除0 === -0 的情况。 return +a === 0 "htmlcode">var a = { name: 'test' }; a['test1'] = a; var b = { name: 'test' }; b['test1'] = b; _.isEqual(a, b); //trueunderscore使用aStack和bStack作比较的代码:
aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; }上面的测试代码中,a、b对象的test1属性都引用了它们自身,这样的对象在比较时会消耗不必要的时间,因为只要a和b的test1属性都等于其某个父对象,那么可以认为a和b相等,因为这个被递归的方法返回之后,还要继续比较它们对应的那个父对象,父对象相等,则引用的对象属性必相等,这样的处理方法节省了很多的时间,也提高了underscore的性能。
4.4 优先级分明,有的放矢
underscore的处理具有很强的优先级,比如在比较数组对象时,先比较数组的长度,数组长度不相同则数组必定不相等;比如在比较基本对象时,优先比较对象键的数目,键数目不等则对象必定不等;比如在比较两个对象参数之前,优先对比Object.prototype.toString返回的字符串,如果基本类型不同,那么两个对象必定不相等。
这样的主次分明的对比,大大提高了underscore的工作效率。所以说每一个小小的细节,都可以体现出作者的处心积虑。阅读源码,能够使我们学习到太多的东西。
5 underscore的缺陷之处
我们可以在其他方法中看到underscore对ES6中新特征的支持,比如_.is[Type]方法已经支持检测Map(_.isMap)和Set(_.isSet)等类型了。但是_.isEqual却没有对Set和Map结构的支持。如果我们使用_.isEqual比较两个Map或者两个Set,总是会得到true的结果,因为它们可以通过所有的检测。
在underscore的官方GitHub repo上,我看到有同学已经提交了PR添加了_.isEqual对Set和Map的支持。
我们可以看一下源码:
var size = a.size; // Ensure that both objects are of the same size before comparing deep equality. if (b.size !== size) return false; while (size--) { // Deep compare the keys of each member, using SameValueZero (isEq) for the keys if (!(isEq(a.keys().next().value, b.keys().next().value, aStack, bStack))) return false; // If the objects are maps deep compare the values. Value equality does not use SameValueZero. if (className === '[object Map]') { if (!(eq(a.values().next().value, b.values().next().value, aStack, bStack))) return false; } }可以看到其思路如下:
1 比较两参数的长度(或者说是键值对数),长度不一者即为不等,返回false。
2 如果长度相等,就逐一递归比较它们的每一项,有任意一项不等者就返回false。
3 全部通过则可以认为是相等的,返回true。这段代码有一个很巧妙的地方在于它没有区分到底是Map对象还是Set对象,先直接使用a.keys().next().value以及b.keys().next().value获取Set的元素值或者Map的键。后面再进行类型判断,如果是Map对象的话,再使用a.values().next().value以及b.values().next().value获取Map的键值,Map对象还需要比较其键值是否相等。
个人认为,这段代码也有其局限性,因为Set和Map可以认为是一个数据集,这区别于数组对象。我们可以说[1,2,3]不等于[2,1,3],因为其相同元素的位置不同;但是我认为new Set([1,2,3])应该认为等于new Set([2,1,3]),因为Set是无序的,它内部的元素具有单一性。
获取更多underscore源码解读:GitHub
好了这篇文章就介绍这么多,更多关于js相等操作符请看下面的相关文章。
JavaScript,变量相等
免责声明:本站文章均来自网站采集或用户投稿,网站不提供任何软件下载或自行开发的软件! 如有用户或公司发现本站内容信息存在侵权行为,请邮件告知! 858582#qq.com