探索JavaScript的Symbol

141 阅读7分钟

Baseline Widely available

Supported in Chrome

Supported in Edge

Supported in Firefox

Supported in Safari

我记得我第一次在 JavaScript 中遇到符号。那是 2015 年,和许多开发人员一样,我想,“太好了,另一个需要担心的原始类型。”

但是随着我职业生涯的发展,我开始欣赏这些古怪的小原语。它们以字符串和数字无法匹配的方式解决了一些有趣的问题。

符号与其他 JavaScript 原语不同,因为它们保证是唯一的。

当你用Symbol('description')创建一个符号时,你得到的东西永远不会等同于任何其他符号,即使是用相同的描述创建的符号。这种独特性使它们在特定用例中强大。

const symbol1 = Symbol('description');
const symbol2 = Symbol('description');
console.log(symbol1 === symbol2); // false

符号的真正力量在处理对象时显现出来。与字符串或数字不同,符号可以用作属性键,而不会与现有属性发生冲突。这使得它们对于在不干扰现有代码的情况下向对象添加功能非常宝贵。

const metadata = Symbol('elementMetadata');
function attachMetadata(element, data) {
  element[metadata] = data;
  return element;
}

const div = document.createElement('div');
const divWithMetadata = attachMetadata(div, { lastUpdated: Date.now() });
console.log(divWithMetadata[metadata]); // { lastUpdated: 1684244400000 }

当您使用符号作为属性键时,它不会显示在Object.keys()或正常for...in循环中。

const nameKey = Symbol('name');
const person = {
  [nameKey]: 'Alex',
  city: 'London'
};

// Regular enumeration won't show Symbol properties
console.log(Object.keys(person));     // ['city']
console.log(Object.entries(person));  // [['city', 'London']]

for (let key in person) {
  console.log(key);                   // Only logs: 'city'
}

// But we can still access Symbol properties
console.log(Object.getOwnPropertySymbols(person));  // [Symbol(name)]
console.log(person[nameKey]);         // 'Alex'

您仍然可以通过Object.getOwnPropertySymbols()访问这些属性,但这需要有意的努力。这在对象的公共接口和其内部状态之间创建了自然的分离。

全局符号注册表为符号使用增加了另一个维度。虽然普通符号总是唯一的,但有时您需要在代码的不同部分共享符号。这就是Symbol.for()的用武之地:

// Using Symbol.for() for shared Symbols across modules
const PRIORITY_LEVEL = Symbol.for('priority');
const PROCESS_MESSAGE = Symbol.for('processMessage');
function createMessage(content, priority = 1) {
  const message = {
    content,
    [PRIORITY_LEVEL]: priority,
    [PROCESS_MESSAGE]() {
      return `Processing: ${this.content} (Priority: ${this[PRIORITY_LEVEL]})`;
    }
  };
  return message;
}
function processMessage(message) {
  if (message[PROCESS_MESSAGE]) {
    return message[PROCESS_MESSAGE]();
  }
  throw new Error('Invalid message format');
}

// Usage
const msg = createMessage('Hello World', 2);
console.log(processMessage(msg)); // "Processing: Hello World (Priority: 2)"

// Symbols from registry are shared
console.log(Symbol.for('processMessage') === PROCESS_MESSAGE); // true

// But regular Symbols are not
console.log(Symbol('processMessage') === Symbol('processMessage')); // false

对象文字中的括号[]允许我们使用 Symbol 作为属性键。

JavaScript 提供了内置符号,可让您修改对象在不同情况下的行为方式。这些被称为众所周知的符号,它们让我们了解核心语言特性。

一个常见的用例是使用Symbol.iterator使对象可迭代。这让我们可以用自己for...ofof 循环处理我们自己的对象,就像我们处理数组一样:

