抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

0 TypeScript 介绍

TypeScript 是微软开发的开源编程语言,在 JavaScript 的基础上添加了静态类型基于类的面向对象编程,是 JavaScript 的超集。

TypeScript 文件通过其编译器转译为 JavaScript 代码运行。

该语言作者是 Anders Hejlsberg,他也是 C# 的首席架构师。

ES 和 TS

ECMAScript 是 Ecma国际指定的 JavaScript 标准,TypeScript也是 ES 的超集。

TypeScript 和 JavaScript 对比
TypeScriptJavaScript
JS 超集,解决大型项目的代码复杂性脚本语言,创建动态网页
需要编译成 JS 再运行解释型语言,直接运行
强类型、有静态/动态类型弱类型,无静态类型
支持模块、泛型和接口不支持

1 基础类型

TypeScript 声明变量有 3 种形式,分别是 let var const

  • var 关键字声明,作用于函数内,存在变量提升(即声明行为发生在作用域最开始)。
  • let 关键字声明,作用于代码块 {} 内,不存在变量提升。
  • const 关键字声明同 let,但不允许重新分配变量,用来声明常量。
let 变量名: 类型 = 值;   // 赋值并定义类型
let a: number = 7;

let 变量名: 类型;       // 只定义类型
let a: boolean;

let 变量名 = 值;        // 只赋值,任意类型
let a = 7;

let 变量名;             // 不赋值不定义
let a;

因为 var 声明会出现变量提升的作用,其变量对整个函数,如果在 for 循环等地方涉及输出或者计数时会出现原有的赋值被覆盖的问题,而且它可以重复定义同一个变量。这两种情况都不会让编译器检查出来,但是并不符合开发者原本的意图。

更提倡使用 let 声明,符合一般逻辑。

如果一个变量不需要对它将进行写入,那么采用 const 声明会符合最小特权原则,也更容易推测数据的流动。

类型断言(Type Assertion)——更改类型

更改已有的变量类型在 TypeScript 中称为类型断言。更改方式为 <更改的类型> 变量名 或者 变量名 as 类型

首先定义一个任意类型的变量 a,它并非字符串,不能被字符串赋值,也不能应用字符串相关的函数进行处理。

let a: any = 'this is a string' // 此时 a 为任意类型而非字符串,无法被字符串赋值
let strLength: number = (<string>a).length; // 类型断言改变类型,以应用字符串的函数
let strLength: number = (a as string).length // as 的断言方法

这个过程编译成 JavaScript 会重新定义一遍改变量。

TypeScript 类型表
类型示例
基本类型
booleanx: boolean = true
numberx: number = 7
stringx:string = '10'
undefinedx: undefined = undefined
nullx: null = null
引用类型&其他类型
objectx: object = {age: '14', color: 'white'}
arrayx: array = [1, '2', 3.0]
functionx: function = (args) => {console.log(args)}
symbolx: symbol = Symbol('id')
TS 补充类型
anyx: any = null
neverfunction error(msg): never => {throw new Error(msg)}
enumenum Color {red = 1, green, blue}
tuplex: [string, number] = ['name', 12]

类型注解

TS 的声明变量类型的方式是注解,在变量后面加上 : 类型,使用小写。

//字符串注解
const xxx: string = 'string type';

// 函数参数类型
function params(value: string){ // 参数为 string 类型
    console.log(value) 
}

// 函数返回值类型注解
function returnValue(): string{ // 函数的返回值为 string 类型
    return 'value is string'
}

boolean 布尔

基本布尔 true/false 类型,在 JavaScript 和 TypeScript 中都是 boolean

let isDone: boolean = false;

number 数字

JS 和 TS 中的所有数字都是浮点数,类型为 number。支持二/八/十/十六进制。

let decLiteral: number = 7;
let hexLiteral: number = 0xf00d;    // 十六进制
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744;   // 八进制

string 字符串

JavaScript 同 TypeScript,字符串用单双引号都可以。

let name1: string = "miao"
var name1 = "miao"

模板字符串用来定义多行文本和内嵌表达式,这种字符串用反引号包围,以 ${expr} 形式嵌入表达式。

