JavaScript:关于数组的四道面试题

CarterLi

已知后端返回一个对象数组,格式类似这样:

const arr = [
  { id: 1, name: 'a', birth: 896630400000 },
  { id: 2, name: 'b', birth: 725817600000 },
  ...,
]

按要求写代码:

  1. 按照 name 属性降序排序(字母顺序从大到小)
  2. 去除 id 属性相同的元素(如出现重复,后出现的覆盖先出现的,不要求保留原始顺序)
  3. 过滤出所有的95后(birth >= 1995年1月1日)
  4. 如何做前端分页

由于公司是大数据处理业务,数组相关操作使用还是挺频繁的。这是我们公司前端JS面试题中的编程题部分。虽然都是基础,但是答上来的似乎不多。下面我们依次分析。

  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么
  • 在看答案前不自己先试试么

数组排序

  • 按照 name 属性降序排序(字母顺序从大到小)

看到排序当然可以联想到 Array#sort。大家都写过 [1, 3, 2].sort((a, b) => a - b),但是这个数组有点特殊,它是对象数组。排序规则也有些特殊:是按照字符串排序,而且是降序排序。

考点:

  1. Array#sort
  2. String#localCompare

Array#sort 这个这个毫无疑问。手写排序算法的人会被一概刷掉。难点在于在 sort 的回调函数中如何比较字符串的大小。首先字符串相减是不对的,对于 'a'-'b' 这个表达式,JS 引擎会首先把字符串 'a''b' 转换为数字,因为 JS 标准规定只有数字才能相减。于是:

+'a' // => NaN
+'b' // => NaN
'a' - 'b' = NaN - NaN = NaN

有些同学会提出挨个比较字符编码的方法,此法虽然可行,但是你要知道字符串长度是不固定的,你必须判断前一位相等后比下一位,于是不得不引入循环。这并不是面试官想要的答案。

虽然字符串不可相减,但是字符串却可以用大于小于号比较大小,而且比较的方式就是字典序依次比较。

'a' < 'b' // true
'a' < 'a' // false
'aa' < 'ab' // true

使用比较运算符比较字符串的问题就是你无法一次性得到他们的准确关系。如果 a < b 返回 false,还有两种可能,一种是 a 等于 b,另一种是 a 大于 b。所以如果使用比较运算符,你就必须在 sort 的回调函数中判断两次。

arr.sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0); // 记得是逆序

最优方案是使用 String#localeCompare,这个函数可以一次性返回正负零,完美适用于 Array#sort

arr.sort((a, b) => b.name.localeCompare(a.name)); // 逆序,所以 b 在 a 前面

数组去重

  • 去除 id 属性相同的元素(如出现重复,后出现的覆盖先出现的,不要求保留原始顺序)

网上看过一些面试题的同学,看到数组去重肯定马上会联想到 Set,这个玩意用来做数组去重简直是帅到没朋友

[...new Set([1, 1, 2, 3, 3, 1])] // => [1, 2, 3]

然而这道题却不按套路出牌:它要去重的是对象数组,不是简单的数字数组。

遇到这道题时可以按照这样的思路来:题目说需要按照字段 id 去重,那么 id 是必需拿出来单独处理的。但是 id 去重却不是去 id 本身,而是去 id 所关联的对象,那么肯定需要一个 id 到对象的映射。想到映射这一层,那么很容易联想到对象和 Map

考点:

  1. 对象 key 的唯一性
  2. Map

如果使用对象,笨一点的话可以这样:

const map = {};
const resultArr = [];

arr.forEach(x => {
  if (!map[x]) {
    map[x] = true;
    resultArr.push(x);
  }
}

这种写法不完全符合题设要求,它总会保留最先出现的值。如果要保留后出现的值,可以把数组先反转再遍历。

arr.concat().reverse().forEach(...) // reverse 会改变数组的值,用之前需先克隆原始数组

这里可能有同学会提到 reduceRight,这里建议如果你不能保证回调函数为纯函数,请不要使用 map 或 reduce。

另外一个方式是始终用后面的对象覆盖前面的。

const map = {};
arr.forEach(x => {
  map[x] = x;
}

const resultArr = [];
for (let key in map) {
  // 严谨的话,这里要用 hasOwnProperty 去除父级对象上的 key
  resultArr.push(map[key]);
}

分为两步,前一步有些类似把数组转换为对象,后一步就是取一个对象里的所有值。前者可以用 Object#assignObject#fromEntries 代替,后者就是 Object#values。所以简洁的写法是这样:

Object.values(Object.assign(...arr.map(x => ({ [x.id]: x }))));

对象其实不是一个完美的解决方案,因为对象的 key 都会被强制转换为字符串,虽然题设中的 id 都是数字类型,但是难保后面会出现字符串的数字。完美的解决方案是 Set 的同胞兄弟 Map

[...new Map(arr.map(x => [x.id, x])).values()] // 注意 Map#values 返回的不是数组而是一个迭代器,需要手工再转为数组

数组过滤

  • 过滤出所有的95后(birth >= 1995年1月1日)

考点:

  1. Array#filter
  2. Date

此题明显比前面两题简单不少,但此题却很少有人完全答对,问题就出在日期对象的使用上

我在之前的博文 说过 Date 对象的坑:当你使用数字年月日构造 Date 对象时记得月份是从 0 开始的;当你使用字符串构造 Date 对象时请使用斜杠(/)避免出现时区问题。

arr.filter(x => x.birth >= new Date(1995, 0, 1);

前端分页

  • 如何做前端分页

考点:

  1. Array#slice
  2. 分页的基本概念

所谓“分页”就是从数组中截取要显示的部分放到页面上显示,截取就是 slice,但似乎好多人把 slice 和 splice 搞混。

slice 是截取数组的一部分,但不改变原数组,两个参数都是下标。splice 虽然也可以作为截取数组使用,但 splice 会改变原数组,而且 splice 要求的第二个参数是截取项的长度。slice 就是截取数组,而 splice 通常用作数组中部的插入删除操作。

slice 和 splice,中间差了一个字母 p,用法含义大不相同。

const from = (页码 - 1) * 每页条数;
arr.slice(from, from + 每页条数);

数组对象有很多的原生方法,数组操作是 JS 数据操作中最常用的操作,经常温习一下没错的。

阅读 3.7k

我大EOI前端
[链接] 虽然公司尚小,但是志向不小
1.3k 声望
99 粉丝
0 条评论
1.3k 声望
99 粉丝
文章目录
宣传栏