首先我们来复习一下数组原型上这些会对数组进行遍历的操作:

方法返回值作用
forEach无返回值遍历数组,对每个元素执行回调函数
map新数组遍历数组,对每个元素执行回调函数并返回新的元素,组成新的数组
filter新数组遍历数组,筛选出满足条件的元素,组成新的数组
every布尔值遍历数组,判断是否所有元素都满足条件
some布尔值遍历数组,判断是否有至少一个元素满足条件
reduce累积值遍历数组,对累积值和当前元素执行回调函数,返回最终的累积值

他们都接受一个回调函数和一个可选值作为参数:

forEach、map、filter、every、some这五种方法的回调函数都接受三个参数,分别是:

  1. 当前元素(element):数组中正在处理的当前元素。
  2. 当前索引(index):数组中正在处理的当前元素的索引。
  3. 数组(array):调用了该方法的数组。

可选值是 thisArg,执行 callbackFn 时用作 this 的值,如果没有传入,则回调函数中的 this 值为 undefined

reduce方法的回调函数接受四个参数,分别是:

  1. 累积值(accumulator):累积器累积回调函数的返回值;它是上一次调用回调函数时返回的累积值,或者初始值(如果提供了的话)。
  2. 当前元素(currentValue):数组中正在处理的当前元素。
  3. 当前索引(currentIndex):数组中正在处理的当前元素的索引。
  4. 数组(array):调用了reduce方法的数组。

可选值是 initialValue ,第一次调用回调时初始化 accumulator 的值。如果指定了 initialValue,则 callbackFn 从数组中的第一个值作为 currentValue 开始执行。如果没有指定 initialValue,则 accumulator 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值作为 currentValue 开始执行。在这种情况下,如果数组为空(没有第一个值可以作为 accumulator 返回),则会抛出错误。

好了,大致上的功能和使用方式就是这样,接下来让我们动手实现他们吧:

forEach

/**
 * 使用ES6语法实现数组的forEach方法
 * @param {Function} callback - 回调函数
 * @param {Object} [thisArg] - 回调函数中的this值
 */
Array.prototype.myForEach = function(callback, thisArg) {
  // 判断callback是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }
 
  // 获取数组长度
  let len = this.length;
 
  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 判断数组中该元素是否存在
    if (Object.prototype.hasOwnProperty.call(this, i)) {
      // 调用回调函数,改变this指向为thisArg
      callback.call(thisArg, this[i], i, this);
    }
  }
}

map

/**
 * 使用ES6语法实现数组的map方法
 * @param {Function} callback - 回调函数
 * @param {Object} [thisArg] - 回调函数中的this值
 * @return {Array} - 一个新数组,每个元素都是回调函数的返回值
 */
Array.prototype.myMap = function(callback, thisArg) {
  // 判断callback是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }
 
  // 获取数组长度
  let len = this.length;
 
  // 创建新数组
  let result = new Array(len);
 
  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 判断数组中该元素是否存在
    if (Object.prototype.hasOwnProperty.call(this, i)) {
      // 调用回调函数,改变this指向为thisArg
      result[i] = callback.call(thisArg, this[i], i, this);
    }
  }
 
  // 返回新数组
  return result;
}

filter

/**
 * 使用ES6语法实现数组的filter方法
 * @param {Function} callback - 回调函数
 * @param {Object} [thisArg] - 回调函数中的this值
 * @return {Array} - 一个新数组,包含所有满足条件的元素
 */
Array.prototype.myFilter = function(callback, thisArg) {
  // 判断callback是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }
 
  // 获取数组长度
  let len = this.length;
 
  // 创建新数组
  let result = [];
 
  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 判断数组中该元素是否存在
    if (Object.prototype.hasOwnProperty.call(this, i)) {
      // 调用回调函数,改变this指向为thisArg
      if (callback.call(thisArg, this[i], i, this)) {
        result.push(this[i]);
      }
    }
  }
 
  // 返回新数组
  return result;
}

every

/**
 * 使用ES6语法实现数组的every方法
 * @param {Function} callback - 回调函数
 * @param {Object} [thisArg] - 回调函数中的this值
 * @return {Boolean} - 如果数组中的每个元素都满足回调函数的测试,则返回true,否则返回false
 */
