文章目录
第一章 快速入门
1、TypeScript 简介
- TypeScript 是 JavaScript 的超集。
- 它对 JS 进行了扩展,向 JS 中引入了类型的概念,并添加了许多新的特性。
- TS 代码需要通过编译器编译为 JS,然后再交由 JS 解析器执行。
- TS 完全兼容 JS,换言之,任何的 JS 代码都可以直接当成 TS 使用。
- 相较于 JS 而言,TS 拥有了静态类型,更加严格的语法,更强大的功能;TS 可以在代码执行前就完成代码的检查,减小了运行时异常的出现的几率;TS 代码可以编译为任意版本的 JS 代码,可有效解决不同 JS 运行环境的兼容问题;同样的功能,TS 的代码量要大于 JS,但由于 TS 的代码结构更加清晰,变量类型更加明确,在后期代码的维护中 TS 却远远胜于 JS。
2、TypeScript 开发环境搭建
- 下载 Node.js
- 安装 Node.js
- 使用 npm 全局安装 typescript
- 进入命令行
- 输入:npm i -g typescript
- 创建一个 ts 文件
- 使用 tsc 对 ts 文件进行编译
- 进入命令行
- 进入 ts 文件所在目录
- 执行命令:tsc xxx.ts
3、类型
类型声明
- 类型声明是 TS 非常重要的一个特点
- 通过类型声明可以指定 TS 中变量(参数、形参)的类型
- 指定类型后,当为变量赋值时,TS 编译器会自动检查值是否符合类型声明,符合则赋值,否则报错
- 简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值
// 语法 let 变量: 类型; let 变量: 类型 = 值; function fn(参数: 类型, 参数: 类型): 类型{ ... }
基础类型
类型都是小写
类型 | 例子 | 描述 |
---|---|---|
number | 1, -33, 2.5 | 任意数字 |
string | ‘hi’, “hi”, “hi” | 任意字符串 |
boolean | true、false | 布尔值 true 或 false |
字面量 | 其本身 | 限制变量的值就是该字面量的值 |
any | * | 任意类型,能赋值给其他类型的变量 |
unknown | * | 类型安全的 any,不能赋值给其他类型的变量 |
void | 空值(undefined) | 没有值(或 undefined) |
never | 没有值 | 不能是任何值 |
object | {name:‘孙悟空’} | 任意的 JS 对象 |
array | [1,2,3] | 任意 JS 数组 |
tuple | [4,5] | 元组,TS 新增类型,固定长度数组 |
enum | enum{A, B} | 枚举,TS 中新增类型 |
number
支持十进制、十六进制、八进制、二进制
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;
string
支持模板字符串
let color: string = "blue";
color = 'red';
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}. I'll be ${age + 1} years old next month.`;
boolean
let isDone: boolean = false;
字面量
也可以使用字面量去指定变量的类型,通过字面量可以确定变量的取值范围,用 | 设定多个可选值。
let color: 'red' | 'blue' | 'black';
let num: 1 | 2 | 3 | 4 | 5;
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre"); // 报错 Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic"); // 报错 Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
字面推断
当你使用对象初始化变量时,TypeScript 假定该对象的属性可能会在以后更改值。
基于此,会产生一些意外的影响。
// 例一
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
上面例子中,第一个例子 TypeScript 不假定将 1 分配给先前具有 0 的字段是错误的,即它不认为 couter: 0 是字面量,而认为 { counter: 0 } 是对象且对象属性可变,所以不会报错。
// 例二
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method); // 报错 Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'
第二个例子 req.method 被推断为 string,而不是 “GET”,所以 TypeScript 认为此代码有错误。
有两种方法解决:
你可以通过在任一位置添加类型断言来更改推断:
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");
你可以使用 as const 将整个对象转换为类型字面:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
any
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any 类型来标记这些变量。
// 显式 any
let d: any = 4;
d = 'hello';
d = true;
let s:string;
s = d; // d 的类型是 any,它可以赋值给任意变量
// 有一个数组,它包含了不同的类型的数据
let list: any[] = [1, true, "free"];
list[1] = 100;
对比 object
在对现有代码进行改写的时候,any 类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 Object 虽然有相似的作用, 但是 Object 类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法。
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
noImplicitAny
当你没有指定类型,并且 TypeScript 不能从上下文推断它时,编译器通常会默认为 any。不过,因为 any 没有经过类型检查,通常希望避免这种情况。
// s 隐式 any
function fn(s) {
console.log(s.subtr(3))
}
TypeScript 默认 noImplicitAny 为 false,隐式 any 行为不报错。如果你希望对隐式声明 any 类型进行限制并报错,可以通过以下步骤开启。
在 tsconfig.json 中配置 noImplicitAny 选项为 true:
{
"compilerOptions": {
"noImplicitAny": true
}
}
或者在命令行中添加 --noImplicitAny 选项:
tsc --noImplicitAny yourfile.ts
unknown
let notSure: unknown = 4;
notSure = 'hello';
let s:string;
// s = notSure; // 报错,unknown类型的变量,不能直接赋值给其他变量
if(typeof notSure === "string"){
s = notSure;
}
void
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void。声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null。
// 空的,没有返回值,或者 undefined
let unusable: void = undefined;
function error(message: string): void {
console.log(message);
}
null 和 undefined
TypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和null。 和 void 相似,它们的本身的类型用处不是很大。
strictNullChecks 关闭
默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给任何类型的变量。
缺乏对这些值的检查往往是错误的主要来源;
strictNullChecks 开启
然而,当你指定了 strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。 这能避免很多常见的问题。 也许在某处你想传入一个 string 或 null 或 undefined,你可以使用联合类型 string | null | undefined。鼓励尽可能地使用 strictNullChecks。
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
当开启了配置,当值为 null 或 undefined 时,你需要在对该值使用方法或属性之前测试这些值。
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
非空断言运算符(后缀 !)
TypeScript 还具有一种特殊的语法,可以在不进行任何显式检查的情况下从类型中删除 null 和 undefined。在任何表达式之后写 ! 实际上是一个类型断言,该值不是 null 或 undefined:
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
liveDangerously()
就像其他类型断言一样,这不会改变代码的运行时行为,所以当你知道值不能是 null 或 undefined 时,使用 ! 很重要。
never
// 返回 never 的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为 never
function fail() {
return error("Something failed");
}
// 返回 never 的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
object
{} 用来指定对象中可以包含哪些属性。语法:{属性名:属性值,属性名:属性值}
let a: object; // object 表示一个 js 对象
a = {};
a = function () {
};
可选属性
在属性名后边加上?,表示属性是可选的
let b: {name: string, age?: number};
b = {name: '孙悟空', age: 18};
索引签名
[propName: string]: any 表示任意类型、任意个数的属性
let c: {name: string, [propName: string]: any};
c = {name: '猪八戒', age: 18, gender: '男'};
函数
设置函数结构的类型声明。包含参数注解和返回值注解。语法:(形参:类型, 形参:类型 …) => 返回值
let d: (a: number ,b: number)=>number;
// d = function (n1: number, n2: number): number{
// return 10;
// }
返回 Promise 的函数,如果你想注释一个返回 Promise 的函数的返回类型,你应该使用 Promise 类型:
async function getFavoriteNumber(): Promise<number> {
return 26;
}
匿名函数的情况,不同于普通函数的地方在于,可以通过变量自动推导出变量的类型。
const names = ["Alice", "Bob", "Eve"];
// Contextual typing for function - parameter s inferred to have type string
names.forEach(function (s) {
console.log(s.toUpperCase());
});
// Contextual typing also applies to arrow functions
names.forEach((s) => {
console.log(s.toUpperCase());
});
array
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
// 元组类型
let x: [string, number];
x = ["hello", 10]; // 正确
x = [10, "hello"]; // 错误
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型
enum
使用枚举我们可以定义一些带名字的常量。
某个属性值在多个值之间选值时可以用 enum 枚举类型限制。
数字枚举
设置 Direction.Up 为 1,Direction.Down、Direction.Left、Direction.Right 依次为 2,3,4。
enum Direction {
Up = 1,
Down,
Left,
Right
}
如果不设置,Direction.Up 、Direction.Down、Direction.Left、Direction.Right 依次为 0,1,2,3。
enum Direction {
Up,
Down,
Left,
Right
}
也可以全部手动设置。
enum Direction {
Up = 2,
Down = 4,
Left = 6,
Right = 8
}
使用枚举很简单:使用枚举的名字来访问枚举类型,通过枚举的属性来访问枚举成员。
enum Response {
No = 0,
Yes = 1,
}
// message 限制为枚举类型,只能赋值为枚举成员值
function respond(recipient: string, message: Response): void {
// ...
}
// 通过枚举属性获取枚举成员值,注意此处实际获取的是值 1
respond("Princess Caroline", Response.Yes)
字符串枚举
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
异构枚举
一般不这样写
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
计算的和常量成员
每个枚举成员都带有一个值,它可以是常量或计算出来的。 当满足如下条件时,枚举成员被当作是常量:
-
它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值 0:
// E.X is constant: enum E { X }
-
它不带有初始化器且它之前的枚举成员是一个 数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加 1。
enum E1 { X, Y, Z } enum E2 { A = 1, B, C }
-
枚举成员使用常量枚举表达式初始化。 常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式
- 一个枚举表达式字面量(主要是字符串字面量或数字字面量)
- 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
- 带括号的常量枚举表达式
- 一元运算符 +, -, ~ 其中之一应用在了常量枚举表达式
- 常量枚举表达式做为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^ 的操作对象。 若常数枚举表达式求值后为 NaN 或 Infinity,则会在编译阶段报错
所有其他情况被当作计算的值
enum FileAccess {
// 常量成员
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 计算成员
G = "123".length
}
联合枚举与枚举成员的类型
枚举成员可以作为类型进行限制
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,
}
枚举类型本身是枚举成员的联合
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// ~~~~~~~~~~~
// Error! 此比较似乎是无意的,因为类型“E.Foo”和“E.Bar”没有重叠
}
}
运行时的枚举
创建一个以枚举属性名做为对象成员的对象。
enum E {
X, Y, Z
}
function f(obj: { X: number }) {
return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E);
反向映射
数字枚举成员具有反向映射,从枚举值可以映射到枚举名字。即枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
const 枚举
常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
外部枚举
外部枚举用来描述已经存在的枚举类型的形状。
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
declare enum Enum {
A = 1,
B,
C = 2
}
不太常见的基础类型
bigint
从 ES2020 开始,JavaScript 中有一个基础类型用于非常大的整数,BigInt:
const oneHundred: bigint = BigInt(100);
const anotherHundred: bigint = 100n;
symbol
JavaScript 中有一个基础类型用于通过函数 Symbol() 创建全局唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
高级类型
联合类型
联合类型表示一个值可以是几种类型之一
let num: 1 | 2 | 3 | 4 | 5;
let a: string | number;
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
printId(101); // OK
printId("202"); // OK
printId({ myID: 22342 }); // Error
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员,否则会报错。
// 例一 没有使用共有的成员,报错
function printId(id: number | string) {
console.log(id.toUpperCase()); // 报错
// Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
}
// 例二 使用共有的成员,不报错,也不用使用类型缩小等去处理
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
缩小联合
// 例一 简单类型的类型缩小可以用 typeof
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
// 例二 数组类型的类型缩小可以用 Array.isArray()
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
交叉类型
交叉类型,交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
// &表示同时满足限制
let j: { name: string } & { age: number };
j = {name: '孙悟空', age: 18};
类型缩小
详见 类型缩小
类型推论
TS拥有自动的类型判断机制
let a: number; // 声明一个变量 a,同时指定它的类型为 number
a = 10; // a 的类型设置为了 number,在以后的使用过程中 a 的值只能是数字
a = 33;
// a = 'hello'; // 此行代码会报错,因为变量 a 的类型是 number,不能赋值字符串
当对变量的声明和赋值是同时进行的,TS 编译器会自动判断变量的类型
let c: boolean = false;
所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明
let c = false;
c = true;
类型断言
有些情况下,变量的类型对于我们来说是很明确,但是 TS 编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型,断言有两种形式:
第一种 as 语法
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
第二种尖括号语法
let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
与类型注释一样,类型断言被编译器删除,不会影响代码的运行时行为。因为类型断言在编译时被删除,所以没有与类型断言关联的运行时检查。如果类型断言错误,则不会产生异常或 null。
限制
TypeScript 只允许类型断言转换为更具体或更不具体的类型版本。此规则可防止 “impossible” 强制,例如:
const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
避开限制
有时,此规则可能过于保守,并且不允许可能有效的更复杂的强制转换。如果发生这种情况,你可以使用两个断言,首先是 any(或 unknown,我们稍后会介绍),然后是所需的类型:
const a = expr as any as T;
类型别名
共用一个类型限制时,可以设置一个类型别名 type 属性。可以定义任何类型的别名,比如对象类型、联合类型等等
// 例一 字面量联合类型
type myType = 1 | 2 | 3 | 4 | 5;
let k: myType;
let l: myType;
let m: myType;
k = 2;
// 例二 对象类型
type Point = {
x: number;
y: number;
};
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
对比接口
类型别名和接口非常相似,在很多情况下你可以在它们之间自由选择。interface 的几乎所有功能都在 type 中可用,主要区别在于无法重新打开类型以添加新属性,而接口始终可扩展。
扩展接口
interface 通过继承扩展接口
interface Animal {
name: string;
}
interface Bear extends Animal {
honey: boolean;
}
const bear = getBear();
bear.name;
bear.honey;
type 通过交叉扩展类型
type Animal = {
name: string;
}
type Bear = Animal & {
honey: boolean;
}
const bear = getBear();
bear.name;
bear.honey;
向现有接口添加新字段
interface 可以添加新字段
接口重名做复合,多个重名接口的限制都起作用。
interface Window {
title: string;
}
interface Window {
ts: TypeScriptAPI;
}
const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
type 类型创建后无法更改
类型不能重名。
type Window = {
title: string;
}
type Window = {
ts: TypeScriptAPI;
}
// Error: Duplicate identifier 'Window'.
除此以外,
-
在 TypeScript 4.2 版之前,类型别名可能出现在错误信息中,有时代替等效的匿名类型(可能需要也可能不需要)。接口将始终在错误消息中命名。——即接口在报错中始终被命名为类型别名。
// 下面三个例子都报错 // Type 'number' is not assignable to type 'string'.(2322) // The expected type comes from property 'name' which is declared here on type 'Mammal/Lizard/Arachnid' // 例一 interface Mammal { name: string } function echoMammal(m: Mammal) { console.log(m.name) } echoMammal({ name: 12343 }) // 报错 // 例二 type Lizard = { name: string } function echoLizard(l: Lizard) { console.log(l.name) } echoLizard({ name: 12345}) // 报错 // 例三 type Arachnid = Omit<{ name: string, legs: 8 }, 'legs'> function echoSpider(l: Arachnid) { console.log(l.name) } echoSpider({ name: 12345, legs: 8}) // 报错
-
类型别名不得参与在声明合并中,但接口可以。——接口可以通过同名接口进行复合,类型别名不能同名。
见上面【向现有接口添加新字段】的示例。
-
接口只能用于声明对象的形状,而不是重命名基础类型。——接口不能继承基本类型,类型别名可以重命名基础类型。
interface AnObject1 {
value: string
}
type AnObject2 = {
value: string
}
type SanitizedString = string
type EvenNumber = number
// 报错:An interface cannot extend a primitive type like 'string'; an interface can only extend named types and classes
// 'extends' clause of exported interface 'X' has or is using private name 'string'.(4022)
interface X extends string {
}
- 接口名称将在错误消息中显示为 总是以原来的形式出现,但仅当它们被名称使用时。
类型兼容性
TypeScript 结构化类型系统的基本规则是,如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// OK, because of structural typing
p = new Person();
比较原始类型和对象类型
检查 y 是否能赋值给 x,编译器检查 x 中的每个属性,看是否能在 y 中也找到对应属性。
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;
function greet(n: Named) {
console.log('Hello, ' + n.name);
}
greet(y); // OK
比较两个函数
要查看 x 是否能赋值给 y,首先看它们的参数列表。 x 的每个参数必须能在 y 里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x 的每个参数在 y 中都能找到对应的参数,所以允许赋值。
第二个赋值错误,因为 y 有个必需的第二个参数,但是 x 并没有,所以不允许赋值。
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
如何处理返回值类型,创建两个仅是返回值类型不同的函数。类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error, because x() lacks a location property
第二章:面向对象
面向对象是程序中一个非常重要的思想,它被很多同学理解成了一个比较难,比较深奥的问题,其实不然。面向对象很简单,简而言之就是程序之中所有的操作都需要通过对象来完成。
举例来说:
- 操作浏览器要使用window对象
- 操作网页要使用document对象
- 操作控制台要使用console对象
一切操作都要通过对象,也就是所谓的面向对象,那么对象到底是什么呢?这就要先说到程序是什么,计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体,比如:照片是对一个具体的人的抽象,汽车模型是对具体汽车的抽象等等。程序也是对事物的抽象,在程序中我们可以表示一个人、一条狗、一把枪、一颗子弹等等所有的事物。一个事物到了程序中就变成了一个对象。
在程序中所有的对象都被分成了两个部分数据和功能,以人为例,人的姓名、性别、年龄、身高、体重等属于数据,人可以说话、走路、吃饭、睡觉这些属于人的功能。数据在对象中被成为属性,而功能就被称为方法。所以简而言之,在程序中一切皆是对象。
1、类(class)
要想面向对象,操作对象,首先便要拥有对象,那么下一个问题就是如何创建对象。要创建对象,必须要先定义类,所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象,举例来说:可以通过Person类来创建人的对象,通过Dog类创建狗的对象,通过Car类来创建汽车的对象,不同的类可以用来创建不同的对象。
定义类:
class 类名 {
属性名: 类型;
constructor(参数: 类型){
this.属性名 = 参数;
}
方法名(){
....
}
}
示例:
class Person{
name: string;
age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
sayHello(){
console.log(`大家好,我是${this.name}`);
}
}
使用类:
// 调用之前定义的构造函数,创建一个 Person 类型的新对象,并执行构造函数初始化它
const p = new Person("猕猴桃", 2);
p.sayHello();
封装
对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装。
实例属性
类的普通属性和方法,叫实例属性或实例方法,通过类的实例访问。
class Person{
name = '孙悟空';
getName(){
return this.name
}
}
let p = new Person();
console.log(p.name);
静态属性
静态属性(方法),也称为类属性。使用静态属性无需创建实例,通过类即可直接使用。
class Tools{
static PI = 3.1415926;
static sum(num1: number, num2: number){
return num1 + num2
}
}
console.log(Tools.PI);
console.log(Tools.sum(123, 456));
只读属性
默认情况下,对象的属性是可以任意的修改的,为了确保数据的安全性,在 TS 中可以对属性的权限进行设置
如果在声明属性时添加一个 readonly,则属性便成了只读属性无法修改。 只读属性必须在声明时或构造函数里被初始化。
class Person{
readonly name = '孙悟空';
getName(){
return this.name
}
}
let p = new Person();
console.log(p.name);
// p.name = '猪八戒'; // 报错,name 属性只读,不能修改
属性修饰符
-
public(默认值),可以在类、子类和对象中访问
class Person{ public name: string; // 写或什么都不写都是public public age: number; constructor(name: string, age: number){ this.name = name; // 可以在类中修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); } } class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中可以修改 } } const p = new Person('孙悟空', 18); p.name = '猪八戒';// 可以通过对象修改
-
protected ,可以在类、子类中访问
protected 也可以限制构造函数,如果限制了构造函数,该类将不能被实例化,但可以被子类继承class Person{ protected name: string; protected age: number; constructor(name: string, age: number){ this.name = name; // 可以修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); } } class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中可以修改 } } const p = new Person('孙悟空', 18); p.name = '猪八戒';// 不能修改
-
private 只能在类中访问
class Person{ private name: string; private age: number; constructor(name: string, age: number){ this.name = name; // 可以修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); } } class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中不能修改 } } const p = new Person('孙悟空', 18); p.name = '猪八戒';// 不能修改
属性存取器
对于一些不希望被任意修改的属性,可以将其设置为 private,直接将其设置为 private将导致无法再通过对象修改其中的属性。我们可以在类中定义一组读取、设置属性的方法,这种对属性读取或设置的属性被称为属性的存取器。读取属性的方法叫做 setter 方法,设置属性的方法叫做 getter 方法
只带有 get 不带有 set 的存取器自动被推断为 readonly。
// 密码验证通过才可能修改用户名
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
alert(employee.fullName);
}
this
在类中,使用this表示当前对象
继承
继承时面向对象中的又一个特性。通过继承可以将其他类中的属性和方法引入到当前类中。通过继承可以在不修改类的情况下完成对类的扩展。
类从基类中继承了属性和方法。 这里, Dog 是一个 派生类,它派生自 Animal 基类,通过 extends 关键字。 派生类通常被称作子类,基类通常被称作超类。
class Animal{
name: string;
age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
}
class Dog extends Animal{
bark(){
console.log(`${this.name}在汪汪叫!`);
}
}
const dog = new Dog('旺财', 4);
dog.bark();
super
在子类中可以使用 super 来完成对父类的引用
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
console.log('动物在叫~');
}
}
class Dog extends Animal{
age: number;
constructor(name: string, age: number) {
// 如果在子类中写了构造函数,在子类构造函数中必须对父类的构造函数进行调用
// 在构造函数里访问 this 的属性之前,我们 一定要调用 super()
super(name); // 调用父类的构造函数
this.age = age;
}
sayHello() {
// 在类的方法中 super 就表示当前类的父类
// super.sayHello();
console.log('汪汪汪汪!');
}
}
const dog = new Dog('旺财', 3);
dog.sayHello();
重写
发生继承时,如果子类中的方法会替换掉父类中的同名方法,这就称为方法的重写
class Animal{
name: string;
age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
run(){
console.log(`父类中的run方法!`);
}
}
class Dog extends Animal{
bark(){
console.log(`${this.name}在汪汪叫!`);
}
run(){
console.log(`子类中的run方法,会重写父类中的run方法!`);
}
}
const dog = new Dog('旺财', 4);
dog.bark();
抽象
抽象类(abstract class)是专门用来被其他类所继承的类,它只能被其他类所继承不能用来创建实例。使用 abstract 开头的方法叫做抽象方法,抽象方法没有方法体只能定义在抽象类中,继承抽象类时抽象方法必须要实现。
abstract class Animal {
abstract run(): void
}
class Dog extends Animal {
run() {
console.log('狗在跑~')
}
bark() {
console.log('汪汪')
}
}
const animal: Animal = new Dog() // 允许创建抽象类子类的实例,类型限制为父类抽象类
//const animal = new Animal(); // 报错,抽象类不允许实例化
animal.run() // 正确,抽象类中包含 bark() 方法,且子类进行了实现
// animal.bark() // 报错,抽象类中不包含 run() 方法
高级用法
构造函数
当你在 TypeScript 里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的实例的类型,其次,也创建了一个叫做构造函数的值。
类的实例的类型
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
编译为 Javascript 的语法,其中 let Greeter 被赋值为构造函数。之后调用 new 并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有实例部分与静态部分这两个部分。
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
用法对比
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
// 类的实例类型用法
const greeter1: Greeter = new Greeter();
console.log(greeter1.greet());
// 类的构造函数用法
const GreeterMaker: typeof Greeter = Greeter;
GreeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new GreeterMaker();
console.log(greeter2.greet());
把类当接口
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
2、接口(Interface)
接口的作用类似于抽象类,不同点在于接口中的所有方法和属性都是没有实值的,换句话说接口中的所有方法都是抽象方法。
TypeScript 的核心原则之一是对值所具有的结构进行类型检查,接口的作用可以实现这一目的。
对象类型
接口主要负责定义一个类的结构,接口可以去限制一个对象,对象只有包含接口中定义的所有属性和方法时才能匹配接口。同时,可以让一个类去实现接口,实现接口时类中要包含接口中的所有属性。
interface Person{
name: string;
sayHello():void;
}
function fn(per: Person){
per.sayHello();
}
fn({name:'孙悟空', sayHello() {console.log(`Hello, 我是 ${this.name}`)}});
可选属性
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
只读数组
TypeScript 具有 ReadonlyArray 类型,它与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error! 只读数组直接赋值给普通数组是不可以的
a = ro as number[]; // 只读数组可以通过断言赋值给普通数组
readonly vs const
最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用 readonly。
额外属性
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
函数类型
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
// 函数声明参数不一定和接口定义的参数名一致。函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
// 函数声明也可以不显示的声明参数和返回值类型,会进行自动推断去匹配接口限制
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
可索引的类型
与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型。
定义了 StringArray 接口,它具有索引签名。 这个索引签名表示了当用 number 去索引StringArray 时会得到 string 类型的返回值。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
TypeScript 支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript 会将它转换成 string 然后再去索引对象。 也就是说用 100(一个 number)去索引等同于使用"100"(一个 string)去索引,因此两者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
字符串索引签名能够很好的描述 dictionary 模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property 和 obj[“property”] 两种形式都可以。 下面的例子里, name 的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
你可以将索引签名设置为只读,这样就防止了给索引赋值。
自我理解这种限制和数组类似,但数组功能没有其强大,比如设置索引只读、索引类型限制。
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
类类型
实现接口
接口描述了类的公共部分,允许类中有私有部分。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
继承接口
继承一个接口
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
继承多个接口
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
混合类型
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;