可以用 for of 遍历 Object 吗

发布时间 2023-11-27 16:50:39作者: 柯基与佩奇

什么是迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。 ——《设计模式:可复用面向对象软件的基础》

可以说迭代器模式就是为了遍历存在的。提到遍历,大家都对那些手段耳熟能详了,下面先简单列一下各种数据类型的遍历:

遍历数组

for 循环
forEach
map
reduce
keys
values
for of
......

其中 keys、values、for of 需要 Iterator 支持,后面会介绍 Iterator

遍历 Map/Set

keys
entries
forEach
......

遍历 Object

  1. for in
  2. 先 Object.keys(obj)得到对象每个属性的数组, 然后使用数组的遍历方法遍历每个 key,就能获取 每个 key 对应的 value

Iterator 和 for of

Iterator 是 ES6 提出的一个接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。

Iterator 的作用

为各种数据结构,提供一个统一的、简便的访问接口。
ES6 提出了新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。

Iterator 的遍历过程

既然数组是支持 for...of 循环的,那数组肯定部署了 Iterator 接口,通过它来看看 Iterator 的遍历过程。

从图中能看出:

  1. Iterator 接口返回了一个有 next 方法的对象。
  2. 每调用一次 next,依次返回了数组中的项,直到它指向数据结构的结束位置。
  3. 返回的结果是一个对象,对象中包含了当前值 value 和 当前是否结束 done

用 for of 遍历 Object

回到标题中的问题,现在如何去让一个对象也可以用 for of 来遍历它呢?

根据上面讲到的内容,需要给对象也部署 Iterator 接口(其实就是在 Object.prototype 上实现一个以 Symbol.iterator 为名的 function,这个 function 返回一个有 next 方法的对象,每调用一次 next, 能够依次返回数组中的项,直到它指向数据结构的结束位置 )

function objectIterator() {
  const keys = Object.keys(this);
  let index = 0;
  return {
    next: () => {
      const done = index >= keys.length;
      const value = done ? undefined : this[keys[index]];
      index++;
      return {
        done,
        value,
      };
    },
  };
}

Object.prototype[Symbol.iterator] = objectIterator;

const obj = {
  key: "1",
  value: "2",
};

for (const iterator of obj) {
  console.log(iterator);
}

既然说对象没有 Symbol.iterator 方法,那么为什么对象也能实现解构呢?

解构的本质

在这里就不再对解构的一些语法进行讲解了,如果有需要就自行去 MDN 进行查阅吧,那里讲得更详细。

先来看一个 demo,定义有如下的对象,代码如下所示:

const obj = {
  nickname: 7,
  age: 18,
  address: "西安",
};

let { nickname, ...reset } = obj;

通过控制台输出 nicknane 的值为 7,而 reset 的值为一个对象:

{
  "age": 18,
  "address": "西安"
}

通过断点调试去查看变量的值也正是发现它会复制给一个新变量,这个变量名也就是左边解构出来的字段,如下图所示:

又因为这是一个新的赋值,所以你修改解构出来的值也并不会改变源对象的值,因为这已经是一个全新的值,请看代码:

const obj = {
  nickname: 7,
  age: 18,
  address: "西安",
};

let { nickname, ...rest } = obj;
console.log(obj);
console.log(nickname);
nickname = 777;
console.log(obj);
console.log(nickname);

看到上图的输出了吧,对象原样输出,修改 nickname 只会改变外部变量 nickname 的值,并不会改变源对象的值。

相面的对象解构,实际上是经过了这样的过程:

let nickname = obj.nickname;

剩余参数解构原理

在上面中讲到对象解构的原理确实很简单,但是剩余参数这里做的事情比较多,但是并不是说很难,通过 Babel 可以查看到这个事情的本质,先来看一个 demo:

const obj = {
  nickname: 7,
  age: 18,
  address: "西安",
  [Symbol("test")]: "111",
};

const { nickname, ...rest } = obj;

将这些代码将去放去 Babel 进行代码编译,最终代码会被编译成以下代码:

function _objectWithoutProperties(source, excluded) {
  if (source == null) return {};
  var target = _objectWithoutPropertiesLoose(source, excluded);
  var key, i;
  if (Object.getOwnPropertySymbols) {
    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
    console.log(sourceSymbolKeys);
    for (i = 0; i < sourceSymbolKeys.length; i++) {
      key = sourceSymbolKeys[i];
      if (excluded.indexOf(key) >= 0) continue;
      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
      target[key] = source[key];
    }
  }
  return target;
}
function _objectWithoutPropertiesLoose(source, excluded) {
  if (source == null) return {};
  var target = {};
  var sourceKeys = Object.keys(source);
  var key, i;
  for (i = 0; i < sourceKeys.length; i++) {
    key = sourceKeys[i];
    if (excluded.indexOf(key) >= 0) continue;
    target[key] = source[key];
  }
  console.log(target);
  return target;
}

var obj = {
  nickname: 7,
  age: 18,
  address: "西安",
  [Symbol("test")]: "111",
};

var nickname = obj.nickname,
  rest = _objectWithoutProperties(obj, ["nickname"]);

上面这段代码主要做的事情是有如下几点:

  1. 调用 _objectWithoutProperties 函数,并传入 obj 对象和前面已经解构过的值;
  2. 在该函数中调用 _objectWithoutPropertiesLoose 函数获取到剩余的属性或者方法;
  3. 因为 Object.keys 方法无法遍历 Symbol 属性,所以又继续调用 Object.getOwnPropertySymbols 方法获取剩下的 Symbol 属性;
  4. 最后生成一个新的对象并返回;

所以最终总结成一句话就是,对象的结构就是: 创建新变量 -> 枚举属性 -> 复制属性并赋值。