判断JavaScript中的两个变量是否相等的操作符

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?id=harmony:egal).
	//除了0 === -0这个特例之外,其余所有a === b的例子都代表它们相等。
	//应当判断0 !== -0,但是JavaScript中0 === -0。
	//下面这行代码就是为了解决这个问题。
	//当a !== 0或者1/a === 1/b时返回true,一旦a === 0并且1/a !== 1/b就返回false。
	//而a === 0且1/a !== 1/b就代表a,b有一个为0,有一个为-0。
	if (a === b) return a !== 0 || 1 / a === 1 / b;
	//一旦a、b不严格相等,就进入后续检测。
	//a == b成立但是a === b不成立的例子中需要排除null和undefined,其余例子需要后续判断。
	// `null` or `undefined` only equal to itself (strict comparison).
	//一旦a或者b中有一个为null就代表另一个为undefined,这种情况可以直接排除。
	if (a == null || b == null) return false;
	// `NaN`s are equivalent, but non-reflexive.
	//自身不等于自身的情况,一旦a,b都为NaN,则可以返回true。
	if (a !== a) return b !== b;
	// Exhaust primitive checks
	//如果a,b都不为JavaScript对象,那么经过以上监测之后还不严格相等的话就可以直接断定a不等于b。
	var type = typeof a;
	if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
	//如果a,b是JavaScript对象,还需要做后续深入的判断。
	return deepEq(a, b, aStack, bStack);
};

对于源码的解读我已经作为注释写在了源码中。 那么根据源码,可以将其逻辑抽象出来:

deepEq的源码:

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 ? 1 / +a === 1 / b : +a === +b;
		case '[object Date]':
		//Date类型以及Boolean类型都可以转换为number类型进行比较。
		//在变量前加一个加号“+”,可以强制转换为数值型。
		//在Date型变量前加一个加号“+”可以将Date转化为毫秒形式;Boolean类型同上(转换为0或者1)。
		case '[object Boolean]':
			// Coerce dates and booleans to numeric primitive values. Dates are compared by their
			// millisecond representations. Note that invalid dates with millisecond representations
			// of `NaN` are not equivalent.
			return +a === +b;
		case '[object Symbol]':
			return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
	}

	var areArrays = className === '[object Array]';
	//如果不是数组对象。
	if (!areArrays) {
		if (typeof a != 'object' || typeof b != 'object') return false;

		// Objects with different constructors are not equivalent, but `Object`s or `Array`s
		// from different frames are.
		//比较两个非数组对象的构造函数。
		var aCtor = a.constructor, bCtor = b.constructor;
		if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
			_.isFunction(bCtor) && bCtor instanceof bCtor)
			&& ('constructor' in a && 'constructor' in b)) {
			return false;
		}
	}
	// Assume equality for cyclic structures. The algorithm for detecting cyclic
	// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.

	// Initializing stack of traversed objects.
	// It's done here since we only need them for objects and arrays comparison.
	//初次调用eq函数时,aStack以及bStack均未被传递,在循环递归的时候,会被传递进来。
	//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;
	}

	// Add the first object to the stack of traversed objects.
	//初次调用eq函数时,就把两个参数放入到参数堆栈中去,保存起来方便递归调用时使用。
	aStack.push(a);
	bStack.push(b);

	// Recursively compare objects and arrays.
	//如果是数组对象。
	if (areArrays) {
		// Compare array lengths to determine if a deep comparison is necessary.
		length = a.length;
		//长度不等,直接返回false认定为数组不相等。
		if (length !== b.length) return false;
		// Deep compare the contents, ignoring non-numeric properties.
		while (length--) {
			//递归调用。
			if (!eq(a[length], b[length], aStack, bStack)) return false;
		}
	} else {
		// Deep compare objects.
		//对比纯对象。
		var keys = _.keys(a), key;
		length = keys.length;
		// Ensure that both objects contain the same number of properties before comparing deep equality.
		//对比属性数量,如果数量不等,直接返回false。
		if (_.keys(b).length !== length) return false;
		while (length--) {
			// Deep compare each member
			key = keys[length];
			if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
		}
	}
	// Remove the first object from the stack of traversed objects.
	//循环递归结束,把a,b堆栈中的元素推出。
	aStack.pop();
	bStack.pop();
	return true;
};

对于源码的解读我已经作为注释写在了源码中。 那么根据源码,可以将其逻辑抽象出来:

1 使用Object.prototype.toString方法获取两参数类型,如果两参数的原始数据类型都不同,那么可以认为两个参数不相等。
2 如果进入了第二步,那么说明两个参数的原始类型相同。针对获取到的字符串进行分类,如果是除Object和Array之外的类型,进行处理。

