TypeScript完全支持ES2015中引入的class关键字。
与其他JavaScript语言特性一样,TypeScript添加了类型注释和其他语法,允许您表达类和其他类型之间的关系。
类的成员
下面是一个空的类:
class Point {}
这个类现在毫无用处,现在往这个类里面加点成员
字段
在类上声明字段,编程了可读写的public 属性
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
属性初始化: 类实例化的时候,会初始赋值
class Point {
x = 0;
y = 0;
}
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);
与const、let和var一样,类属性的初始值设定项将用于推断其类型:
const pt = new Point();
pt.x = "0";
//Type 'string' is not assignable to type 'number'.
–strictPropertyInitialization
StricPropertyInitialization设置控制类字段是否需要在构造函数中初始化。
正常构建需要初始化:
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
不初始化使用!断言,也不会报错:
class OKGreeter {
// Not initialized, but no error
name!: string;
}
readonly
只读属性,不多介绍,只能读取不能赋值。
注意:构造函数内可以赋值
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
//Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok";
//Cannot assign to 'name' because it is a read-only property.
构造函数
类构造函数与函数非常相似。可以添加带有类型注释、默认值和重载的参数:
class Point {
x: number;
y: number;
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
super 调用
如果有基类,必须在构造函数中调用super
,且在使用this
之前
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
//'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
Methods
在类中函数属性统称为方法
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
let x: number = 0;
class C {
x: string = "hello";
m() {
// This is trying to modify 'x' from line 1, not the class property
this.x = "world";
x = 4
// Type 'string' is not assignable to type 'number'.
}
}
Getters / Setters
类也可以有访问器:
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
注意,没有额外逻辑的字段支持的get/set对在JavaScript中很少有用。如果在get/set操作期间不需要添加额外的逻辑,那么公开公共字段就可以了。
TypeScript对访问器有一些特殊的推理规则:
- 只有get 没有set ,这个属性自动变成raedonly
- 如果set 的参数没有明确指出,那么按照get 类型推断
- Getters and setters必须具有相同的成员可见性(public,private)
class Thing {
_size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// Don't allow NaN, Infinity, etc
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
索引签名
类可以声明索引签名;它们的工作方式与其他对象类型的索引签名相同:
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
因为索引签名类型还需要捕获方法的类型,所以很难有效地使用这些类型。一般来说,最好将索引数据存储在另一个地方,而不是类实例本身。
类的实现和继承
与其他具有面向对象特性的语言一样,JavaScript中的类可以从基类继承。
implements
一个类可以准守一个或者多个接口去实现它:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
//Class 'Ball' incorrectly implements interface 'Pingable'.
//Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
pong() {
console.log("pong!");
}
}
类同时可以准守多个接口取实现,例如 class C implements A, B {
可选值不会要求,实现类去实现:
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;
Property 'y' does not exist on type 'C'.
extends (继承)
类可以从基类扩展。派生类具有其基类的所有属性和方法,还定义其他成员。
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);
方法重写
子类继承父类之后,可以重写属性和方法
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
面向对象的特征父类可以指向子类(变量多态):
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
初始化顺序
在某些情况下,JavaScript类的初始化顺序可能令人惊讶。让我们来考虑这个代码:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// Prints "base", not "derived"
const d = new Derived();
这里发生了什么?
JavaScript定义的类初始化顺序是:
- 基类字段已初始化
- 基类构造函数运行
- 派生类字段已初始化
- 派生类构造函数运行
这意味着基类构造函数在自己的构造函数中看到了自己的name值,因为派生类字段初始化尚未运行。
类成员访问权限
您可以使用TypeScript来控制某些方法或属性是否对类之外的代码可见。
public
类成员的默认权限就是public,public是任何地方都可以访问:
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
因为public已经是默认的可见性修饰符,所以您不需要在类成员上编写它,但是出于样式/可读性的原因,您可以选择这样做。
protected
protected只有类和子类可以访问
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
//Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
暴露保护成员
派生类需要准守基类的约定,同时派生类也可以更改重写的成员访问权限。
class Base {
protected m = 10;
protected n = 11;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
console.log(d.n); // 受保护
请注意,Derived已经能够自由地读写m,因此这并不能有意义地改变这种情况的“安全性”。这里要注意的主要事情是,在派生类中,如果不是有意的,我们需要小心地重复受保护的修饰符。
跨层次保护访问
对于通过基类引用访问受保护成员是否合法,不同的OOP语言存在分歧:
class Base {
protected x: number = 1;
}
class Derived1 extends Base {
protected x: number = 5;
}
class Derived2 extends Base {
f1(other: Derived2) {
other.x = 10;
}
f2(other: Base) {
other.x = 10;
//Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.
}
}
例如,Java认为这是合法的。另一方面,C++和C++选择了这个代码应该是非法的。
TypeScript和C++,C#一样。 C# 有解释为什么要这么做:
Why Can’t I Access A Protected Member From A Derived Class?
private
private 不允许外部访问
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
//Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
Property 'x' is private and only accessible within class 'Base'.
}
}
因为private 不允许访问,所以子类继承无法重写
class Base {
private x = 0;
}
class Derived extends Base {
Class 'Derived' incorrectly extends base class 'Base'.
//Property 'x' is private in type 'Base' but not in type 'Derived'.
x = 1;
}
跨实例访问private 成员
对于同一类的不同实例是否可以访问彼此的私有成员,不同的OOP语言存在分歧。虽然java、C++、C++、SWIFT和PHP这样的语言允许这样做,但Ruby却没有。
TypeScript允许跨实例私有访问:
class A {
private x = 10;
public sameAs(other: A) {
// No error
return other.x === this.x;
}
}
注意事项
与TypeScript类型系统的其他方面一样,只有在类型检查期间才强制执行私有和受保护的。这意味着JavaScript运行时构造(如in或简单属性查找)仍然可以访问私有或受保护的成员:
class MySafe {
private secretKey = 12345;
}
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);
如果需要保护类中的值不受恶意参与者的攻击,则应该使用提供硬运行时隐私的机制,例如闭包、弱映射或私有字段。
静态成员
类可能有静态成员。这些成员与类的特定实例没有关联。可以通过类构造函数对象本身访问它们:
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
静态成员还可以使用public、protected和private可见性修饰符:
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
//Property 'x' is private and only accessible within class 'MyClass'.
静态成员也会被继承:
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
}
let myGreeting = Derived.getGreeting();
特殊静态名称
name
,length
,call
等原型属性(prototype)不能当做静态属性
class S {
static name = "S!";
Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
为什么没有静态类?
TypeScript(和JavaScript)不像C#和Java那样有一个称为静态类的构造。
这些构造之所以存在,是因为这些语言强制所有数据和函数都在一个类中;因为TypeScript中不存在这种限制,所以不需要它们。在JavaScript/TypeScript中,只有一个实例的类通常表示为普通对象。
Java静态类参考:
public class MainInStaticClass {
static class Main{
static void main() {
//将主方法写到静态内部类中,从而不必为每个源文件都这种一个类似的主方法
new MainInStaticClass().print();
}
}
public static void main(String[] args){
new MainInStaticClass().print();
}
public void print(){
System.out.println("main in static inner class");
}
}
public class TestMain {
public static void main(String[] args) {
// TODO Auto-generated method stub
// new MainInStaticClass().print();
MainInStaticClass.Main.main();
new MainInStaticClass.Main();
}
}
例如,我们不需要TypeScript中的“static class”语法,因为常规对象(甚至顶级函数)也可以完成这项工作:
// Unnecessary "static" class
class MyStaticClass {
static doSomething() {}
}
// Preferred (alternative 1)
function doSomething() {}
// Preferred (alternative 2)
const MyHelperObject = {
dosomething() {},
};
泛型类
类,很像接口,可以是泛型的。用new实例化泛型类时,其类型参数的推断方式与函数调用中的相同:
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b = new Box("hello!");
静态属性中使用泛型
下面代码是不合法的:
class Box<Type> {
static defaultValue: Type;
//Static members cannot reference class type parameters.
}
ts是类型擦除的,变量在运行时中,只有一个类型。成员类型的确定在实例中推断出来,静态属性的分配在于实例初始化之前,无法确定类型。 (猜测,请大神推翻告知)
this
在类中的运行时
重要的是要记住,TypeScript不会改变JavaScript的运行时行为,JavaScript以具有一些特殊的运行时行为而闻名。
JavaScript对此的处理确实不同寻常:
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// Prints "obj", not "MyClass"
console.log(obj.getName());
长话短说,默认情况下,函数中的值取决于函数的调用方式。在这个例子中,因为函数是通过obj引用调用的,所以它的值是obj而不是类实例。
这很少是你想发生的事!TypeScript提供了一些减轻或防止此类错误的方法。
箭头函数
如果您有一个函数经常被调用,而这种调用方式会丢失其上下文,那么使用arrow函数属性而不是方法定义是有意义的:
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());
这有一些取舍:
-
this
这个值在TS中没有类型检查,在运行时才能确定 -
这将使用更多的内存,因为每个类实例都有自己的副本,每个函数都是这样定义的
-
不能在派生类中使用super.getName,因为原型链中没有从中获取基类方法的条目
this
参数
在方法或函数定义中,名为this的初始参数在TypeScript中有特殊意义。这些参数在编译过程中被删除:
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
/* ... */
}
// JavaScript output
function fn(x) {
/* ... */
}
TypeScript检查使用this参数调用函数是否在正确的上下文中完成。我们不需要使用arrow函数,而是可以向方法定义中添加this参数,以静态地强制正确调用方法:
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
// Error, would crash
const g = c.getName;
console.log(g());
此方法采用与箭头函数方法相反的折衷方法:
- JavaScript调用程序可能仍然错误地使用class方法而没有意识到它
- 每个类定义只分配一个函数,而不是每个类实例分配一个函数
- 基本方法定义仍然可以通过super调用。
this
类型
在类中,称为this的特殊类型动态地引用当前类的类型。让我们看看这有多有用:
class Box {
contents: string = "";
set(value: string) {
//(method) Box.set(value: string): this
this.contents = value;
return this;
}
}
这里,TypeScript推断set的返回类型是this,而不是Box。现在让我们创建Box的子类:
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox();
const b = a.set("hello");
const b: ClearableBox
你也可以在参数标注中使用this类型
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
这与编写other:Box不同-如果您有一个派生类,那么它的sameAs方法现在只接受该派生类的其他实例:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
class DerivedBox extends Box {
otherContent: string = "?";
}
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
//Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
// Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
this
基础类型保护
对于类和接口中的方法,可以在返回位置使用这个is类型。当与类型缩小(例如if语句)混合时,目标对象的类型将缩小到指定的类型。
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
interface Networked {
host: string;
}
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
if (fso.isFile()) {
fso.content;
const fso: FileRep
} else if (fso.isDirectory()) {
fso.children;
const fso: Directory
} else if (fso.isNetworked()) {
fso.host;
const fso: Networked & FileSystemObject
}
基于this的类型保护的一个常见用例是允许对特定字段进行延迟验证。例如,当hasValue被验证为true时,这种情况会从保存在框中的值中删除一个未定义的值:
class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box();
box.value = "Gameboy";
box.value;
//(property) Box<unknown>.value?: unknown
if (box.hasValue()) {
box.value;
//(property) value: unknown
}
参数属性
TypeScript提供了特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。这些称为参数属性,通过在构造函数参数前面加上可见性修饰符public、private、protected或readonly来创建。结果字段获取这些修饰符:
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
console.log(a.x);
(property) Params.x: number
console.log(a.z);
类表达式
类表达式与类声明非常相似。唯一真正的区别是类表达式不需要名称,尽管我们可以通过它们最终绑定到的任何标识符来引用它们:
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
const m = new someClass("Hello, world");
抽象类和成员
TypeScript中的类、方法和字段可以是抽象的。
抽象方法或抽象字段是尚未提供实现的方法或字段。这些成员必须存在于不能直接实例化的抽象类中。
抽象类的作用是作为实现所有抽象成员的子类的基类。当一个类没有任何抽象成员时,就说它是具体的。
让我们看一个例子
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
const b = new Base();
//Cannot create an instance of an abstract class.
我们不能用new实例化Base,因为它是抽象的。相反,我们需要生成一个派生类并实现抽象成员:
class Derived extends Base {
getName() {
return "world";
}
}
const d = new Derived();
d.printName();
请注意,如果忘记实现基类的抽象成员,则会出现错误:
class Derived extends Base {
//Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
// forgot to do anything
}
抽象构造签名
有时您希望接受某个类构造函数,该构造函数生成从某个抽象类派生的类的实例。
例如,您可能希望编写以下代码:
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
function greet(ctor: typeof Base) {
const instance = new ctor();
instance.printName();
}
TypeScript正确地告诉您,您正试图实例化一个抽象类。毕竟,根据greet的定义,编写这段代码是完全合法的,它最终会构造一个抽象类:
// Bad!
greet(Base);
作为替代,您需要编写一个函数来接受具有构造签名的内容:
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base);
//Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
//Cannot assign an abstract constructor type to a non-abstract constructor type.
类之间的关系
在大多数情况下,TypeScript中的类与其他类型在结构上进行比较。
例如,这两个类可以互相替换,因为它们是相同的:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();
相同道理,如果类的结构是包含关系,超集可以转向子集类型:
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();
这听起来很简单,但也有一些情况似乎比其他情况更奇怪。
空类没有成员。在结构类型系统中,没有成员的类型通常是其他类型的超类型。所以如果你写了一个空类(不要!),任何东西都可以代替它:
class Empty {}
function fn(x: Empty) {
// can't do anything with 'x', so I won't
}
// All OK!
fn(window);
fn({});
fn(fn);