【译】JavaScript 中的 Symbols 怎么用

3,863 阅读3分钟

本篇采用意译,原文链接在文章末尾附上。

为了防止属性名冲突, JavaScript 介绍了一种 symbols 的类型。在 2015 - 2019 中,symbols 提供一种方法去模拟私有属性。

简介

创建 symbol 最简单的方式是调用 Symbol() 方法。有两个关键属性使得 symbols 变得特殊:

  1. Symbols 可以用于对象 key。只有字符串和 symbol 可以被用于对象 key。
  2. 任何两个 sybmols 都不相等
const symbol1 = Symbol();
const symbol2 = Symbol();

symbol1 === symbol2; // false

const obj = {};
obj[symbol1] = 'Hello';
obj[symbol2] = 'World';

obj[symbol1]; // 'Hello'
obj[symbol2]; // 'World'

尽管 symbol() 看起来是个对象,实际上它也属于 7 种基本类型。对 Symbol 使用 new 操作符会导致一个错误。

const symbol1 = Symbol();

typeof symbol1; // 'symbol'
symbol1 instanceof Object; // false

// Throws "TypeError: Symbol is not a constructor"
new Symbol();

描述符

Symbol 方法使用单个字符串参数当做描述符。Symbol 的描述符只是用于 debug 的目的。描述符在 symbol 调用 toString 的时候出现。然而,两个相同描述符的 symbol 也是不相等的。

const symbol1 = Symbol('my symbol');
const symbol2 = Symbol('my symbol');

symbol1 === symbol2; // false
console.log(symbol1); // 'Symbol(my symbol)'

通常情况下,除非你有合适的理由,不然一般不建议使用全局 symbol 注册,这么做有可能导致命名冲突。

命名冲突

JavaScript 中的第一个内置 symbol 是 Symbol.iterator。一个有 Symbol.iterator 方法当做迭代的对象。也就意味着。你可以使用这个对象作为循环的右操作符。

比如获取斐波那契数列:

const fibonacci = {
  [Symbol.iterator]: function*() {
    let a = 1;
    let b = 1;
    let temp;

    yield b;

    while (true) {
      temp = a;
      a = a + b;
      b = temp;
      yield b;
    }
  }
};

// Prints every Fibonacci number less than 100
for (const x of fibonacci) {
  if (x >= 100) {
    break;
  }
  console.log(x);
}

为什么 Symbol.iterator 是 symbol 而不是 string? 假设不使用 Symbol.iterator,迭代名定义为一个字符串属性的 iterator。也就是说,假设有一个可迭代的类,如下:

class MyClass {
  constructor(obj) {
    Object.assign(this, obj);
  }

  iterator() {
    const keys = Object.keys(this);
    let i = 0;
    return (function*() {
      if (i >= keys.length) {
        return;
      }
      yield keys[i++];
    })();
  }
}

MyClass 允许你迭代对象 keys。但是上面的类有个潜在的错误。假设用户故意给对象传递一个 iterator 的属性。比如:

const obj = new MyClass({ iterator: 'not a function' });

这样的话,迭代就会失效。JavaScript 在你使用 for/of 迭代时,会抛出一个错误 obj is not iterable。这是因为上面的代码覆盖了类中的迭代属性。这是类似原型污染的安全问题。在想当然拷贝用户数据的时候容易发生这样的问题,尤其是 proto 和 constructor 这样的属性。

关键模式在于 symbol 可以清楚的分割用户数据和对象数据。由于符号无法用JSON表示,因此不存在将数据传递到具有错误 Symbol.iterator 属性的 Express API 的风险。 在将用户数据与内置函数和方法(如Mongoose模型)混合的对象中,可以使用符号来确保用户数据不会与内置功能冲突。

私有属性

既然任意两个 symbol 都不相等,symbol 可以方便的模拟 JavaScript 中的私有属性。 Symbols 不会在 Object.key(),中出现,因为除非你明确 export 一个 symbol,没有任何代码可以访问到这个属性,除非使用 Object.getOwnPropertySymbols() 方法。

function getObj() {
  const symbol = Symbol('test');
  const obj = {};
  obj[symbol] = 'test';
  return obj;
}

const obj = getObj();

Object.keys(obj); // []

// Unless you explicitly have a reference to the symbol, you can't access the
// symbol property.
obj[Symbol('test')]; // undefined

// You can still get a reference to the symbol using `getOwnPropertySymbols()`
const [symbol] = Object.getOwnPropertySymbols(obj);
obj[symbol]; // 'test'

Symbols 作为私有属性方便的一点是,它不会在 JSON.stringify() 中出现。更详细的内容,请参考这里

最后

Symbols 处理对象内部状态保证用户数据和程序数据分离是很不错的一个工具。使用 symbols 就不需要再加上各种前缀表示程序状态。下次可以试试 symbol。

pic