分组函数

0 阅读6分钟

纯JavaScript手写分组函数代码,同时支持数组分组对象分组

/**
 * ================= 手写分组函数(数组分组 + 对象分组)=================
 * 纯JS实现,无任何依赖
 */

// ------------------- 1. 数组分组 -------------------

/**
 * 数组分组 - 按指定条件对数组元素进行分组
 * @param {Array} array - 需要分组的数组
 * @param {string|Function} keySelector - 分组依据:属性名或自定义函数
 * @param {boolean} [unique=false] - 是否去重(同一组内去重)
 * @returns {Object} 分组后的对象
 */
function groupArray(array, keySelector, unique = false) {
  return array.reduce((result, item, index) => {
    // 计算分组键
    const key = typeof keySelector === 'function'
      ? keySelector(item, index, array)
      : item[keySelector];
    
    // 处理null/undefined键
    const groupKey = key === null ? 'null' 
      : key === undefined ? 'undefined'
      : key;
    
    // 如果该组不存在,初始化
    if (!Object.hasOwnProperty.call(result, groupKey)) {
      result[groupKey] = [];
    }
    
    // 如果需要去重,检查是否已存在
    if (unique) {
      const exists = result[groupKey].some(existingItem => 
        JSON.stringify(existingItem) === JSON.stringify(item)
      );
      if (!exists) {
        result[groupKey].push(item);
      }
    } else {
      result[groupKey].push(item);
    }
    
    return result;
  }, {});
}

/**
 * 数组分组 - 返回Map版本(支持任意类型键)
 * @param {Array} array - 需要分组的数组
 * @param {string|Function} keySelector - 分组依据
 * @returns {Map} Map对象,键可以是任意类型
 */
function groupArrayToMap(array, keySelector) {
  return array.reduce((map, item, index) => {
    const key = typeof keySelector === 'function'
      ? keySelector(item, index, array)
      : item[keySelector];
    
    if (!map.has(key)) {
      map.set(key, []);
    }
    map.get(key).push(item);
    return map;
  }, new Map());
}

/**
 * 数组分组 - 多级分组
 * @param {Array} array - 需要分组的数组
 * @param {Array} keySelectors - 分组依据数组,如 ['dept', 'level']
 * @returns {Object} 嵌套的分组对象
 */
function groupArrayMulti(array, keySelectors) {
  if (!keySelectors || keySelectors.length === 0) {
    return array;
  }
  
  const currentKey = keySelectors[0];
  const remainingKeys = keySelectors.slice(1);
  
  // 先按第一个键分组
  const grouped = groupArray(array, currentKey);
  
  // 如果还有剩余键,递归分组
  if (remainingKeys.length > 0) {
    Object.keys(grouped).forEach(key => {
      grouped[key] = groupArrayMulti(grouped[key], remainingKeys);
    });
  }
  
  return grouped;
}

/**
 * 数组分组 - 按范围分组(数值范围)
 * @param {Array} array - 需要分组的数组
 * @param {string} field - 数值字段名
 * @param {Array} ranges - 范围数组,如 [{min:0,max:20}, {min:20,max:40}]
 * @returns {Object} 分组结果
 */
function groupArrayByRange(array, field, ranges) {
  return array.reduce((result, item) => {
    const value = item[field];
    let rangeKey = '其他';
    
    for (let i = 0; i < ranges.length; i++) {
      const range = ranges[i];
      if (value >= range.min && value < range.max) {
        rangeKey = `${range.min}-${range.max}`;
        break;
      }
    }
    
    if (!result[rangeKey]) {
      result[rangeKey] = [];
    }
    result[rangeKey].push(item);
    return result;
  }, {});
}

// ------------------- 2. 对象分组 -------------------

/**
 * 对象分组 - 按属性值对对象进行分组
 * @param {Object} obj - 需要分组的对象
 * @param {Function} keySelector - 从每个值中提取分组键的函数
 * @returns {Object} 分组后的对象
 */
function groupObject(obj, keySelector) {
  return Object.entries(obj).reduce((result, [key, value]) => {
    const groupKey = keySelector(value, key, obj);
    
    // 处理null/undefined
    const finalKey = groupKey === null ? 'null' 
      : groupKey === undefined ? 'undefined'
      : groupKey;
    
    if (!result[finalKey]) {
      result[finalKey] = {};
    }
    
    result[finalKey][key] = value;
    return result;
  }, {});
}

