前言
Js数组去重已经有很多种实现方式:包括逐个检索对比(使用Array.property.indexOf),先排序后对比,使用hash表,利用ES6中的Set()等。这些数组去重办法中速度最快的是hash表,最安全也最慢的是逐个检索对比(先排序后对比是优化成先分组再逐个检索),而ES6的Set对象目前浏览器兼容不全。
有没有结合那些以上方式的优点,像hash表一样快,和Array.property.indexOf一样全,又没有兼容问题的解决方案呢?
有!
Js中的基本类型
Undefined, Number, String,Boolean, Null, Object,除了前5种是值传递类型,其他所有的都是引用类型。
待解决问题
假设有o1 = o2 = new Object()
hash表去重方式的缺陷在于不能判断[o1,o2,{}]中的o1与o2是同一个对象的引用,因为得到的结果都是 ”object”。
那么如果有办法判断引用变量后面是同一个对象,就可以达到Array.property.indexOf方式所做的效果了。但是js本身又不提供对象内存地址索引,所以,那就造一个吧。
标记法
比如有个人类,在学校老师叫他做“小明”,在家爸妈叫他做“乖乖”,邻居叫他做“别人家的孩子”,虽然叫法不同,但是实际都是指向同一个人类个体。有一次商家搞活动给小孩子发免费糖果,一个小屁孩只能领一次,为了避免熊孩子以“小明”的名义领一次,又以“乖乖”的名义领一次,商家想了一个办法,每一个来领糖果的小孩子,先检查手掌有没有商家画的logo,如果没有,就可以领糖果,发完糖果后就在小孩子手掌上画商家logo,下次再来时只要看手掌上是不是已经有logo了,有就说明已经领过了,就不发糖果。同时为了不让logo与小孩子身上其他涂涂画画的东西相同,商家特意找了一种叫“全宇宙重复概率可能性低到接近不存在”的符号当logo,成功解决了涂鸦与logo相似的问题。小孩子领了糖果也吃完了,怕回家被爸妈发现,就去洗了手上的logo,就像一切都没有发生过一样。
所以呢,只要用一个uuid(商家logo)作为属性值添加到对象里面(画到手掌上),就可以判断这个对象在前面已经被收录(领过糖果)了,也就达到检测多个引用变量是否指向同一个对象的效果了。引用类型在去重之后,还要将uuid属性清除掉,避免后续该对象的使用出现问题。
使用uuid做标记key名,目的是避免与对象上本来就存在的属性名冲突,比如原对象有个minName属性名,标记名就不能叫minName,所以选uuid做标记属性名是为了与该对象当前所有可能存在的属性名区分,而每一个对象都是独立的,那么只需要一个uuid字符串就可以了。
具体代码实现与数据测试
1 !(function() { 2 "user strict"; 3 4 /** 5 * 创建一个 带有默认格式的uuid字符串 6 * @param {Number} length 4个字符串为一组的组数数量 default:8 7 * @param {String} connectString 组数之间连接符 default:"_" 8 * @return uuidString:7953_1e59_9cc4_8f44_ab7d_c959_3b5c_ef94 9 * @author Z 10 */ 11 12 function UUID(length, connectString) { 13 var uuid = [], 14 max0x = 0x10000, //65535+1 15 i; 16 17 // 设置参数num,connectString默认值 18 length = Math.abs(0 | length) || 8; 19 typeof connectString !== "string" && (connectString = "_"); 20 21 for (i = 0; i < length; i++) { 22 uuid.push((Math.random() * max0x | 0).toString(16)); 23 } 24 25 return uuid.join(connectString); 26 } 27 // demo 28 // UUID() ===> eae1_bb78_1f00_4ef2_c5d0_d28b_6708_e73f 29 // UUID(null,"-") ===> ff15-8779-ce3d-3c83-cab3-b645-9150-cdd2 30 // UUID(4) ===> d9d9_7a23_4ce7_c5c0 31 // UUID(4,"&") ===> 8703&6daf&8daa&f36d 32 /** ----test UUID()--- */ 33 // console.log(UUID()); 34 // console.log(UUID(null, "-")); 35 // console.log(UUID(4)); 36 // console.log(UUID(4, "&")); 37 38 39 40 /** 41 * 用于拓展操作Array对象的工具集合 42 * @type {Object} 43 */ 44 var _arrayExpand = {}; 45 46 /** 47 * 将传入的数组去掉重复的项,并返回一个新数组。 48 * @param {Array} array 目标去重数组,原始数组 49 * @param {Boolean} isQuick 可选参数, 等于true时使用判断步骤更少的hash去重 50 * @return {Array} 去重后的新数组 51 * @author Z 52 */ 53 _arrayExpand.unique = function(array, isQuick) { 54 var temp = [], 55 valueHash = { 56 "number": {}, 57 "string": {}, 58 "boolean": {}, 59 "undefined": {}, 60 "null": true // 只需要判断一次即可 61 }, 62 objectHash = {}, 63 objectRecord = [], 64 quickHash = {}, 65 keyUUID, item, type, i, len, _set 66 67 // 优先使用 ES6 中的 Set对象 68 if (window.Set) { 69 _set = new Set(array); 70 for (i of _set) { 71 if (_set.has(i)) { 72 temp.push(i); 73 } 74 } 75 return temp; 76 } 77 78 // 已知所有项都是值传递类型的数组,可以指定使用快速去重,isQuick指定为true值时生效 79 if (isQuick === true) { 80 for (i = 0, len = array.length; i < len; i++) { 81 item = array[i]; 82 if (!quickHash[item]) { 83 temp.push(item); 84 quickHash[item] = true; 85 } 86 } 87 } 88 // 包含引用类型的混合数组去重 89 else { 90 // 标记属性使用uuid,避免与其他属性冲突, 使用上面的UUID创建,或者直接手写。 91 keyUUID = UUID(); 92 93 for (i = 0, len = array.length; i < len; i++) { 94 item = array[i]; 95 type = valueHash[typeof item]; 96 // number, string, boolean, undefined 97 if (type) { 98 if (!type[item]) { 99 temp.push(item); 100 type[item] = true; 101 } 102 } 103 // object, null 104 else { 105 // object 106 if (item) { 107 if (!item.hasOwnProperty(keyUUID)) { 108 temp.push(item); 109 item[keyUUID] = true; 110 // 标记污染了原先的object,后续需要清除标记 111 objectRecord.push(item); 112 } 113 } 114 // null 115 else if (valueHash.null) { 116 temp.push(item); 117 valueHash.null = false; 118 } 119 } 120 } 121 122 // 清除标记 123 if (objectRecord.length) { 124 for (i = 0, len = objectRecord.length; i < len; i++) { 125 delete objectRecord[i][keyUUID]; 126 } 127 } 128 129 } 130 131 return temp; 132 }; 133 // demo 134 // _arrayExpand.unique([1,"1",{o1},{o1},{m1},{m2}, new Number(1), null,undefined,NaN,Infinity,null,undefined,NaN,Infinity]) ===> [1,"1",{o1},{m1},{m2}, new Number(1), null,undefined,NaN,Infinity] 135 // _arrayExpand.unique([1,2,3,4,1,2,3,4], true) ===> [1,2,3,4] 136 /** -----test _arrayExpand.unique()--- */ 137 // var o1 = {}, 138 // o2 = o1, 139 // o3 = o1; 140 // console.log(_arrayExpand.unique([new Number(1), 1, 1, {}, "1", {}, true, null, "true", null, undefined, 0, NaN, NaN, o2, Infinity, o1, o3])); 141 142 143 // 添加到window,对外调用 144 var _ArrayExpandName = "ArrayExpand"; 145 if (window.hasOwnProperty(_ArrayExpandName)) { 146 alert(_ArrayExpandName + " was existed in the window"); 147 } else { 148 window[_ArrayExpandName] = _arrayExpand; 149 } 150 // openApi demo 151 // ArrayExpand[key](arguments) 152 // window.ArrayExpand[key](arguments) 153 154 }(undefined));
但是,但是,总是又有但是,更加全面的去重是先检测当对象已经被阻止扩展或者冻结,如Object.preventExtensions,Object.seal,Object.freeze之类,就不能使用标记法了,只能上用ES6的Set对象,或者降级使用Array.property.indexOf。
end! 谨以此文写给[yoyohao]
Bruce-CZ原创