Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript专题之如何判断两个对象相等 #41

Open
mqyqingfeng opened this issue Aug 9, 2017 · 60 comments
Open

JavaScript专题之如何判断两个对象相等 #41

mqyqingfeng opened this issue Aug 9, 2017 · 60 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Aug 9, 2017

前言

虽然标题写的是如何判断两个对象相等,但本篇我们不仅仅判断两个对象相等,实际上,我们要做到的是如何判断两个参数相等,而这必然会涉及到多种类型的判断。

相等

什么是相等?在《JavaScript专题之去重》中,我们认为只要 === 的结果为 true,两者就相等,然而今天我们重新定义相等:

我们认为:

  1. NaN 和 NaN 是相等
  2. [1] 和 [1] 是相等
  3. {value: 1} 和 {value: 1} 是相等

不仅仅是这些长得一样的,还有

  1. 1 和 new Number(1) 是相等
  2. 'Curly' 和 new String('Curly') 是相等
  3. true 和 new Boolean(true) 是相等

更复杂的我们会在接下来的内容中看到。

目标

我们的目标是写一个 eq 函数用来判断两个参数是否相等,使用效果如下:

function eq(a, b) { ... }

var a = [1];
var b = [1];
console.log(eq(a, b)) // true

在写这个看似很简单的函数之前,我们首先了解在一些简单的情况下是如何判断的?

+0 与 -0

如果 a === b 的结果为 true, 那么 a 和 b 就是相等的吗?一般情况下,当然是这样的,但是有一个特殊的例子,就是 +0 和 -0。

JavaScript “处心积虑”的想抹平两者的差异:

// 表现1
console.log(+0 === -0); // true

// 表现2
(-0).toString() // '0'
(+0).toString() // '0'

// 表现3
-0 < +0 // false
+0 < -0 // false

即便如此,两者依然是不同的:

1 / +0 // Infinity
1 / -0 // -Infinity

1 / +0 === 1 / -0 // false

也许你会好奇为什么要有 +0 和 -0 呢?

这是因为 JavaScript 采用了IEEE_754 浮点数表示法(几乎所有现代编程语言所采用),这是一种二进制表示法,按照这个标准,最高位是符号位(0 代表正,1 代表负),剩下的用于表示大小。而对于零这个边界值 ,1000(-0) 和 0000(0)都是表示 0 ,这才有了正负零的区别。

也许你会好奇什么时候会产生 -0 呢?

Math.round(-0.1) // -0

那么我们又该如何在 === 结果为 true 的时候,区别 0 和 -0 得出正确的结果呢?我们可以这样做:

function eq(a, b){
    if (a === b) return a !== 0 || 1 / a === 1 / b;
    return false;
}

console.log(eq(0, 0)) // true
console.log(eq(0, -0)) // false

NaN

在本篇,我们认为 NaN 和 NaN 是相等的,那又该如何判断出 NaN 呢?

console.log(NaN === NaN); // false

利用 NaN 不等于自身的特性,我们可以区别出 NaN,那么这个 eq 函数又该怎么写呢?

function eq(a, b) {
    if (a !== a) return b !== b;
}

console.log(eq(NaN, NaN)); // true

eq 函数

现在,我们已经可以去写 eq 函数的第一版了。

// eq 第一版
// 用来过滤掉简单的类型比较,复杂的对象使用 deepEq 函数进行处理
function eq(a, b) {

    // === 结果为 true 的区别出 +0 和 -0
    if (a === b) return a !== 0 || 1 / a === 1 / b;

    // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数
    if (a == null || b == null) return false;

    // 判断 NaN
    if (a !== a) return b !== b;

    // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

    // 更复杂的对象使用 deepEq 函数进行深度比较
    return deepEq(a, b);
};

也许你会好奇是不是少了一个 typeof b !== function?

试想如果我们添加上了这句,当 a 是基本类型,而 b 是函数的时候,就会进入 deepEq 函数,而去掉这一句,就会进入直接进入 false,实际上 基本类型和函数肯定是不会相等的,所以这样做代码又少,又可以让一种情况更早退出。