/**
 * 对象分组 - 按值类型分组
 * @param {Object} obj - 需要分组的对象
 * @returns {Object} 按类型分组的结果
 */
function groupObjectByType(obj) {
  return Object.entries(obj).reduce((result, [key, value]) => {
    const type = value === null ? 'null'
      : Array.isArray(value) ? 'array'
      : typeof value;
    
    if (!result[type]) {
      result[type] = {};
    }
    
    result[type][key] = value;
    return result;
  }, {});
}

/**
 * 对象分组 - 按值范围分组(适用于数值)
 * @param {Object} obj - 需要分组的对象
 * @param {Array} ranges - 范围数组
 * @returns {Object} 分组结果
 */
function groupObjectByValueRange(obj, ranges) {
  return Object.entries(obj).reduce((result, [key, value]) => {
    if (typeof value !== 'number') {
      if (!result.other) result.other = {};
      result.other[key] = value;
      return result;
    }
    
    let rangeKey = '其他';
    for (let i = 0; i < ranges.length; i++) {
      const range = ranges[i];
      if (value >= range.min && value < range.max) {
        rangeKey = `${range.min}-${range.max}`;
        break;
      }
    }
    
    if (!result[rangeKey]) {
      result[rangeKey] = {};
    }
    result[rangeKey][key] = value;
    return result;
  }, {});
}

// ------------------- 3. 通用工具函数 -------------------

/**
 * 通用分组接口 - 自动识别数组或对象
 * @param {Array|Object} data - 需要分组的数据
 * @param {string|Function} keySelector - 分组依据
 * @param {Object} options - 配置选项
 * @returns {Object|Map} 分组结果
 */
function groupBy(data, keySelector, options = {}) {
  const { returnMap = false, unique = false, multiLevel = false } = options;
  
  // 数组分组
  if (Array.isArray(data)) {
    if (returnMap) {
      return groupArrayToMap(data, keySelector);
    }
    if (multiLevel && Array.isArray(keySelector)) {
      return groupArrayMulti(data, keySelector);
    }
    return groupArray(data, keySelector, unique);
  }
  
  // 对象分组
  else if (data && typeof data === 'object') {
    if (typeof keySelector !== 'function') {
      throw new Error('对象分组时keySelector必须是函数');
    }
    return groupObject(data, keySelector);
  }
  
  throw new Error('数据必须是数组或对象');
}

// ------------------- 4. 完整示例和测试 -------------------

// 测试数据
const people = [
  { id: 1, name: '赵', dept: '技术', age: 25, salary: 8000 },
  { id: 2, name: '钱', dept: '市场', age: 31, salary: 12000 },
  { id: 3, name: '孙', dept: '技术', age: 29, salary: 9500 },
  { id: 4, name: '李', dept: '市场', age: 27, salary: 11000 },
  { id: 5, name: '周', dept: '产品', age: 35, salary: 15000 },
  { id: 6, name: '吴', dept: '技术', age: 22, salary: 7000 },
  { id: 7, name: '赵', dept: '技术', age: 25, salary: 8000 } // 重复数据
];

const testObj = {
  user1: { name: '赵', age: 25, dept: '技术' },
  user2: { name: '钱', age: 31, dept: '市场' },
  user3: { name: '孙', age: 29, dept: '技术' },
  user4: { name: '李', age: 27, dept: '市场' },
  user5: { name: '周', age: 35, dept: '产品' },
  score1: 95,
  score2: 87,
  score3: 76,
  title: '测试',
  active: true,
  tags: ['js', '分组'],
  empty: null
};

// 测试1:数组基础分组
console.log('===== 1. 数组基础分组 =====');
console.log('按部门分组:', groupArray(people, 'dept'));

// 测试2:数组分组(去重)
console.log('\n===== 2. 数组分组(去重) =====');
console.log('按部门分组(去重):', groupArray(people, 'dept', true));

// 测试3:数组自定义函数分组
console.log('\n===== 3. 自定义函数分组 =====');
const byAgeRange = groupArray(people, p => {
  if (p.age < 25) return '20-24岁';
  if (p.age < 30) return '25-29岁';
  if (p.age < 35) return '30-34岁';
  return '35岁及以上';
});
console.log('按年龄范围分组:', byAgeRange);

// 测试4:Map版本
console.log('\n===== 4. Map版本 =====');
const mapResult = groupArrayToMap(people, 'dept');
console.log('Map结果:', mapResult);
console.log('技术部门人数:', mapResult.get('技术').length);

