JavaScript和TypeScript中的符号

来源:http://www.chinese-glasses.com 作者:Web前端 人气:82 发布时间:2020-04-29
摘要:时间: 2019-09-15阅读: 175标签: 符号 类型别名会给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。 symbol是 Ja

时间: 2019-09-15阅读: 175标签: 符号

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

symbol是 JavaScript 和 TypeScript 中的原始数据类型,可用于对象属性。与number和string相比,symbol具有一些独特的功能,使它脱颖而出。

type Name = string;type NameResolver = () => string;type NameOrResolver = Name | NameResolver;function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); }}

JavaScript 中的符号

起别名不会新建一个类型 ,它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

可以用Symbol()工厂函数创建符号:

同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

constTITLE =Symbol('title')
type Container<T> = { value: T };

Symbol没有构造函数。该参数是可选描述。通过调用工厂函数,为TITLE分配了这个新创建的符号的唯一值。此符号现在是唯一的,可与所有其他符号区分开,并且不会与具有相同描述的任何其他符号冲突。

我们也可以使用类型别名来在属性里引用自己:

const ACADEMIC_TITLE = Symbol('title')const ARTICLE_TITLE = Symbol('title')if(ACADEMIC_TITLE === ARTICLE_TITLE) { // THis is never true}
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>;}

该描述可帮助你在开发期间获取有关符号的信息:

与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

console.log(ACADEMIC_TITLE.description) // titleconsole.log(ACADEMIC_TITLE.toString()) // Symbol(title)
type LinkedList<T> = T & { next: LinkedList<T> };interface Person { name: string;}var people: LinkedList<Person>;var s = people.name;var s = people.next.name;var s = people.next.next.name;var s = people.next.next.next.name;

如果你想拥有独特且唯一的可比值,那么符号就很棒。对于运行时切换或模式比较:

我们使用 type 创建类型别名。类型别名常用于联合类型类型别名不能出现在声明右侧的任何地方。

// A shitty logging frameworkconst 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); }}

像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。

符号也可用作属性键,但不可迭代,这对序列化很有用

type Alias = { num: number }interface Interface { num: number;}declare function aliased(arg: Alias): Alias;declare function interfaced(arg: Interface): Interface;
const print = Symbol('print')const user = { name: 'Stefan', age: 37, [print]: function() { console.log(`${this.name} is ${this.age} years old`) }}JSON.stringify(user) // { name: 'Stefan', age: 37 }user[print]() // Stefan is 37 years old