String 对象

现在我们开始写 deepEq 函数,一个要处理的重大难题就是 'Curly' 和 new String('Curly') 如何判断成相等?

两者的类型都不一样呐!不信我们看 typeof 的操作结果:

console.log(typeof 'Curly'); // string
console.log(typeof new String('Curly')); // object

可是我们在《JavaScript专题之类型判断上》中还学习过更多的方法判断类型,比如 Object.prototype.toString:

var toString = Object.prototype.toString;
toString.call('Curly'); // "[object String]"
toString.call(new String('Curly')); // "[object String]"

神奇的是使用 toString 方法两者判断的结果却是一致的,可是就算知道了这一点,还是不知道如何判断字符串和字符串包装对象是相等的呢?

那我们利用隐式类型转换呢?

console.log('Curly' + '' === new String('Curly') + ''); // true

看来我们已经有了思路:如果 a 和 b 的 Object.prototype.toString的结果一致,并且都是"[object String]",那我们就使用 '' + a === '' + b 进行判断。

可是不止有 String 对象呐,Boolean、Number、RegExp、Date呢?

更多对象

跟 String 同样的思路,利用隐式类型转换。

Boolean

var a = true;
var b = new Boolean(true);

console.log(+a === +b) // true

Date

var a = new Date(2009, 9, 25);
var b = new Date(2009, 9, 25);

console.log(+a === +b) // true

RegExp

var a = /a/i;
var b = new RegExp(/a/i);

console.log('' + a === '' + b) // true

Number

var a = 1;
var b = new Number(1);

console.log(+a === +b) // true

嗯哼?你确定 Number 能这么简单的判断?

var a = Number(NaN);
var b = Number(NaN);

console.log(+a === +b); // false

可是 a 和 b 应该被判断成 true 的呐~

那么我们就改成这样:

var a = Number(NaN);
var b = Number(NaN);

function eq() {
    // 判断 Number(NaN) Object(NaN) 等情况
    if (+a !== +a) return +b !== +b;
    // 其他判断 ...
}

console.log(eq(a, b)); // true

deepEq 函数

现在我们可以写一点 deepEq 函数了。

var toString = Object.prototype.toString;

function deepEq(a, b) {
    var className = toString.call(a);
    if (className !== toString.call(b)) return false;

    switch (className) {
        case '[object RegExp]':
        case '[object String]':
            return '' + a === '' + b;
        case '[object Number]':
            if (+a !== +a) return +b !== +b;
            return +a === 0 ? 1 / +a === 1 / b : +a === +b;
      case '[object Date]':
      case '[object Boolean]':
            return +a === +b;
    }

    // 其他判断
}

构造函数实例

我们看个例子:

function Person() {
    this.name = name;
}

function Animal() {
    this.name = name
}

var person = new Person('Kevin');
var animal = new Animal('Kevin');

eq(person, animal) // ???

虽然 personanimal 都是 {name: 'Kevin'},但是 personanimal 属于不同构造函数的实例,为了做出区分,我们认为是不同的对象。

如果两个对象所属的构造函数对象不同,两个对象就一定不相等吗?

并不一定,我们再举个例子:

var attrs = Object.create(null);
attrs.name = "Bob";
eq(attrs, {name: "Bob"}); // ???

尽管 attrs 没有原型,{name: "Bob"} 的构造函数是 Object,但是在实际应用中,只要他们有着相同的键值对,我们依然认为是相等。

从函数设计的角度来看,我们不应该让他们相等,但是从实践的角度,我们让他们相等,所以相等就是一件如此随意的事情吗?!对啊,我也在想:undersocre,你怎么能如此随意呢!!!

哎,吐槽完了,我们还是要接着写这个相等函数,我们可以先做个判断,对于不同构造函数下的实例直接返回 false。

function isFunction(obj) {
    return toString.call(obj) === '[object Function]'
}

