如何实现一个全等函数(JavaScript)

100 阅读7分钟

如何才算相等

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

参(chao)考(xi)underscore 的实现

测试用例

虽然函数还没完成,但是得先把饼画出去,不然怎么招商引资?

const { eq } = require('./eq.js')
const runTests = (tests, message) => {
  describe(message, function() {
    tests.forEach(([args, res], i) => {
      let argMsg = ''
      try {
        argMsg = args.toString()
      } catch (error) {
        
      }
      test(`${i}-${argMsg}`, () => {
        expect(eq(...args)).toEqual(res)
      });
    })
  })
}
const Tests = [
  {
    tests: [
      [[0, 0], true],
      [[0, -0], false],
      [[1, 1], true],
      [[-1, 1], false],
      [[-1, -1], true],
      [[1, new Number(1)], true],
      [[-1, new Number(-1)], true],
      [[NaN, NaN], true],
      [[NaN, 1], false],
      [['1', 1], false],
      [[true, 1], false],
      [[false, 1], false],
      [[null, 1], false],
      [[{}, 1], false],
      [[[], 1], false],
      [[function(){}, 1], false],
      [[undefined, 1], false],
      [[new String(1), 1], false],
      [[new Boolean(1), 1], false],
      [[new String('1'), 1], false],
      [[new Boolean(true), 1], false],
    ],
    message: 'number'
  },
  {
    tests: [
      [['', ''], true],
      [['1', ''], false],
      [['1', '1'], true],
      [[true, ''], false],
      [[false, ''], false],
      [[-1, '-1'], false],
      [[1, '1'], false],
      [[NaN, 'NaN'], false],
      [[new String('NaN'), 'NaN'], true],
      [[{}, ''], false],
      [[[], ''], false],
      [[function(){}, ''], false],
      [[null, ''], false],
      [[undefined, ''], false],
      [[new Boolean('1'), '1'], false],
      [[new Boolean(true), '1'], false],
    ],
    message: 'string'
  },
  {
    tests: [
      [[/a/, /a/], true],
      [[/a/ig, /a/], false],
      [[/a/, new RegExp('a')], true],
      [[/a/i, new RegExp('a', 'i')], true],
      [[/a/ig, new RegExp('a', 'ig')], true],
    ],
    message: 'RegExp'
  },
  {
    tests: [
      [[new Boolean(true), new Boolean(true)], true],
      [[new Boolean(false), new Boolean(true)], false],
      [[true, true], true],
      [[true, false], false],
      [[true, new Boolean(true)], true],
      [[false, new Boolean(false)], true],
      [[true, new Boolean(false)], false],
      [[undefined, new Boolean(false)], false],
      [[null, new Boolean(false)], false],
      [[[], new Boolean(false)], false],
      [[{}, new Boolean(false)], false],
      [[function(){}, new Boolean(false)], false],
      [[new String('false'), new Boolean(false)], false],
      [[new Number('false'), new Boolean(false)], false],
    ],
    message: 'boolean'
  },
  {
    tests: [
      [[undefined, null], false],
      [[undefined, undefined], true],
      [[null, null], true],
      [[new Boolean(null), null], false],
      [[new String(null), null], false],
      [[new Number(null), null], false],
      [[new Boolean(undefined), undefined], false],
      [[new String(undefined), undefined], false],
      [[new Number(undefined), undefined], false],
      [[[], undefined], false],
      [[{}, undefined], false],
      [[function(){}, undefined], false],
      [[0, undefined], false],
      [['', undefined], false],
      [['0', undefined], false],
      [[0, null], false],
      [['', null], false],
      [['0', null], false],
      [[NaN, null], false],
      [[NaN, undefined], false],
    ],
    message: 'undefined null'
  },
  {
    tests: (function() {
      var s1 = Symbol.for(1)
      var s2 = Symbol.for(1);
      return [
        [[Symbol(1), Symbol(1)], false],
        [[Symbol('1'), Symbol('1')], false],
        [[Symbol('1'), Symbol(1)], false],
        [[s1, s2], true],
      ]
    })(),
    message: 'Symbol'
  },
  {
    tests: (function() {
      var f1 = function(){}
      var f2 = () => {};
      return [
        [[f1, function(){}], false],
        [[f2, () => {}], false],
        [[f1, f1], true],
        [[f2, f2], true],
        [[f2, f1], false],
      ]
    })(),
    message: 'function'
  },
  {
    tests: [
      [[new Date(), new Date()], true],
      [[new Date(), new Date(1)], false],
    ],
    message: 'date'
  },
  {
    tests: (function() {
      function A(a){
        this.a = a
      }
      function B(a){
        this.a = a
      }
      var a = new A(1)
      var b = new A(1)
      var d = new A(2)
      var c = new B(2)
      var e = {
        a: 1,
        e
      }
      var f = {
        a: 1,
        f
      }
      var g = {
        a: 1,
        f
      }
      return [
        [[a, b], true],
        [[a, c], false],
        [[a, d], false],
        [[{}, []], false],
        [[{}, {}], true],
        [[{a: 1}, {a: 1}], true],
        [[{a: 2}, {a: 1}], false],
        [[{a: 1, b: {c : 1, d: 2}}, {a: 1, b: {c : 1, d: 2, }}], true],
        [[[{a: 1, b: {c : 1, d: 2}}], [{a: 1, b: {c : 1, d: 2, }}]], true],
        [[{a: 1, b: {c : 1, d: 2}}, {a: 1, b: {c : 1, d: 2, e:1 }}], false],
        [[e, f], true],
        [[e, g], true],
        [[[1,2,3], [1,2,3]], true],
      ]
    })(),
    message: 'object'
  }
]