// Making an object iterable with Symbol.iterator
const tasks = {
  items: ['write code', 'review PR', 'fix bugs'],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.items.length) {
          return { value: this.items[index++], done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

// Now we can use for...of
for (let task of tasks) {
  console.log(task); // 'write code', 'review PR', 'fix bugs'
}

另一个强大的著名符号是Symbol.toPrimitive。它允许我们控制对象如何转换为原始值,如数字或字符串。当对象需要处理不同类型的操作时,这变得很有用:

const user = {
  name: 'Alex',
  score: 42,
  [Symbol.toPrimitive](hint) {
    // JavaScript tells us what type it wants with the 'hint' parameter
    // hint can be: 'number', 'string', or 'default'
    switch (hint) {
      case 'number':
        return this.score;    // When JavaScript needs a number (like +user)
      case 'string':
        return this.name;     // When JavaScript needs a string (like `${user}`)
      default:
        return `${this.name} (${this.score})`; // For other operations (like user + '')
    }
  }
};

// Examples of how JavaScript uses these conversions:
console.log(+user);        // + operator wants a number, gets 42
console.log(`${user}`);    // Template literal wants a string, gets "Alex"
console.log(user + '');    // + with string uses default, gets "Alex (42)"

Symbol.toPrimitive让我们控制我们的对象如何转换为不同的类型。JavaScript 通过'hint'参数告诉我们它想要什么类型。

使用 Symbols 进行继承Symbol.species

在 JavaScript 中使用数组时,我们有时需要限制它们可以保存什么样的值。这就是专门的数组的用武之地,但是它们可以使用map()filter()

一个可以保存任何类型值的普通 JavaScript 数组:

// Regular array - accepts anything
const regularArray = [1, "hello", true];
regularArray.push(42);       // ✅ Works
regularArray.push("world");  // ✅ Works
regularArray.push({});       // ✅ Works

具有特殊规则或行为的数组-例如只接受某些类型的值:

// Specialized array - only accepts numbers
const createNumberArray = (...numbers) => {
  const array = [...numbers];
  array.push = function(item) {
    if (typeof item !== 'number') {
      throw new Error('Only numbers allowed');
    }
    return Array.prototype.push.call(this, item);
  };
  return array;
};

const numberArray = createNumberArray(1, 2, 3);
numberArray.push(4);     // ✅ Works
numberArray.push("5");   // ❌ Error: Only numbers allowed

可以这样想:常规数组就像一个打开的盒子,可以接受任何东西,而专用数组就像一个投币口,只接受特定的项目(在这种情况下是数字)。

Symbol.species解决的问题是:当你在一个专门的数组上使用像map()这样的方法时,你希望结果也是专门的,还是只是一个普通的数组?

// specialized array that only accepts numbers
const createNumberArray = (...numbers) => {
  const array = [...numbers];
  // Restrict push to only allow numbers
  array.push = function(item) {
    if (typeof item !== 'number') {
      throw new Error('Only numbers allowed');
    }
    return Array.prototype.push.call(this, item);
  };
  return array;
};
// Test it
const nums = createNumberArray(1, 2, 3);

nums.push(4);     // Works ✅

nums.push('5');   // Error! ❌ "Only numbers allowed"

// When we map this array, the restrictions carry over unexpectedly

const doubled = nums.map(x => x * 2);

doubled.push('6'); // Error! ❌ Still restricted to numbers

我们可以通过告诉 JavaScript 对派生操作使用常规数组来解决这个问题。以下是Symbol.species如何解决这个问题:

const createNumberArray = (...numbers) => {
  const array = [...numbers];
  array.push = function(item) {
    if (typeof item !== 'number') {
      throw new Error('Only numbers allowed');
    }
    return Array.prototype.push.call(this, item);
  };

  // Tell JavaScript to use regular arrays for operations like map()

  Object.defineProperty(array.constructor, Symbol.species, {
    get: function() { return Array; }
  });

  return array;
};

const nums = createNumberArray(1, 2, 3);

nums.push(4);     // Works ✅

nums.push('5');   // Error! ❌ (as expected for nums)

const doubled = nums.map(x => x * 2);

doubled.push('6'); // Works! ✅ (doubled is a regular array)

Symbol.species修复了意外继承的限制。原始数组保持专用,但派生数组(来自 map、filter 等)成为常规数组。

注意:Symbol.species 正在讨论从 JavaScript 中删除的可能性。

符号限制和问题

使用符号并不总是简单的。尝试使用 JSON 时会出现一个常见的混淆。符号属性在 JSON 序列化期间完全消失:

const API_KEY = Symbol('apiKey');

// Use that Symbol as a property key
const userData = {
 [API_KEY]: 'abc123xyz',      // Hidden API key using our Symbol
 username: 'alex'             // Normal property anyone can see
};

// Later, we can access the API key using our saved Symbol
console.log(userData[API_KEY]); // prints: 'abc123xyz'

// But when we save to JSON, it still disappears
const savedData = JSON.stringify(userData);
console.log(savedData);         // Only shows: {"username":"alex"}

把 JSON 想象成一个看不到符号的复印机。当你复制(stringify)时,任何用 Symbol 键存储的东西都变得不可见。一旦它不可见,在制作新对象(解析)时就没有办法把它带回来。

符号的字符串强制会导致另一个常见的陷阱。虽然您可能希望符号像其他原语一样工作,但它们对类型转换有严格的规则:

const label = Symbol('myLabel');
// This throws an error
console.log(label + ' is my label'); // TypeError

// Instead, you must explicitly convert to string
console.log(String(label) + ' is my label'); // "Symbol(myLabel) is my label"

使用符号处理内存可能很棘手,尤其是在使用全局符号注册表时。当没有引用保留时,可以垃圾收集常规符号,但注册表符号会保留:

// Regular Symbol can be garbage collected
let regularSymbol = Symbol('temp');
regularSymbol = null; // Symbol can be cleaned up

// Registry Symbol persists
Symbol.for('permanent'); // Creates registry entry

// Even if we don't keep a reference, it stays in registry
console.log(Symbol.for('permanent') === Symbol.for('permanent')); // true

模块之间的符号共享显示了一种有趣的模式。使用Symbol.for()时,符号将在整个应用程序中可用,而常规符号保持唯一:

// In module A
const SHARED_KEY = Symbol.for('app.sharedKey');

const moduleA = {
  [SHARED_KEY]: 'secret value'
};

// In module B - even in a different file
const sameKey = Symbol.for('app.sharedKey');
console.log(SHARED_KEY === sameKey);                // true
console.log(moduleA[sameKey]);                      // 'secret value'

// Regular Symbols don't share
const regularSymbol = Symbol('regular');
const anotherRegular = Symbol('regular');
console.log(regularSymbol === anotherRegular);      // false

Symbol.for()创建像共享密钥一样工作的符号-应用程序的任何部分都可以使用相同的名称访问相同的符号。另一方面,常规符号始终是唯一的,即使它们具有相同的名称。

何时使用符号

符号在特定情况下会发光。当您需要真正唯一的属性键时,请使用它们,例如添加不会干扰现有属性的元数据。它们非常适合通过众所周知的符号创建专门的对象行为,注册表Symbol.for()有助于在整个应用程序中共享常量。

// Use symbols for private-like properties
const userIdSymbol = Symbol('id');
const user = {
  [userIdSymbol]: 123,
  name: 'Alex'
};

// Leverage symbols for special behaviors
const customIterator = {
  [Symbol.iterator]() {
    // Implement custom iterator logic
  }
};

// Share constants across modules using Symbol.for()
const SHARED_ACTION = Symbol.for('action');

符号起初可能看起来不寻常,但它们解决了 JavaScript 中的实际问题。当我们需要时,它们提供唯一性,当我们需要时提供隐私,并在我们需要时挂钩到 JavaScript 的内部行为。