[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"content-array_unique_in_javascript":3,"$f12ex0xn00ppo4":23},{"id":4,"type":5,"slug":6,"title":7,"date":8,"category":9,"tags":10,"body_markdown":14,"permalink":15,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"html":20,"excerpt":21,"cover":22},168,"article","array_unique_in_javascript","也谈JavaScript数组去重","2017-01-05 09:00:00","web",[11,12,13],"JavaScript","数组","去重","\nJavaScript的数组去重是一个老生常谈的话题了。随便搜一搜就能找到非常多不同版本的解法。\n\n昨天在微博上看到一篇文章，也写数组去重，主要推崇的方法是将利用数组元素当作对象key来去重。我在微博转发了“用对象key去重不是个好办法…”然后作者问什么才是推荐的方法。\n\n细想一下，这样一个看似简单的需求，如果要做到完备，涉及的知识和需要注意的地方着实不少，于是诞生此文。\n\n## 定义重复（相等）\n\n要去重，首先得定义，什么叫作“重复”，即具体到代码而言，两个数据在什么情况下可以算是相等的。这并不是一个很容易的问题。\n\n对于原始值而言，我们很容易想到`1`和`1`是相等的，`'1'`和`'1'`也是相等的。那么，`1`和`'1'`是相等的么？\n\n如果这个问题还好说，只要回答“是”或者“不是”即可。那么下面这些情况就没那么容易了。\n\n\u003C!-- more -->\n\n### NaN\n\n初看`NaN`时，很容易把它当成和`null`、`undefined`一样的独立数据类型。但其实，它是数字类型。\n\n```javascript\n\u002F\u002F number\nconsole.log(typeof NaN);\n```\n\n根据规范，比较运算中只要有一个值为NaN，则比较结果为`false`，所以会有下面这些看起来略蛋疼的结论：\n\n```javascript\n\u002F\u002F 全都是false\n0 \u003C NaN;\n0 > NaN;\n0 == NaN;\n0 === NaN;\n```\n\n以最后一个表达式`0 === NaN`为例，在规范中有明确规定（\u003Chttp:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-strict-equality-comparison>）：\n\n> 4. If Type(x) is Number, then\n>   a. If x is NaN, return false.\n>   b. If y is NaN, return false.\n>   c. If x is the same Number value as y, return true.\n>   d. If x is +0 and y is −0, return true.\n>   e. If x is −0 and y is +0, return true.\n>   f. Return false.\n\n这意味着任何涉及到`NaN`的情况都不能简单地使用比较运算来判定是否相等。比较科学的方法只能是使用`isNaN()`：\n\n```javascript\nvar a = NaN;\nvar b = NaN;\n\n\u002F\u002F true\nconsole.log(isNaN(a) && isNaN(b));\n```\n\n### 原始值和包装对象\n\n看完`NaN`是不是头都大了。好了，我们来轻松一下，看一看原始值和包装对象这一对冤家。\n\n如果你研究过`'a'.trim()`这样的代码的话，不知道是否产生过这样的疑问：`'a'`明明是一个原始值（字符串），它为什么可以直接调用`.trim()`方法呢？当然，很可能你已经知道答案：因为JS在执行这样的代码的时候会对原始值做一次包装，让`'a'`变成一个字符串对象，然后执行这个对象的方法，执行完之后再把这个包装对象脱掉。可以用下面的代码来理解：\n\n```javascript\n\u002F\u002F 'a'.trim();\nvar tmp = new String('a');\ntmp.trim();\n```\n\n这段代码只是辅助我们理解的。但包装对象这个概念在JS中却是真实存在的。\n\n```javascript\nvar a = new String('a');\nvar b = 'b';\n```\n\n`a`即是一个包装对象，它和`b`一样，代表一个字符串。它们都可以使用字符串的各种方法（比如`trim()`），也可以参与字符串运算（`+`号连接等）。\n\n但他们有一个关键的区别：类型不同！\n\n```javascript\ntypeof a; \u002F\u002F object\ntypeof b; \u002F\u002F string\n```\n\n在做字符串比较的时候，类型的不同会导致结果有一些出乎意料：\n\n```javascript\nvar a1 = 'a';\nvar a2 = new String('a');\nvar a3 = new String('a');\n\na1 == a2; \u002F\u002F true\na1 == a3; \u002F\u002F true\na2 == a3; \u002F\u002F false\na1 === a2; \u002F\u002F false\na1 === a3; \u002F\u002F false\na2 === a3; \u002F\u002F false\n```\n\n同样是表示字符串`a`的变量，在使用严格比较时竟然不是相等的，在直觉上这是一件比较难接受的事情，在各种开发场景下，也非常容易忽略这些细节。\n\n### 对象和对象\n\n在涉及比较的时候，还会碰到对象。具体而言，大致可以分为三种情况：纯对象、实例对象、其它类型的对象。\n\n**纯对象**\n\n> 纯对象（plain object）具体指什么并不是非常明确，为减少不必要的争议，下文中使用纯对象指代由字面量生成的、成员中不含函数和日期、正则表达式等类型的对象。\n\n如果直接拿两个对象进行比较，不管是`==`还是`===`，毫无疑问都是不相等的。但是在实际使用时，这样的规则是否一定满足我们的需求？举个例子，我们的应用中有两个配置项：\n\n```javascript\n\u002F\u002F 原来有两个属性\n\u002F\u002F var prop1 = 1;\n\u002F\u002F var prop2 = 2;\n\n\u002F\u002F 重构代码时两个属性被放到同一个对象中\n\nvar config = {\n    prop1: 1,\n    prop2: 2\n};\n```\n\n假设在某些场景下，我们需要比较两次运行的配置项是否相同。在重构前，我们分别比较两次运行的`prop1`和`prop2`即可。而在重构后，我们可能需要比较`config`对象所代表的配置项是否一致。在这样的场景下，直接用`==`或者`===`来比较对象，得到的并不是我们期望的结果。\n\n在这样的场景下，我们可能需要自定义一些方法来处理对象的比较。常见的可能是通过`JSON.stringify()`对对象进行序列化之后再比较字符串，当然这个过程并非完全可靠，只是一个思路。\n\n> 如果你觉得这个场景是无中生有的话，可以再回想一下断言库，同样是基于对象成员，判断结果是否和预期相符。\n\n**实例对象**\n\n实例对象主要指通过构造函数（类）生成的对象。这样的对象和纯对象一样，直接比较都是不等的，但也会碰到需要判断是否是同一对象的情况。一般而言，因为这种对象有比较复杂的内部结构（甚至有一部分数据在原型上），无法直接从外部比较是否相等。比较靠谱的判断方法是由构造函数（类）来提供静态方法或者实例方法来判断是否相等。\n\n```javascript\nvar a = Klass();\nvar b = Klass();\n\nKlass.isEqual(a, b);\n```\n\n**其它对象**\n\n其它对象主要指数组、日期、正则表达式等这类在`Object`基础上派生出来的对象。这类对象各有各的特殊性，一般需要根据场景来构造判断方法，决定两个对象是否相等。\n\n比如，日期对象，可能需要通过`Date.prototype.getTime()`方法获取时间戳来判断是否表示同一时刻。正则表达式可能需要通过`toString()`方法获取到原始字面量来判断是否是相同的正则表达式。\n\n### ==和===\n\n在一些文章中，看到某一些数组去重的方法，在判断元素是否相等时，使用的是`==`比较运算符。众所周知，这个运算符在比较前会先查看元素类型，当类型不一致时会做隐式类型转换。这其实是一种非常不严谨的做法。因为无法区分在做隐匿类型转换后值一样的元素，例如`0`、`''`、`false`、`null`、`undefined`等。\n\n同时，还有可能出现一些只能黑人问号的结果，例如：\n\n```javascript\n[] == ![]; \u002F\u002Ftrue\n```\n\n### Array.prototype.indexOf()\n\n在一些版本的去重中，用到了`Array.prototype.indexOf()`方法：\n\n```javascript\nfunction unique(arr) {\n    return arr.filter(function(item, index){\n        \u002F\u002F indexOf返回第一个索引值，\n        \u002F\u002F 如果当前索引不是第一个索引，说明是重复值\n        return arr.indexOf(item) === index;\n    });\n}\n```\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    arr.forEach(function(item){\n        if(ret.indexOf(item) === -1){\n            ret.push(item);\n        }\n    });\n    return ret;\n}\n```\n\n既然`==`和`===`在元素相等的比较中是有巨大差别的，那么`indexOf`的情况又如何呢？大部分的文章都没有提及这点，于是只好求助规范。通过规范（\u003Chttp:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-array.prototype.indexof>），我们知道了`indexOf()`使用的是严格比较，也就是`===`。\n\n> 再次强调：按照前文所述，`===`不能处理`NaN`的相等性判断。\n\n### Array.prototype.includes()\n\n`Array.prototype.includes()`是ES2016中新增的方法，用于判断数组中是否包含某个元素，所以上面使用`indexOf()`方法的第二个版本可以改写成如下版本：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    arr.forEach(function(item){\n        if(!ret.includes(item)){\n            ret.push(item);\n        }\n    });\n    return ret;\n}\n```\n\n那么，你猜猜，`includes()`又是用什么方法来比较的呢？如果想当然的话，会觉得肯定跟`indexOf()`一样喽。但是，程序员的世界里最怕想当然。翻一翻规范，发现它其实是使用的另一种比较方法，叫作“SameValueZero”比较（\u003Chttps:\u002F\u002Ftc39.github.io\u002Fecma262\u002F2016\u002F#sec-samevaluezero>）。\n\n> 1. If Type(x) is different from Type(y), return false.\n> 2. If Type(x) is Number, then\n>   a. If x is NaN and y is NaN, return true.\n>   b. If x is +0 and y is -0, return true.\n>   c. If x is -0 and y is +0, return true.\n>   d. If x is the same Number value as y, return true.\n>   e. Return false.\n> 3. Return SameValueNonNumber(x, y).\n\n注意`2.a`，如果`x`和`y`都是`NaN`，则返回`true`！也就是`includes()`是可以正确判断是否包含了`NaN`的。我们写一段代码验证一下：\n\n```javascript\nvar arr = [1, 2, NaN];\narr.indexOf(NaN); \u002F\u002F -1\narr.includes(NaN); \u002F\u002F true\n```\n\n可以看到`indexOf()`和`includes()`对待`NaN`的行为是完全不一样的。\n\n## 一些方案\n\n从上面的一大段文字中，我们可以看到，要判断两个元素是否相等（重复）并不是一件简单的事情。在了解了这个背景后，我们来看一些前面没有涉及到的去重方案。\n\n### 遍历\n\n双重遍历是最容易想到的去重方案：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var isRepeat;\n    for(var i=0; i\u003Clen; i++) {\n        isRepeat = false;\n        for(var j=i+1; j\u003Clen; j++) {\n            if(arr[i] === arr[j]){\n                isRepeat = true;\n                break;\n            }\n        }\n        if(!isRepeat){\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n双重遍历还有一个优化版本，但是原理和复杂度几乎完全一样：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    for(var i=0; i\u003Clen; i++){\n        for(var j=i+1; j\u003Clen; j++){\n            if(arr[i] === arr[j]){\n                j = ++i;\n            }\n        }\n        ret.push(arr[i]);\n    }\n    return ret;\n}\n```\n\n这种方案没什么大问题，用于去重的比较部分也是自己编写实现（`arr[i] === arr[j]`），所以相等性可以自己针对上文说到的各种情况加以特殊处理。唯一比较受诟病的是使用了双重循环，时间复杂度比较高，性能一般。\n\n### 使用对象key来去重\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    for(var i=0; i\u003Clen; i++){\n        if(!tmp[arr[i]]){\n            tmp[arr[i]] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n这种方法是利用了对象（`tmp`）的key不可以重复的特性来进行去重。但由于对象key只能为字符串，因此这种去重方法有许多局限性：\n\n1. 无法区分隐式类型转换成字符串后一样的值，比如`1`和`'1'`\n2. 无法处理复杂数据类型，比如对象（因为对象作为key会变成`[object Object]`）\n3. 特殊数据，比如`'__proto__'`会挂掉，因为`tmp`对象的`__proto__`属性无法被重写\n\n对于第一点，有人提出可以为对象的key增加一个类型，或者将类型放到对象的value中来解决：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    var tmpKey;\n    for(var i=0; i\u003Clen; i++){\n        tmpKey = typeof arr[i] + arr[i];\n        if(!tmp[tmpKey]){\n            tmp[tmpKey] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n该方案也同时解决第三个问题。\n\n而第二个问题，如果像上文所说，在允许对对象进行自定义的比较规则，也可以将对象序列化之后作为key来使用。这里为简单起见，使用`JSON.stringify()`进行序列化。\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    var tmpKey;\n    for(var i=0; i\u003Clen; i++){\n        tmpKey = typeof arr[i] + JSON.stringify(arr[i]);\n        if(!tmp[tmpKey]){\n            tmp[tmpKey] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n### Map Key\n\n可以看到，使用对象key来处理数组去重的问题，其实是一件比较麻烦的事情，处理不好很容易导致结果不正确。而这些问题的根本原因就是因为key在使用时有限制。\n\n那么，能不能有一种key使用没有限制的对象呢？答案是——真的有！那就是ES2015中的`Map`。\n\n> `Map`是一种新的数据类型，可以把它想象成key类型没有限制的对象。此外，它的存取使用单独的`get()`、`set()`接口。\n\n```javascript\nvar tmp = new Map();\ntmp.set(1, 1);\ntmp.get(1); \u002F\u002F 1\n\ntmp.set('2', 2);\ntmp.get('2'); \u002F\u002F 2\n\ntmp.set(true, 3);\ntmp.get(true); \u002F\u002F 3\n\ntmp.set(undefined, 4);\ntmp.get(undefined); \u002F\u002F 4\n\ntmp.set(NaN, 5);\ntmp.get(NaN); \u002F\u002F 5\n\nvar arr = [], obj = {};\n\ntmp.set(arr, 6);\ntmp.get(arr); \u002F\u002F 6\n\ntmp.set(obj, 7);\ntmp.get(obj); \u002F\u002F 7\n```\n\n由于Map使用单独的接口来存取数据，所以不用担心key会和内置属性重名（如上文提到的`__proto__`）。使用`Map`改写一下我们的去重方法：\n\n```javascript\nfunction unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = new Map();\n    for(var i=0; i\u003Clen; i++){\n        if(!tmp.get(arr[i])){\n            tmp.set(arr[i], 1);\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n```\n\n### Set\n\n既然都用到了ES2015，数组这件事情不能再简单一点么？当然可以。\n\n除了`Map`以外，ES2015还引入了一种叫作`Set`的数据类型。顾名思义，`Set`就是集合的意思，它不允许重复元素出现，这一点和数学中对集合的定义还是比较像的。\n\n```javascript\nvar s = new Set();\ns.add(1);\ns.add('1');\ns.add(null);\ns.add(undefined);\ns.add(NaN);\ns.add(true);\ns.add([]);\ns.add({});\n```\n\n如果你重复添加同一个元素的话，`Set`中只会存在一个。包括`NaN`也是这样。于是我们想到，这么好的特性，要是能和数组互相转换，不就可以去重了吗？\n\n```javascript\nfunction unique(arr){\n    var set = new Set(arr);\n    return Array.from(set);\n}\n```\n\n我们讨论了这么久的事情，居然两行代码搞定了，简直不可思议。\n\n然而，不要只顾着高兴了。有一句话是这么说的“不要因为走得太远而忘了为什么出发”。我们为什么要为数组去重呢？因为我们想得到不重复的元素列表。而既然已经有`Set`了，我们为什么还要舍近求远，使用数组呢？是不是在需要去重的情况下，直接使用`Set`就解决问题了？这个问题值得思考。\n\n## 小结\n\n最后，用一个测试用例总结一下文中出现的各种去重方法：\n\n```javascript\nvar arr = [1,1,'1','1',0,0,'0','0',undefined,undefined,null,null,NaN,NaN,{},{},[],[],\u002Fa\u002F,\u002Fa\u002F]\nconsole.log(unique(arr));\n```\n\n> 测试中没有定义对象的比较方法，因此默认情况下，对象不去重是正确的结果，去重是不正确的结果。\n\n|方法      |结果                                              |说明                                     |\n|----------|--------------------------------------------------|-----------------------------------------|\n|indexOf#1 |NaN被去掉                                         |                                         |\n|indexOf#2 |NaN重复                                           |                                         |\n|includes  |正确                                              |                                         |\n|双重循环#1|NaN重复                                           |                                         |\n|双重循环#2|NaN重复                                           |                                         |\n|对象#1    |字符串和数字无法区分，对象、数组、正则表达式被去重|                                         |\n|对象#2    |对象、数组、正则表达式被去重                      |                                         |\n|对象#3    |对象、数组被去重，正则表达式被消失                |JSON.stringify(\u002Fa\u002F)结果为{}，和空对象一样|\n|Map       |正确                                              |　                                       |\n|Set       |正确                                              |　                                       |\n\n最后的最后：任何脱离场景谈技术都是妄谈，本文也一样。去重这道题，没有正确答案，请根据场景选择合适的去重方法。\n","\u002Farticle\u002Farray_unique_in_javascript.html",null,0,"2026-08-28 04:37:17","published","\u003Cp>JavaScript的数组去重是一个老生常谈的话题了。随便搜一搜就能找到非常多不同版本的解法。\u003C\u002Fp>\n\u003Cp>昨天在微博上看到一篇文章，也写数组去重，主要推崇的方法是将利用数组元素当作对象key来去重。我在微博转发了“用对象key去重不是个好办法…”然后作者问什么才是推荐的方法。\u003C\u002Fp>\n\u003Cp>细想一下，这样一个看似简单的需求，如果要做到完备，涉及的知识和需要注意的地方着实不少，于是诞生此文。\u003C\u002Fp>\n\u003Ch2>定义重复（相等）\u003C\u002Fh2>\n\u003Cp>要去重，首先得定义，什么叫作“重复”，即具体到代码而言，两个数据在什么情况下可以算是相等的。这并不是一个很容易的问题。\u003C\u002Fp>\n\u003Cp>对于原始值而言，我们很容易想到\u003Ccode>1\u003C\u002Fcode>和\u003Ccode>1\u003C\u002Fcode>是相等的，\u003Ccode>'1'\u003C\u002Fcode>和\u003Ccode>'1'\u003C\u002Fcode>也是相等的。那么，\u003Ccode>1\u003C\u002Fcode>和\u003Ccode>'1'\u003C\u002Fcode>是相等的么？\u003C\u002Fp>\n\u003Cp>如果这个问题还好说，只要回答“是”或者“不是”即可。那么下面这些情况就没那么容易了。\u003C\u002Fp>\n\u003Ch3>NaN\u003C\u002Fh3>\n\u003Cp>初看\u003Ccode>NaN\u003C\u002Fcode>时，很容易把它当成和\u003Ccode>null\u003C\u002Fcode>、\u003Ccode>undefined\u003C\u002Fcode>一样的独立数据类型。但其实，它是数字类型。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F number\nconsole.log(typeof NaN);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>根据规范，比较运算中只要有一个值为NaN，则比较结果为\u003Ccode>false\u003C\u002Fcode>，所以会有下面这些看起来略蛋疼的结论：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 全都是false\n0 &lt; NaN;\n0 &gt; NaN;\n0 == NaN;\n0 === NaN;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>以最后一个表达式\u003Ccode>0 === NaN\u003C\u002Fcode>为例，在规范中有明确规定（\u003Ca href=\"http:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-strict-equality-comparison\">http:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-strict-equality-comparison\u003C\u002Fa>）：\u003C\u002Fp>\n\u003Cblockquote>\n\u003Col start=\"4\">\n\u003Cli>If Type(x) is Number, then\na. If x is NaN, return false.\nb. If y is NaN, return false.\nc. If x is the same Number value as y, return true.\nd. If x is +0 and y is −0, return true.\ne. If x is −0 and y is +0, return true.\nf. Return false.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003C\u002Fblockquote>\n\u003Cp>这意味着任何涉及到\u003Ccode>NaN\u003C\u002Fcode>的情况都不能简单地使用比较运算来判定是否相等。比较科学的方法只能是使用\u003Ccode>isNaN()\u003C\u002Fcode>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>var a = NaN;\nvar b = NaN;\n\n\u002F\u002F true\nconsole.log(isNaN(a) &amp;&amp; isNaN(b));\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>原始值和包装对象\u003C\u002Fh3>\n\u003Cp>看完\u003Ccode>NaN\u003C\u002Fcode>是不是头都大了。好了，我们来轻松一下，看一看原始值和包装对象这一对冤家。\u003C\u002Fp>\n\u003Cp>如果你研究过\u003Ccode>'a'.trim()\u003C\u002Fcode>这样的代码的话，不知道是否产生过这样的疑问：\u003Ccode>'a'\u003C\u002Fcode>明明是一个原始值（字符串），它为什么可以直接调用\u003Ccode>.trim()\u003C\u002Fcode>方法呢？当然，很可能你已经知道答案：因为JS在执行这样的代码的时候会对原始值做一次包装，让\u003Ccode>'a'\u003C\u002Fcode>变成一个字符串对象，然后执行这个对象的方法，执行完之后再把这个包装对象脱掉。可以用下面的代码来理解：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 'a'.trim();\nvar tmp = new String('a');\ntmp.trim();\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这段代码只是辅助我们理解的。但包装对象这个概念在JS中却是真实存在的。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>var a = new String('a');\nvar b = 'b';\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>a\u003C\u002Fcode>即是一个包装对象，它和\u003Ccode>b\u003C\u002Fcode>一样，代表一个字符串。它们都可以使用字符串的各种方法（比如\u003Ccode>trim()\u003C\u002Fcode>），也可以参与字符串运算（\u003Ccode>+\u003C\u002Fcode>号连接等）。\u003C\u002Fp>\n\u003Cp>但他们有一个关键的区别：类型不同！\u003C\u002Fp>\n\u003Cpre>\u003Ccode>typeof a; \u002F\u002F object\ntypeof b; \u002F\u002F string\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在做字符串比较的时候，类型的不同会导致结果有一些出乎意料：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>var a1 = 'a';\nvar a2 = new String('a');\nvar a3 = new String('a');\n\na1 == a2; \u002F\u002F true\na1 == a3; \u002F\u002F true\na2 == a3; \u002F\u002F false\na1 === a2; \u002F\u002F false\na1 === a3; \u002F\u002F false\na2 === a3; \u002F\u002F false\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>同样是表示字符串\u003Ccode>a\u003C\u002Fcode>的变量，在使用严格比较时竟然不是相等的，在直觉上这是一件比较难接受的事情，在各种开发场景下，也非常容易忽略这些细节。\u003C\u002Fp>\n\u003Ch3>对象和对象\u003C\u002Fh3>\n\u003Cp>在涉及比较的时候，还会碰到对象。具体而言，大致可以分为三种情况：纯对象、实例对象、其它类型的对象。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>纯对象\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>纯对象（plain object）具体指什么并不是非常明确，为减少不必要的争议，下文中使用纯对象指代由字面量生成的、成员中不含函数和日期、正则表达式等类型的对象。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>如果直接拿两个对象进行比较，不管是\u003Ccode>==\u003C\u002Fcode>还是\u003Ccode>===\u003C\u002Fcode>，毫无疑问都是不相等的。但是在实际使用时，这样的规则是否一定满足我们的需求？举个例子，我们的应用中有两个配置项：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 原来有两个属性\n\u002F\u002F var prop1 = 1;\n\u002F\u002F var prop2 = 2;\n\n\u002F\u002F 重构代码时两个属性被放到同一个对象中\n\nvar config = {\n    prop1: 1,\n    prop2: 2\n};\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>假设在某些场景下，我们需要比较两次运行的配置项是否相同。在重构前，我们分别比较两次运行的\u003Ccode>prop1\u003C\u002Fcode>和\u003Ccode>prop2\u003C\u002Fcode>即可。而在重构后，我们可能需要比较\u003Ccode>config\u003C\u002Fcode>对象所代表的配置项是否一致。在这样的场景下，直接用\u003Ccode>==\u003C\u002Fcode>或者\u003Ccode>===\u003C\u002Fcode>来比较对象，得到的并不是我们期望的结果。\u003C\u002Fp>\n\u003Cp>在这样的场景下，我们可能需要自定义一些方法来处理对象的比较。常见的可能是通过\u003Ccode>JSON.stringify()\u003C\u002Fcode>对对象进行序列化之后再比较字符串，当然这个过程并非完全可靠，只是一个思路。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>如果你觉得这个场景是无中生有的话，可以再回想一下断言库，同样是基于对象成员，判断结果是否和预期相符。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>\u003Cstrong>实例对象\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>实例对象主要指通过构造函数（类）生成的对象。这样的对象和纯对象一样，直接比较都是不等的，但也会碰到需要判断是否是同一对象的情况。一般而言，因为这种对象有比较复杂的内部结构（甚至有一部分数据在原型上），无法直接从外部比较是否相等。比较靠谱的判断方法是由构造函数（类）来提供静态方法或者实例方法来判断是否相等。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>var a = Klass();\nvar b = Klass();\n\nKlass.isEqual(a, b);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>其它对象\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>其它对象主要指数组、日期、正则表达式等这类在\u003Ccode>Object\u003C\u002Fcode>基础上派生出来的对象。这类对象各有各的特殊性，一般需要根据场景来构造判断方法，决定两个对象是否相等。\u003C\u002Fp>\n\u003Cp>比如，日期对象，可能需要通过\u003Ccode>Date.prototype.getTime()\u003C\u002Fcode>方法获取时间戳来判断是否表示同一时刻。正则表达式可能需要通过\u003Ccode>toString()\u003C\u002Fcode>方法获取到原始字面量来判断是否是相同的正则表达式。\u003C\u002Fp>\n\u003Ch3>==和===\u003C\u002Fh3>\n\u003Cp>在一些文章中，看到某一些数组去重的方法，在判断元素是否相等时，使用的是\u003Ccode>==\u003C\u002Fcode>比较运算符。众所周知，这个运算符在比较前会先查看元素类型，当类型不一致时会做隐式类型转换。这其实是一种非常不严谨的做法。因为无法区分在做隐匿类型转换后值一样的元素，例如\u003Ccode>0\u003C\u002Fcode>、\u003Ccode>''\u003C\u002Fcode>、\u003Ccode>false\u003C\u002Fcode>、\u003Ccode>null\u003C\u002Fcode>、\u003Ccode>undefined\u003C\u002Fcode>等。\u003C\u002Fp>\n\u003Cp>同时，还有可能出现一些只能黑人问号的结果，例如：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>[] == ![]; \u002F\u002Ftrue\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Array.prototype.indexOf()\u003C\u002Fh3>\n\u003Cp>在一些版本的去重中，用到了\u003Ccode>Array.prototype.indexOf()\u003C\u002Fcode>方法：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    return arr.filter(function(item, index){\n        \u002F\u002F indexOf返回第一个索引值，\n        \u002F\u002F 如果当前索引不是第一个索引，说明是重复值\n        return arr.indexOf(item) === index;\n    });\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    arr.forEach(function(item){\n        if(ret.indexOf(item) === -1){\n            ret.push(item);\n        }\n    });\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>既然\u003Ccode>==\u003C\u002Fcode>和\u003Ccode>===\u003C\u002Fcode>在元素相等的比较中是有巨大差别的，那么\u003Ccode>indexOf\u003C\u002Fcode>的情况又如何呢？大部分的文章都没有提及这点，于是只好求助规范。通过规范（\u003Ca href=\"http:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-array.prototype.indexof\">http:\u002F\u002Fwww.ecma-international.org\u002Fecma-262\u002F6.0\u002F#sec-array.prototype.indexof\u003C\u002Fa>），我们知道了\u003Ccode>indexOf()\u003C\u002Fcode>使用的是严格比较，也就是\u003Ccode>===\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>再次强调：按照前文所述，\u003Ccode>===\u003C\u002Fcode>不能处理\u003Ccode>NaN\u003C\u002Fcode>的相等性判断。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3>Array.prototype.includes()\u003C\u002Fh3>\n\u003Cp>\u003Ccode>Array.prototype.includes()\u003C\u002Fcode>是ES2016中新增的方法，用于判断数组中是否包含某个元素，所以上面使用\u003Ccode>indexOf()\u003C\u002Fcode>方法的第二个版本可以改写成如下版本：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    arr.forEach(function(item){\n        if(!ret.includes(item)){\n            ret.push(item);\n        }\n    });\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>那么，你猜猜，\u003Ccode>includes()\u003C\u002Fcode>又是用什么方法来比较的呢？如果想当然的话，会觉得肯定跟\u003Ccode>indexOf()\u003C\u002Fcode>一样喽。但是，程序员的世界里最怕想当然。翻一翻规范，发现它其实是使用的另一种比较方法，叫作“SameValueZero”比较（\u003Ca href=\"https:\u002F\u002Ftc39.github.io\u002Fecma262\u002F2016\u002F#sec-samevaluezero\">https:\u002F\u002Ftc39.github.io\u002Fecma262\u002F2016\u002F#sec-samevaluezero\u003C\u002Fa>）。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Col>\n\u003Cli>If Type(x) is different from Type(y), return false.\u003C\u002Fli>\n\u003Cli>If Type(x) is Number, then\na. If x is NaN and y is NaN, return true.\nb. If x is +0 and y is -0, return true.\nc. If x is -0 and y is +0, return true.\nd. If x is the same Number value as y, return true.\ne. Return false.\u003C\u002Fli>\n\u003Cli>Return SameValueNonNumber(x, y).\u003C\u002Fli>\n\u003C\u002Fol>\n\u003C\u002Fblockquote>\n\u003Cp>注意\u003Ccode>2.a\u003C\u002Fcode>，如果\u003Ccode>x\u003C\u002Fcode>和\u003Ccode>y\u003C\u002Fcode>都是\u003Ccode>NaN\u003C\u002Fcode>，则返回\u003Ccode>true\u003C\u002Fcode>！也就是\u003Ccode>includes()\u003C\u002Fcode>是可以正确判断是否包含了\u003Ccode>NaN\u003C\u002Fcode>的。我们写一段代码验证一下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>var arr = [1, 2, NaN];\narr.indexOf(NaN); \u002F\u002F -1\narr.includes(NaN); \u002F\u002F true\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>可以看到\u003Ccode>indexOf()\u003C\u002Fcode>和\u003Ccode>includes()\u003C\u002Fcode>对待\u003Ccode>NaN\u003C\u002Fcode>的行为是完全不一样的。\u003C\u002Fp>\n\u003Ch2>一些方案\u003C\u002Fh2>\n\u003Cp>从上面的一大段文字中，我们可以看到，要判断两个元素是否相等（重复）并不是一件简单的事情。在了解了这个背景后，我们来看一些前面没有涉及到的去重方案。\u003C\u002Fp>\n\u003Ch3>遍历\u003C\u002Fh3>\n\u003Cp>双重遍历是最容易想到的去重方案：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var isRepeat;\n    for(var i=0; i&lt;len; i++) {\n        isRepeat = false;\n        for(var j=i+1; j&lt;len; j++) {\n            if(arr[i] === arr[j]){\n                isRepeat = true;\n                break;\n            }\n        }\n        if(!isRepeat){\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>双重遍历还有一个优化版本，但是原理和复杂度几乎完全一样：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    for(var i=0; i&lt;len; i++){\n        for(var j=i+1; j&lt;len; j++){\n            if(arr[i] === arr[j]){\n                j = ++i;\n            }\n        }\n        ret.push(arr[i]);\n    }\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这种方案没什么大问题，用于去重的比较部分也是自己编写实现（\u003Ccode>arr[i] === arr[j]\u003C\u002Fcode>），所以相等性可以自己针对上文说到的各种情况加以特殊处理。唯一比较受诟病的是使用了双重循环，时间复杂度比较高，性能一般。\u003C\u002Fp>\n\u003Ch3>使用对象key来去重\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    for(var i=0; i&lt;len; i++){\n        if(!tmp[arr[i]]){\n            tmp[arr[i]] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这种方法是利用了对象（\u003Ccode>tmp\u003C\u002Fcode>）的key不可以重复的特性来进行去重。但由于对象key只能为字符串，因此这种去重方法有许多局限性：\u003C\u002Fp>\n\u003Col>\n\u003Cli>无法区分隐式类型转换成字符串后一样的值，比如\u003Ccode>1\u003C\u002Fcode>和\u003Ccode>'1'\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>无法处理复杂数据类型，比如对象（因为对象作为key会变成\u003Ccode>[object Object]\u003C\u002Fcode>）\u003C\u002Fli>\n\u003Cli>特殊数据，比如\u003Ccode>'__proto__'\u003C\u002Fcode>会挂掉，因为\u003Ccode>tmp\u003C\u002Fcode>对象的\u003Ccode>__proto__\u003C\u002Fcode>属性无法被重写\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>对于第一点，有人提出可以为对象的key增加一个类型，或者将类型放到对象的value中来解决：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    var tmpKey;\n    for(var i=0; i&lt;len; i++){\n        tmpKey = typeof arr[i] + arr[i];\n        if(!tmp[tmpKey]){\n            tmp[tmpKey] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>该方案也同时解决第三个问题。\u003C\u002Fp>\n\u003Cp>而第二个问题，如果像上文所说，在允许对对象进行自定义的比较规则，也可以将对象序列化之后作为key来使用。这里为简单起见，使用\u003Ccode>JSON.stringify()\u003C\u002Fcode>进行序列化。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = {};\n    var tmpKey;\n    for(var i=0; i&lt;len; i++){\n        tmpKey = typeof arr[i] + JSON.stringify(arr[i]);\n        if(!tmp[tmpKey]){\n            tmp[tmpKey] = 1;\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Map Key\u003C\u002Fh3>\n\u003Cp>可以看到，使用对象key来处理数组去重的问题，其实是一件比较麻烦的事情，处理不好很容易导致结果不正确。而这些问题的根本原因就是因为key在使用时有限制。\u003C\u002Fp>\n\u003Cp>那么，能不能有一种key使用没有限制的对象呢？答案是——真的有！那就是ES2015中的\u003Ccode>Map\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>\u003Ccode>Map\u003C\u002Fcode>是一种新的数据类型，可以把它想象成key类型没有限制的对象。此外，它的存取使用单独的\u003Ccode>get()\u003C\u002Fcode>、\u003Ccode>set()\u003C\u002Fcode>接口。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cpre>\u003Ccode>var tmp = new Map();\ntmp.set(1, 1);\ntmp.get(1); \u002F\u002F 1\n\ntmp.set('2', 2);\ntmp.get('2'); \u002F\u002F 2\n\ntmp.set(true, 3);\ntmp.get(true); \u002F\u002F 3\n\ntmp.set(undefined, 4);\ntmp.get(undefined); \u002F\u002F 4\n\ntmp.set(NaN, 5);\ntmp.get(NaN); \u002F\u002F 5\n\nvar arr = [], obj = {};\n\ntmp.set(arr, 6);\ntmp.get(arr); \u002F\u002F 6\n\ntmp.set(obj, 7);\ntmp.get(obj); \u002F\u002F 7\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>由于Map使用单独的接口来存取数据，所以不用担心key会和内置属性重名（如上文提到的\u003Ccode>__proto__\u003C\u002Fcode>）。使用\u003Ccode>Map\u003C\u002Fcode>改写一下我们的去重方法：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr) {\n    var ret = [];\n    var len = arr.length;\n    var tmp = new Map();\n    for(var i=0; i&lt;len; i++){\n        if(!tmp.get(arr[i])){\n            tmp.set(arr[i], 1);\n            ret.push(arr[i]);\n        }\n    }\n    return ret;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Set\u003C\u002Fh3>\n\u003Cp>既然都用到了ES2015，数组这件事情不能再简单一点么？当然可以。\u003C\u002Fp>\n\u003Cp>除了\u003Ccode>Map\u003C\u002Fcode>以外，ES2015还引入了一种叫作\u003Ccode>Set\u003C\u002Fcode>的数据类型。顾名思义，\u003Ccode>Set\u003C\u002Fcode>就是集合的意思，它不允许重复元素出现，这一点和数学中对集合的定义还是比较像的。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>var s = new Set();\ns.add(1);\ns.add('1');\ns.add(null);\ns.add(undefined);\ns.add(NaN);\ns.add(true);\ns.add([]);\ns.add({});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>如果你重复添加同一个元素的话，\u003Ccode>Set\u003C\u002Fcode>中只会存在一个。包括\u003Ccode>NaN\u003C\u002Fcode>也是这样。于是我们想到，这么好的特性，要是能和数组互相转换，不就可以去重了吗？\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function unique(arr){\n    var set = new Set(arr);\n    return Array.from(set);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>我们讨论了这么久的事情，居然两行代码搞定了，简直不可思议。\u003C\u002Fp>\n\u003Cp>然而，不要只顾着高兴了。有一句话是这么说的“不要因为走得太远而忘了为什么出发”。我们为什么要为数组去重呢？因为我们想得到不重复的元素列表。而既然已经有\u003Ccode>Set\u003C\u002Fcode>了，我们为什么还要舍近求远，使用数组呢？是不是在需要去重的情况下，直接使用\u003Ccode>Set\u003C\u002Fcode>就解决问题了？这个问题值得思考。\u003C\u002Fp>\n\u003Ch2>小结\u003C\u002Fh2>\n\u003Cp>最后，用一个测试用例总结一下文中出现的各种去重方法：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>var arr = [1,1,'1','1',0,0,'0','0',undefined,undefined,null,null,NaN,NaN,{},{},[],[],\u002Fa\u002F,\u002Fa\u002F]\nconsole.log(unique(arr));\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n\u003Cp>测试中没有定义对象的比较方法，因此默认情况下，对象不去重是正确的结果，去重是不正确的结果。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>方法\u003C\u002Fth>\n\u003Cth>结果\u003C\u002Fth>\n\u003Cth>说明\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>indexOf#1\u003C\u002Ftd>\n\u003Ctd>NaN被去掉\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>indexOf#2\u003C\u002Ftd>\n\u003Ctd>NaN重复\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>includes\u003C\u002Ftd>\n\u003Ctd>正确\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>双重循环#1\u003C\u002Ftd>\n\u003Ctd>NaN重复\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>双重循环#2\u003C\u002Ftd>\n\u003Ctd>NaN重复\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>对象#1\u003C\u002Ftd>\n\u003Ctd>字符串和数字无法区分，对象、数组、正则表达式被去重\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>对象#2\u003C\u002Ftd>\n\u003Ctd>对象、数组、正则表达式被去重\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>对象#3\u003C\u002Ftd>\n\u003Ctd>对象、数组被去重，正则表达式被消失\u003C\u002Ftd>\n\u003Ctd>JSON.stringify(\u002Fa\u002F)结果为{}，和空对象一样\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Map\u003C\u002Ftd>\n\u003Ctd>正确\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Set\u003C\u002Ftd>\n\u003Ctd>正确\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>最后的最后：任何脱离场景谈技术都是妄谈，本文也一样。去重这道题，没有正确答案，请根据场景选择合适的去重方法。\u003C\u002Fp>\n","\u003Cp>JavaScript的数组去重是一个老生常谈的话题了。随便搜一搜就能找到非常多不同版本的解法。\u003C\u002Fp>\n\u003Cp>昨天在微博上看到一篇文章，也写数组去重，主要推崇的方法是将利用数组元素当作对象key来去重。我在微博转发了“用对象key去重不是个好办法…”然后作者问什么才是推荐的方法。\u003C\u002Fp>\n\u003Cp>细想一下，这样一个看似简单的需求，如果要做到完备，涉及的知识和需要注意的地方着实不少，于是诞生此文。\u003C\u002Fp>\n\u003Ch2>定义重复（相等）\u003C\u002Fh2>\n\u003Cp>要去重，首先得定义，什么叫作“重复”，即具体到代码而言，两个数据在什么情况下可以算是相等的。这并不是一个很容易的问题。\u003C\u002Fp>\n\u003Cp>对于原始值而言，我们很容易想到\u003Ccode>1\u003C\u002Fcode>和\u003Ccode>1\u003C\u002Fcode>是相等的，\u003Ccode>'1'\u003C\u002Fcode>和\u003Ccode>'1'\u003C\u002Fcode>也是相等的。那么，\u003Ccode>1\u003C\u002Fcode>和\u003Ccode>'1'\u003C\u002Fcode>是相等的么？\u003C\u002Fp>\n\u003Cp>如果这个问题还好说，只要回答“是”或者“不是”即可。那么下面这些情况就没那么容易了。\u003C\u002Fp>\n","",{"total":24,"totalRoots":25,"comments":26,"pv":17},10,6,[27,34,40,46,52,58,64,70,74,78],{"id":28,"content":29,"user_id":30,"nick":31,"link":22,"date":32,"rid":17,"vote_up":17,"vote_down":17,"visible":33},144,"\u003Cp>好文，已阅。\u003C\u002Fp>",37,"basecss","2017-01-06 07:38:42",true,{"id":35,"content":36,"user_id":37,"nick":38,"link":22,"date":39,"rid":28,"vote_up":17,"vote_down":17,"visible":33},145,"\u003Cp>谢基大赏脸！\u003C\u002Fp>",2,"TooBug","2017-01-06 09:19:17",{"id":41,"content":42,"user_id":43,"nick":44,"link":22,"date":45,"rid":17,"vote_up":17,"vote_down":17,"visible":33},146,"\u003Cp>在 原始值和包装对象 这个部分 为什么 a2===a3 也是false，他们是严格相等的吧？\u003C\u002Fp>",67,"Pingxia Hu","2017-01-06 13:32:40",{"id":47,"content":48,"user_id":49,"nick":50,"link":22,"date":51,"rid":41,"vote_up":17,"vote_down":17,"visible":33},147,"\u003Cp>文中已经解释过了，是两个不同的对象\u003C\u002Fp>",68,"Wenliang Dai","2017-01-17 15:38:52",{"id":53,"content":54,"user_id":55,"nick":56,"link":22,"date":57,"rid":17,"vote_up":17,"vote_down":17,"visible":33},148,"\u003Cp>const unique = (arr) => [...new Set(arr)] 搞定。\u003C\u002Fp>",69,"fytriht","2017-02-26 07:21:10",{"id":59,"content":60,"user_id":61,"nick":62,"link":22,"date":63,"rid":17,"vote_up":17,"vote_down":17,"visible":33},149,"\u003Cp>写的真好，赞！\u003C\u002Fp>",70,"曾浩","2017-05-11 12:50:00",{"id":65,"content":66,"user_id":67,"nick":68,"link":22,"date":69,"rid":17,"vote_up":17,"vote_down":17,"visible":33},150,"\u003Cp>想到个问题：{a:1,b:2}和{b:2,a:1}应该也属于去重的范围吧，这种情况上述的几种方法好像都是行不通的\u003C\u002Fp>",71,"Damonare","2017-06-01 10:04:48",{"id":71,"content":72,"user_id":37,"nick":38,"link":22,"date":73,"rid":65,"vote_up":17,"vote_down":17,"visible":33},151,"\u003Cp>是否属于应该去重的要视情况而定。举个例子，最近看到了一个使用对象作为常量的例子。\u003C\u002Fp>\u003Cp>var BEFORE = {};\u003Cbr>var AFTER = {};\u003C\u002Fp>\u003Cp>判断的时候，直接判断一对象obj === BEFORE即可。这种情况下对象是不应该去重的。\u003Cbr> 前面也说了，如果纯对象要去重的话，需要自己写方法判断，文中为了简单，使用了JSON.stringify()，也说了，这种方案其实是不可靠的。 \u003Cbr>或者再总结一下，这种场景已经不属于通用场景了，一定是需要有开发者自己的逻辑介入才可以完成的。\u003C\u002Fp>","2017-06-01 10:11:45",{"id":75,"content":76,"user_id":67,"nick":68,"link":22,"date":77,"rid":71,"vote_up":17,"vote_down":17,"visible":33},152,"\u003Cp>嗯，是这样的\u003C\u002Fp>","2017-06-01 10:45:33",{"id":79,"content":80,"user_id":81,"nick":82,"link":22,"date":83,"rid":17,"vote_up":17,"vote_down":17,"visible":33},153,"\u003Cp>坑爹的是原生js的isNaN()传undefined也是true\u003C\u002Fp>",72,"王希超","2017-07-14 01:48:11"]