你不知道的Array.prototype.group/groupToMap与实现

1,992 阅读4分钟

前言

什么的group?我咋没听说数组有这个方法?抱歉,MDN文档也没有。 消息来源于大淘宝前端技术的一则推文,新增的[].group已经提上日程了。 image.png github围观地址:github.com/tc39/propos…

你将收获

  1. 加深对this和原型链的理解
  2. group的实现
  3. groupToMap的实现
  4. 数组方法手写模板

邂逅

Array.prototype.group

我的理解:group()方法创建一个新对象,其包含的key由所提供的函数指定的分组依据,value对应通过函数分组测试的所有元素。 groupToMap(),用法相似,但key可以是对象类型。
听不懂没关系,直接看官方的例子+语法: image.png

语法

Array.prototype.group(`callbackfn`[,`thisArg`])
  • callbackfn应该是一个接受三个参数的函数。Group 按升序对数组中的每个元素调用callbackfn一次,并构造一个新的数组对象。callbackfn返回的每个值都被强制为属性键,并且关联的元素根据此属性键包含在构造对象的数组中。
  • 如果提供了thisArg参数,它将作为callbackfn的每次调用的this值。如果未提供,则改为使用未定义的。
  • 使用三个参数调用callbackfn:元素的值、元素的索引和被遍历的对象。
  • Group不会直接对调用它的对象产生影响,但对callbackfn的调用可能会对该对象产生影响。
  • 在第一次调用callbackfn之前设置 group 处理的元素的范围。callbackfn不会访问在对组的调用开始后附加到数组的元素。如果数组中现有的元素在传递给callbackfn时改变了它们的值,那么当组访问它们的时候它们的值就是这个值; 在组调用开始之后和被访问之前删除的元素仍然被访问,并且要么从原型中查找,要么没有定义。
  • Group的返回值是一个不继承Object的对象。
Array.prototype.group(`callbackfn`[,`thisArg`])
  • 同上,但GroupToMap的返回值是一个Map。

实现

先上代码

Array.prototype.group = function (callback, thisArg = null) {
  // 参数合法性判断
  if (typeof callback !== "function") {
    throw new TypeError(`${callback} is not a function`);
  }
  const arr = this;
  const length = this.length;
  const grouper = Object.create(null);

  for (let i = 0; i < length; i++){
    const key = callback.call(thisArg, arr[i], i, arr)
    if (!grouper[key]){
        grouper[key] = [ arr[i] ]
    }else {
        grouper[key].push(arr[i])
    }
  } 
  return grouper;
};
// 测试
const array = [1, 2, 3, 4, 5]
const res = array.group((num, index, array) => {
  return num % 2 === 0 ? 'even': 'odd'
})
console.log(res);// { odd: [ 1, 3, 5 ], even: [ 2, 4 ] }

Array.prototype.groupToMap = function (callback, thisArg = null) {
  // 参数合法性判断
  if (typeof callback !== "function") {
    throw new TypeError(`${callback} is not a function`);
  }
  const arr = this;
  const length = this.length;
  const grouper = new Map();

  for (let i = 0; i < length; i++){
    const key = callback.call(thisArg, arr[i], i, arr)
    const groupedList = grouper.get(key);
    if (!groupedList){
        grouper.set(key,[ arr[i] ])
    }else {
        groupedList.push(arr[i])
    }
  } 
  return grouper;
};
const odd  = { odd: true };
const even = { even: true };
const array = [1, 2, 3, 4, 5]
const map = array.groupToMap((num, index, array) => {
  return num % 2 === 0 ? even: odd;
});
console.log(map);// Map(2) { { odd: true } => [ 1, 3, 5 ], { even: true } => [ 2, 4 ] }

逐步分解

第1/2点考察了this和原型链,有的人在简历上写熟悉this和原型链,挑一个数组方法让其实现却不知从何下手,并不是真的深入理解。

  1. [].group可知,让每个数组对象都能使用该方法,可将其挂在数组原型对象上 Array.prototype.group = function(){}
  2. 函数内如何获取调用group方法的数组,答案是通过this,这里涉及到this的指向,[].group()是隐式绑定,this指向该对象
  3. 参数处理:第一个参数callback必填,否则抛出错误,第二个参数thisArg可不传,我们给默认值null
  4. group返回值是对象,对象每个key是分组条件,value对应符合的元素 { key1:[], key2:[], ... } 但官方语法提到,该对象不继承自Object,我们通过Object.create(null)来创建一个没有原型对象为null的对象
  5. group会进行一次数组的遍历,我们采用最朴素的for循环
  6. 在第一次调用callback时,需要group处理的元素的范围就已经确定了,此处我们记住数组的长度,作为for循环的循环次数;故callback的执行期间往后追加元素并不会被处理,插入到范围内的则会
  7. 每次循环,执行callback并绑定this,可用call/apply
  8. callback的执行结果为分组的条件,如果grouper对象没有对应的key则创建数组并加入当前元素,否则就push追加到数组中;分组的多少,取决于callback返回值的种类
  9. 最后返回grouper对象
  10. groupToMap方法只需在group基础上,将容器换成Map(其key不局限于字符串,可以是对象),对map的使用set/get进行存取数据

最后,以上并非最佳实现,如有不足或错误,欢迎斧正~

通用模板

基于以上代码,可以抽取重复的片段,作为每次手写的一个框架,再在这个框架上去实现某个数组方法

Array.prototype.myXXX = function (callback, thisArg = null) {
    // 参数合法性判断
    if (typeof callback !== "function") {
      throw new TypeError(`${callback} is not a function`);
    }
    const arr = this;
    const length = this.length;
    // 有返回值的可以在此放一个默认值
  
    for (let i = 0; i < length; i++){
      const res = callback.call(thisArg, arr[i], i, arr)
      // 需要callback的执行结果,或对结果进行一个处理
    } 
    // return
};

总结与思考

留给大家的思考题(才疏学浅不知为何)

  • 为什么group/groupToMap合并为一个API,显然前者实现是后者的一个特例
  • Group的返回值为什么需要是一个不继承Object的对象。