获取一个对象的hash值

我们比较两个对象,比较好的一种办法是直接比较两个对象的hash值,对于相同结构和内容的对象,hash值应该相等。怎么获取对象的hash值?

/**
 * 获取一个字符串的hash
 * @param {*} str
 */
function getStringHash(str) {
	let hash = 5381
	let i = str.length

	while(i) {
		hash = (hash * 33) ^ str.charCodeAt(--i)
	}

	return hash >>> 0
}

/**
 * 格式化对象,类似JSON.stringify,但是支持自引用嵌套
 * @param {*} obj
 */
export function stringifyObject(obj) {
	const exists = [obj] // 存储已经处理过的,避免死循环
	const used = [] // 记录被用到的引用标记
	const stringifyObjectByKeys = (obj) => {
		if (isArray(obj)) {
			let items = obj.map((item) => {
				if (item && typeof item === 'object') {
					return stringifyObjectByKeys(item)
				}
				else {
					return JSON.stringify(item)
				}
			})
			let str = '[' + items.join(',') + ']'
			return str
		}

		let str = '{'
		let keys = Object.keys(obj)
		let total = keys.length
		keys.sort()
		keys.forEach((key, i) => {
			let value = obj[key]
			str += key + ':'

			if (value && typeof value === 'object') {
				let index = exists.indexOf(value)
				if (index > -1) {
					str += '#' + index
					used.push(index)
				}
				else {
					exists.push(value)
					let num = exists.length - 1
					str += '#' + num + stringifyObjectByKeys(value)
				}
			}
			else {
				str += JSON.stringify(value)
			}

			if (i < total - 1) {
				str += ','
			}
		})
		str += '}'
		return str
	}
	let str = stringifyObjectByKeys(obj)

	exists.forEach((item, i) => {
		if (!used.includes(i)) {
			str = str.replace(new RegExp(`:#${i}`, 'g'), ':')
		}
	})

	if (used.includes(0)) {
		str = '#0' + str
	}

	return str
}

/**
 * 获取一个对象的hash值
 * @param {*} obj
 */
export function getObjectHash(obj) {
	if (typeof obj !== 'object') {
		return
	}

	let str = stringifyObject(obj)
	let hash = getStringHash(str)
	return hash
}

这里面考虑到了一个非常严重的问题,就是当一个对象如果存在自引用的情况,普通的遍历会引起死循环而导致程序挂掉,但这个函数不会,它会用一个临时变量去记录已经处理过的对象,如果在下一次处理到一个对象时,首先会去临时变量里面查找,如果找到的话,会把这个变量的索引值作为参考系数,直接用于hash计算。