function deepEq(a, b) {
    // 接着上面的内容
    var areArrays = className === '[object Array]';
    // 不是数组
    if (!areArrays) {
        // 过滤掉两个函数的情况
        if (typeof a != 'object' || typeof b != 'object') return false;

        var aCtor = a.constructor, bCtor = b.constructor;
        // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦
        if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) {
            return false;
        }
    }

    // 下面还有好多判断
}

数组相等

现在终于可以进入我们期待已久的数组和对象的判断,不过其实这个很简单,就是递归遍历一遍……

function deepEq(a, b) {
    // 再接着上面的内容
    if (areArrays) {

        length = a.length;
        if (length !== b.length) return false;

        while (length--) {
            if (!eq(a[length], b[length])) return false;
         }
    } 
    else {

        var keys = Object.keys(a), key;
        length = keys.length;

        if (Object.keys(b).length !== length) return false;

        while (length--) {
            key = keys[length];
            if (!(b.hasOwnProperty(key) && eq(a[key], b[key]))) return false;
        }
    }
    return true;

}

循环引用

如果觉得这就结束了,简直是太天真,因为最难的部分才终于要开始,这个问题就是循环引用!

举个简单的例子:

a = {abc: null};
b = {abc: null};
a.abc = a;
b.abc = b;

eq(a, b)

再复杂一点的,比如:

a = {foo: {b: {foo: {c: {foo: null}}}}};
b = {foo: {b: {foo: {c: {foo: null}}}}};
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;

eq(a, b)

为了给大家演示下循环引用,大家可以把下面这段已经精简过的代码复制到浏览器中尝试:

// demo
var a, b;

a = { foo: { b: { foo: { c: { foo: null } } } } };
b = { foo: { b: { foo: { c: { foo: null } } } } };
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;

function eq(a, b, aStack, bStack) {
    if (typeof a == 'number') {
        return a === b;
    }

    return deepEq(a, b)
}

function deepEq(a, b) {

    var keys = Object.keys(a);
    var length = keys.length;
    var key;

    while (length--) {
        key = keys[length]

        // 这是为了让你看到代码其实一直在执行
        console.log(a[key], b[key])

        if (!eq(a[key], b[key])) return false;
    }

    return true;

}

eq(a, b)

嗯,以上的代码是死循环。

那么,我们又该如何解决这个问题呢?underscore 的思路是 eq 的时候,多传递两个参数为 aStack 和 bStack,用来储存 a 和 b 递归比较过程中的 a 和 b 的值,咋说的这么绕口呢?
我们直接看个精简的例子:

var a, b;

a = { foo: { b: { foo: { c: { foo: null } } } } };
b = { foo: { b: { foo: { c: { foo: null } } } } };
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;

function eq(a, b, aStack, bStack) {
    if (typeof a == 'number') {
        return a === b;
    }

    return deepEq(a, b, aStack, bStack)
}

function deepEq(a, b, aStack, bStack) {

    aStack = aStack || [];
    bStack = bStack || [];

    var length = aStack.length;

    while (length--) {
        if (aStack[length] === a) {
              return bStack[length] === b;
        }
    }

    aStack.push(a);
    bStack.push(b);

    var keys = Object.keys(a);
    var length = keys.length;
    var key;

    while (length--) {
        key = keys[length]

        console.log(a[key], b[key], aStack, bStack)

        if (!eq(a[key], b[key], aStack, bStack)) return false;
    }

    // aStack.pop();
    // bStack.pop();
    return true;

}

console.log(eq(a, b))

之所以注释掉 aStack.pop()bStack.pop()这两句,是为了方便大家查看 aStack bStack的值。

最终的 eq 函数

最终的代码如下:

var toString = Object.prototype.toString;

function isFunction(obj) {
    return toString.call(obj) === '[object Function]'
}

function eq(a, b, aStack, bStack) {

    // === 结果为 true 的区别出 +0 和 -0
    if (a === b) return a !== 0 || 1 / a === 1 / b;

    // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数
    if (a == null || b == null) return false;

    // 判断 NaN
    if (a !== a) return b !== b;

    // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

    // 更复杂的对象使用 deepEq 函数进行深度比较
    return deepEq(a, b, aStack, bStack);
};