Tests.forEach(({tests, message}) => runTests(tests, message))

开始写 eq 函数

function eq(a, b){
  
}

module.exports = {
    eq
}

image.png

基本类型比较部分

function eq(a, b){
  // 处理 -0 和 +0 的情况和相同的基本类型的比较
  if(a === b) return a !== 0 || 1 / a === 1 / b
  // 处理 NaN
  if(a !== a) return b !== b
  // 处理 null 的情况
  if(a == null || b == null) return false
  // 过滤 a 或 b 中存在一个基本类型的情况
  var type = typeof a
  if(type !== 'function' && type !== 'object' && typeof b !== 'object') return false
  // 开始对象类型的比较
  return deepEqual(a, b)
}

这里主要着重处理了 -0NaN 的情况

-0 & 0

这个显然是不相等的,但是在JavaScript中是相等的

image.png

真的要研究为什么会有 -0 是因为底层存储使用的是带符号位的二进制存储 number 类型,-0 则是符号位为 10 的情况,+0 则为 00,符号位为 0

对象类型该如何比较?

function deepEqual(a, b) {
  var typeA = Object.prototype.toString.call(a)
  var typeB = Object.prototype.toString.call(b)
  // 类型都不一样,没法相爱
  if(typeA !== typeB) return false
  // 通过隐式类型转换处理为基本类型再比较
  switch(typeA) {
    // 处理 Number() new Number() 的情况
    case '[object Number]':
      // + new Number(1) === + new Number(1).valueOf()
      if(+a !== +a) return +b !== +b
      if(+a === +b) return 1 / a === 1 / b
  }
}

此时已经通过 number 的测试用例辣

image.png

接着处理 new String()new RegExp() 这种情况

function deepEqual(a, b) {
  var typeA = Object.prototype.toString.call(a)
  var typeB = Object.prototype.toString.call(b)
  // 类型都不一样,没法相爱
  if(typeA !== typeB) return false
  // 通过隐式类型转换处理为基本类型再比较
  switch(typeA) {
    // 处理 Number() new Number() 的情况
    case '[object Number]':
      // + new Number(1) === + new Number(1).valueOf()
      if(+a !== +a) return +b !== +b
      if(+a === +b) return 1 / a === 1 / b
    case '[object RegExp]':
    case '[object String]':
      // '' + new RegExp() === '' + new RegExp().toString()
      // '' + new String() === '' + new String().toString()
      return '' + a === '' + b
  }
}

image.png

以同样的思路处理 new Boolean()new Date() 的情况

image.png

顺手处理下 Symbol 类型

function deepEqual(a, b) {
  var typeA = Object.prototype.toString.call(a)
  var typeB = Object.prototype.toString.call(b)
  // 类型都不一样,没法相爱
  if(typeA !== typeB) return false
  // 通过隐式类型转换处理为基本类型再比较
  switch(typeA) {
    // 处理 Number() new Number() 的情况
    case '[object Number]':
      // + new Number(1) === + new Number(1).valueOf()
      if(+a !== +a) return +b !== +b
      if(+a === +b) return 1 / a === 1 / b
    case '[object RegExp]':
    case '[object String]':
      // '' + new RegExp() === '' + new RegExp().toString()
      // '' + new String() === '' + new String().toString()
      return '' + a === '' + b
    case '[object Boolean]':
    case '[object Date]':
      return +a === +b
    case '[object Symbol]':
      return Symbol.prototype.valueOf.call(a) === Symbol.prototype.valueOf.call(b)
  }
}

image.png

处理 function

...
  // 到这里还剩下三种类型,Array || Object || function
  var isArray = typeA === '[object Array]';
  if(!isArray) {
    // 这里过滤 function 类型
    if(typeof a !== 'object') return false
  }
...

image.png

  // 到这里还剩下三种类型,Array || Object || function
  var isArray = typeA === '[object Array]';
  if(!isArray) {
    // 这里过滤 function 类型
    if(typeof a !== 'object') return false
    // 这里需要判断对象类型是否是同一个构造器
    var aCtor = a.constructor
    var bCtor = b.constructor
    if(
      aCtor !== bCtor &&
      typeof aCtor !== 'function' &&
      aCtor instanceof aCtor &&
      typeof bCtor !== 'function' &&
      bCtor instanceof bCtor &&
      ('constructor' in a && 'constructor' in b)
    ){
      return false
    }
  }
  // 到这里已经是要么是数组,要么是同类型的实例对象

接下去该怎么对数组/对象的每一项进行比较呢?显然得考虑深度问题,所以得引入递归

