ts:从入门到放弃(三)

简介: 前文已经学习了数组、函数、联合类型和内置对象等,接下来一鼓作气,先过一遍ts的入门知识。

前文已经学习了数组、函数、联合类型和内置对象等,接下来一鼓作气,先过一遍ts的入门知识。

基础

声明文件

当使用第三方库时,我们需要引入其声明文件,这样才可以获得代码补全、接口提示等功能。存放声明语句的文件,就是声明文件,一般以.d.ts为后缀名。一些常用的第三方库有自己的声明文件,可以通过npm install的方式安装,可以现在页面搜索:https://www.typescriptlang.org/dt/search/

我们就来自己写一个声明文件,首先新建一个index.d.ts,然后在该文件中新增一些声明语句:

声明全局变量

declare var xxx,除了使用declare var,也可以使用declare let、declare const。使用declare const声明的是全局常量,是不能再修改的。声明全局变量时,声明语句中只是定义类型,而不是具体的实现。

declare const userName: string;
// declare const userName: string = 'hello'; // 不能定义具体的实现

声明全局函数

在声明全局函数的语句中,函数也是可以保持重载的。声明时也不能有具体的实现。

declare function sayHello(): any
declare function sayHello(name: string): any

声明全局类

只定义类的结构,不能定义具体的实现

declare class Animals {
  name: string;
  constructor(name: string);
  say(): void;
}

声明全局枚举

declare enum Color {
  Blue,
  White,
  Black,
  Red
}
let color = Color.Red;

Type

类型别名

在使用联合类型和第三方类型的时候,我们可以考虑给此类型定义一个变量,这样就可以方便其他地方使用。使用方法:type 类型名 = 联合类型或单一类型。

type Str = string; // 定义一个字符串类型
type Func = () => string; // 定义一个返回值为字符串类型的函数类型
type StrOrFunc = Str | Func; // 联合类型
function func(str: StrOrFunc): string {
  if (typeof str === 'string') {
    return str;
  }
  return str();
}

字符串字面量类型

这也是一种类型,用来约束变量具体的取值范围

type Color = 'Red' | 'Blue' | 'Green';
let color:Color = 'Red'; // 编译正常
// let color:Color = 'red'; // 编译错误

元组

数组一般是相同类型的集合,当然我们也可以使用泛型,这样可以让数组中存放各种类型的数据。数组对于存放数据的顺序没有要求,只要满足类型即可。

let arr: Array<number | string> = ['2', 1]; // 定义一个泛型数组,index=0的数据是字符串还是数字对于数组来说是不关心的

元组是不同类型的集合,当然也可以是同一类型。其对存放的数据有顺序要求,并且在初始化时会限制越界。

let num: [number, number] = [1, 2]
// let num: [number, number] = [1, 2, 3]; // 编译错误
let str: [string, number] = ['1', 2];
// let str: [string, number] = [1, '2']; // 编译错误

如果我们想继续在元组中添加元素,即使会越界,那么也只能添加兼容元组类型的数据才可以。

str.push('3'); // 正常
// str.push(false); // 编译错误

枚举

如果要限制取值范围,可以考虑使用枚举,例如星期、颜色、方向等。定义时使用enum。

enum Direct {
  Left,
  Right,
  Up,
  Down
}
console.log(Direct.Left, Direct[0]); // 0 Left

在ES6以前,js通过构造函数的方式来实现类的概念;在ES6中,可以直接使用class来声明一个类。ts在ES6的基础上,新增了类的一些特性。定义一个类,一般来说有属性、方法、构造函数,在通过new实例化类时,会自动调用构造函数。静态属性和静态方法属于类,只能通过类访问,而不能通过类的实例访问。

class Car {
  name: string; // 属性
  static age: number;
  constructor(name: string) {
    this.name = name;
  }; // 构造函数
  run(): void {}; // 方法
  static say(): void {}; // 静态方法
}
let car = new Car('car'); // 实例化

继承

继承采用关键词extends,子类继承父类的非私有属性和方法,子类中可通过super来访问父类的构造函数和方法。

class ACar extends Car {
  constructor(name: string) {
    super(name)
  }
  run(): void {
    super.run();
  }
}

修饰符

在类中可使用的修饰符有:public、private、protected。修饰符可用于修饰属性和方法,具体来说:

  • public表示公有的,其修饰的属性和方法在任务地方都可以被访问,受限是最小的,默认为public
  • private表示私有的,其修饰的属性和方法只能在当前类中使用,不能在其子类或者其他类中访问,受限是最大的
  • protected表示受保护的,其修饰的属性和方法只能在当前类和子类中访问,受限适中

