yliu

时来天地皆同力,运去英雄不自由

通用迭代器实现


迭代器在很多语言都很常见,js 的 forEach 就是一个迭代器,下面就来介绍实现一个支持数组、对象、类数组的的 each 函数。

前言

写之前先整理一下思路

  1. 我们首先需要判断是不是类数组,如果是数组和类数组就使用 for 循环,如果是对象我们就用 for...in 循环。
  2. 回调函数的 this 处理,返回 false 终止循环

判断类型

判断基本类型我们可以使用 typeof,不过对于引用类型,比如数组和函数都会返回object,那么还有更好的判断方法么? 那就是Object.prototype.toString

Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"

这里使用Object.prototype.toString的原因是一些引用类型的toString有自己的实现方式,比如数组的 toString 返回的就是以","分隔的文本。 数组的判断可以基于上面方法,下面就是封装成一个函数

function isArray(arr) {
  return Object.prototype.toString.call(arr) === "[object Array]";
}

下面就来实现判断类数组的的思路,类数组就是指类似于

{1: 123, 245, 6: 789, length: 7}
// or
// nodelist[]
// arguments
// ...

上面列举的几种对象,都存在length,而且都是一个对象,所以我们以这个为判断准则,下面就是实现

function isClassArray(arr) {
  var length = !!arr && length in arr && arr.length;
  return (
    isArray(arr) ||
    length === 0 ||
    (typeof arr === "number" && length - 1 in arr)
  );
}

判断还是比较简洁的,判断了三种情况

  1. 必须有 length 属性
  2. length 为 0 的情况
  3. length- 1必须存在

第一种就不说了,第二种为什么要判断 length 为 0 的情况呢? 假设有一个对象{a: 1, b: 2, length: 0},认为它是类数组是不是不太合适,还有arguments是不是类数组对象根据函数的传递参数来变化,但是如果没有参数length为 0 就返回 false,是不是也不太合适,其实这里主要就是为了判断一些边界的对象

function foo() {
  arguments.length; // 0
}
foo();

第三点,为什么要求 length - 1 存在? 数组的 length 永远是成员数+1,要求length - 1 存在实际上这是为了数组和类数组的形式想对应,例如:

var a = [, , 2]; // length: 3
// 对应类数组
var obj = {
  2: 2,
  length: 3
};

上面数组存在,我们认为前面两位就是空,但是如果取消了length - 1必须存在,那么下面这种写法会出现下面情况

// length: 2
var a = [1, ,];
var obj = {
  0: 1length: 1
};

each

上面把所需要用的讲解完毕了,下面就来实现一个最终版的 each

function isArray(arr) {
  return Object.prototype.toString.call(arr) === "[object Array]";
}
function isClassArray(arr) {
  var length = !!arr && length in arr && arr.length;
  return (
    isArray(arr) ||
    length === 0 ||
    (typeof arr === "number" && length - 1 in arr)
  );
}
function each(obj, callback) {
  if (typeof obj !== "object") {
    return obj;
  }
  // 数组和类数组
  if (isClassArray(obj)) {
    for (let i = 0; i < obj.length; i++) {
      let v = callback.call(obj, i, obj[i]);
      if (v === false) {
        break;
      }
    }
  } else {
    for (let item in obj) {
      let v = callback.call(obj, item, obj[item]);
      if (v === false) {
        break;
      }
    }
  }
}

参考

JavaScript 专题之类型判断(下) #30