原文链接 https://fettblog.eu/symbols-in-javascript-and-typescript/
Symbol是一个JavaScript与TypeScript内建的数据类型. Symbol与其他数据类型相比, 能够作为对象的属性键值来使用. 与number
和string
相比, symbol
具备一些使它别具一格的特性.
Symbol
可以通过Symbol()
工厂函数来创建:
const TITLE = Symbol('title');
Symbol
本身没有构建函数. 可选参数是一个用于描述Symbol
的字符串.
通过调用工厂函数, 新鲜出炉刚刚被创建的Symbol
的唯一的值被赋给了我们的常量TITLE
.
这个Symbol现在是全局唯一的, 与其他所有的Symbol都不相等, 即使它们拥有相同的description
作为创建时的参数.
const ACADEMIC_TITLE = Symbol('title');
const ARTICLE_TITLE = Symbol('title');
ACADEMIC_TITLE === ARTICLE_TITLE; // 永远为false
Description仅仅是用来帮助开发者在开发阶段获取Symbol相关信息的
console.log(ACADEMIC_TITLE.description) // title
console.log(ACADEMIC_TITLE.toString()) // Symbol(title)
在需要比较专有,唯一的值时, Symbol是非常合适的. 对于运行时的switch
或者mode comparisons
:
// 一个很丢人的Log框架
const LEVEL_INFO = Symbol('INFO')
const LEVEL_DEBUG = Symbol('DEBUG')
const LEVEL_WARN = Symbol('WARN')
const LEVEL_ERROR = Symbol('ERROR')
function log(msg, level) {
switch(level) {
case LEVEL_WARN:
console.warn(msg); break
case LEVEL_ERROR:
console.error(msg); break;
case LEVEL_DEBUG:
console.log(msg);
debugger; break;
case LEVEL_INFO:
console.log(msg);
}
}
Symbols也能作为属性键值来使用, 但需要注意, 作为键值使用时, 并不是iterable的. 这对序列化是需要注意的地方
const print = Symbol('print')
const user = {
name: 'Stefan',
age: 37,
[print]: 'print out',
}
JSON.stringify(user) // { name: 'Stefan', age: 37 }
user[print] // print out
通过全局注册Symbol, 可以在整个应用中访问到Symbol
Symbol.for('print') // 创建一个全局的Symbol
const user = {
name: 'Stefan',
age: 37,
// 使用全局Symbol
[Symbol.for('print')]: function() {
console.log(`${this.name} is ${this.age} years old`)
}
}
第一次调用Symbol.for
会创建一个symbol, 第二次调用会访问到这个symbol. 如果symbol的值是个变量, 可以通过Symbol.keyFor()
来查询到这个值的键
const usedSymbolKeys = []
function extendObject(obj, symbol, value) {
//嗯...这个Symbol是什么的来着?
const key = Symbol.keyFor(symbol)
//行吧, 最好把它存下来
if(!usedSymbolKeys.includes(key)) {
usedSymbolKeys.push(key)
}
obj[symnbol] = value
}
//现在是时候来看看我们都有什么Symbol了
function printAllValues(obj) {
usedSymbolKeys.forEach(key => {
console.log(obj[Symbol.for(key)])
})
}
漂亮!
TypeScript对Symbols有着完备的支持, 并且symbol在TypeScript的类型系统中也是重要的组成成员. symbol
本身是一个数据类型注解. 参考这个之前出现过的extendObject
的function例子. 我们可以通过使用symbol
类型来允许symbols去extend我们的对象:
const sym = Symbol('foo')
function extendObject(obj: any, sym: symbol, value: any) {
obj[sym] = value
}
extendObject({}, sym, 42)
与此同时我们也拥有unique symbol
这一子类型. unique symbol
与声明紧密绑定. 只有明确的"这一个"symbol能够符合类型注解的要求.
此外, 要获取到unique symbol
需要通过typeof
操作符来获取
const PROD: unique symbol = Symbol('Production mode')
const DEV: unique symbol = Symbol('Development mode')
function showWarning(msg: string, mode: typeof DEV | typeof PROD) {
// ...
}
Symbols的处于TS与JS的名词性(Nominal)与不透明(Opaque)类型之间的交集. 并且是在runtime时, 最接近名词性类型校验的东西. 也是一个很好的重建结构, 比如enums
, 的方法.
译者注: 此处提到的Nominal与Opaque的翻译确实存在一些问题, 实际上举一个例子就能明白 Nominal类型是意义简单的, 能够从字面意义明白其意义的类型 const astr: string = 'test'; 这里面的string实际上就是一个Nominal的type Opaque类型是不透明的, 不明晰其结构和逻辑的 const asomeStr: SomeStr = astr; type SomeStr = string; 这里的SomeStr实际上就是一个Opaque的type, 此外asomeStr的赋值语句会报错. 重在理解, wiki上的定义着实有点不明不白
Symbols有一个很有趣的应用环境 -- 重建enum (re-create enum). 就如同JavaScript在运行时的行为那样.
enums
在TypeScript中是不透明的. 这意味着不能给enum变量赋予字符串的值, TypeScript将这些enum看做独一无二的存在.
enum Colors {
Red = 'Red',
Green = 'Green',
Blue = 'Blue',
}
const c1: Colors = Colors.Red;
const c2: Colors = 'Red'; // Error会被抛出
此外还有下面这个有点意思的情况
enum Moods {
Happy = 'Happy',
Blue = 'Blue'
}
Moods.Blue === Colors.Blue; //will always be false
即使具有相同的值, 因为处在不同的enum之下, TypeScript认为他们是不相同的, 各自独一的, 也因此是不可比较的. 所以其中需要作出一些处理
在JavaScript中可以通过Symbol来定义enum从而达到类似的效果
// All Color symbols
const COLOR_RED: unique symbol = Symbol('RED')
const COLOR_ORANGE: unique symbol = Symbol('ORANGE')
const COLOR_YELLOW: unique symbol = Symbol('YELLOW')
const COLOR_GREEN: unique symbol = Symbol('GREEN')
const COLOR_BLUE: unique symbol = Symbol('BLUE')
const COLOR_INDIGO: unique symbol = Symbol('INDIGO')
const COLOR_VIOLET: unique symbol = Symbol('VIOLET')
const COLOR_BLACK: unique symbol = Symbol('BLACK')
// All colors except Black
const Colors = {
COLOR_RED,
COLOR_ORANGE,
COLOR_YELLOW,
COLOR_GREEN,
COLOR_BLUE,
COLOR_INDIGO,
COLOR_VIOLET
} as const;
我们可以像TypeScript中使用Enums那样来使用这些symbols, 同时, 这些Symbols之间也是不可比较的.
function getHexValue(color) {
switch(color) {
case Colors.COLOR_RED: return '#ff0000'
//...
}
}
const MOOD_HAPPY: unique symbol = Symbol('HAPPY')
const MOOD_BLUE: unique symbol = Symbol('BLUE')
// All colors except Black
const Moods = {
MOOD_HAPPY,
MOOD_BLUE
} as const;
Moods.MOOD_BLUE === Colors.COLOR_BLUE // will always be false
这里有一些我们想要补充的TypeScript的注解
unique symbol
意味着我们给其赋予const值不能被改变的const
, TypeScript将不再让所有的symbol能够作为值被赋予到其中, 而是只有精确的"那一个"symbol可以.这允许我们在为函数声明定义symbol enums时能够保证更好的类型安全. 为了获得一个对象的所有属性的类型, 我们定义一个辅助类型
type ValuesWithKeys<T, K extends keyof T> = T[K];
type Values<T> = ValuesWithKeys<T, keyof T>
注意需要使用as const
, 这使得有效值的范围被限制在一个严格的范围之内
随后, 一个函数的声明可以像这样:
function getHexValue(color: Values<typeof Colors>) {
switch(color) {
case COLOR_RED:
// Good
case Colors.COLOR_BLUE:
// Good
break;
case COLOR_BLACK:
// TypeScript会报错, 因为COLOR_BLACK并没有被声明
break;
}
}
当同时使用symbol作为键与键值时, 可以跳过之前的辅助类型直接使用
const ColorEnum = {
[COLOR_RED]: COLOR_RED,
[COLOR_YELLOW]: COLOR_YELLOW,
[COLOR_ORANGE]: COLOR_ORANGE,
[COLOR_GREEN]: COLOR_GREEN,
[COLOR_BLUE]: COLOR_BLUE,
[COLOR_INDIGO]: COLOR_INDIGO,
[COLOR_VIOLET]: COLOR_VIOLET,
}
function getHexValueWithSymbolKeys(color: keyof typeof ColorEnum) {
switch(color) {
case ColorEnum[COLOR_BLUE]:
// ?
break;
case COLOR_RED:
// ?
break;
case COLOR_BLACK:
// ?
break;
}
}
这使得我们能够在编译时与运行时都能够获得类型安全性. 前者通过TypeScript的unique symbol
, 后者通过JavaScript的Symbol的独一性.