前端

JavaScript可迭代性与枚举性

梗概

JavaScript中的迭代性和枚举性是两个相关但不同的概念:

  • 可迭代性(Iterability): 决定对象是否可以使用for...of展开运算符解构赋值等特性
  • 可枚举性(Enumerability): 决定对象属性是否可以被for...inObject.keys()等方法枚举

1. 可迭代性 (Iterability)

1.1 什么是可迭代对象

具有迭代器的对象称为可迭代对象(Iterable)。一个对象要成为可迭代对象,必须实现@@iterator方法,即对象的Symbol.iterator属性必须是一个返回迭代器的函数。

1.2 内置可迭代对象

JavaScript中以下内置类型都是可迭代的:

  1. Array
  2. String
  3. Map
  4. Set
  5. TypedArray
  6. Arguments
  7. NodeList

1.3 与可迭代性相关的API

1.3.1 for…of 循环

// 遍历数组
const array = [1, 2, 3];
for (const item of array) {
  console.log(item); // 1, 2, 3
}
 
// 遍历字符串
const str = "hello";
for (const char of str) {
  console.log(char); // "h", "e", "l", "l", "o"
}

for...of循环本质上是使用目标对象所提供的迭代器进行迭代,因此遍历的顺序和内容是由目标对象内部决定的。

1.3.2 展开运算符 (…)

展开运算符可以将可迭代对象展开为单独的元素:

// 展开数组
const array1 = [1, 2, 3];
const array2 = [4, 5, 6];
const combined = [...array1, ...array2]; // [1, 2, 3, 4, 5, 6]
 
// 展开字符串
const chars = [...'hello']; // ['h', 'e', 'l', 'l', 'o']
 
// 转换可迭代对象为数组
const set = new Set([1, 2, 3]);
const arrayFromSet = [...set]; // [1, 2, 3]

展开运算符是浅拷贝的一种实现方式。

1.3.3 解构赋值

解构赋值可以从可迭代对象中提取值并赋给变量:

// 数组解构
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1, 2, 3
 
// 忽略某些值
const [first, , third] = [1, 2, 3];
console.log(first, third); // 1, 3
 
// 结合展开运算符
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail); // 1, [2, 3, 4]
 
// 解构字符串
const [x, y, ...rest] = 'hello';
console.log(x, y, rest); // 'h', 'e', ['l', 'l', 'o']

任何具有迭代器的对象都能被当成数组解构。可以给解构时声明的变量赋予默认值,未匹配到的值将是undefined。

1.4 自定义可迭代对象

可以为对象定义迭代器使其成为可迭代对象:

const myIterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};
 
for (const item of myIterable) {
  console.log(item); // 1, 2, 3
}

1.5 判断对象是否为可迭代

可以通过检查对象是否具有Symbol.iterator方法来判断对象是否可迭代:

function isIterable(obj) {
  return obj != null && typeof obj[Symbol.iterator] === 'function';
}
 
console.log(isIterable([])); // true
console.log(isIterable('hello')); // true
console.log(isIterable(new Map())); // true
console.log(isIterable({})); // false

2. 可枚举性 (Enumerability)

可枚举性是对象属性的一个特性,它决定了该属性是否会在某些操作中被包含或排除。

2.1 与可枚举性相关的API

2.1.1 for…in 循环

遍历对象自身和原型链上的所有可枚举属性:

const obj = { a: 1, b: 2 };
Object.prototype.c = 3;
 
for (const key in obj) {
  console.log(key); // 'a', 'b', 'c'(包含继承的属性)
  
  if (obj.hasOwnProperty(key)) {
    console.log(`自身属性: ${key}`); // 'a', 'b'(不包含继承的属性)
  }
}

2.1.2 Object.keys(obj)

返回对象自身的可枚举属性名组成的数组:

const obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', {
  value: 3,
  enumerable: false // 不可枚举
});
Object.prototype.d = 4;
 
console.log(Object.keys(obj)); // ['a', 'b'](不包括不可枚举属性和继承属性)

2.1.3 JSON.stringify(obj)

序列化对象时,只会包含对象自身的可枚举属性:

const obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', {
  value: 3,
  enumerable: false
});
 
console.log(JSON.stringify(obj)); // '{"a":1,"b":2}'(不包含不可枚举属性)

2.2 设置属性的可枚举性

可以使用Object.defineProperty()Object.defineProperties()设置属性的可枚举性:

const obj = { a: 1 }; // 默认可枚举
 
// 添加不可枚举属性
Object.defineProperty(obj, 'b', {
  value: 2,
  enumerable: false,
  writable: true,
  configurable: true
});
 
console.log(obj.b); // 2
console.log(Object.keys(obj)); // ['a']
 
// 批量设置属性
Object.defineProperties(obj, {
  c: {
    value: 3,
    enumerable: true
  },
  d: {
    value: 4,
    enumerable: false
  }
});
 
console.log(Object.keys(obj)); // ['a', 'c']

2.3 检查属性的可枚举性

使用propertyIsEnumerable()方法检查属性是否可枚举:

const obj = { a: 1 };
Object.defineProperty(obj, 'b', {
  value: 2,
  enumerable: false
});
 
console.log(obj.propertyIsEnumerable('a')); // true
console.log(obj.propertyIsEnumerable('b')); // false

3. 可迭代性与可枚举性的区别

  1. 适用对象不同

    • 可迭代性适用于整个对象
    • 可枚举性适用于对象的属性
  2. 相关API不同

    • 可迭代对象可用于:for...of、展开运算符、解构赋值
    • 可枚举属性可用于:for...inObject.keys()JSON.stringify()
  3. 实现方式不同

    • 可迭代通过实现Symbol.iterator方法
    • 可枚举通过设置属性的enumerable特性

4. 常见误解澄清

  • 对象本身并不可迭代:标准对象({})默认不可迭代,除非显式实现迭代器
  • 每个JS对象属性都有”可枚举性”:这是正确的,属性可以是可枚举的或不可枚举的
  • 每个JS对象可定义迭代器:这是正确的,任何对象都可以通过定义Symbol.iterator方法变为可迭代对象

5. 最佳实践

  1. 遍历自身属性:使用Object.keys()Object.getOwnPropertyNames()而非for...in循环
  2. 检查属性存在:使用obj.hasOwnProperty(key)而非简单的key in obj
  3. 库开发:将内部属性设为不可枚举,避免影响用户代码
  4. 序列化控制:利用不可枚举属性控制JSON序列化输出
  5. 性能考虑Object.keys()通常比for...inhasOwnProperty组合更快