RegExp以及String对象转化为字符串进行比较。
Number类型的话,需要先使用+运算符强制转化为基本数据类型中的数值型,然后处理特例。比如NaN === NaN,0 !== -0.
Date以及Boolean对象转化为数字类型进行对比。(+运算符强制转换,Date转化为13位的毫秒形式,Boolean转化为0或1)
Symbol类型使用Symbol.prototype.valueOf获取字符串,然后进行对比(即认为传递给Symbol函数相同字符串所获取到的Symbol对象应该相等)。

3 经过以上比较,所剩类型基本只剩Array和基本对象了。如果不是数组对象,那么构造函数不同的对象可以被认为是不相等的对象。
4 初始化对象栈aStack以及bStack,因为初次调用deepEq函数时不会传递这两个参数,所以需要手动初始化。因为之后比较的数组对象以及基本对象需要用到对象栈,所以现在应该把当前的a,b推入到两个栈中。
5 针对数组,先比较长度,长度不等则数组不等。长度相等再递归调用deepGet比较数组的每一项,有一项不等则返回false。
6 基本对象类型比较,先使用_.keys获取对象的所有键。键数量不同的两对象不同,如果键数目相等,再递归调用deepEq比较每一个键的属性,有一个键值不等则返回false。
7 经过所有检测如果都没有返回false的话,可以认为两参数相等,返回true。在返回之前会把栈中的数据推出一个。

4 underscore的精髓

4.1 将RegExp对象和String对象用相同方法处理

有同学可能会疑惑:/[a-z]/gi与/[a-z]ig/在意义上是一样的,但是转化为字符串之后比较会不会是不相等的?

这是一个非常好的问题,同时也是underscore处理的巧妙之所在。在JavaScript中,RegExp对象重写了toString方法,所以在强制将RegExp对象转化为字符串时,flags会按规定顺序排列,所以将之前两个RegExp对象转化为字符串,都会得到/[a-z]/gi。这就是underscore可以放心大胆的将RegExp对象转化为字符串处理的原因。

4.2 Date对象和Boolean对象使用相同方法处理

underscore选择将Date对象和Boolean对象都转化为数值进行处理,这避免了纷繁复杂的类型转换,简单粗暴。而且作者没有使用强制转换方法进行转换,而是只使用了一个“+”符号,就强制将Date对象和Boolean对象转换成了数值型数据。

4.3 使用对象栈保存当前比较对象的上下文

很多童鞋在阅读源码时,可能会很疑惑aStack以及bStack的作用在哪里。aStack和bStack用于保存当前比较对象的上下文,这使得我们在比较某个对象的子属性时,还可以获取到其自身。这样做的好处就在于我们可以比较循环引用的对象。

var a = {
  name: 'test'
};
a['test1'] = a;
var b = {
  name: 'test'
};
b['test1'] = b;
_.isEqual(a, b);
//true

underscore使用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相等操作符请看下面的相关文章。

(0)