对于构造函数,如果用private修饰,表示不可被实例化,也不能被继承。

class Car2 {
  private constructor(name: string) {}
}
// new Car2('hello'); // 编译错误

如果是用protected修饰构造函数,则表示不可被实例化,只能被继承。

class Car3 {
  protected constructor(name: string) {}
}
class Car4 extends Car3 {}
// new Car3('hello'); // 编译错误

抽象类和抽象方法

使用abstract修饰的类为抽象类,抽象类不可被实例化,只能用于继承。使用abstract修饰的方法为抽象方法,抽象方法在子类中必须被实现。抽象类可以有成员属性和成员方法,不一定有抽象方法;有抽象方法的类一定是抽象类。抽象方法只能定义,不能有具体的实现;子类继承抽象类时,必须实现抽象类的抽象方法。

abstract class Car5 {
  name: string;
  constructor(name: string) {
    this.name = name
  };
  say():void {
    console.log('hello world');
  };
  abstract run(): void;
}
class Car6 extends Car5{
  run(): void {
    console.log('world hello'); // 子类必需实现抽象方法
  }
}

"多态"

封装、继承、多态是面向对象的三大特征,这对于TypeScript来说也是符合的。ts中的类可以作为类型使用,约束定义的变量的值;还可以作为多态使用。

abstract class Animal {
  abstract eat(): void;
}
class Cat extends Animal {
  eat(): void {
    console.log('cat eat');
  }
}
class Dog extends Animal {
  eat(): void {
    console.log('dog eat');
  }
}
let a:Animal = new Dog();
a = new Cat();
a.eat();

接口

之前已经学习了类的相关知识,也知道了类的继承。但是类之间的继承只能是单继承,如果我们想要多个类中的特性,那么就需要考虑接口了。接口就是将一些共有的特性抽象出来,这一点和抽象类有点相似。定义接口使用interface,接口中定义的属性和方法不能有具体的实现,这一点不同于抽象类。

interface Alarm {
  alert(): void; // 每辆车都有“跑”这个特性,我们就将其抽象出来
}
interface Light {
  lightOn(): void;
}

实现接口

类可以使用implements实现某一个或多个接口。

class CarA implements Alarm,Light {
  lightOn(): void {}; // 实现类需要实现接口中所有的方法
  alert(): void {}; // 实现类需要实现接口中所有的方法
}

接口继承接口

接口可看成是一种特殊的类,两个接口间也是可以继承的,继承后的子接口拥有父接口中的属性和方法。

interface LightAndAlarm extends Alarm {
  lightOn(): void;
}
class CarB implements LightAndAlarm {
  lightOn(): void {}; // 实现类需要实现接口中所有的方法
  alert(): void {}; // 实现类需要实现接口中所有的方法
}

接口继承类

在几乎所有的面向对象中,接口是不能继承自类的,因为接口和类在定义上有很大的不同,但是在TypeScript中是可以的。如果难以理解,那我们就一步一步来。

  1. 首先,当我们在声明一个类时,除了会创建一个类对象,还会创建一个同名的类型

    class Person {
      name: string;
      // private sex: string = 'unkown';
      static age: number = 0
      constructor(name: string) {
        this.name = name;
      };
      getName(): string {
        return this.name;
      };
      static hello(): void{};
      // protected say(): void{};
    }
    // 此时的Person作为一个类型约束,其不包含构造函数、静态属性和方法
    function getUserName(p: Person) {
      // new p(); // 编译错误,类型Person没有构造函数
      console.log('getUserName', p.getName());
    }
    getUserName(new Person('张三'))
  2. 这个Person类型,我们可以使用一个PersonInstance接口代替

    interface PersonInstance {
      name: string,
      getName(): any
    }
    function getUserName2(p: PersonInstance) {
      // new p(); // 编译错误,类型Person没有构造函数
      console.log('getUserName2', p.getName());
    }
    getUserName2(new Person('李四'))
  3. 接口继承类,实际上等价于接口继承一个类型接口,该类型接口包含类的非构造函数、非静态属性和非静态方法,成员方法将只保留定义而没有具体的实现

    interface Student extends Person {
      addr: string;
    }
    class Stu implements Student {
      addr: string = '';
      name: string = '';
      getName(): string {
        return 'getName'
      }
    }
  4. 接口继承类,类中不应该有私有属性和私有方法,也不能有受保护的属性和方法。

接口虽然可以继承自类,但实际上是继承的类型。不建议使用接口继承类