// 测试5:多级分组
console.log('\n===== 5. 多级分组 =====');
const multiGroup = groupArrayMulti(people, ['dept', 'age']);
console.log('按部门-年龄多级分组:', JSON.stringify(multiGroup, null, 2));

// 测试6:范围分组
console.log('\n===== 6. 范围分组 =====');
const salaryRanges = [
  { min: 0, max: 8000 },
  { min: 8000, max: 10000 },
  { min: 10000, max: 15000 },
  { min: 15000, max: Infinity }
];
const bySalary = groupArrayByRange(people, 'salary', salaryRanges);
console.log('按薪资范围分组:', bySalary);

// 测试7:对象分组
console.log('\n===== 7. 对象分组 =====');
const groupedObj = groupObject(testObj, (value) => {
  if (value && typeof value === 'object' && !Array.isArray(value)) return 'objects';
  if (Array.isArray(value)) return 'arrays';
  return typeof value;
});
console.log('对象按值类型分组:', groupedObj);

// 测试8:对象按类型分组
console.log('\n===== 8. 对象按类型分组 =====');
const byType = groupObjectByType(testObj);
console.log('按JavaScript类型分组:', byType);

// 测试9:对象按数值范围分组
console.log('\n===== 9. 对象按数值范围分组 =====');
const scoreRanges = [
  { min: 90, max: 100 },
  { min: 80, max: 90 },
  { min: 70, max: 80 },
  { min: 0, max: 70 }
];
const byScoreRange = groupObjectByValueRange({
  math: 95,
  english: 87,
  chinese: 76,
  physics: 68,
  name: '测试'
}, scoreRanges);
console.log('按分数范围分组:', byScoreRange);

// 测试10:通用接口
console.log('\n===== 10. 通用分组接口 =====');
console.log('通用接口-数组:', groupBy(people, 'dept'));
console.log('通用接口-对象:', groupBy(testObj, v => typeof v));

// 测试11:性能测试
console.log('\n===== 11. 性能测试 =====');
function generateTestData(count) {
  const depts = ['技术', '市场', '产品', '设计', '运营'];
  const data = [];
  for (let i = 0; i < count; i++) {
    data.push({
      id: i,
      dept: depts[Math.floor(Math.random() * depts.length)],
      age: 20 + Math.floor(Math.random() * 30),
      salary: 5000 + Math.floor(Math.random() * 15000)
    });
  }
  return data;
}

const bigData = generateTestData(100000);
console.time('数组分组性能');
const bigResult = groupArray(bigData, 'dept');
console.timeEnd('数组分组性能');
console.log(`数据量:${bigData.length},分组数:${Object.keys(bigResult).length}`);

// ------------------- 5. 极简版本 -------------------

/**
 * 最简版本 - 一行代码实现数组分组
 */
const simpleGroupArray = (arr, fn) => 
  arr.reduce((acc, item) => ((acc[typeof fn === 'function' ? fn(item) : item[fn]] = acc[typeof fn === 'function' ? fn(item) : item[fn]] || []).push(item), acc), {});

/**
 * 最简版本 - 对象分组
 */
const simpleGroupObject = (obj, fn) => 
  Object.entries(obj).reduce((acc, [k, v]) => ((acc[fn(v, k)] = acc[fn(v, k)] || {})[k] = v, acc), {});

// 测试极简版本
console.log('\n===== 12. 极简版本测试 =====');
console.log('极简数组分组:', simpleGroupArray(people, 'dept'));
console.log('极简对象分组:', simpleGroupObject(testObj, v => typeof v));

这个纯JS代码包含了:

📦 数组分组功能

  • groupArray - 基础数组分组(支持去重)
  • groupArrayToMap - 返回Map对象(支持任意类型键)
  • groupArrayMulti - 多级分组(如先按部门,再按年龄)
  • groupArrayByRange - 按数值范围分组

📋 对象分组功能

  • groupObject - 基础对象分组
  • groupObjectByType - 按JavaScript类型分组
  • groupObjectByValueRange - 按数值范围分组

🎯 通用工具

  • groupBy - 自动识别数组/对象的统一接口
  • 多个极简版本实现

特点

  • 纯JS实现,零依赖
  • 处理null/undefined边界情况
  • 支持去重选项
  • 包含完整示例和性能测试
  • 多种风格实现(常规版、Map版、极简版)