function deepEq(a, b, aStack, bStack) {

    // a 和 b 的内部属性 [[class]] 相同时 返回 true
    var className = toString.call(a);
    if (className !== toString.call(b)) return false;

    switch (className) {
        case '[object RegExp]':
        case '[object String]':
            return '' + a === '' + b;
        case '[object Number]':
            if (+a !== +a) return +b !== +b;
            return +a === 0 ? 1 / +a === 1 / b : +a === +b;
        case '[object Date]':
        case '[object Boolean]':
            return +a === +b;
    }

    var areArrays = className === '[object Array]';
    // 不是数组
    if (!areArrays) {
        // 过滤掉两个函数的情况
        if (typeof a != 'object' || typeof b != 'object') return false;

        var aCtor = a.constructor,
            bCtor = b.constructor;
        // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦
        if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) {
            return false;
        }
    }


    aStack = aStack || [];
    bStack = bStack || [];
    var length = aStack.length;

    // 检查是否有循环引用的部分
    while (length--) {
        if (aStack[length] === a) {
            return bStack[length] === b;
        }
    }

    aStack.push(a);
    bStack.push(b);

    // 数组判断
    if (areArrays) {

        length = a.length;
        if (length !== b.length) return false;

        while (length--) {
            if (!eq(a[length], b[length], aStack, bStack)) return false;
        }
    }
    // 对象判断
    else {

        var keys = Object.keys(a),
            key;
        length = keys.length;

        if (Object.keys(b).length !== length) return false;
        while (length--) {

            key = keys[length];
            if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) return false;
        }
    }

    aStack.pop();
    bStack.pop();
    return true;

}

console.log(eq(0, 0)) // true
console.log(eq(0, -0)) // false

console.log(eq(NaN, NaN)); // true
console.log(eq(Number(NaN), Number(NaN))); // true

console.log(eq('Curly', new String('Curly'))); // true

console.log(eq([1], [1])); // true
console.log(eq({ value: 1 }, { value: 1 })); // true

var a, b;

a = { foo: { b: { foo: { c: { foo: null } } } } };
b = { foo: { b: { foo: { c: { foo: null } } } } };
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;

console.log(eq(a, b)) // true

真让人感叹一句:eq 不愧是 underscore 中实现代码行数最多的函数了!

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@FrontToEnd
Copy link

当初看underscore的eq源码看的真是头大!

@liuxinqiong
Copy link

真是头大,还好楼主解释清楚,不知道大神们都是如何考虑到这么多的情况的

@mqyqingfeng
Copy link
Owner Author

@liuxinqiong 这或许就是开源项目的好处,会不停有人提 issue 帮忙改进~

@mqyqingfeng
Copy link
Owner Author

@zdliuccit Object.is 就比 === 多了两个判断,一个是 NaN,一个是 +0 和 -0,然后 +0 和 -0 还被你说成缺陷 😂

if (!Object.is) {
  Object.is = function(x, y) {
    // SameValue algorithm
    if (x === y) { // Steps 1-5, 7-10
      // Steps 6.b-6.e: +0 != -0
      return x !== 0 || 1 / x === 1 / y;
    } else {
      // Step 6.a: NaN == NaN
      return x !== x && y !== y;
    }
  };
}

@jimStyle88
Copy link

做为一个前端菜鸟,表示看不懂阿

@mqyqingfeng
Copy link
Owner Author

@jimStyle88 哪里没有看懂?可以留言讨论呀~

@jimStyle88
Copy link

我的技术水平水平问题,不过还是会经常逛逛你的github ,挺详细的。

@shouhe
Copy link

shouhe commented Oct 30, 2017

循环引用的比较还是有点复杂的

@Yxliam
Copy link

Yxliam commented Oct 31, 2017

if (a !== a) return b !== b;
请问下这个怎么理解?

@shouhe
Copy link

shouhe commented Oct 31, 2017

@jerrymark1 判断NAN

@Yxliam
Copy link

Yxliam commented Oct 31, 2017

