TS -- (4)类、类与接口、泛型
2019-10-29:
学习内容:类、类与接口、泛型
补充:
1、ts中interface与class的区别:
interface:接口只声明成员方法,不做实现。
class:类声明并实现方法。
也就是说:interface只是定义了这个接口会有什么,但是没有告诉你具体是什么。
2.extends 与 implement的区别:
(1)extends是继承父类,只要那个类不是声明为final或者那个类定义为abstract的就能继承。
(2)java中不支持多重继承,但是可以用接口来实现,这样就要用到implements,继承只能继承一个类,但implements可以实现多个接口,用逗号分开就行了
class A extends B implements C,D,E
implements就是实现的意思, 顾名思义它实现一个已经定义好的接口中的方法!
一、类(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
静态方法
使用 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)公共、私有与受保护的修饰符:
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public
、private
和 protected
。这些编译到js是一样的,也就是说js不设置这三者
-
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public
的 -
private
修饰的属性或方法是私有的,不能在声明它的类的外部访问 -
protected
修饰的属性或方法是受保护的,它和private
类似,区别是它在子类中也是允许被访问的
(1)当一个成员被标记成private时,它不能在声明它的类的外部访问(在子类和类的实现的对象中都不能访问。在子类可以通过调用使用这个属性的方法来间接来使用这个属性。):
// 实例访问类成员属于外部访问
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // 错误: 'name' 是私有的.
TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有 private
或 protected
成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private
成员,那么只有当另外一个类型中也存在这样一个 private
成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected
成员也使用这个规则。
(2)protected和private类似,但是protected
成员在派生类中仍然可以访问(在子类和类的实现的对象中允许访问),仅针对对象。
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name)
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); // 错误
我们不能在 Person
类外使用 name
,但是我们仍然可以通过 Employee
类的实例方法访问,因为 Employee
是由 Person
派生而来的。
当构造函数修饰为 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 的编译结果中,仍然会存在这个类。
(4)存取器:
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
例子: 我们把对 fullName
的直接访问改成了可以检查密码的 set
方法。
目标:我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。
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); }
对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get
不带有 set
的存取器自动被推断为 readonly
。 这在从代码生成 .d.ts
文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
使用 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方法
二、类与接口:
接口(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)接口继承类:
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
一个接口可以继承多个接口,创建出多个接口的合成接口。
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;
例子2:
class Control { private state: any; } interface SelectableControl extends Control { select(): void; } // 接口继承类 class Button extends Control implements SelectableControl { select() { } } class TextBox extends Control { select() { } } // 理解一下为什么它也算SelectableControl 的子类? 接口的定义:除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。 // 错误:“Image”类型缺少“state”属性。 class Image implements SelectableControl { select() { } } class Location { }
SelectableControl
包含了Control
的所有成员,包括私有成员state
。 因为 state
是私有成员,所以只能够是Control
的子类们才能实现SelectableControl
接口。 因为只有 Control
的子类才能够拥有一个声明于Control
的私有成员state
,这对私有成员的兼容性是必需的。
在Control
类内部,是允许通过SelectableControl
的实例来访问私有成员state
的。 实际上, SelectableControl
接口和拥有select
方法的Control
类是一样的。 Button
和TextBox
类是SelectableControl
的子类(因为它们都继承自Control
并有select
方法),但Image
和Location
类并不是这样的。
(3)混合类型:
因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。
// c这个对象就是作为函数和对象同时使用 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;
三、泛型:
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
例子: any和泛型的区别:
使用any:使用any
类型会导致这个函数可以接收任何类型的arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
function identity(arg: any): any {
return arg;
}
给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
function identity<T>(arg: T): T {
return arg;
}
注意我们没必要使用尖括号(<>
)来明确地传入类型;编译器可以查看myString
的值,然后把T
设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
例子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; }