Skip to content

1. 研究

这里问的是两个对象相等,你直接使用 == 或 === 比较的话,比较的是两个对象的引用地址,所以这里问的应该是对象的结构较。 举个例子,下面这种就属于“结构相等”,当然也可以出现多维数组和嵌套对象。

js
let arr1 = ['a', 'b']
let arr2 = ['a', 'b']
let obj1 = {0: 'a', 1: 'b'}
let obj2 = {0: 'a', 1: 'b'}
isEqual(arr1, arr2) // true
isEqual(obj1, obj2) // true
isEqual(arr1, obj1) // false
let arr1 = ['a', 'b']
let arr2 = ['a', 'b']
let obj1 = {0: 'a', 1: 'b'}
let obj2 = {0: 'a', 1: 'b'}
isEqual(arr1, arr2) // true
isEqual(obj1, obj2) // true
isEqual(arr1, obj1) // false

下面来研究一下这个问题,我们知道 JS 中的对象有很多类型,这里我的研究范围是:

  • ArrayPlain
  • Object
  • Map
  • Set
  • Function 其他的类型,有的不可枚举( WeakMap 、WeakSet),有的不好比较(Function,不可能比较函数签名和函数体吧;

还有 Date 等没有比较意义,如果不是同一个引用地址,直接返回 false 了)。

2. 代码

js
// 我们先完成一些类型判断辅助函数,使得我们的分析可以继续。
function isObject(item) {
	// 判断是否为对象类型
	return Object(item) === item
}

function typeOf(item) {
	// 判断所有类型
	return Object.prototype.toString.call(item).slice(8, -1)
}
typeOf([1, 2]) // 'Array'
// 我们的函数签名是 isEqual(item1, item2)
function isEqual(item1, item2) {
	if (!isObject(item1) && !isObject(item2)) {
		// 两个都是基本类型,直接进行严格比较
		if (Number.isNaN(item1) && Number.isNaN(item2)) {
			// 注意特判 isEqual(NaN, NaN)
			return true
		}
		return item1 === item2
	}
	if (!isObject(item1) || !isObject(item2)) {
		// 一个对象,一个基本类型,直接返回 false
		// 对于基本类型和对象类型,为了防止 == 导致的类型转换,直接返回 false
		return false
	}
	// 辅助函数typeOf()
	if (typeOf(item1) !== typeOf(item2)) {
		// 类型不同,直接返回 false
		return false
	}
	// 后面比较的都是类型相同的对象类型
	if (item1 === item2) {
		// 直接比较引用地址,相等则返回 true
		return true
	}
	// 对于 Array 和 Plain Object。所谓 Plain Object 就是 JS 中普通的键值对对象({name: 'peter', age: 20})。
		// 其实数组也可以算是一种特殊普通对象,因为键值都是数字,所以一起使用 for k in item 进行递归处理(考虑多维数组和嵌套对象)。
	if (['Array', 'Object'].includes(typeOf(item1))) {
		//  plain object 和 array,for ... in 比较每一项值
		let l1 = Object.keys(item1).length
		let l2 = Object.keys(item2).length
		if (l1 !== l2) {
			return false
		}
		for (let k in item1) {
			if (!isEqual(item1[k], item2[k])) {
				return false
			}
		}
		return true
	}
	// 最后处理 Map 和 Set 类型,之所以放到一起,是因为可以通过 [...item] 将其转换为数组,然后进行递归比较。
	if (['Map', 'Set'].includes(typeOf(item1))) {
		// 处理 map、set 类型,转换为数组再比较
		const [arr1, arr2] = [
			[...item1],
			[...item2]
		]
		return isEqual(arr1, arr2)
	}
	// 其他的暂时全部返回 fasle,可以理解为没有比较的意义,比如比较两个函数或者两个 Date 对象之类的。
	return false
}
// 我们先完成一些类型判断辅助函数,使得我们的分析可以继续。
function isObject(item) {
	// 判断是否为对象类型
	return Object(item) === item
}