Array.prototype.myEvery = function(callback, thisArg) {
  // 判断callback是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }
 
  // 获取数组长度
  let len = this.length;
 
  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 判断数组中该元素是否存在
    if (Object.prototype.hasOwnProperty.call(this, i)) {
      // 调用回调函数,改变this指向为thisArg
      let result = callback.call(thisArg, this[i], i, this);
      // 如果回调函数返回false,则返回false
      if (!result) {
        return false;
      }
    }
  }
 
  // 如果遍历完整个数组都没有返回false,则返回true
  return true;
}

some

/**
 * 使用ES6语法实现数组的some方法
 * @param {Function} callback - 回调函数
 * @param {Object} [thisArg] - 回调函数中的this值
 * @return {Boolean} - 如果数组中至少有一个元素满足回调函数的测试,则返回true,否则返回false
 */
Array.prototype.mySome = function(callback, thisArg) {
  // 判断callback是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }
 
  // 获取数组长度
  let len = this.length;
 
  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 判断数组中该元素是否存在
    if (Object.prototype.hasOwnProperty.call(this, i)) {
      // 调用回调函数,改变this指向为thisArg
      let result = callback.call(thisArg, this[i], i, this);
      // 如果回调函数返回true,则返回true
      if (result) {
        return true;
      }
    }
  }
 
  // 如果遍历完整个数组都没有返回true,则返回false
  return false;
}

reduce

/**
 * 使用ES6语法实现数组的reduce方法
 * @param {Function} callback - 回调函数
 * @param {any} [initialValue] - 初始值
 * @return {any} - 函数累计处理的结果
 */
Array.prototype.myReduce = function(callback, initialValue) {
  // 判断callback是否为函数
  if (typeof callback !== 'function') {
    throw new TypeError(`${callback} is not a function`);
  }
 
  // 获取数组长度
  let len = this.length;
 
  // 定义累积值和起始索引
  let accumulator, startIndex;
 
  // 如果提供了初始值,则累积值为初始值,起始索引为0
  if (arguments.length >= 2) {
    accumulator = initialValue;
    startIndex = 0;
  } else {
    // 如果没有提供初始值,则累积值为数组中第一个存在的元素,起始索引为1
    startIndex = -1;
    for (let i = 0; i < len; i++) {
      if (Object.prototype.hasOwnProperty.call(this, i)) {
        accumulator = this[i];
        startIndex = i + 1;
        break;
      }
    }
 
    // 如果数组为空且没有提供初始值,则抛出错误
    if (startIndex === -1) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
  }
 
  // 遍历数组
  for (let i = startIndex; i < len; i++) {
    // 判断数组中该元素是否存在
    if (Object.prototype.hasOwnProperty.call(this, i)) {
      // 调用回调函数,更新累积值
      accumulator = callback(accumulator, this[i], i, this);
    }
  }
 
  // 返回累积值
  return accumulator;
}

思考

其实只要理解了 forEach 的实现,再往后推导实现其他的方法就不难了。值得注意的是,我们在判断数组中第i位是否存在的时候使用了 Object.prototype.hasOwnProperty.call(this, i),这是为什么呢?这是为了避免数组中的空位带来影响:

let arr = [1, , undefined];
console.log(arr[1]); // undefined
console.log(arr[2]); // undefined
 
let arr = [1, , undefined];
console.log(arr.hasOwnProperty(1)); // false
console.log(arr.hasOwnProperty(2)); // true

如果数组中某一位的元素正好是 undefined,那么如果直接通过 this[i] 读取他我们是无法判断这一位到底是空位,还是值为 undefined 的元素。举个例子:

let arr = [1, , 3];
let result = arr.map(x => x * 2);
console.log(result); // [2, 空, 6]

而如果我们在 myMap 中不做 if(Object.prototype.hasOwnProperty.call(this, i)) 的判断:

let arr = [1, , 3];
let result = arr.myMap(x => x * 2);
console.log(result); // [2, NaN, 6]

可以发现二者对于空位的返回结果是不同的,因为后者没有跳过对 undefined 元素的处理,导致 undefined 值乘以2,得到 NaN 值。原生的方法中,对空位是会跳过处理的,而对值为 undefined 的元素不会,在我们手写实现的时候要注意到这一点。