TypeScript
1.TypeScript和JavaScript
1.1 TypeScript 简介
- TypeScript 是 JavaScript 的超集,它可以编译成纯 JavaScript;
- TypeScript 基于 ECMAScript 标准进行拓展,支持 ECMAScript 未来提案中的特性,如装饰器、异步功能等;
- TypeScript 编译的 JavaScript 可以在任何浏览器运行,TypeScript 编译工具可以运行在任何操作系统上;
- TypeScript 起源于开发较大规模 JavaScript 应用程序的需求。由微软在2012年发布了首个公开版本
2. 为什么要用 TypeScript
2.1 静态类型
JavaScript 只会在 运行时
才去做数据类型检查,而 TypeScript 作为静态类型语言,其数据类型是在 编译期间
确定的,编写代码的时候要明确变量的数据类型,使用 TypeScript 后,这些低级错误将不再发生。
2.2 三大框架支持
- Angular 是 TypeScript 最早的支持者,Angular 官方推荐使用 TypeScript 来创建应用;
- React 虽然经常与 Flow 一起使用,但是对 TypeScript 的支持也很友好;
- Vue3.0 正式版即将发布,由 TypeScript 编写。
2.3 兼容 JavaScript 的灵活性
TypeScript 虽然严谨,但没有丧失 JavaScript 的灵活性,TypeScript 非常包容:
- TypeScript 通过
tsconfig.json
来配置对类型的检查严格程度; - 可以把
.js
文件直接重命名为.ts
; - 可以通过将类型定义为
any
来实现兼容任意类型; - 即使 TypeScript 编译错误,也可以生成 JavaScript 文件。
3. 基本类型
3.1 类型声明
-
类型声明是TS非常重要的一个特点;
-
通过类型声明可以指定TS中变量(参数、形参)的类型;
-
指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错;
-
简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值;
-
语法:
let 变量: 类型; let 变量: 类型 = 值; function fn(参数: 类型, 参数: 类型) : 类型{ ... }
3.2 自动类型判断
- TS拥有自动的类型判断机制;
- 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型;
- 所以如果你的变量的声明和赋值是同时进行的,可以省略掉类型声明。
3.3 类型
实例 | 例子 | 描述 |
---|---|---|
number | 1,-33,2.5 | 任意数字 |
string | ‘hi’,“hi”,hi | 任意字符串 |
boolean | true、false | 布尔值true或false |
字面量 | 其本身 | 限制变量的值就是该字面量的值 |
any | * | 任意类型 |
unknown | * | 类型安全的array |
void | 空值(undefined) | 没有值(或undefined) |
never | 没有值 | 不能是任何值 |
object | {name:‘李泽言’} | 任意的JS对象 |
array | [1, 2, 3] | 任意JS数组 |
tuple | [4, 5] | 元素,TS新增类型,固定长度数组 |
enum | enum{A, B} | 枚举,TS中新增类型 |
3.4 类型的别名
// 类型的别名
type myType = 1 | 2 | 3 | 4 | 5;
let k : myType;
let l : myType;
let m : myType;
k = 2;
3.5 类型断言
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
另一个为as
语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有as
语法断言是被允许的。
4. 编译选项
4.1 自动编译文件
-
编译文件时,使用-w指令后,TS编译器会自动监视文件的变化情况,并在文件发生变化时对文件进行重新编译;
-
示例:
tsc xxx.ts -w
4.2 自动编译整个项目
-
如果直接使用tsc指令,则可以自动将当前项目下的所有ts文件编译为js文件;
-
但是能直接使用tsc命令的前提是,要先在项目根目录下创建一个ts的配置文件tsconfig.json;
-
tsconfig.json是一个JSON文件,添加配置文件后,只需tsc命令即可完成对整个项目的编译;
-
配置选项:
-
include
-
定义希望被编译文件所在的目录;
-
默认值:["* * / *"];
-
示例:
// 所有src目录下的文件都会被编译 "include": ["src/**/*", "tests/**/*"]
-
-
exclude
-
定义需要排除在外的目录;
-
默认值:[“node_modules”, “bower_components”, “jspm_packages”];
-
示例:
// src下hello目录下的文件都不会被编译 "exclude": ["./src/hello/**/*"]
-
-
extends
-
定义被继承的配置文件;
-
示例:
// 当前配置文件中会自动包含config目录下base.json中的所有配置信息 "extends": "./configs/base"
-
-
files
-
指定被编译文件的列表,只有需要编译的文件少时才会用到
-
示例:
// 列表中所有的文件都会被TS编译器编译 "files": [ "core.ts", "sys.ts", "types.ts", "scanner.ts", ... ]
-
-
compilerOptions
- target::用来指定ES被编译为ES的版本,“target”: “es2015”;
- module::指定要使用的模块化的规范,“module”: “es2015”;
- lib:用来指定项目中要使用的库,一般不需要设置;
- outDir:用来指定编译后文件所在的目录,“outDir”: “./dist”;
- outFile:将代码合并为一个文件,“outFile”: “./dist/app.js”;
- allowJs:是否对js文件编译,默认为false,“allowJs”: false;
- checkJs:是否检查js代码是否符合语法规范,默认为false,“checkJs”: false;
- removeComments:是否移除注释,“removeComments”: false;
- noEmit:不生成编译后的文件,“noEmit”: false;
- noEmitOnError:当有错误时不生成编译后的文件,“noEmitOnError”: false;
- alwaysStrict:编译后的文件是否使用严格模式,“alwaysStrict”: false;
- noImplicitAny:不允许出现隐式的any类型,“noImplicitAny”: false;
- noImplicitThis:不允许不明确的this,“noImplicitThis”: true;
- strictNullChecks:严格的检查空值,“strictNullChecks”: false;
- strict:严格检查的总开关,“strict”: true
-
5. 面向对象
面向对象是程序中一个非常重要的思想,它被很多同学理解成了一个比较难,比较深奥的问题,其实不然,面向对象很简单,简而言之就是程序之中所有的操作都需要通过对象来完成。
- 举例来说:
- 操作浏览器要使用window对象;
- 操作网页要使用document对象;
- 操作控制台要使用console对象。
一切操作都要通过对象,也就是所谓的面向对象,那么对象到底是什么呢?这就要先说到程序是什么,计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体,比如:照片是对一个具体的人的抽象,汽车模型是对具体汽车的抽象等等。程序也是对事物的抽象,在程序中我们可以表示一个人、一条狗、一把枪、一颗子弹等等所有的事物。一个事物到了程序中就变成了一个对象。
在程序中所有的对象都被分成了两个部分数据和功能,以人为例,人的姓名、性别、年龄、身高、体重等属于数据,人可以说话、走路、吃饭、睡觉这些属于人的功能。数据在对象中被称为属性,而功能就被称为方法。所以简而言之,在程序中一切皆是对象。
5.1 类
要想面向对象,操作对象,首先便要拥有对象,那么下一个问题就是如何创建对象。要创建对象,必须要先定义类,所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象,举例来说:可以通过Person类来创建人的对象,通过Dog类创建狗的对象,通过car类来创建汽车的对象,不同的类可以用来创建不同的对象。
例子:
// 使用class关键字来定义一个类
class Person {
// 定义实例属性
name : string = 'lizeyan';
// 在属性前使用static关键字可以定义类属性(静态属性)
static age : number = 18;
// readonly 只读属性
readonly gender : string = '男';
// 定义方法
sayHello() {
console.log('大家好');
}
}
const person = new Person();
console.log(person);
// console.log(person.name, person.age);
// 静态属性需要类访问
console.log(Person.age);
person.sayHello();
5.2 构造函数和this
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中,而TypeScript的构造函数用关键字constructor来实现,可以通过this(和java/C#一样代表对象实例的成员访问)关键字来访问当前类体中的属性和方法。
例子:
class Dog {
name : string;
age : number;
// 构造函数会在对象调用的时候执行
constructor(name : string, age : number) {
console.log('构造函数被执行了');
console.log(this);
this.name = name;
this.age = age;
}
bark() {
alert('汪汪汪');
}
}
const dog = new Dog('lizeyan', 18);
console.log(dog);
5.3 继承
子类继承了父类之后,就会将父类中定义的非 private 属性以及方法都继承下来,遵循开闭原则。
例子:
// 定义一个动物类
class Animal{
name : string;
age : number;
constructor(name : string, age : number) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(this.name + '在叫');
}
}
// 定义一个表示狗的类 开闭原则:允许扩展,禁止修改
class Dog extends Animal{
sayHello() {
console.log('wangwangwang');
}
run() {
console.log('run');
}
}
5.4 super关键字
传统的js,使用prototype实现父、子类继承,如果父、子类有同名的方法,子类去调用父类的同名方法需要用 “父类.prototype.method.call(this)”,但是在typescript中,提供了一个关键字super,指向父类,super.method() 这样就可以达到调用父类同名的方法。
例子:
class Animal {
name : string;
constructor(name : string) {
this.name = name;
}
sayHello() {
console.log(this.name + '在叫');
}
}
class Dog extends Animal {
age : number;
constructor(name : string, age : number) {
super(name); // 调用父类的构造函数
this.age = age;
}
sayHello() {
// super当前类的父类
super.sayHello();
}
}
5.5 抽象类
以abstract开头的是抽象类,抽象类和其他类区别不大,只是不能用来创建对象,抽象类就是专门用来被继承的类,抽象类可以添加抽象方法,定义一个抽象方法 使用abstract开头,没有方法体,抽象方法只能定义在抽象类中,子类必须对抽象方法重写。
例子:
abstract class Animal {
name : string;
constructor(name : string) {
this.name = name;
}
abstract sayHello() : void;
}
class Dog extends Animal {
sayHello() {
console.log('lizeyan');
}
}
5.6 接口
接口用来定义一个类结构 用来定义一个类中应该包含哪些方法和属性 接口也可以当成类型声明去使用,接口可以在定义类的时候限制类的结构,它所有的属性都不能有实际的值,只定义对象的结构,而不考虑实际值,且它里面的所有的方法都是抽象方法。
例子:
interface myInterface {
name: string;
age: number;
gender: string;
};
const inter : myInterface = {
name: 'lizeyan',
age: 18,
gender: 'male'
};
interface myInter {
name : string;
sayHello() : void;
};
class myClass implements myInter {
name: string;
constructor(name : string) {
this.name = name;
}
sayHello() {
console.log('大家好!');
}
}
5.7 属性的封装
TS可以在属性前添加修饰符,如下:
- public 公共访问;
- private 只能在类内部访问,可用getter 方法用来读取属性、setter 方法用来设置属性
- proteced 只能在当前类和当前类的子类中访问。
例子:
class Person {
private name : string;
private age : number;
constructor(name : string, age : number) {
this.name = name;
this.age = age;
}
// TS中设置getter、setter方法的方式
get Name() {
return this.name;
}
set Name(value : string) {
this.name = value
}
}
const person = new Person('lizeyan', 18);
console.log(person);
person.Name
person.Name = 'hanzo'
class C {
//可以直接将属性定义在构造函数中
constructor(public name : string, public age : number) {}
}
const c = new C('xxx', 11)
5.8 泛型
在定义函数或类时,如果遇到类型不明确就可以使用泛型。
例子:
function fn(a : number) : number {
return a;
}
function fnN<T>(a : T) : T {
return a;
}
function fn2<T, K>(a : T, b : K) : T {
console.log(b);
return a;
}
fn2<number, string>(123, 'hello');
interface Inter{
length : number;
}
function fn3<T extends Inter>(a : T) : number {
return a.length
}
class myClass<T> {
name : T;
constructor(name : T){
this.name = name;
}
}
5.9 剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments
来访问所有传入的参数。
在TypeScript里,你可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...
)后面给定的名字,你可以在函数体内使用这个数组。
这个省略号也会在带有剩余参数的函数类型定义上使用到:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
5.10 枚举
5.10.1 数字枚举
enum Direction {
Up = 1,
Down,
Left,
Right
}
如上,我们定义了一个数字枚举, Up
使用初始化为 1
。 其余的成员会从 1
开始自动增长。 换句话说, Direction.Up
的值为 1
, Down
为 2
, Left
为 3
, Right
为 4
。
我们还可以完全不使用初始化器:
enum Direction {
Up,
Down,
Left,
Right,
}
现在, Up
的值为 0
, Down
的值为 1
等等。 当我们不在乎成员的值的时候,这种自增长的行为是很有用处的,但是要注意每个枚举成员的值都是不同的。
使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:
enum Response {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: Response): void {
// ...
}
respond("Princess Caroline", Response.Yes)
数字枚举可以被混入到计算过的和常量成员。 简短地说,不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。 换句话说,下面的情况是不被允许的:
enum E {
A = getSomeValue(),
B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}
5.10.2 字符串枚举
字符串枚举的概念很简单,但是有细微的运行时的差别。 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
5.10.3 计算的和常量成员
每个枚举成员都带有一个值,它可以是 常量或 计算出来的。 当满足如下条件时,枚举成员被当作是常量:
-
它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值
0
:// E.X is constant: enum E { X }
-
它不带有初始化器且它之前的枚举成员是一个 数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加1:
// All enum members in 'E1' and 'E2' are constant. enum E1 { X, Y, Z } enum E2 { A = 1, B, C }
-
枚举成员使用 常量枚举表达式初始化。 常数枚举表达式是TypeScript表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:
- 一个枚举表达式字面量(主要是字符串字面量或数字字面量);
- 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的);
- 带括号的常量枚举表达式;
- 一元运算符
+
,-
,~
其中之一应用在了常量枚举表达式; - 常量枚举表达式作为二元运算符
+
,-
,*
,/
,%
,<<
,>>
,>>>
,&
,|
,^
的操作对象。 若常数枚举表达式求值后为NaN
或Infinity
,则会在编译阶段报错。
所有其它情况的枚举成员被当作是需要计算得出的值。
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length
}
5.10.4 联合枚举与枚举成员的类型
存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。 字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为:
- 任何字符串字面量(例如:
"foo"
,"bar"
,"baz"
); - 任何数字字面量(例如:
1
,100
); - 应用了一元
-
符号的数字字面量(例如:-1
,-100
)。
当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。
首先,枚举成员成为了类型! 例如,我们可以说某些成员 只能是枚举成员的值:
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// ~~~~~~~~~~~~~~~~ Error!
radius: 100,
}
5.10.5 const枚举
大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const
枚举。 常量枚举通过在枚举上使用 const
修饰符来定义。
const enum Enum {
A = 1,
B = A * 2
}
5.10.6 外部枚举
外部枚举用来描述已经存在的枚举类型的形状。
declare enum Enum {
A = 1,
B,
C = 2
}
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
5.11 类型推断
TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型,例如:
let x = 3;
变量x
的类型被推断为数字, 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时,大多数情况下,类型推论是直截了当地。
5.11.1 最佳通用类型
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如:
let x = [0, 1, null];
为了推断x
的类型,我们必须考虑所有元素的类型。 这里有两种选择:number
和null
。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:
let zoo = [new Rhino(), new Elephant(), new Snake()];
这里,我们想让zoo被推断为Animal[]
类型,但是这个数组里没有对象是Animal
类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型:
(Rhino | Elephant | Snake)[]
5.11.2 上下文类型
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //<- Error
};
这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown
函数的类型来推断右边函数表达式的类型。 因此,就能推断出mouseEvent
参数的类型了。 如果函数表达式不是在上下文类型的位置,mouseEvent
参数的类型需要指定为any
,这样也不会报错了。
如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子:
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.button); //<- Now, no error is given
};
这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
这个例子里,最佳通用类型有4个候选者:Animal
,Rhino
,Elephant
和Snake
。 当然,Animal
会被做为最佳通用类型。
5.12 高级类型
5.12.1 交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable
同时是Person
和Serializable
和Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
5.12.2 联合类型
联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入number
或string
类型的参数。 例如下面的函数:
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
padLeft("Hello world", 4); // returns " Hello world"
联合类型表示一个值可以是几种类型之一。 我们用竖线(|
)分隔每个类型,所以number | string | boolean
表示一个值可以是number
,string
,或boolean
。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是A | B
,我们能够确定的是它包含了A
和B
中共有的成员。 这个例子里,Bird
具有一个fly
成员。 我们不能确定一个Bird | Fish
类型的变量是否有fly
方法。 如果变量在运行时是Fish
类型,那么调用pet.fly()
就出错了。
5.12.3 类型保护与区分类型
用户自定义的类型保护
这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道pet
的类型的话就好了。
TypeScript里的类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
在这个例子里,pet is Fish
就是类型谓词。 谓词为parameterName is Type
这种形式,parameterName
必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用isFish
时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
typeof类型保护
现在我们回过头来看看怎么使用联合类型书写padLeft
代码。 我们可以像下面这样利用类型断言来写:
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
instanceof类型保护
instanceof
类型保护是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:
interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 类型细化为'StringPadder'
}
instanceof
的右侧要求是一个构造函数,TypeScript将细化为:
- 此构造函数的
prototype
属性的类型,如果它的类型不为any
的话 - 构造签名所返回的类型的联合
5.12.4 可以为null的类型
TypeScript具有两种特殊的类型,null
和undefined
,它们分别具有值null和undefined. 我们在基础类型一节里已经做过简要说明。 默认情况下,类型检查器认为null
与undefined
可以赋值给任何类型。 null
与undefined
是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。
--strictNullChecks
标记可以解决此错误:当你声明一个变量时,它不会自动地包含null
或undefined
。 你可以使用联合类型明确的包含它们:
let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能赋值给'string | null'
注意,按照JavaScript的语义,TypeScript会把null
和undefined
区别对待。 string | null
,string | undefined
和string | undefined | null
是不同的类型。
5.12.5 可选参数和可选属性
使用了--strictNullChecks
,可选参数会被自动地加上| undefined
:
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
可选属性也会有同样的处理:
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
5.12.6 类型保护和类型断言
由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除null
,幸运的是这与在JavaScript里写的代码一致:
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
这里很明显地去除了null
,你也可以使用短路运算符:
function f(sn: string | null): string {
return sn || "default";
}
如果编译器不能够去除null
或undefined
,你可以使用类型断言手动去除。 语法是添加!
后缀:identifier!
从identifier
的类型里去除了null
和undefined
:
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
}
name = name || "Bob";
return postfix("great");
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
6. Symbols
6.1 介绍
自ECMAScript 2015起,symbol
成为了一种新的原生类型,就像number
和string
一样。
symbol
类型的值是通过Symbol
构造函数创建的:
let sym1 = Symbol();
let sym2 = Symbol("key"); // 可选的字符串key
Symbols是不可改变且唯一的:
let sym2 = Symbol("key");
let sym3 = Symbol("key");
sym2 === sym3; // false, symbols是唯一的
像字符串一样,symbols也可以被用做对象属性的键:
let sym = Symbol();
let obj = {
[sym]: "value"
};
console.log(obj[sym]); // "value"
Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员:
const getClassNameSymbol = Symbol();
class C {
[getClassNameSymbol](){
return "C";
}
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
6.2 内置symbols
除了用户定义的symbols,还有一些已经众所周知的内置symbols,内置symbols用来表示语言内部的行为,以下为这些symbols的列表:
方法 | 作用 |
---|---|
Symbol.hasInstance | 方法,会被instanceof 运算符调用,构造器对象用来识别一个对象是否是其实例 |
Symbol.isConcatSpreadable | 布尔值,表示当在一个对象上调用Array.prototype.concat 时,这个对象的数组元素是否可展开 |
Symbol.iterator | 方法,被for-of 语句调用,返回对象的默认迭代器 |
Symbol.match | 方法,被String.prototype.match 调用,正则表达式用来匹配字符串 |
Symbol.replace | 方法,被String.prototype.replace 调用,正则表达式用来替换字符串中匹配的子串 |
Symbol.search | 方法,被String.prototype.search 调用,正则表达式返回被匹配部分在字符串中的索引 |
Symbol.species | 函数值,为一个构造函数,用来创建派生对象 |
Symbol.split | 方法,被String.prototype.split 调用,正则表达式来用分割字符串 |
Symbol.toPrimitive | 方法,被ToPrimitive 抽象操作调用,把对象转换为相应的原始值 |
Symbol.toStringTag | 方法,被内置方法Object.prototype.toString 调用,返回创建对象时默认的字符串描述 |
Symbol.unscopables | 对象,它自己拥有的属性会被with 作用域排除在外 |
7. Iterators 和 Generators
7.1 可迭代性
当一个对象实现了Symbol.iterator
属性时,我们认为它是可迭代的。 一些内置的类型如Array
,Map
,Set
,String
,Int32Array
,Uint32Array
等都已经实现了各自的Symbol.iterator
,对象上的Symbol.iterator
函数负责返回供迭代的值。
for..of
语句
for..of
会遍历可迭代的对象,调用对象上的Symbol.iterator
方法。 下面是在数组上使用for..of
的简单例子:
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
for..of
vs. for..in
语句
for..of
和for..in
均可迭代一个列表;但是用于迭代的值却不同,for..in
迭代的是对象的 键 的列表,而for..of
则迭代对象的键对应的值。
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // "4", "5", "6"
}
另一个区别是for..in
可以操作任何对象;它提供了查看对象属性的一种方法。 但是for..of
关注于迭代对象的值。内置对象Map
和Set
已经实现了Symbol.iterator
方法,让我们可以访问它们保存的值:
let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";
for (let pet in pets) {
console.log(pet); // "species"
}
for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}
7.2 代码生成
目标为 ES5 和 ES3
当生成目标为ES5或ES3,迭代器只允许在Array
类型上使用。 在非数组值上使用for..of
语句会得到一个错误,就算这些非数组值已经实现了Symbol.iterator
属性。
编译器会生成一个简单的for
循环做为for..of
循环,比如:
let numbers = [1, 2, 3];
for (let num of numbers) {
console.log(num);
}
生成的代码为:
var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
var num = numbers[_i];
console.log(num);
}
目标为 ECMAScript 2015 或更高
当目标为兼容ECMAScipt 2015的引擎时,编译器会生成相应引擎的for..of
内置迭代器实现方式。