另一个重要区别是类型别名不能被 extendsimplements(自己也不能 extendsimplements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

全局符号注册表

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

存在一个全局符号注册表,允许你在整个程序中访问 token。

type Easing = "ease-in" | "ease-out" | "ease-in-out";class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { } else if (easing === "ease-in-out") { } else { // error! should not pass null or undefined. } }}let button = new UIElement();button.animate(0, 0, "ease-in");button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
Symbol.for('print') // creates a global symbolconst user = { name: 'Stefan', age: 37, // uses the global symbol [Symbol.for('print')]: function() { console.log(`${this.name} is ${this.age} years old`) }}

你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

首先调用Symbol.for创建一个符号,第二个调用使用相同的符号。如果将符号值存储在变量中并想知道键,则可以使用Symbol.keyFor()

数组合并了相同类型的对象,而元组合并了不同类型的对象。元组起源于函数编程语言,在这些语言中频繁使用元组。定义一对值分别为 stringnumber的元组:

const usedSymbolKeys = []function extendObject(obj, symbol, value) { //Oh, what symbol is this? const key = Symbol.keyFor(symbol) //Alright, let's better store this if(!usedSymbolKeys.includes(key)) { usedSymbolKeys.push(key) } obj[symnbol] = value}// now it's time to retreive them allfunction printAllValues(obj) { usedSymbolKeys.forEach(key = { console.log(obj[Symbol.for(key)]) })}
let xcatliu: [string, number] = ['Xcat Liu', 25];

漂亮!

当赋值或访问一个已知索引的元素时,会得到正确的类型:

TypeScript中的符号

let xcatliu: [string, number];xcatliu[0] = 'Xcat Liu';xcatliu[1] = 25;xcatliu[0].slice;xcatliu[1].toFixed;

TypeScript 完全支持符号,它是类型系统中的主要成员。symbol本身是所有可能符号的数据类型注释。请参阅前面的extendObject函数。为了允许所有符号扩展我们的对象,可以使用symbol类型:

也可以只赋值其中一项:

const sym = Symbol('foo')function extendObject(obj: any, sym: symbol, value: any) { obj[sym] = value}extendObject({}, sym, 42) // Works with all symbols
let xcatliu: [string, number];xcatliu[0] = 'Xcat Liu';

还有子类型unique symbol。unique symbol与声明紧密相关,只允许在 const 声明中引用这个确切的符号。

但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

你可以将 TypeScript 中的名义类型视为 JavaScript 中的名义值。

let xcatliu: [string, number];xcatliu = ['Xcat Liu', 25];//下面两种都会报错let xcatliu: [string, number] = ['Xcat Liu'];//这样也会报错let xcatliu: [string, number];xcatliu = ['Xcat Liu'];xcatliu[1] = 25;

要获得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) { // ...}
let xcatliu: [string, number];xcatliu = ['Xcat Liu', 25];//这个不会报错xcatliu.push('http://xcatliu.com/');//这个会报错xcatliu.push;

符号位于 TypeScript 和 JavaScript 中名义类型和不透明类型的交集。并且是我们在运行时最接近标称类型检查的事情。这是一种用来重建像enum这样结构的很好的方法。

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。枚举类型中是包含双向映射的,即(value -> name)和(name -> value)数字枚举

运行时枚举

enum Direction { Up = 1, Down, Left, Right}console.info(Direction);//双向映射console.info(Direction.Down);//获取枚举的值console.info(Direction[2]); //获取枚举值对应的名称定义

一个有趣的符号例子是在 JavaScript 中重新创建运行时的enum行为。 TypeScript 中的 enum 是不透明的。这实际上意味着你不能将字符串值分配给enum类型,因为 TypeScript 会将它们视为唯一的:

如上,我们定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。 换句话说, Direction.Up的值为 1, Down为 2, Left为 3, Right为 4。我们还可以完全不使用初始化器:

enum Colors { Red = 'Red', Green = 'Green', Blue = 'Blue',}const c1: Colors = Colors.Red;const c2: Colors = 'Red'; // 无法直接分配
enum Direction { Up, Down, Left, Right,}

如果你做一下比较,会发现非常有趣:

使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:

enum Moods { Happy = 'Happy', Blue = 'Blue'}// This condition will always return 'false' since the// types 'Moods.Blue' and 'Colors.Blue' have no overlap.if(Moods.Blue === Colors.Blue) { // Nope}
enum Response { No = 0, Yes = 1,}function respond(recipient: string, message: Response): void { // ...}respond("Princess Caroline", Response.Yes)

即使使用相同的值类型,在枚举中它们也足够独特,以便 TypeScript 认为它们不具有可比性。

字符串枚举在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

在 JavaScript 领域,我们可以使用符号创建类似的枚举。在以下例子中查看彩虹和黑色的颜色。我们的“枚举”Colors仅包含颜色而并非黑色的符号:

enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT",}
// All Color symbolsconst 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 Blackconst Colors = { COLOR_RED, COLOR_ORANGE, COLOR_YELLOW, COLOR_GREEN, COLOR_BLUE, COLOR_INDIGO, COLOR_VIOLET} as const;

常数项和计算所得项枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

我们可以像使用enum一样使用这个 symbol:

//不会报错enum Color {Red, Green, Blue = "blue".length};//会报错enum Color {Red = "red".length, Green, Blue};
function getHexValue(color) { switch(color) { case Colors.COLOR_RED: return '#ff0000' //... }}

上面的例子中,"blue".length 就是一个计算所得项。如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错。

并且 symbol 无法比较:

当满足以下条件时,枚举成员被当作是常数:

const MOOD_HAPPY: unique symbol = Symbol('HAPPY')const MOOD_BLUE: unique symbol = Symbol('BLUE')// 除黑色外的所有颜色const Moods = { MOOD_HAPPY, MOOD_BLUE} as const;// 因为类型,这种情况总是会返回'false'// 'typeof MOOD_BLUE' 和 'typeof COLOR_BLUE' 并不重叠.if(Moods.MOOD_BLUE === Colors.COLOR_BLUE) { // Nope}
  1. 不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0。
  2. 枚举成员使用常数枚举表达式初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:
    • 数字字面量
    • 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用
    • 带括号的常数枚举表达式
    • +, - ,~ 一元运算符应用于常数枚举表达式
    • +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符,常数枚举表达式作为其一个操作对象。若常数枚举表达式求值后为NaN或Infinity,则会在编译阶段报错。

我们要添加一些 TypeScript 注释:

所有其它情况的枚举成员被当作是需要计算得出的值。

我们将所有符号键(和值)声明为unique symbols,这意味着我们分配符号的常量永远不会改变。我们将“枚举”对象声明为as const。有了它,TypeScript 就会将类型设置为允许每个符号,只允许我们定义的完全相同的符号。

常数枚举当访问枚举值时,为了避免生成多余的代码和间接引用,可以使用常数枚举。 常数枚举是在enum关键字前使用const修饰符。常数枚举只能使用常数枚举表达式并且不同于常规的枚举的是它们在编译阶段会被删除。 常数枚举成员在使用的地方被内联进来。 这是因为常数枚举不可能有计算成员

这允许我们在为函数声明定义符号“枚举”时获得更多的类型安全性。我们从辅助类型开始,从对象中获取所有值类型。

const enum Direction { //常数 Up, //0 Down = 3, //3 Left = Down + 4, //7 Right, //8 //计算的值 center = [1,2,3,4].length //error}let directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right];
type ValuesWithKeysT, K extends keyof T = T[K];type ValuesT = ValuesWithKeysT, keyof T

编译结果:

记住,我们使用了as const,这意味着我们的值被缩小到精确的值类型(例如,类型是COLOR_RED)而不是它们的总体类型(symbol)。

var directions = [0 /* Up */, 3 /* Down */, 7 /* Left */, 8 /* Right */];

有了它,就可以声明我们的功能:

外部枚举外部枚举(Ambient Enums)是使用 declare enum 定义的枚举类型:

function getHexValue(color: Valuestypeof Colors) { switch(color) { case COLOR_RED: // super fine, is in our type case Colors.COLOR_BLUE: // also super fine, is in our type break; case COLOR_BLACK: // what? What is this??? TypeScript errors break; }}
declare enum Directions { Up, Down, Left, Right}let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

本文由10bet发布于Web前端,转载请注明出处:JavaScript和TypeScript中的符号

关键词:

最火资讯