跟着ECMAScript 规范,手写数组方法之reduce

27 阅读4分钟

reduce 就是通过一个累积器 ,把数组中的所有元素“遍历-回调-累积”,最终得到一个单一的结果。

主干

Array.prototype.myReduce = function(callback, initialValue) {
  // 1. 初始化累积器
  let accumulator = initialValue;
  
  // 2. 遍历数组
  for (let i = 0; i < this.length; i++) {
    // 3. 调用回调函数,更新累积器
    accumulator = callback(accumulator, this[i], i, this);
  }

  // 4. 返回最终结果
  return accumulator;
};

树枝

防御性编程

问题:主干版本没有进行任何参数校验,如果传入非函数或 this 为 null 会出错。

添加的树枝:在函数开头增加参数校验。

Array.prototype.myReduce = function(callback, initialValue) {
  // 【新增】防御性编程:检查 this 和 callback
  if (this == null) throw new TypeError('this is null or undefined');
  if (typeof callback !== 'function') throw new TypeError('callback must be a function');

  let accumulator = initialValue;
  for (let i = 0; i < this.length; i++) {
    accumulator = callback(accumulator, this[i], i, this);
  }
  return accumulator;
};

处理类数组对象

问题:主干版本直接使用 this,但在类数组对象(如 arguments、字符串)上调用会出错。

添加的树枝:将 this 转换为对象,并安全地获取其长度。

Array.prototype.myReduce = function(callback, initialValue) {
  if (this == null) throw new TypeError('this is null or undefined');
  if (typeof callback !== 'function') throw new TypeError('callback must be a function');

  // 【新增】处理类数组对象
  const O = Object(this); // 确保this总是一个对象
  const len = O.length >>> 0; // 确保长度是非负整数

  let accumulator = initialValue;
  // 【修改】遍历 O 而不是 this
  for (let i = 0; i < len; i++) {
    accumulator = callback(accumulator, O[i], i, O);
  }
  return accumulator;
};

正确处理初始值逻辑

问题:主干版本对 initialValue 的处理不完善,没有考虑无初始值时的情况,这会导致错误。

添加的树枝:重写初始化逻辑,根据有无 initialValue 设置不同的起始点和累积器初始值。

Array.prototype.myReduce = function(callback, initialValue) {
  if (this == null) throw new TypeError('this is null or undefined');
  if (typeof callback !== 'function') throw new TypeError('callback must be a function');
  const O = Object(this);
  const len = O.length >>> 0;

  // 【新增】处理初始值的复杂逻辑
  let k = 0; // 起始索引
  let accumulator; // 累积器

  if (arguments.length >= 2) { // 有初始值
    accumulator = initialValue;
  } else { // 无初始值
    // 找到第一个存在的元素作为初始值
    while (k < len && !(k in O)) k++;
    if (k >= len) throw new TypeError('Reduce of empty array with no initial value');
    accumulator = O[k++];
  }

  // 【修改】从 k 开始遍历
  for (; k < len; k++) {
    accumulator = callback(accumulator, O[k], k, O);
  }
  return accumulator;
};

处理稀疏数组

问题:即使到了上一步,for 循环仍然会把空位(empty slot)当作 undefined 来处理,不符合原生 reduce 行为。

添加的树枝:在循环内部增加判断,跳过稀疏数组的空位。

Array.prototype.myReduce = function(callback, initialValue) {
  if (this == null) throw new TypeError('this is null or undefined');
  if (typeof callback !== 'function') throw new TypeError('callback must be a function');
  const O = Object(this);
  const len = O.length >>> 0;
  let k = 0;
  let accumulator;
  if (arguments.length >= 2) {
    accumulator = initialValue;
  } else {
    while (k < len && !(k in O)) k++;
    if (k >= len) throw new TypeError('Reduce of empty array with no initial value');
    accumulator = O[k++];
  }

  // 【新增】处理稀疏数组:跳过空位
  while (k < len) {
    if (k in O) { // 关键:检查索引是否存在
      accumulator = callback(accumulator, O[k], k, O);
    }
    k++;
  }

  return accumulator;
};

测试