@shouhe 比如a是 {'a':1} 然后执行{'a':1} !== {'a':1} 不是直接返回 b !== b 了,没有执行下去了啊

@Yxliam
Copy link

Yxliam commented Oct 31, 2017

明白了 @shouhe ,谢谢

@bailinlin
Copy link

bailinlin commented Nov 14, 2017

啊啊啊,循环引用好变态,看了好几遍~

@zhangenming
Copy link

@mqyqingfeng
console.log(eq(1,'1'))
console.log(eq(Infinity,new String('Infinity')))

是否有必要返回true

@HuangQiii
Copy link

其实我大部分情况下,都是用JSON.stringify来判断的

@mqyqingfeng
Copy link
Owner Author

@HuangQiii 哈哈,够用就可以啦~

@mqyqingfeng
Copy link
Owner Author

@zhangenming 如果问的是"是否有必要"的话,这个其实要看需求是什么样的…… 在现在的实现中,这两个结果都会返回 false,因为两个参数并不是相同的类型,无论是使用 typeof 还是 Object.prototype.toString 的结果,对于 1 和 New Number(1) 而言,虽然 typeof 的结果不同,但是 Object.prototype.toString 的结果确是一样的

@liujuntao123
Copy link

image
大神,请问一下这里该怎么理解呢?

@liujuntao123
Copy link

image
这里的length是第一次声明,但是前面却没有var,是故意升级为全局变量,还是遗漏了呢?我看了下全部的代码,感觉应该是遗漏了吧?

@mqyqingfeng
Copy link
Owner Author

@liujuntao123 这段是用来判断两个不是同一个构造函数的实例对象是不相等的,之所以 aCtor instanceof aCtor 是用来判断 Object 构造函数的,因为:

Object instanceof Object // true

所以如果 aCtor 是函数,并且 aCtor instanceof aCtor 就说明 aCtor 是 Object 函数

@mqyqingfeng
Copy link
Owner Author

@liujuntao123 感谢指出哈~ 并不是故意升级为全局变量的,在写这一块的时候,确实是第一次使用 length 变量,理应使用 var 声明,不过从最终的源码中看的话,因为之前已经声明了 length 变量,所以这里才会没有使用 var 声明,直接覆盖了变量

default

@liujuntao123
Copy link

@mqyqingfeng 明白了,感谢!
image

@liujuntao123
Copy link

liujuntao123 commented Nov 28, 2017

再问一个问题哈,实际项目中什么场景下会出现循环引用的情况呢?如果没有的话,为什么underscore不选择直接跳出去呢?

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Dec 1, 2017

@liujuntao123 我在项目中也没有遇到循环引用的问题……至于为什么不跳出来,或许是因为当判断这样一个循环引用对象的时候

a = { foo: { b: { foo: { c: { foo: null } } } } };
b = { foo: { b: { foo: { c: { foo: null } } } } };
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;

只有当 clone 到最深层次的对象的时候,才能发现这是一个循环引用,既然都 clone 到最后了,直接跳出来不就白 clone 了~

@dowinweb
Copy link

假装看懂了,离写出这样高大上的代码还差十万八千里

@zhangjing28
Copy link

image
这里的过滤掉两个函数的情况,是取的关系,那么为什么下面的判断还要加上isFunction的判断呐?
对于这两部分的判断不是很清楚,麻烦帮忙解释下,谢谢

@lishihong
Copy link

var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
这里为什么还要判断typeof b != 'object'啊

应该是为了让这种情况进deepEq里面去:a=2,b=Number(2);

@HXWfromDJTU
Copy link

循环引用的比较还是有点复杂的

感觉最后这个对象和数组的比较,可以借鉴用来做深拷贝了...(又是另一个无底洞问题)

@Annie2694
Copy link

请问用了stack之后,就不会变成无限循环了吗

@Mcqueengit
Copy link

感谢作者的分享,另外在 构造函数实例处,是不是aCtor和bCtor只要有一个不是Object构造函数就可以满足条件了
QQ截图20190517162256

@TCutter
Copy link

TCutter commented May 22, 2019