function typeOf(item) {
	// 判断所有类型
	return Object.prototype.toString.call(item).slice(8, -1)
}
typeOf([1, 2]) // 'Array'
// 我们的函数签名是 isEqual(item1, item2)
function isEqual(item1, item2) {
	if (!isObject(item1) && !isObject(item2)) {
		// 两个都是基本类型,直接进行严格比较
		if (Number.isNaN(item1) && Number.isNaN(item2)) {
			// 注意特判 isEqual(NaN, NaN)
			return true
		}
		return item1 === item2
	}
	if (!isObject(item1) || !isObject(item2)) {
		// 一个对象,一个基本类型,直接返回 false
		// 对于基本类型和对象类型,为了防止 == 导致的类型转换,直接返回 false
		return false
	}
	// 辅助函数typeOf()
	if (typeOf(item1) !== typeOf(item2)) {
		// 类型不同,直接返回 false
		return false
	}
	// 后面比较的都是类型相同的对象类型
	if (item1 === item2) {
		// 直接比较引用地址,相等则返回 true
		return true
	}
	// 对于 Array 和 Plain Object。所谓 Plain Object 就是 JS 中普通的键值对对象({name: 'peter', age: 20})。
		// 其实数组也可以算是一种特殊普通对象,因为键值都是数字,所以一起使用 for k in item 进行递归处理(考虑多维数组和嵌套对象)。
	if (['Array', 'Object'].includes(typeOf(item1))) {
		//  plain object 和 array,for ... in 比较每一项值
		let l1 = Object.keys(item1).length
		let l2 = Object.keys(item2).length
		if (l1 !== l2) {
			return false
		}
		for (let k in item1) {
			if (!isEqual(item1[k], item2[k])) {
				return false
			}
		}
		return true
	}
	// 最后处理 Map 和 Set 类型,之所以放到一起,是因为可以通过 [...item] 将其转换为数组,然后进行递归比较。
	if (['Map', 'Set'].includes(typeOf(item1))) {
		// 处理 map、set 类型,转换为数组再比较
		const [arr1, arr2] = [
			[...item1],
			[...item2]
		]
		return isEqual(arr1, arr2)
	}
	// 其他的暂时全部返回 fasle,可以理解为没有比较的意义,比如比较两个函数或者两个 Date 对象之类的。
	return false
}

03. 测试用例

js
// 测试用例
console.log(isEqual(1, '1'))
console.log(isEqual(null, undefined))
console.log(isEqual(100, 100))
console.log(isEqual(NaN, NaN))
console.log(isEqual(NaN, 'aaa'))
console.log(isEqual([1, 2, 3], [1, 2, 3]))
console.log(isEqual([1, 2, 3], [1, 2]))
console.log(isEqual([1, 2, [3, [4, 5, 6]]], [1, 2, [3, [4, 5, 6]]]))
console.log(isEqual({
	a: 'a',
	b: 'b'
}, {
	a: 'a',
	b: 'b'
}))
console.log(isEqual({
	0: 'a',
	1: 'b'
}, ['a', 'b']))

let s1 = Symbol.for('s1'),
	s2 = Symbol.for('s1'),
	s3 = Symbol('s3')
s4 = Symbol('s4')
s5 = Symbol('s4')
console.log(isEqual(s1, s2))
console.log(isEqual(s3, s4))
console.log(isEqual(s4, s5))

console.log(isEqual(add1, add1))
console.log(isEqual(add1, add2))

function add1(a, b) {
	return a + b
}

function add2(a, b) {
	return a + b
}

let map1 = new Map().set(1, 'a').set(2, 'b')
let map2 = new Map().set(1, 'a').set(2, 'b')
let map3 = new Map().set(1, {
	a: 'a'
}).set(2, 'b').set(3, [1, 2])
let map4 = new Map().set(1, {
	a: 'a'
}).set(2, 'b').set(3, [1, 2])
console.log(isEqual(map1, map2))
console.log(isEqual(map3, map4))

let set1 = new Set([1, 2, 2, 3, {
	a: 1
}])
let set2 = new Set([1, 2, 2, 3, {
	a: 1
}])
let set3 = new Set([1, 2, 2, 3, {
	a: 2
}])
console.log(isEqual(set1, set2))
console.log(isEqual(set2, set3))
// 测试用例
console.log(isEqual(1, '1'))
console.log(isEqual(null, undefined))
console.log(isEqual(100, 100))
console.log(isEqual(NaN, NaN))
console.log(isEqual(NaN, 'aaa'))
console.log(isEqual([1, 2, 3], [1, 2, 3]))
console.log(isEqual([1, 2, 3], [1, 2]))
console.log(isEqual([1, 2, [3, [4, 5, 6]]], [1, 2, [3, [4, 5, 6]]]))
console.log(isEqual({
	a: 'a',
	b: 'b'
}, {
	a: 'a',
	b: 'b'
}))
console.log(isEqual({
	0: 'a',
	1: 'b'
}, ['a', 'b']))

let s1 = Symbol.for('s1'),
	s2 = Symbol.for('s1'),
	s3 = Symbol('s3')
s4 = Symbol('s4')
s5 = Symbol('s4')
console.log(isEqual(s1, s2))
console.log(isEqual(s3, s4))
console.log(isEqual(s4, s5))

console.log(isEqual(add1, add1))
console.log(isEqual(add1, add2))

function add1(a, b) {
	return a + b
}

function add2(a, b) {
	return a + b
}

let map1 = new Map().set(1, 'a').set(2, 'b')
let map2 = new Map().set(1, 'a').set(2, 'b')
let map3 = new Map().set(1, {
	a: 'a'
}).set(2, 'b').set(3, [1, 2])
let map4 = new Map().set(1, {
	a: 'a'
}).set(2, 'b').set(3, [1, 2])
console.log(isEqual(map1, map2))
console.log(isEqual(map3, map4))

let set1 = new Set([1, 2, 2, 3, {
	a: 1
}])
let set2 = new Set([1, 2, 2, 3, {
	a: 1
}])
let set3 = new Set([1, 2, 2, 3, {
	a: 2
}])
console.log(isEqual(set1, set2))
console.log(isEqual(set2, set3))