相关推荐

  • JS之相等操作符详解

    1.相等 == 和不相等 != 先转换操作数再比较相等性 在转换不同的数据类型时,相等和不相等遵循以下规则: 1.1 如果有一个操作数是布尔值,则在比较相等性前先将其转换为数值---false转换为0,true转换为1; 1.2 如果一个操作数是字符串,另一个操作数是数值,则在比较相等性前将字符串转换为数值 1.3 如果一个操作数是对象昂,另一个不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则比较 1.4 null和undefined是相等的 1.5 在比较相等性之前,

  • javascript中的相等操作符(==与===区别)

    1.前言 我们在编程的过程中,经常会遇到判断两个变量是否相等的情况,ECMAscript提供了两种相等操作符"=="和"==="来判断,这两种操作都会返回一个boolean值.一般来说,我们把"=="称之为相等,称"==="为全等.当两相比较的两个变量数据类型一致时,情况比较简单,而当操作符两边的变量类型不一致,甚至其中的某个变量是对象时,情况就比较复杂了,下面分别介绍当操作数类型不同时运算结果会怎么样. 2.全等操作符 &q

  • JavaScript中的相等操作符使用详解

    ECMAScript 中的相等操作符由两个等于号 ( == ) 表示,如果两个操作数相等,则返回 true. 相等操作符会先转换操作数(通常称为强制转型),然后比较它们的相等性. 在转换不同的数据类型时,相等操作符遵循下列基本规则: 1. 如果有一个操作数是布尔值,则在比较相等性之前,将其转换为数值: 2. 如果一个操作数是字符串,另一个操作数是数值,在比较之前先将字符串转换为数值: 3. 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面

  • 判断JavaScript中的两个变量是否相等的操作符

    1.为什么要判断? 可能有些同学看到这个标题就会产生疑惑,为什么我们要判断JavaScript中的两个变量是否相等,JavaScript不是已经提供了双等号"=="以及三等号"==="给我们使用了吗? 其实,JavaScript虽然给我们提供了相等运算符,但是还是存在一些缺陷,这些缺陷不符合我们的思维习惯,有可能在使用的时候得到一些意外的结果.为了避免这种情况的出现,我们需要自己函数来实现JavaScript变量之间的对比. 2.JavaScript等号运算符存在哪

  • 详解JavaScript中任意两数加减的解决方案

    目录 写在前面 分析填坑思路 解决整数加减的坑 转换科学计算 解决整数减法的坑 解决小数加法的坑 解决小数减法的坑 解决整数加小数的通用问题 总结 写在前面 本文是从初步解决到最终解决的思路,文章篇幅较长 虽然是一篇从0开始的文章,中间的思维跳跃可能比较大 代码的解析都在文章的思路分析和注释里,全文会帮助理解的几个关键词 1.Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 2.15长度的字符串 3.padStart 和 padEnd 分析填坑思

  • Python实现在一行中交换两个变量

    我们已经讨论了在没有临时变量的情况下交换两个整数的不同方法.如何在不使用库函数的情况下换成一行? 1) Python: 在Python中,有一个简单且语法简洁的结构来交换变量,我们只需要写“x, y = y, x”. 2)C/C++: 下面是一个普遍提供的经典解决方案: // 使用按位异或交换(C/C++ 中的错误解决方案) x ^= y ^= x ^= y; 上述解决方案在 C/C++ 中是错误的,因为它会导致未定义的行为(编译器可以自由地以任何方式运行).原因是,如果修改之间没有序列点,则在

  • javascript中使用未定义变量或值的情况分析

    本文实例讲述了javascript中使用未定义变量或值的情况.分享给大家供大家参考,具体如下: javascript里面一般不能使用未定义的值,但是下面几种情况除外: 1. 赋值语句中: a=9; alert(a) //9 赋值语句中需要赋值的变量没定义会先定义,再赋值.另外从 a=b=c=8 不报错可以看出赋值语句是从右向左执行的. 2. for in语句中: for(key in {name:'goofy'}){ alert(key) //"name" } alert(key) /

  • JavaScript中Hoisting详解 (变量提升与函数声明提升)

    本文主要给大家介绍了关于JavaScript中Hoisting(变量提升与函数声明提升)的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 如何将 函数声明 / 变量 "移动" 到作用域的顶部. 术语 Hoisting(提升) 在很多 JavaScript 博文中被用来解释标识符的解析.其实 Hoisting(提升) 这个词是用来解释 变量 和 函数声明 是如何被提升到 函数或全局 作用域顶部的.你在任何的 JavaScript 文档中找不到这个术语,我们说的

  • 用最简单的方法判断JavaScript中this的指向(推荐)

    都说 JavaScript 是一种很灵活的语言,这其实也可以说它是一个混乱的语言.它把 函数式编程 和 面向对象编程 糅合一起,再加上 动态语言 特性,简直强大无比(其实是不能和C++比的,^_^ ). JS 里的 this 在 function 内部被创建 指向调用时所在函数所绑定的对象(拗口) this 不能被赋值,但可以被 call/apply  改变 目录 * 一个特例 * 开始判断     * 法则一:对象方法中的this指向对象本身(箭头函数形式的除外)     * 法则二:多层嵌套

  • 分析 JavaScript 中令人困惑的变量赋值

    Javascript是一门弱类型的语言,声明变量不需要声明其类型,var x 就可以等于任何类型的值. 比如: var str = "string...."; var arr = ["this","is","array"]; var obj = {name:"caizhongqi",age:26,sex:"male"}; 这些都是正确的,这似乎非常简单方便,但是这种方便也会带来一些令人难

  • 举例讲解如何判断JavaScript中对象的类型

    在编写js程序的过程中,可能常常需要判断一个对象的类型,例如你写了一个函数,需要通过判断不同的参数类型来编写不同的代码. 首先可能会想到typeof操作符,看下面的这个例子: <script type="text/javascript"> var object = {}; var b = true; alert(typeof object + " " + typeof b); </script> 得到的结果如下: 从上面的结果可以看到,用ty

  • Java中==符号与equals()的使用详解(测试两个变量是否相等)

    Java 程序中测试两个变量是否相等有两种方式:一种是利用 == 运算符,另一种是利用equals()方法. 当使用 == 来判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同),则只要两个变量的值相等,就返回true. 但是对于两个引用类型变量,只有它们指向同一个对象时, == 判断才会返回true. == 不可用于比较类型上没有父子关系的两个对象. 很多书上说equals()方法是判断两个对象的值相等.这种说法不准确.实际上equals()方法是O

  • JavaScript中的变量定义与储存介绍

    与C.Java等编程语言不同,JavaScript中的变量是无类型的,所有的变量定义使用的关键词均为var: 复制代码 代码如下: var a; var m, n; var x=42, y="test"; 如果定义变量后未对该变量进行赋值,那么该变量的值为undefined.如上面代码中的a.m.n三个变量的值均为undefined. 由于JS中变量是无类型的,因此完全可以对同一个变量进行不同类型的赋值,如: 复制代码 代码如下: var b = "temp"; c

随机推荐