2019-10-29:
学习内容:类、类与接口、泛型
一、类(ES6,ES7,TS类的区别):
传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class
。
TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。
(1)类的概念: What is class?
<-- 知识回顾:ES6中类的用法 -->
属性和方法:
使用 class
定义类,使用 constructor
定义构造函数。
通过 new
生成新实例的时候,会自动调用构造函数。
class Animal { constructor(name) { this.name = name; } sayHi() { return `My name is ${this.name}`; } // 方法 } let a = new Animal('Jack'); console.log(a.sayHi()); // My name is Jack
类的继承
使用 extends
关键字实现继承,子类中使用 super
关键字来调用父类的构造函数和方法。
class Cat extends Animal { constructor(name) { super(name); // 调用父类的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi() } } let c = new Cat('Tom'); // Tom console.log(c.sayHi()); // Meow, My name is Tom
存取器
使用 getter 和 setter 可以改变属性的赋值和读取行为:
class Animal { constructor(name) { this.name = name; } get name() { return 'Jack'; } set name(value) { console.log('setter: ' + value); } } let a = new Animal('Kitty'); // setter: Kitty a.name = 'Tom'; // setter: Tom console.log(a.name); // Jack // 赋值时用到set方法,取值时用到get方法
静态方法
使用 static
修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:
class Animal { static isAnimal(a) { return a instanceof Animal; } } let a = new Animal('Jack'); Animal.isAnimal(a); // true a.isAnimal(a); // TypeError: a.isAnimal is not a function
<-- ES7中类的用法 -->
ES7 中有一些关于类的提案,TypeScript 也实现了它们。
实例属性:
ES6 中实例的属性只能通过constroctor中的 this.xxx
来定义,ES7 提案中可以直接在类里面定义:
class Animal { name = 'Jack'; constructor() { // ... } } let a = new Animal(); console.log(a.name); // Jack
静态属性:
ES7 提案中,可以使用 static
定义一个静态属性:
class Animal { static num = 42; constructor() { // ... } } console.log(Animal.num); // 42
<-- TS中类的用法 -->
(1)public private 和 protected:
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public
、private
和 protected
。这些编译到js是一样的,也就是说js不设置这三者
-
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public
的 -
private
修饰的属性或方法是私有的,不能在声明它的类的外部访问 -
protected
修饰的属性或方法是受保护的,它和private
类似,区别是它在子类中也是允许被访问的
private
了
class Animal { private name; public constructor(name) { this.name = name; } } let a = new Animal('Jack'); console.log(a.name); // Jack a.name = 'Tom'; // index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'. // index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
当构造函数修饰为 private
时,该类不允许被继承或者实例化;当构造函数修饰为 protected
时,该类只允许被继承
例子2: 修饰符还可以使用在构造函数参数中,等同于类中定义该属性,使代码更简洁
class Animal { // public name: string; public constructor (public name) { this.name = name; } }
(2)readonly
只读属性关键字,只允许出现在属性声明或索引签名中。
class Animal { readonly name; public constructor(name) { this.name = name; } } let a = new Animal('Jack'); console.log(a.name); // Jack a.name = 'Tom'; // Error // index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.
注意:如果 readonly
和其他访问修饰符同时存在的话,需要写在其后面。写在构造函数中定义该属性
class Animal { // public readonly name; public constructor(public readonly name) { this.name = name; } }
(3)抽象类:
abstract
用于定义抽象类和其中的抽象方法。
-- 什么是抽象类?
A:首先,抽象类是不允许被实例化的
其次,抽象类中的抽象方法必须被子类实现
abstract class Animal { public name; public constructor(name) { this.name = name; } public abstract sayHi(); } class Cat extends Animal { public sayHi() { console.log(`Meow, My name is ${this.name}`); } } let cat = new Cat('Tom');
需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类。
二、类与接口:
接口(Interfaces)可以用于对「对象的形状(Shape)」进行描述。
对类的一部分行为进行抽象。
(1)类实现(implements)接口:
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements
关键字来实现。这个特性大大提高了面向对象的灵活性。
举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它
一个类只能继承自另一个类,但是可以实现多个接口:
interface Alarm { alert(); } interface Light { lightOn(); lightOff(); } class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); } }
(2)接口继承类:
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
(3)混合类型:
使用接口的方式来定义一个函数需要符合的形状。见《接口》部分
三、泛型:
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
例子1:
Array<any>
允许数组的每一项都为任意类型,但是没有准确的定义返回值的类型。我们预期的是,数组中每一项都应该是输入的 value
的类型。改用泛型:
function createArray<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray<string>(3, 'x'); // ['x', 'x', 'x']
我们在函数名后添加了 <T>
,其中 T
用来指代任意输入的类型,在后面的输入 value: T
和输出 Array<T>
中即可使用了。
例子2: 一次定义多个泛型参数
function swap<T, U>(tuple: [T, U]): [U, T] { return [tuple[1], tuple[0]]; } swap([7, 'seven']); // ['seven', 7]
例子3: 泛型约束
由于泛型事先不知道类型,当我想这个函数里使用length方法时,会报错。这时需要对泛型进行约束,满足可以使用length方法:
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; }
使用了 extends
约束了泛型 T
必须符合接口 Lengthwise
的形状,也就是必须包含 length
属性。
例子4: 多个类型参数之间也可以互相约束
extends:继承
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 });
上例中,我们使用了两个类型参数,其中要求 T
继承 U
,这样就保证了 U
上不会出现 T
中不存在的字段。
例子5: 泛型接口
使用含有泛型的接口来定义函数的形状:
interface CreateArrayFunc { <T>(length: number, value: T): Array<T>; } let createArray: CreateArrayFunc; createArray = function<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray(3, 'x'); // ['x', 'x', 'x']
进一步,我们可以把泛型参数提前到接口名上:
interface CreateArrayFunc<T> { (length: number, value: T): Array<T>; } let createArray: CreateArrayFunc<any>; // 此时在使用泛型接口的时候,需要定义泛型的类型 createArray = function<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; } createArray(3, 'x'); // ['x', 'x', 'x']
例子6: 泛型类
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; };
例子7: 泛型参数的默认类型
当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用
function createArray<T = string>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; } return result; }