let name1: string = `miao`;
let age: number = 18;
let sentence: string = `Hello, my name is ${name1}.
I'll be ${age + 1} years old next month.`;

等同于

let sentence: string = "Hello, my name is " + name1 + "\n" + "I'll be " + (age + 1) + " years old next month.";

Array 数组

TypeScript 数组同 JS,有两种方式定义。

  1. 在元素类型后面加上 变量名: 类型[]
  2. 泛型数组 变量名: Array<元素类型>
let list: number[] = [1, 2, 3];

let list: Array<number> = [1, 2, 3]; // 泛型

Tuple 元组

TypeScript 的元组可以表示已知元素数量和类型的数组,各元素的类型可不相同。

let x: [string, number];
x = ['hello', 10];      // 必须和声明时的类型相同

当访问元素越界的时候,会使用联合类型替代,但必须是已有的类型。

x[3] = 'world'; //OK 赋值给(string | number)类型

x[6] = true;    // Error 布尔不在已有的类型中,不是这个联合类型 string | number 范围

enum 枚举

enum 类型是对 JavaScript 标准数据类型的补充,其特性同 C#,可以用枚举类型为一组数值赋予名字。

enum Color {Red = 1, Green, Blue} // 没有分号
let c:Color = Color.Red;
let colorName: string = Color[2];

console.log(colorName); // 'Green'

默认情况下,枚举的数值从 0 开始增长,即未指定值时,Red = 0, Green = 1, Blue = 2 依次增长。

any 动态类型

在不清楚变量类型的时候可以使用 any 来声明类型,此时类型检查器不会对这些值进行检查,它们直接通过编译阶段。

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;//

本质上,所有的类型都是 any 的子类型,any 是 TypeScript 的一个顶级类型,是一个类型系统逃逸舱。

需要注意的是,在使用该类型时因其过高的级别,无法使用 TypeScript 的很多保护机制,会产生一定的风险。

unknown 动态类型

unknown 类型是有保护的 any 类型,二者特性基本相同。所有类型都可以赋值给 unknown,其是 TypeScript 的另一种顶级类型。

let value: unknown;

虽然 unknown 类型的变量可以被赋予任何值,但其只能赋值给 any 或者 unknown 类型本身。

void 空

void 类型表示没有任何类型,只能被赋值为 undefinednull

let unusable: void = undefined;

其在没有返回值的时候定义函数很有用。

另外,undefinednull 也是各自的类型,其既可以当值赋给其他类型,也可以作为类型声明。可以认为其是所有类型的子类型。

never 不存在

never 类型表示不存在的值的类型,可以认为错误的返回值类型为 never。其是任何类型的子类型,可以给任何类型赋值。但没有类型是它的子类型,any 也不可以给 never 赋值。

使用 never 可以避免出现新增联合类型没有对应的实现,是用来保证类型安全的重要方式。

object 其他

object 类型是除了 number, string, boolean, symbol, null, undefined 以外的类型。

2 对象属性/方法

JavaScript 的各种衍生类型是基于 Object 构造出来的,所以对象的操作方式也适用于数组、元组等数据类型。

点表示

对象的名字是一个命名空间(namespace),访问命名空间内部采用 命名空间.项目 的方式。

person.age; // 访问属性
person.interests[1]; // 访问数组子元素
person.bio() // 方法调用

使用创建对象的方式可以方便地访问对象的属性,类似于结构体。

const message = {
    first: 'Hello',
    second: 'World'
} // 创建对象

console.log(message.first);
console.log(message.second);
// HelloWorld

中括号

中括号 [] 是另一种访问对象属性的方式,对象.项目 等价于 对象['项目']

person['age'];              // person.age
person['name']['first'];    // person.name.first

在构造数组或者类数组格式的数据时,只能用中括号的形式访问元素,不能用点方式。

const message = ['Hello', 'world'];

console.log(message[0]); // Hello

对象成员

对象成员即对象中的各个值。用以上两种方法既可以更新已存在的成员属性,也可以创建新的成员。

person.age = 45;
person['message']['last'] = 'Hello';
person.farewell = function() {alert("Farewell!")}

console.log(person.message.last);   // Hello
person.function();                  // Farewell!

