也谈面试必备问题之 JavaScript 数组去重
Why underscore
(觉得这部分眼熟的可以直接跳到下一段了...)
最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。
阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。为什么是 underscore?最主要的原因是 underscore 简短精悍(约 1.5k 行),封装了 100 多个有用的方法,耦合度低,非常适合逐个方法阅读,适合楼主这样的 JavaScript 初学者。从中,你不仅可以学到用 void 0 代替 undefined 避免 undefined 被重写等一些小技巧 ,也可以学到变量类型判断、函数节流&函数去抖等常用的方法,还可以学到很多浏览器兼容的 hack,更可以学到作者的整体设计思路以及 API 设计的原理(向后兼容)。
之后楼主会写一系列的文章跟大家分享在源码阅读中学习到的知识。
- underscore-1.8.3 源码解读项目地址 https://github.com/hanzichi/underscore-analysis
 - underscore-1.8.3 源码全文注释 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/underscore-1.8.3-analysis.js
 - underscore-1.8.3 源码解读系列文章 https://github.com/hanzichi/underscore-analysis/issues
 
欢迎围观~ (如果有兴趣,欢迎 star & watch~)您的关注是楼主继续写作的动力
数组去重
今天要聊的,也是我以前笔试时碰到过的一个问题,数组去重,不知道现在的笔试题还考不考这个?
数组去重,一般需求是给你一个数组,调用去重方法,返回数值副本,副本中没有重复元素。一般来说,两个元素通过 === 比较返回 true 的视为相同元素,需要去重,所以,1 和 "1" 是不同的元素,1 和 new Number(1) 是不同的元素,{} 和 {} 是不同的元素(引用不同)。(当然如果需求认为 {} 和 {} 算作相同的元素,那么解法就不一样了)
方法一
无需思考,我们可以得到 O(n^2) 复杂度的解法。定义一个变量数组 res 保存结果,遍历需要去重的数组,如果该元素已经存在在 res 中了,则说明是重复的元素,如果没有,则放入 res 中。
function unique(a) {
  var res = [];
  for (var i = 0, len = a.length; i < len; i++) {
    var item = a[i];
    for (var j = 0, jLen = res.length; j < jLen; j++) {
      if (res[j] === item)
        break;
    }
    if (j === jLen)
      res.push(item);
  }
  return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => [1, "1", "2"]
代码非常简单,那么是否能更简洁些?如果不考虑浏览器兼容,我们可以用 ES5 提供的 Array.prototype.indexOf 方法来简化代码。
function unique(a) {
  var res = [];
  for (var i = 0, len = a.length; i < len; i++) {
    var item = a[i];
    (res.indexOf(item) === -1) && res.push(item);
  }
  return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => [1, "1", "2"]
既然用了 indexOf,那么不妨再加上 filter。
function unique(a) {
  var res = a.filter(function(item, index, array) {
    return array.indexOf(item) === index;
  });
  return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => [1, "1", "2"]
方法二
法一是将原数组中的元素和结果数组中的元素一一比较,我们可以换个思路,将原数组中重复元素的最后一个元素放入结果数组中。
function unique(a) {
  var res = [];
  for (var i = 0, len = a.length; i < len; i++) {
    for (var j = i + 1; j < len; j++) {
      // 这一步十分巧妙
      // 如果发现相同元素
      // 则 i 自增进入下一个循环比较
      if (a[i] === a[j])
        j = ++i;
    }
    res.push(a[i]);
  }
  return res;
}
var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => ["1", "2", 1]
虽然复杂度还是 O(n^2),但是可以看到结果不同,1 出现在了数组最后面,因为结果数组取的是元素最后一次出现的位置。
方法三(sort)
如果笔试面试时只答出了上面这样 O(n^2) 的方案,可能还不能使面试官满意,下面就来说几种进阶方案。
将数组用 sort 排序后,理论上相同的元素会被放在相邻的位置,那么比较前后位置的元素就可以了。
function unique(a) {
  return a.concat().sort().filter(function(item, pos, ary) {
    return !pos || item != ary[pos - 1];
  });
}
var a = [1, 1, 3, 2, 1, 2, 4];
var ans = unique(a);
console.log(ans); // => [1, 2, 3, 4]
但是问题又来了,1 和 "1" 会被排在一起,不同的 Object 会被排在一起,因为它们 toString() 的结果相同,所以会出现这样的错误:
var a = [1, 1, 3, 2, 1, 2, 4, '1'];
var ans = unique(a);
console.log(ans); // => [1, 2, 3, 4]
当然你完全可以针对数组中可能出现的不同类型,来写这个比较函数。不过这似乎有点麻烦。
方法四 (object)
用 JavaScript 中的 Object 对象来当做哈希表,这也是几年前笔试时的解法,跟 sort 一样,可以去重完全由 Number 基本类型组成的数组。
function unique(a) {
  var seen = {};
  return a.filter(function(item) {
    return seen.hasOwnProperty(item) ? false : (seen[item] = true);
  });
}
var a = [1, 1, 3, 2, 1, 2, 4];
var ans = unique(a);
console.log(ans); // => [1, 3, 2, 4]
还是和方法三一样的问题,因为 Object 的 key 值都是 String 类型,所以对于 1 和 "1" 无法分别,我们可以稍微改进下,将类型也存入 key 中。
function unique(a) {
  var ret = [];
  var hash = {};
  for (var i = 0, len = a.length; i < len; i++) {
    var item = a[i];
    var key = typeof(item) + item;
    if (hash[key] !== 1) {
      ret.push(item);
      hash[key] = 1;
    }
  }
  return ret;
}
var a = [1, 1, 3, 2, '4', 1, 2, 4, '1'];
var ans = unique(a);
console.log(ans); // => [1, 3, 2, "4", 4, "1"]
虽然解决了讨厌的 1 和 "1" 的问题,但是还有别的问题!
var a = [{name: "hanzichi"}, {age: 30}, new String(1), new Number(1)];
var ans = unique(a);
console.log(ans); // => [Object, String]
但是如果数组元素全部是基础类型的 Number 值,键值对法应该是最高效的!
方法五 (ES6)
ES6 部署了 Set 以及 Array.from 方法,太强大了!如果浏览器支持,完全可以这样:
function unique(a) {
  return Array.from(new Set(a));
}
var a = [{name: "hanzichi"}, {age: 30}, new String(1), new Number(1)];
var ans = unique(a);
console.log(ans); // => [Object, Object, String, Number]
_.unique
最后来看看 underscore 对此的实现方式,underscore 将此封装到了 _.unique 方法中,调用方式为 _.unique(array, [isSorted], [iteratee])。其中第一个参数是必须的,是需要去重的数组,第二个参数可选,如果数组有序,则可以传入布尔值 true,第三个参数可选,如果需要对数组迭代的结果去重,则可以传入一个迭代函数。而数组元素去重是基于 === 运算符的。
其实很简单,underscore 中的实现方式和上面的方法一相似。
我们来看它的核心代码:
for (var i = 0, length = getLength(array); i < length; i++) {
  var value = array[i],
      // 如果指定了迭代函数
      // 则对数组每一个元素进行迭代
      computed = iteratee ? iteratee(value, i, array) : value;
  // 如果是有序数组,则当前元素只需跟上一个元素对比即可
  // 用 seen 变量保存上一个元素
  if (isSorted) {
    // 如果 i === 0,则直接 push
    // 否则比较当前元素是否和前一个元素相等
    if (!i || seen !== computed) result.push(value);
    // seen 保存当前元素,供下一次对比
    seen = computed;
  } else if (iteratee) {
    // 如果 seen[] 中没有 computed 这个元素值
    if (!_.contains(seen, computed)) {
      seen.push(computed);
      result.push(value);
    }
  } else if (!_.contains(result, value)) {
    // 如果不用经过迭代函数计算,也就不用 seen[] 变量了
    result.push(value);
  }
}
外面的循环遍历数组元素,对于每个元素,如果数组有序,则和前一个元素比较,如果相同,则已经出现过,不加入到结果数组中,否则则加入。而如果有迭代函数,则计算传入迭代函数后的值,对值去重,调用 .contains 方法,而该方法的核心就是调用 .indexOf 方法,和我们上面说的方法一异曲同工。
关于 _.unique 方法的详细代码,可以参考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L519-L547
Read More
也谈面试必备问题之 JavaScript 数组去重的更多相关文章
- 从JavaScript 数组去重看兼容性有关问题,及性能优化(摘自玉伯博客)
		
JavaScript 数组去重经常出现在前端招聘的笔试题里,比如: 有数组 var arr = ['a', 'b', 'c', '1', 0, 'c', 1, '', 1, 0],请用 JavaScr ...
 - JavaScript数组去重方法及测试结果
		
最近看到一些人的去面试web前端,都说碰到过问JavaScript数组去重的问题,我也学习了一下做下总结. 实际上最有代表性也就三种方法:数组双重循环,对象哈希,排序后去重. 这三种方法我都做了性能测 ...
 - javascript数组去重算法-----3
		
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
 - javascript数组去重算法-----2
		
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
 - javascript数组去重算法-----1
		
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
 - javascript数组去重算法-----5
		
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
 - javascript数组去重算法-----4(另一种写法__2)
		
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
 - javascript数组去重算法-----4(另一种写法)
		
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
 - javascript数组去重算法-----4
		
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
 
随机推荐
- 【HTML DOM】Node.nodeValue的用法
			
目录结构: // contents structure [-] 语法 注意 详述 实例 参考文章 Note.noteValue 属性返回或设置当前属性的值. 语法 value = node.nodeV ...
 - CSS3的自定义动画帧
			
CSS3新增的动画帧非常绚丽,可以简单实现一些动画效果,目前除IE外各大主流浏览器都支持 本文演示三个:transform: scale3d(x, y, z)-缩放;.transform: trans ...
 - asp.net mvc 简易通用自定义Pager实现分页
			
asp.net mvc 自定义Pager实现分页 Intro 一个WEB应用程序中经常会用到数据分页,本文将实现一个简单通用的分页组件,包含一个 PagerModel (用来保存页码信息),一个 Ht ...
 - crm on premise IFD 部署下提供oauth 2.0 集成自定义应用
			
很多情况下我们的CRM系统会和弟三方应用集成,一般情况我们会开发一个中间站点来提供web api 给弟三方应用. 参考:http://alexanderdevelopment.net/post/201 ...
 - tomcat accesslog日志扩展
			
由于工作需要,最近对tomcat的日志进行了一些研究,发现其日志大致可以分为两类,一类是运行日志,即平常我们所说的catalina.out日志,由tomcat内部代码调用logger打印出来的:另一类 ...
 - 【超全整理】J2EE集成开发环境MyEclipse使用心得汇总
			
一.首先我们为什么需要MyEclipse? 下面允许我做一些简要的介绍: 应该大家都知道另一个MyEclipse的近亲——Eclipse的优点:免费.程序代码排版功能.有中文汉化包.可增设许多功能强大 ...
 - 用C#缩小照片上传到各种空间
			
中秋到了,首先祝各位猿友节日快乐!!! 本博文的原名称是“跟我一起用C#压缩照片上传到各种空间”,评论上有人开骂,没办法我这人就是自信霸气,但是既然有人提出来我还是改掉吧,如果文章写得不好的地方欢迎大 ...
 - 自动显示隐藏布局的listView
			
借助View的OnTouchListener接口来监听listView的滑动,通过比较与上次坐标的大小,判断滑动方向,并通过滑动方向来判断是否需显示或者隐藏对应的布局,并且带有动画效果. 1.自动显示 ...
 - iOS 学习 - 22  异步解析 JSON,使用 FMDB 存储,TableView 显示
			
前提是已经知道了有哪些 key 值 Model 类: .h @interface ListModel : NSObject @property (nonatomic, copy)NSString *t ...
 - Android 框架学习之 第一天 okhttp & Retrofit
			
最近面试,一直被问道新技术新框架,这块是短板,慢慢补吧. 关于框架的学习,分几个步骤 I.框架的使用 II.框架主流使用的版本和Android对应的版本 III.框架的衍生使用比如okhttp就会有R ...