// 1. 基本功能测试:数组求和(有初始值)
console.log('--- 1. 基本求和(有初始值) ---');
const sum1 = [1, 2, 3, 4].myReduce((acc, cur) => acc + cur, 10);
console.log('myReduce 结果:', sum1); // 期望: 20
console.log('原生 reduce 结果:', [1, 2, 3, 4].reduce((acc, cur) => acc + cur, 10)); // 期望: 20
console.log('---------------------------\n');


// 2. 基本功能测试:数组求和(无初始值)
console.log('--- 2. 基本求和(无初始值) ---');
const sum2 = [1, 2, 3, 4].myReduce((acc, cur) => acc + cur);
console.log('myReduce 结果:', sum2); // 期望: 10
console.log('原生 reduce 结果:', [1, 2, 3, 4].reduce((acc, cur) => acc + cur)); // 期望: 10
console.log('---------------------------\n');


// 3. 边界情况测试:空数组(有初始值)
console.log('--- 3. 空数组(有初始值) ---');
const emptyWithInitial = [].myReduce((acc, cur) => acc + cur, 'start');
console.log('myReduce 结果:', emptyWithInitial); // 期望: 'start'
console.log('原生 reduce 结果:', [].reduce((acc, cur) => acc + cur, 'start')); // 期望: 'start'
console.log('---------------------------\n');


// 4. 边界情况测试:空数组(无初始值)
console.log('--- 4. 空数组(无初始值) ---');
try {
  [].myReduce((acc, cur) => acc + cur);
} catch (e) {
  console.log('myReduce 捕获到错误:', e.message); // 期望: 'Reduce of empty array with no initial value'
}
try {
  [].reduce((acc, cur) => acc + cur);
} catch (e) {
  console.log('原生 reduce 捕获到错误:', e.message); // 期望: 'Reduce of empty array with no initial value'
}
console.log('---------------------------\n');


// 5. 稀疏数组测试:跳过空位
console.log('--- 5. 稀疏数组测试 ---');
const sparse = [1, , 3, , 5];
const sparseSum = sparse.myReduce((acc, cur) => acc + cur, 0);
console.log('稀疏数组:', sparse);
console.log('myReduce 结果:', sparseSum); // 期望: 9 (1+3+5)
console.log('原生 reduce 结果:', sparse.reduce((acc, cur) => acc + cur, 0)); // 期望: 9
console.log('---------------------------\n');


// 6. 类数组对象测试:在 arguments 上调用
console.log('--- 6. 类数组对象测试 ---');
function testArguments() {
  const argsSum = Array.prototype.myReduce.call(arguments, (acc, cur) => acc + cur, 0);
  console.log('arguments 对象:', arguments);
  console.log('myReduce 在 arguments 上的结果:', argsSum); // 期望: 15 (5+10)
  console.log('原生 reduce 在 arguments 上的结果:', Array.prototype.reduce.call(arguments, (acc, cur) => acc + cur, 0)); // 期望: 15
}
testArguments(5, 10);
console.log('---------------------------\n');


// 7. 类数组对象测试:在字符串上调用
console.log('--- 7. 字符串测试 ---');
const reversedStr = Array.prototype.myReduce.call('hello', (acc, cur) => cur + acc, '');
console.log('myReduce 在字符串上的结果:', reversedStr); // 期望: 'olleh'
console.log('原生 reduce 在字符串上的结果:', Array.prototype.reduce.call('hello', (acc, cur) => cur + acc, '')); // 期望: 'olleh'
console.log('---------------------------\n');


// 8. 防御性编程测试:callback 不是函数
console.log('--- 8. callback 不是函数 ---');
try {
  [1, 2, 3].myReduce('not a function');
} catch (e) {
  console.log('myReduce 捕获到错误:', e.message); // 期望: 'callback must be a function'
}
try {
  [1, 2, 3].reduce('not a function');
} catch (e) {
  console.log('原生 reduce 捕获到错误:', e.message); // 期望: 'callback must be a function'
}
console.log('---------------------------\n');

参考

3.实现数组map、filter、reduce_哔哩哔哩_bilibili