泛型

泛型就是泛指的类型,在定义函数、类和接口的时候,先不指定类型,等到运行时再指定某一类型。使用泛型有一个好处,就是可以减少使用any这种不确定的类型。定义泛型的时候我们常用T表示某种未知的类型,使用T只是一个约定,事实上可以使用你想用的字符。

function createArray<T>(length: number, value: T): Array<T> {
  let res: T[] = [];
  for(let i = 0; i < length; i++) {
    res.push(value)
  }
  return res;
}

然后在调用时才指定T的具体类型

console.log(createArray<number>(5, 1));// [1,1,1,1,1]
console.log(createArray<string>(5, '1'));// ['1','1','1','1','1']

也可以不指定类型,ts会根据类型推论得到类型

console.log(createArray(5, '1'));// ['1','1','1','1','1']

多个泛型参数

如果一个函数有多个泛型参数,那就定义多个泛型类型。例如我们要实现一个交换两个变量的函数:

function swap<T,U>(t: [T, U]): [U,T] {
  return [t[1], t[0]];
}
console.log(swap([1,'2'])); // ['2', 1]

泛型约束

泛型的类型是不确定的,只有到运行时才会确定下来,因此在函数中访问泛型参数的属性或方法,可能会报错。泛型采用extends一个接口来约束泛型的类型,以保证在使用时不会报错。

interface Length {
  length: number
}
function getLength<T extends Length>(value: T): number {
  return value.length; // T上面可能不存在length,因此需要泛型约束
}

泛型参数彼此之间也是可以相互约束的。

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
      target[id] = (<T>source)[id];
  }
  return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });

泛型接口

interface createArrayFunc<T> {
  (length: number, value: T): Array<T>
}

注意在使用泛型接口时,需要指定泛型接口的类型。一般为any

let createArray2: createArrayFunc<string> = function<T> (length: number, value: T): Array<T> {
  let res: T[] = [];
  for(let i = 0; i < length; i++) {
    res.push(value)
  }
  return res;
}
console.log(createArray2(4, 'a')); // ['a', 'a', 'a', 'a']

泛型类

与泛型接口类似,泛型也可以用于定义类

class AddNumber<T> {
  value: T | undefined;
  add: ((x: T, y: T) => T) | undefined
}
let addNumber = new AddNumber<number>();
addNumber.value = 0;
addNumber.add = function(x: number, y: number) {
  return x + y;
}
console.log(addNumber.add(1,2));

泛型参数的默认类型

同函数的默认参数类似,泛型也可以设置默认类型,当泛型在使用时没有传入参数类型,并且无法通过类型推论得到类型的话,就会使用默认类型。

function createArray3<T>(length: number, value: T): Array<T> {
  let res: T[] = [];
  for(let i = 0; i < length; i++) {
    res.push(value)
  }
  return res;
}

声明合并

如果定义了名字相同的函数、接口,那么将会通过声明合并成为一个类型。

函数的合并

定义多个同名的函数,会合并为一个函数类型,也就是函数重载

function funcA(name: number): number;
function funcA(name: string): string;
function funcA(name: string | number) : string | number {
  return name;
};

接口的合并

属性在合并时需要保证属性的类型是相同的

interface A {
  name: string;
}
interface A {
  age: number
}
// 等价于
interface A {
  name: string;
  age: number
}

总结

本次学习内容还是挺多的,使用的场景也比较多,比如说怎么定义类型、什么时候使用类、接口应该如何定义才更合理。完整项目可参考:learning-of-typescript

相关文章
|
Java 编译器
TS的重新学习(三)
TS的重新学习(三)
52 0
|
JavaScript Java 编译器
TS的重新学习(二)
TS的重新学习(二)
60 0
|
6月前
ts笔记
ts笔记
|
6月前
|
JavaScript
使用TS的一些基础知识
使用TS的一些基础知识
70 0
|
6月前
|
JavaScript 前端开发 编译器
ts面试题总结
ts面试题总结
120 0
|
JavaScript 前端开发 开发者
ts知识点
ts知识点
54 0
|
JavaScript 安全 Python
TS笔记
TS笔记
61 0
|
JavaScript 前端开发 Java
对TS的重新学习(一)
对TS的重新学习(一)
42 0
|
存储 JavaScript Java
TS的重新学习(四)
TS的重新学习(四)
51 0
|
JavaScript 前端开发 开发工具
CocosCreator 面试题(五)TS有什么优缺点?为什么要用TS?
CocosCreator 面试题(五)TS有什么优缺点?为什么要用TS?
180 0