3 解构与展开

解构数组

解构数组是最简单的结构。表现出来就是可以对应位置进行定义和赋值。

let input = [1, 2];
let [a, b] = input;

console.log(a); // 1
console.log(b); // 2

也可以直接进行交换

// swap variables
[a, b] = [b, a];

函数参数也可以进行解构

function f([a, b]: [number, number]){} // 函数解构数组

数组中使用 ... 语法可以创建剩余变量,即剩下的数组被赋给该变量。如果被赋值的变量数少于数组元素,后面的剩余变量会被忽略。数组赋值的时候会跳过留空的变量。

let [a, ...rest] = [1, 2, 3, 4];
// a = 1
//rest = [2, 3, 4]

let [a] = [1, 2, 3, 4];
// a = 1 剩下的变量被忽略

let [, b, ,d] = [1, 2, 3, 4];
// b = 2
// d = 4

对象解构

对象也可以进行解构。

let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let {ans1, ans2} = o;
// ans1 = "foo"
// ans2 = 12
... 记录剩余变量

使用 ... 语法同样也可以记录剩余变量,剩余变量是对象。

let {x, ...rest} = o; // o 同上

该赋值得到两个变量:x = "foo" rest = {12, "bar"}

类注解赋值

另一种对象里的属性给变量赋值类似于类型注释,在这里要注意区分:

let {a:new1, b:new2} = o; // o 同上

这个这个注释会得到新的变量 new1 = o.a new2 = 0.b

默认值

在设置属性的时候可以设置默认值,在输入 undefined 的时候使用。

function keepWholeObject(wholeObject: {a:string, b?:number}){
    let {a, b = 1001} = wholeObject;
}

此时,bundefined 时,keepWholeObject 函数的变量 wholeObject 的属性 a b 都会有值 1001。

函数声明

解构用于函数声明。

type C = {a: string, b?:number}

// 普通写法
function f(C){}

// 解构函数
function f({a, b}: C):void
{}
// 函数返回值为void,是类型注释
// 函数参数为解构声明

变量展开

展开的操作和解构正好相反。可以将一个数组展开成另一个数组,或者将一个对象展开成另一个对象。

以下是展开数组示例:

let first = [1, 2];
let second = [3, 4];

let bothPlus = [0, ...first, ...second, 5];
// bothPlus = [0, 1, 2, 3, 4, 5]

这种展开操作创建了被展开对象的浅拷贝,被展开的对象不会发生改变。

展开对象要略微复杂一些,展开对象后面的属性会覆盖前面的属性。展开是从左向右进行处理的,排在后面的属性会覆盖掉前面出现过的属性。

let defaults = {food:"spicy", price = 10, ambiance: "noisy"};

let search = {...defaults, food:"rich"};
// search = {food:"rich", price = 10, ambiance: "noisy"}

上面的例子中,因为 food 属性发生了重复,排在后面的值 “rich” 就覆盖掉了前面的。如果将展开放在后面的话,”rich” 就会被 “spicy” 覆盖。

let search = {food:"rich", ...defaults};
// search = {food:"spicy", price = 10, ambiance: "noisy"}

4 类

TypeScript 在 JavaScript 中使用函数基于原型的继承的基础上添加了语法糖——,用来创建可重复使用的组件。

类声明

使用 class 父类{} 的方式来定义类。父类的名字一般首字母大写。