switch (className) {
        case '[object RegExp]':
        case '[object String]':
            return '' + a === '' + b;
        case '[object Number]':
            if (+a !== +a) return +b !== +b;
            return +a === 0 ? 1 / +a === 1 / b : +a === +b;
      case '[object Date]':
      case '[object Boolean]':
            return +a === +b;
    }

作者你好,经测试NumberBooleanDate均可以使用'' + a === '' + b;判断,为什么还要做上面那种区分?

@NameWjp
Copy link

NameWjp commented Jul 13, 2019

@TCutter 你没认真看吧 考虑NaN的情况

@happycaroline
Copy link

用之前Object键值对去重的灵感 可以写成这样吗

function eq(a,b){
    var toString = Object.prototype.toString
    console.log(toString.call(a) + JSON.stringify(a))
    console.log(toString.call(b) + JSON.stringify(b))
    return toString.call(a) + JSON.stringify(a) === toString.call(b) + JSON.stringify(b)
} 

@garfield02
Copy link

`var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

// 更复杂的对象使用 deepEq 函数进行深度比较
return deepEq(a, b);

也许你会好奇是不是少了一个 typeof b !== function?

试想如果我们添加上了这句,当 a 是基本类型,而 b 是函数的时候,就会进入 deepEq 函数,而去掉这一句,就会进入直接进入 false,实际上 基本类型和函数肯定是不会相等的,所以这样做代码又少,又可以让一种情况更早退出。`

大神,既然a是基本类型,b为函数可以直接判断为false;那a为函数,b为基本类型的时候是不是也应该可以判断为false呢?按文中这种判断,当a为函数,b为基本类型的时候貌似会进入deepEq函数啊!

@lvao813
Copy link

lvao813 commented Mar 11, 2020

大佬非常厉害啊,但这个方法还有一点小问题:如果属性中的数据有数组,数组中的数据虽然一致,但顺序不同的话得到结果是false,比如:

let a, b;
a = {foo: {b: {foo: {c: {foo: [2, 1]}}}}};
b = {foo: {b: {foo: {c: {foo: [1, 2]}}}}};
console.log('最终结果=======》', eq(a, b)); // false

如果我的想法有错误还望指正,谢谢!

@baijunpeng666
Copy link

// 过滤掉两个函数的情况
if (typeof a != 'object' || typeof b != 'object') return false
为什么是一个等号,不是两个等号。。。!==

@KeithChou
Copy link

根据这个判断两个对象循环引用的思想,写了个深拷贝处理循环引用的版本。
本质上,就是将递归的值存储到stack中,如果相等,那么直接return回去,避免递归爆栈问题。

function deepCopy (source: object, stack?: any[]): object {
    if (!source) return source
    var root: object = {}
    if (Array.isArray(source)) root = []
    var keys: string[] = Object.keys(source)
    var len: number = keys.length
    var key: any = null

    // 存储递归的值
    // 如果发现循环引用,则直接return,避免递归爆栈
    stack = stack || []
    var stackLen: number = stack.length

    while (stackLen--) {
        if (stack[stackLen] === source) {
            return source
        }
    }
    stack.push(source)
    
    while (len--) {
        key = keys[len]
        if (typeof source[key] === 'object') {
            root[key] = deepCopy(source[key], stack)
        } else {
            root[key] = source[key]
        }
    }

    stack.pop()
    return root
}

@returnMaize
Copy link

returnMaize commented Jul 13, 2020

// 在判断 0 和 -0 的时候
function eq(a, b){
    if (a === b) return a !== 0 || 1 / a === 1 / b;
    return false;
}

// 写成这种形式会不会更好(可读性更强一点,代码量也更少一点)
function eq(a, b) {
  return a === b && a / 1 === b / 1
}

@CrisChr
Copy link

CrisChr commented Aug 21, 2020

构造函数里有个name属性,是否可以通过if(aCons.constructor.name === bCons.constructor.name)来判断两个构造函数实例对象相等?

@daolou
Copy link

daolou commented Aug 21, 2020

function isFunction(obj) {
    return toString.call(obj) === '[object Function]'
}