function eq(a, b, aStack, bStack) {
  ...
  return deepEqual(a, b, aStack, bStack)
}

function deepEqual(a, b, aStack, bStack) {
  ...
  // 到这里已经是要么是数组,要么是同类型的实例对象
  aStack = aStack || []
  bStack = bStack || []
}

通过递归每一项去比对,通过栈来管理深度问题

  // 到这里已经是要么是数组,要么是同类型的实例对象
  aStack = aStack || []
  bStack = bStack || []
  // 优先处理循环引用的问题
  var len = aStack.length
  while (len--) {
    if(aStack[len] === a) return bStack[len] === b
  }
  // 不存在循环引用,将a b 分别入栈
  aStack.push(a)
  bStack.push(b)
  if(isArray){
    var len = a.length
    // 个数不同,不能恋爱
    if(len !== b.length) return false
    while(len--){
      // 详细对每一项进行比对
      if(!eq(a[len], b[len], aStack, bStack)) return false
    }
  }else{
    // 这里偷懒用 Object.keys
    var keys = Object.keys(a)
    var len = keys.length
    // 个数不同,不能恋爱
    if(len !== Object.keys(b).length) return false
    while(len--){
      // 详细对每一项进行比对
      if(!eq(a[keys[len]], b[keys[len]], aStack, bStack)) return false
    }
  }
  // 比对结束了,记得出栈!
  aStack.pop()
  bStack.pop()
  // 大功告成
  return true

image.png

完整代码

/**
 * 
 * @param {any} a 
 * @param {any} b 
 * @param {[]} aStack 
 * @param {[]} bStack 
 * @returns Boolean
 */
function eq(a, b, aStack, bStack) {
  // 处理 -0 和 +0 的情况和相同的基本类型的比较
  if(a === b) return a !== 0 || 1 / a === 1 / b
  // 处理 NaN
  if(a !== a) return b !== b
  // 处理 null 的情况
  if(a == null || b == null) return false
  // 过滤 a 或 b 中存在一个基本类型的情况
  var type = typeof a
  if(type !== 'function' && type !== 'object' && typeof b !== 'object') return false
  // 开始对象类型的比较
  return deepEqual(a, b, aStack, bStack)
}
/**
 * 
 * @param {any} a 
 * @param {any} b 
 * @param {[]} aStack 
 * @param {[]} bStack 
 * @returns Boolean
 */
function deepEqual(a, b, aStack, bStack) {
  var typeA = Object.prototype.toString.call(a)
  var typeB = Object.prototype.toString.call(b)
  // 类型都不一样,没法相爱
  if(typeA !== typeB) return false
  // 通过隐式类型转换处理为基本类型再比较
  switch(typeA) {
    // 处理 Number() new Number() 的情况
    case '[object Number]':
      // + new Number(1) === + new Number(1).valueOf()
      if(+a !== +a) return +b !== +b
      if(+a === +b) return 1 / a === 1 / b
    case '[object RegExp]':
    case '[object String]':
      // '' + new RegExp() === '' + new RegExp().toString()
      // '' + new String() === '' + new String().toString()
      return '' + a === '' + b
    case '[object Boolean]':
    case '[object Date]':
      return +a === +b
    case '[object Symbol]':
      return Symbol.prototype.valueOf.call(a) === Symbol.prototype.valueOf.call(b)
    break;
  }
  // 到这里还剩下三种类型,Array || Object || function
  var isArray = typeA === '[object Array]';
  if(!isArray) {
    // 这里过滤 function 类型
    if(typeof a !== 'object') return false
    // 这里需要判断对象类型是否是同一个构造器
    var aCtor = a.constructor
    var bCtor = b.constructor
    if(
      aCtor !== bCtor &&
      typeof aCtor !== 'function' &&
      aCtor instanceof aCtor &&
      typeof bCtor !== 'function' &&
      bCtor instanceof bCtor &&
      ('constructor' in a && 'constructor' in b)
    ){
      return false
    }
  }
  // 到这里已经是要么是数组,要么是同类型的实例对象
  aStack = aStack || []
  bStack = bStack || []
  // 优先处理循环引用的问题
  var len = aStack.length
  while (len--) {
    if(aStack[len] === a) return bStack[len] === b
  }
  // 不存在循环引用,将a b 分别入栈
  aStack.push(a)
  bStack.push(b)
  if(isArray){
    var len = a.length
    // 个数不同,不能恋爱
    if(len !== b.length) return false
    while(len--){
      // 详细对每一项进行比对
      if(!eq(a[len], b[len], aStack, bStack)) return false
    }
  }else{
    // 这里偷懒用 Object.keys
    var keys = Object.keys(a)
    var len = keys.length
    // 个数不同,不能恋爱
    if(len !== Object.keys(b).length) return false
    while(len--){
      // 详细对每一项进行比对
      if(!eq(a[keys[len]], b[keys[len]], aStack, bStack)) return false
    }
  }
  // 比对结束了,记得出栈!
  aStack.pop()
  bStack.pop()
  // 大功告成
  return true
}

module.exports = {
    eq
}

源码地址