class Greeter{
    greeting: string; // 属性
    constructor(message: string){ // 构造函数
        this.greeting = message;
    }
    greet(){ // 方法
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world"); // greeter 实例

该示例声明了一个 Greeter 类。中有 3 个成员:属性 greeting构造函数 constructor()方法 greet()

  • 属性:定义在类中的变量,在类的域中的所有方法都可以使用,在访问该变量的时候要用 this.变量名 的形式。
  • 构造函数:类初始化时自动执行的方法。采用 constructor(){}的形式来构造。
  • 方法:方法是一个函数,基于该类构造的实例可以用点表示法 实例名.方法() 使用类中的所有方法。

最后一行中使用 new 构造了 Greeter 类的一个实例。实例创建一个 Greeter 类型的新对象,并执行构造函数来初始化。

继承

TypeScript 支持面向对象。基于类的程序设计最基本的模式就是使用继承来扩展现有的类。

class 子类 extends 父类{} 的方法来继承。

// 父类
class Animal{ 
    // 构造函数 用于记录 name
    name: string;
    constructor(theName: string){
        this.name = theName;
    }
    // 方法 move,默认参数 0
    move(distance: number = 0){
        console.log(`${this.name} moved ${distance}m.`); // 模板字符串反引号嵌入输出
    } 
}

// 子类
class Snake extends Animals{ 
    // 构造函数 初始化的时候会自动声明一个 name 变量
    constructor(name: string){
        super(name);
    }
    // 方法 move,和父类方法区分
    move(distance: number = 5){ // 默认值为5
        console.log('Slithering...');
        super.move(distance);   // 使用父类的 move() 方法
    }
}

let a = new Snake("The Python snake");
a.move();

该示例展示了类从基类继承了属性和方法。Animal父类(超类),Dog子类(派生类)。子类有父类的所有功能,因此 Dog 也有 Animal 中的方法 move(),同时多了自己的新方法 bark()

当父类有构造函数时,子类就必须有构造函数,且调用 super()。如果在构造函数里访问 this 属性之前,也必须调用 ·super()·

修饰符

public

TypeScript 中,所有成员默认为 public,不用特别修饰。可以自由访问程序里定义的成员。

C# 要求必须明确地使用 public 指定成员可见,作以区分。

private

当成员被标记为 private 时,只能在其所在的类中访问,无法在外部访问。该成员是声明它的类所独有的。

私有变量命名一般会在前面加一个下划线 _私有变量

class Animal{
    private _name: string;
    constructor(theName: string){this.name = theName;}
}

new Animal("Cat").name; // error 'name' 是 Animal 类私有变量,只能在其中使用

protected

protectedprivate,但可以在子类中访问。

class Person{
    protected name: string; // name 变量私有,但可以继承
    constructor(name: string){this.name = name};
}

class Employee extends Person{
    private department: string;
    constructor(name: string, department: string){
        super(name);        // name 变量由 protected 修饰,因此子类可用
        this.department = department;
    }
    public getElevatorPitch(){
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());

console.log(howard.name)    // error name 变量是 protected 可继承私有

name 属性由 protected 修饰,Person 类之外是无法使用 name 变量,保证该属性不会被直接外部读取到。但是 Employee 类可以使用 name 属性(作为方法中的变量),因为它是 Person 的子类。

readonly

可以使用 readonly 修饰成员将其设置为只读。只读属性必须在声明时或构造函数里被初始化。

class Octopus{
    readonly name: string;
    readonly numberOfLegs: number = 8;  // 声明时初始化只读
    constructor(theName: string){
        this.name = theName;            // 构造函数时初始化只读
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error name 属性是只读的

get/set

TypeScript 可以通过 get/set 截取对对象成员的访问。这是一个存取器功能,get 设置一个存取器,从外部获取数据后保存,set 从保存中取出来使用。

没有存取器的类在设置 public 变量后可以很轻易地修改其中的成员。

// 创建一个 Employee 类
class Employee{
    fullName: string;           // 属性 fullName
}

let employee = new Employee();  // 创建实例 employee
employee.fullName = "Bob Smith";
if (employee.fullName){
    console.log(employee.fullName);
}

上面的例子中 fullName 属性可以在外部随意设置。在下面的版本中加入了存取器功能,就可以设置私有的 _fullName 属性,阻止外部访问,在修改私有属性前通过存取器让访问的操作出现在内部,这样就可以在访问前做出如设置验证的额外操作。

使用 get 存取器(){return this.私有变量} 来设置对应私有变量的存取器。

使用 set 存取器(参数){使用} 将注入存取器的数据赋予参数,并进行使用。

let passport = "123456";        // 创建一个修改信息的验证密码

class Employee{
    private _fullName: string;  // 私有变量 _fullName,阻止对该属性的外部修改

    // 设置存取器 fullName 来获得外部变量
    get fullName(): string{
        return this._fullName;  // get 方法返回可能被修改的那个私有变量
    }

    // 取出存取器 fullName 的参数,并进行使用
    set fullName(newName: string){      // 将存取器中保存的字符串赋予 newName 参数
        if (passwords && passwords == "123456"){
            this._fullName = newName;   // 当密码正确的时候,对_fullName 进行修改
        }
        else{
            console.log("密码错误,没有权限修改");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";        // 存取器可以看做是一个属性
if (employee.fullName){
    alert(employee.fullName);
}

存取器可以直接视为一个属性或者变量,使用 实例.存取器 = 注入的数据 向其中注入外部数据。存取器的作用是让外部环境的数据进入内部环境,这样就能获得内部环境的权限进行操作,比如对 private 修饰的私有变量赋值。

存取器要求编译器在 ES5 以上的版本。

只有 get 的存取器会被自动推断为 readonly 属性,用户在使用该属性时会被提醒。

static

类的静态成员存在于类本身而非实例上,使用 static 创建静态成员。

// 类 用于存储路径
class Router{
    static baseRoute = '/basePath'; // 静态属性
    // 方法 计算路径和
    calculateRoute(path: string){
        return Router.baseRoute + this.commonPrefix + path
    }
    // 构造函数 初始化时定义公共变量 commonPrefix
    constructor (public commonPrefix: string){}
}

let route1 = new Router('/api');    // 实例 route1,赋值给 route1.commonPrefix
let route2 = new Router('/page');   // 实例 route2,赋值给 route2.commonPrefix
console.log(route1.calculateRoute('/main')); // 最终路径 /basePath/api/main
console.log(route2.calculateRoute('/user')); // 最终路径 /bathPath/api/main 

没有参数的类声明由构造函数声明了属性变量后,在创建实例时会将实例参数赋予构造函数声明的属性变量。

abstract

抽象类是作为其他子类的基类(父类)使用的,一般不会被直接实例化。抽象类比接口包含了成员的实现细节。

abstract 关键字用于定义抽象类,或者在类的内部定义抽象方法。

abstract class Animals{
    abstract makeSound(): void;
    move(): void{
        console.log("");
    }
}

5 接口 interface

对数据的结构进行类型检查是 TypeScript 的核心原则之一,称为“鸭式辨型法”或“结构性子类型化”。接口的作用是为这些类型命名,并未代码定义契约

function printLabel(labelledObj:{label: string}){
    console.log(labelledObj.label);
}

let myObj = {size:10, label:"Size 10 Object"};
printLabel(myObj);

首先,类型检查器检查函数 printLabel 的调用。其有 1 个参数,为对象 labelledObj。该对象有名为 label 的类型为 string 的参数。

在传入对象参数的时候可以包含很多类型,大多数情况下编译器只会检查必须的属性是否存在且匹配。

接口使用 interface 接口名{} 的形式定义。使用接口重新描述上面的例子:

// 接口 LabelledValue
interface LabelledValue{
    label: string;
}
// 定义一个函数 printLabel 使用接口传入参数的类型
function printLabel(labelledObj: LabelledValue){
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

上述示例中,接口 LabelledValue 是一个对象,其中有属性变量 label,类型为字符串。接口被传入了函数 printLabel 的参数中,即只要这个函数的参数符合接口里面的定义,就可以被传入。

对象变量 myObj 里面有符合接口定义的参数 label:"Size 10 Object",即便其还有其他参数,接口在传入的时候会忽略掉不符合参数,只用有用的。

类型检查器同样也不会检查属性的顺序,只要存在即可。

可选属性

接口中的属性不全是必须存在的,这是可选属性特性。可选属性在应用“option bags”模式的时候很常用,即给函数传入的参数对象中只有部分的属性有赋值的情况。

interface SquareConfig{
    color?: string;     // ? 标注可选属性
    width?: number;
}
// 该函数的参数 config 使用了上面的接口,定义了两个可选属性
function createSquare(config: SquareConfig): {color:string; area:number}{
    let newSquare = {color: "white", area: 100};
    if (config.color){  // 使用了可选属性 color
        newSquare.color = config.color;
    }
    if (config.width){  // 使用了可选属性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};

评论