阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > Symbol与for...in循环

Symbol与for...in循环

作者:陈川 阅读数:5689人阅读 分类: JavaScript

Symbol类型的基本特性

Symbol是ECMAScript 6引入的新的原始数据类型,表示独一无二的值。它通过Symbol()函数创建:

const sym1 = Symbol();
const sym2 = Symbol('description');

每个Symbol值都是唯一的,即使传入相同的描述字符串:

Symbol('foo') === Symbol('foo')  // false

Symbol的主要用途是作为对象属性的标识符,防止属性名冲突。当需要向对象添加属性时,使用Symbol可以确保不会覆盖已有属性:

const obj = {};
const mySymbol = Symbol();

obj[mySymbol] = 'value';
console.log(obj[mySymbol]);  // "value"

for...in循环的传统行为

在ES5及之前版本中,for...in循环用于遍历对象的所有可枚举属性(包括继承的可枚举属性):

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

for (const key in obj) {
  console.log(key);  // 依次输出 "a", "b"
}

需要注意的是,for...in会遍历原型链上的可枚举属性:

function Parent() {
  this.parentProp = 'parent';
}

function Child() {
  this.childProp = 'child';
}

Child.prototype = new Parent();

const child = new Child();

for (const key in child) {
  console.log(key);  // 依次输出 "childProp", "parentProp"
}

Symbol属性与for...in的交互

ES6规定,Symbol类型的属性不会被for...in循环枚举:

const obj = {
  [Symbol('sym1')]: 'symbol value',
  regularProp: 'regular value'
};

for (const key in obj) {
  console.log(key);  // 只输出 "regularProp"
}

这种行为设计是为了保持向后兼容性,避免现有代码因Symbol属性的引入而受到影响。如果要获取对象的所有Symbol属性,可以使用Object.getOwnPropertySymbols()

const symKeys = Object.getOwnPropertySymbols(obj);
console.log(symKeys);  // [Symbol(sym1)]

完整属性遍历方法比较

ES6提供了多种遍历对象属性的方式,它们对Symbol属性的处理各不相同:

  1. for...in:遍历对象自身及继承的可枚举字符串属性,不包括Symbol
  2. Object.keys():返回对象自身可枚举字符串属性数组,不包括Symbol
  3. Object.getOwnPropertyNames():返回对象自身所有字符串属性数组(包括不可枚举),不包括Symbol
  4. Object.getOwnPropertySymbols():返回对象自身所有Symbol属性数组(包括不可枚举)
  5. Reflect.ownKeys():返回对象自身所有键的数组(包括字符串和Symbol,包括不可枚举)
const obj = {
  [Symbol('sym')]: 'symbol',
  enumProp: 'enumerable',
  nonEnumProp: 'non-enumerable'
};

Object.defineProperty(obj, 'nonEnumProp', {
  enumerable: false
});

// 各种遍历方法比较
console.log(Object.keys(obj));  // ["enumProp"]
console.log(Object.getOwnPropertyNames(obj));  // ["enumProp", "nonEnumProp"]
console.log(Object.getOwnPropertySymbols(obj));  // [Symbol(sym)]
console.log(Reflect.ownKeys(obj));  // ["enumProp", "nonEnumProp", Symbol(sym)]

实际应用场景

Symbol属性不可被for...in枚举的特性,使其非常适合用于定义对象的元数据或内部实现细节:

// 定义元数据Symbol
const METADATA = Symbol('metadata');

class User {
  constructor(name) {
    this.name = name;
    this[METADATA] = {
      createdAt: new Date(),
      modifiedAt: new Date()
    };
  }

  update() {
    this[METADATA].modifiedAt = new Date();
  }
}

const user = new User('Alice');
user.update();

// 常规遍历不会暴露元数据
for (const key in user) {
  console.log(key);  // 只输出 "name"
}

// 但仍可通过特定API访问
console.log(user[METADATA]);  // {createdAt: Date, modifiedAt: Date}

另一个常见用途是实现自定义迭代器:

const collection = {
  items: [1, 2, 3],
  [Symbol.iterator]: function* () {
    for (const item of this.items) {
      yield item;
    }
  }
};

// for...of会使用Symbol.iterator,但for...in不会枚举它
for (const item of collection) {
  console.log(item);  // 1, 2, 3
}

for (const key in collection) {
  console.log(key);  // "items"
}

与JSON序列化的关系

Symbol属性不仅不会被for...in枚举,在JSON序列化时也会被忽略:

const obj = {
  regular: 'value',
  [Symbol('sym')]: 'symbol value'
};

console.log(JSON.stringify(obj));  // {"regular":"value"}

如果需要序列化Symbol属性,需要自定义toJSON方法:

const obj = {
  regular: 'value',
  [Symbol('sym')]: 'symbol value',
  toJSON() {
    const plainObject = {};
    for (const key in this) {
      plainObject[key] = this[key];
    }
    plainObject.symbolValue = this[Symbol.for('sym')];
    return plainObject;
  }
};

console.log(JSON.stringify(obj));  // {"regular":"value","symbolValue":"symbol value"}

全局Symbol注册表的影响

使用Symbol.for()创建的Symbol会被放入全局注册表,但这不影响它们在for...in中的行为:

const globalSym = Symbol.for('global');
const obj = {
  [globalSym]: 'global symbol value',
  localProp: 'local value'
};

for (const key in obj) {
  console.log(key);  // 仍然只输出 "localProp"
}

全局Symbol仍然需要通过特定API访问:

console.log(obj[Symbol.for('global')]);  // "global symbol value"

类中的Symbol属性

在ES6类中,Symbol属性同样不会被for...in枚举:

const PRIVATE = Symbol('private');

class MyClass {
  constructor() {
    this.publicProp = 'public';
    this[PRIVATE] = 'private';
  }

  getPrivate() {
    return this[PRIVATE];
  }
}

const instance = new MyClass();

for (const key in instance) {
  console.log(key);  // 只输出 "publicProp"
}

console.log(instance.getPrivate());  // "private"

原型链上的Symbol属性

原型链上的Symbol属性同样不会被for...in枚举:

const METHOD = Symbol('method');

class Parent {
  [METHOD]() {
    console.log('Parent method');
  }
}

class Child extends Parent {
  regularMethod() {
    console.log('Child method');
  }
}

const child = new Child();

for (const key in child) {
  console.log(key);  // 只输出 "regularMethod"
}

child[METHOD]();  // 可以正常调用 "Parent method"

性能考虑

由于for...in不需要处理Symbol属性,其性能特性与ES5保持一致。当对象包含大量Symbol属性时,不会影响for...in的遍历速度:

const obj = {};
const count = 100000;

// 添加大量Symbol属性
for (let i = 0; i < count; i++) {
  obj[Symbol(i)] = i;
}

// 添加常规属性
obj.regularProp = 'value';

console.time('for...in');
for (const key in obj) {
  // 只遍历到regularProp
}
console.timeEnd('for...in');  // 非常快,不受Symbol数量影响

console.time('getOwnPropertySymbols');
Object.getOwnPropertySymbols(obj);
console.timeEnd('getOwnPropertySymbols');  // 随Symbol数量增加而变慢

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益,请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