判断 是不是函数这块儿 有问题, 有可能是 [object GeneratorFunction][object AsyncFunction]

所以我认为用 typeof 更好些

image

@Hezhongming
Copy link

麻烦问下最后的pop是为了处理内存占用的问题吗

@anjina
Copy link

anjina commented Dec 13, 2020

// 在判断 0 和 -0 的时候
function eq(a, b){
    if (a === b) return a !== 0 || 1 / a === 1 / b;
    return false;
}

// 写成这种形式会不会更好(可读性更强一点,代码量也更少一点)
function eq(a, b) {
  return a === b && a / 1 === b / 1
}

你写的 equal函数, equal('a', 'a') // false

@vipduqian
Copy link

看不懂呀

@troubleFisher
Copy link

现在,我们已经可以去写 eq 函数的第一版了。

// eq 第一版
// 用来过滤掉简单的类型比较,复杂的对象使用 deepEq 函数进行处理
function eq(a, b) {

    // === 结果为 true 的区别出 +0 和 -0
    if (a === b) return a !== 0 || 1 / a === 1 / b;

    // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数
    if (a == null || b == null) return false;

    // 判断 NaN
    if (a !== a) return b !== b;

    // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

    // 更复杂的对象使用 deepEq 函数进行深度比较
    return deepEq(a, b);
};
也许你会好奇是不是少了一个 typeof b !== function?

试想如果我们添加上了这句,当 a 是基本类型,而 b 是函数的时候,就会进入 deepEq 函数,而去掉这一句,就会进入直接进入 false,实际上 基本类型和函数肯定是不会相等的,所以这样做代码又少,又可以让一种情况更早退出。

请问一下

  if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

是不是还可以添加a为object/function,但b为基本类型的情况咧?

@ZHUBFeng
Copy link

  var obj1 = { a: 1 };
  var obj2 = obj1;
  obj2.b = 2

  eq(obj1, obj2) // ==> true

这种情况的话是不是有问题...

@yinju123
Copy link

yinju123 commented Apr 24, 2021

@liujuntao123 这段是用来判断两个不是同一个构造函数的实例对象是不相等的,之所以 aCtor instanceof aCtor 是用来判断 Object 构造函数的,因为:

Object instanceof Object // true

所以如果 aCtor 是函数,并且 aCtor instanceof aCtor 就说明 aCtor 是 Object 函数
好像不一定是 Object 函数

function fn() { }
  fn.prototype.constructor = fn
  fn.__proto__ = fn.prototype
  console.log(111, fn instanceof fn) // true

@yinju123
Copy link

yinju123 commented May 6, 2021

两个函数不相等,为什么还能进入到deepEq

if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

这部分为甚要判断是否为函数,应该只能是object能进入啊

@codeshareman
Copy link

codeshareman commented Jul 20, 2022

关注你很久了,文章思路很清晰,理解很透彻啊!有一个问题想请教下,下面的代码中为何 前两行输出是 true,还望解答下:

console.log(Function instanceof Function); // true
console.log(Object instanceof Object);   // true
console.log(Array instanceof Array); // false 

@GeorgeSmith215
Copy link

image
console.log(Function instanceof Function); // true
这里是因为Function.prototype === Function.__proto__
console.log(Object instanceof Object); // true
这里是因为Object.prototype === Object.__proto__.__proto__
console.log(Array instanceof Array); // false
这里是因为Array.prototype !== Array.__proto__(注:Array.__proto__ === Function.prototype)且Array.prototype !== Array.__proto__.__proto__(注:Array.__proto__.__proto__ === Object.prototype)且Array.prototype !== Array.__proto__.__proto__.__proto__(注:Array.__proto__.__proto__.__proto__ === null)

@GeorgeSmith215
Copy link

补充一下instanceof的模拟实现:

function instance_of(leftVaule, rightVaule) { 
    let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
    leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
    while (true) {
    if (leftVaule === null) {
            return false;
        }
        if (leftVaule === rightProto) {
            return true;
        } 
        leftVaule = leftVaule.__proto__ 
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests