从for in , for of 中以小见大

138 阅读5分钟

又是经典的背八股时,for in 遍历对象的可枚举属性,for of 遍历可迭代对象。那么,什么叫做枚举和迭代呢?什么又是可枚举属性可迭代对象呢?

枚举和迭代分别是什么?

举个生活中的例子

假设你有一个书架,上面摆满了书。每本书都有一个编号(键名)和内容(值)。我们可以通过两种方式来查看这些书:

  1. 枚举(Enumeration) :关注的是书的编号。
  2. 迭代(Iteration) :关注的是书的内容。

1. 枚举

你想知道书架上有哪些书的编号(比如1、2、3...),而不是每本书的内容

这个时候就用到for...in

const bookshelf = {
    1: "JavaScript入门",
    2: "算法导论",
    3: "计算机网络"
};

// 枚举书架上的书编号
for (let number in bookshelf) {
    console.log("书的编号:", number);
}

//输出:
//书的编号: 1
//书的编号: 2
//书的编号: 3

重点是获取“键”(书的编号),而不是“值”(书的内容)

2. 迭代

你想一本一本地阅读书架上的书,关注的是书的内容,而不是编号。

const books = ["JavaScript入门", "算法导论", "计算机网络"];

// 迭代书架上的书内容
for (let book of books) {
    console.log("书的内容:", book);
}

//输出
//书的内容: JavaScript入门
//书的内容: 算法导论
//书的内容: 计算机网络

重点是获取“值”(书的内容),而不是“键”(书的编号)

现在是不是就对枚举和迭代有了基本的概念?那么接下来就用程序员的视角再来看一看枚举和迭代

程序员眼中的枚举和迭代

枚举

枚举指的是通过某种方式逐一访问一个对象的所有属性或键名。它通常用于描述对象的属性是否可以通过某些方法(如 for...inObject.keys())被访问。

特点

  • 枚举的对象通常是键值对 形式的数据结构,比如普通对象。
  • 枚举的核心是访问对象的键名 (属性名),而不是直接访问值。
  • 枚举的结果可能包括对象自身的属性以及从原型链继承的属性(需要手动过滤)。

例子

const obj = { a: 1, b: 2, c: 3 };

// 使用 for...in 枚举对象的键名
for (let key in obj) {
    console.log(key); // 输出:a, b, c
}

// 使用 Object.keys() 获取对象的可枚举属性
console.log(Object.keys(obj)); // 输出:['a', 'b', 'c']

// 使用 Object.entries() 获取键值对
console.log(Object.entries(obj)); // 输出:[['a', 1], ['b', 2], ['c', 3]]

注意

枚举会包含从原型链继承的属性,因此需要结合hasOwnProperty()来过滤掉继承的属性

for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log(key); // 只输出自身属性
    }
}

迭代

迭代指的是通过某种协议逐一访问一个集合(如数组、字符串、MapSet 等)中的元素。迭代强调的是对 的访问,而不是键名。

特点

  • 迭代的对象通常是有序集合 ,比如数组、字符串、MapSet 等。
  • 迭代的核心是访问集合中的 ,而不是键名。

例子

// 数组迭代
const arr = [10, 20, 30];
for (let value of arr) {
    console.log(value); // 输出:10, 20, 30
}

// 字符串迭代
const str = "hello";
for (let char of str) {
    console.log(char); // 输出:h, e, l, l, o
}#

// Map 迭代
const map = new Map([
    ['a', 1],
    ['b', 2]
]);
for (let [key, value] of map) {
    console.log(key, value); // 输出:a 1, b 2
}

// Set 迭代
const set = new Set([1, 2, 3]);
for (let value of set) {
    console.log(value); // 输出:1, 2, 3
}

可枚举属性与可迭代对象

可枚举属性

可枚举属性是指那些在使用特定方法(如 for...inObject.keys() 等)时可以被访问到的属性。

  • 每个对象的属性都有一个内部特性 [[Enumerable]],它决定了该属性是否是可枚举的。
  • 如果 [[Enumerable]]true,则该属性是可枚举的;如果为 false,则是不可枚举的。

如果你知道defineProperty,那么你可以通过他来修改enumerable的值

注意

  • 默认情况下,通过字面量或构造函数创建的对象属性是可枚举的。
  • 内置对象(如 Array.prototypeObject.prototype)上的方法通常是不可枚举的。
  • 使用 Object.defineProperty() 创建的属性,默认是不可枚举的,除非显式设置 enumerable: true
  • 可以使用 Object.getOwnPropertyDescriptor() 来查看某个属性是否是可枚举的:
const obj = { a: 1 };
Object.defineProperty(obj, 'b', { value: 2, enumerable: false });

console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
// 输出:{ value: 1, writable: true, enumerable: true, configurable: true }

console.log(Object.getOwnPropertyDescriptor(obj, 'b'));
// 输出:{ value: 2, writable: false, enumerable: false, configurable: false }

如何遍历可枚举属性:

  • 使用 for...in 遍历对象的所有可枚举属性(包括继承的属性)。
  • 使用 Object.keys() 获取对象自身的所有可枚举属性。
  • 使用 Object.values()Object.entries() 分别获取可枚举属性的值和键值对。
const obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', { value: 3, enumerable: false });

for (let key in obj) {
    console.log(key); // 输出:a, b
}

console.log(Object.keys(obj)); // 输出:['a', 'b']
console.log(Object.values(obj)); // 输出:[1, 2]

可迭代对象

可迭代对象是指那些实现了迭代协议 (Iteration Protocol)的对象。迭代协议允许对象通过 for...of 循环或其他需要迭代的地方(如扩展运算符 ...、解构赋值等)进行遍历。

  • 一个对象要成为可迭代对象,必须实现一个特殊的属性 [Symbol.iterator],并且这个属性的值是一个返回迭代器的方法。

注意

  • 常见的内置可迭代对象包括:数组、字符串、MapSetNodeList 等。
  • 普通对象默认不是可迭代对象,但可以通过自定义 [Symbol.iterator] 方法使其变得可迭代。
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // 输出:'function'

const obj = { a: 1, b: 2 };
console.log(typeof obj[Symbol.iterator]); // 输出:'undefined'

最后

  • for...in用于遍历对象的可枚举属性,它主要用于遍历对象的键,而不是值
  • for...of 用于遍历可迭代对象 的值。
  • 但一个数据类型并不是绝对的,例如,数组的索引是可枚举的,而数组本身是可迭代的