Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 数据类型之 Symbol #23

Open
jejuin opened this issue Nov 20, 2020 · 0 comments
Open

ES6 数据类型之 Symbol #23

jejuin opened this issue Nov 20, 2020 · 0 comments

Comments

@jejuin
Copy link
Owner

jejuin commented Nov 20, 2020

Symbol

ES6 之前对象的属性名只能是字符串,这很容易造成属性名的冲突。

为了解决这一问题,ES6 引入了一种新的原始数据类型 Symbol,用来表示独一无二的值。它是 JavaScript 语言的第七种数据类型。

也就是说,从 ES6 开始,对象的属性名现在可以有两种类型:字符串和 Symbol。

如果对象的属性名是 Symbol 类型的,那么该属性就是独一无二的,可以保证不会与其他任何属性名产生冲突。

生成 Symbol 值

Symbol 值通过 Symbol() 函数生成

let s = Symbol() // 变量 s 是一个独一无二的值
console.log(typeof s) // "symbol"
console.log(s instanceof Symbol); // false

注意Symbol() 函数前不能使用new命令,否则会报错 Uncaught TypeError: Symbol is not a constructor
这是因为 Symbol 是一个原始类型的值,不是对象。

Symbol Description

Symbol() 函数可以接受一个参数,作为 Symbol 值的描述。如果参数不是字符串类型,会执行 ToString() 类型转换操作。

为 Symbol 值添加描述,主要是为了区分不同的 Symbol 值。

如果不添加描述信息,那么它们输出的结果值看起来是相同的(实际并不相等),无法区分。

let s1 = Symbol();
let s2 = Symbol();
console.log(s1) // Symbol()
console.log(s2) // Symbol()

console.log(s1 == s2) // false

添加描述信息:

let s1 = Symbol('foo');
let s2 = Symbol('bar');

console.log(s1) // Symbol(foo)
console.log(s2) // Symbol(bar)

注意Symbol()函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol()函数的返回值是不相等的。

ES2019 提供了一个实例属性description,直接返回 Symbol 的描述。

const sym = Symbol('foo');
console.log(sym.description) // "foo"

类型转换

Symbol 值不能与其他类型的值进行运算,会报错。

let sym = Symbol('My symbol');
console.log("your symbol is " + sym) // Uncaught TypeError: Cannot convert a Symbol value to a string

也就是说,Symbol 类型值不能进行隐式类型转换。 但是,Symbol 值可以显式转为字符串。

let sym = Symbol('My symbol');
console.log(String(sym)) // 'Symbol(My symbol)'
console.log(sym.toString()) // 'Symbol(My symbol)'

另外,Symbol 值也可以转为布尔值(始终为 true),但是不能转为数值。

let sym = Symbol();
console.log(Boolean(sym)) // true
console.log(!sym)  // false

console.log(Number(sym)) // Uncaught TypeError: Cannot convert a Symbol value to a number
console.log(sym + 2) // Uncaught TypeError: Cannot convert a Symbol value to a number

Symbol 作为属性名

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

但是,它也不是私有属性,只是属性名有些特殊。有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

const obj = {
    [Symbol('a')]'Hello',
    [Symbol('b')]'World'
};
const objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols) // [Symbol(a), Symbol(b)]

for (let i in obj) {
  console.log(i); // 无输出
}

console.log(Object.getOwnPropertyNames(obj)) // []

ES6 中的Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let obj = {
  [Symbol('my_key')]1,
  enum2,
  nonEnum3
};
Reflect.ownKeys(obj) //  ["enum", "nonEnum", Symbol(my_key)]

由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

Symbol.for(key)

有时,我们希望重新使用同一个 Symbol 值,Symbol.for(key)方法可以做到这一点。它接受一个参数(如果参数不是字符串类型,会执行 ToString() 类型转换操作),然后在 GlobalSymbolRegistry List 中搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该参数为名称的 Symbol 值,并将其注册到 GlobalSymbolRegistry List 中。

ES6 规范中指出,所有通过 Symbol.for(key) 注册的 Symbol 值,都会存放在全局环境的 GlobalSymbolRegistry List 集合中。不管有没有在全局环境运行。即使是在局部作用域中使用 Symbol.for(key),也会将值注册到全局环境中提供搜索。

image

利用Symbol.for(key)的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值。

let s1 = Symbol.for('foo'); // 第一次执行,即注册名称为 foo 的 Symbol 值
let s2 = Symbol.for('foo'); // 第二次执行,获取到第一次执行时创建的 Symbol 值
s1 === s2 // true

Symbol.keyFor(sym)

Symbol.keyFor(sym)方法返回一个已注册的 Symbol 值的key。

let s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)) // "foo"

let s2 = Symbol("foo");
console.log(Symbol.keyFor(s2)) // undefined

上面代码中,变量 s2 属于未登记的 Symbol 值,所以返回 undefined。

Symbol 应用

消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case 'Triangle'// 魔术字符串
      area = .5 * options.width * options.height;
      break;
     /* ... more code ... */
  }
  return area;
}
getArea('Triangle', { width100, height100 }); // 魔术字符串

上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量。

const shapeType = {
  triangle'Triangle'
};
function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}
getArea(shapeType.triangle, { width100, height100 });

上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。

如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。

const shapeType = {
  triangleSymbol()
};

也就是说,如果一个变量的值没有任何意义,随意取就行的情况下,可以用 Symbol 代替。

内置的 Symbol 值

ES6 提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
image

推荐阅读:阮一峰 Symbol

ES2018 新增了 1 个内置的 Symbol 值:

image

Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of 循环。

const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
    yield "hello";
    yield "async";
    yield "iteration!";
};

(async () => {
    for await (const x of myAsyncIterable) {
        console.log(x);
        //    "hello"
        //    "async"
        //    "iteration!"
    }
})();

参考阅读:

  1. Symbol.asyncIterator
  2. for-await...of

ES2020 新增了 1 个内置的 Symbol 值:

image

参考阅读:Symbol.matchAll

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant