欢迎大家今天我们将学习TypeScript的类、接口、继承和其他面向对象编程(OOP)的概念。
虽然TypeScript和JavaScript可能不是你在谈到面向对象编程时首先想到的语言,但你会惊讶地发现,它对以面向对象的方式构建复杂、强大的组件有多么的支持。最新版本的TypeScript和JavaScript都对其语法进行了修改,使OOP更容易实现。
应大众要求,我们今天将特别关注TypeScript中的OOP概念。我们将从什么是类的基本概述开始,然后转向封装、类的继承、接口等概念!到最后,你应该有机会了解什么是OOP。
到最后,你应该对如何在TypeScript中实现各种OOP概念有一个基本的了解。
什么是类?
在ECMASCript 6 (ES6)发布之前,JavaScript主要是一种功能性编程语言,其继承性是基于原型的。当支持类的语法在2015年被引入时,TypeScript迅速适应,以利用封装和抽象等面向对象的技术。
TypeScript和JavaScript的类是由以下部分组成的:
- 构造器
- 属性
- 方法
类为在JavaScript中创建可重用的组件提供了一个基本结构。它们是面向对象编程(OOP)语言中的一个抽象概念,用于定义对象,并将属性和功能传递给其他类和对象。
对象是通过封装数据和对该数据进行处理的方法而形成的数据结构。因此,你可以认为JavaScript类不是真正意义上的对象,而是更类似于对象的蓝图。
注意:TypeScript完全支持2015年ECMAScript 6(ES6)发布时引入的类语法。
声明一个TypeScript类
类的声明是用来定义一个类的,使用class 关键字和类的名称,以及大括号'{}':
class Fruit {
// this is an empty class
}
类的关键字
类表达式是另一种定义类的方式,但它们可以是命名的或未命名的:
let Fruit = class {
// this class is unnamed
}
let Fruit = class edible_fruits {
// this class is named
}
你可以使用name 关键字访问命名的类表达式:
let Fruit = class edible_fruits {
// this class is named
}
console.log(Fruit.name);
// returns "edible_fruits"
用类进行封装
面向对象编程的一个关键概念是封装。封装需要通过将数据和方法封闭在一个单元中来限制对一个对象的状态的访问。限制对某些数据或组件的访问对于防止外部代码调用特定类中的私有方法非常有用。
TypeScript通过将数据和其相关的方法封装在一个类中来促进封装。
例如,如果你有一个 "类Student",有两个数据元素和一个方法,你可以用下面的语法将它们封装起来:
class Student {
name: string=''
roll: number = 0
getRoll(): number{
return this.roll
}
}
用类进行封装
类的对象
在JavaScript中,对象是可以容纳一个值的变量。
变量fruit_one已经被分配了一个字符串值 "Apple"
对象也是变量,但不是一个单一的值,而是可以分配多个值,写成键:值对。这些键:值对的集合构成了它们所属对象的不同属性:
const fruits = {
name: "Apple",
color: "red",
variety: "Fuji"
};
一个键:值对的集合
要访问类成员(数据或方法),我们必须创建其对象的一个实例。
在下面的例子中,有两个类,Student ,和School 。使用类School 的对象,我们可以尝试用uni.roll 来访问 "学生 "类的一个数据成员。
这样做会返回一个警告:所需的数据不存在于类School 中。相反,我们可以通过注释uni.roll 和取消注释student.roll 来修复这段代码。
自己试试吧!
// Student class
class Student {
name: string = ''
roll: number = 0
getRoll(): number {
return this.roll
}
}
// School class
class School {
name: string =''
location: string = ''
}
// Create objects of each class.
const student = new Student()
const uni = new School()
// Returns Warning: Property 'rule' does not exist on type 'School'
uni.roll = 5;
// The following code is correct.
// Comment the above code and uncomment the following
// student.roll;
类对象
类的构造函数
一个类可以有一个被称为 "构造函数 "的特殊方法或函数,当我们创建一个该类的对象时,它会被自动调用。构造函数可以用来初始化类的数据或在其对象实例化时执行其他动作。然而,一个类没有必要包含一个构造函数。
下面的例子演示了一个带有公共构造函数的 "汽车 "类,该构造函数在对象实例化时被自动调用。同时,该构造函数创建了一个类的实例:
class Car {
// define properties
makeAndModel: string;
year: number = 0
// constructor of Car
public constructor() {
this.makeAndModel = 'Toyota Corolla'
this.year = 2015
}
}
// create an object of the class
const car = new Car()
console.log("\n\n Car make and model : " + car.makeAndModel)
console.log("\n\n Year this " + car.makeAndModel + " was manufactured: " + car.year)
类的构造函数
类的继承
TypeScript支持的另一个面向对象编程的关键概念是继承。继承允许我们从另一个(父类或超级)类中派生出一个类,从而扩展父类的功能。新创建的类被称为子类或次类。
子类继承了其父类的所有属性和方法,但不继承任何私有数据成员或构造函数。
你可以使用extends关键字从父类派生出子类。
语法
class child_class extends parent_class
有三种类型的类继承。
- 单一的:当一个子类从一个父类中继承了类的属性和方法。
- 多重:当一个子类从一个以上的父类中继承类的属性和方法。TypeScript不支持多重继承。
- 多层次:当一个类从另一个子类(如孙子或曾孙子)继承类的属性和方法。
访问修改器
一个访问修饰符限制了类数据和方法的可见性。
TypeScript 提供了三个关键的访问修改器:
publicprivateprotected
公共修改器
TypeScript中的所有类成员默认是公开的,但也可以使用public 关键字使其公开。当没有指定修改器时,这些成员可以在任何地方不受限制地被访问。
一个类的公共方法或数据可以被该类本身或任何其他(派生或非派生)类访问:
class Fruit {
public fruit_plu: number = 4129;
fruit_name: string
}
let apple = new Fruit();
apple.fruit_plu = 4129;
apple.fruit_name = "Fuji Apple";
console.log ("\n\n The PLU code for a " + apple.fruit_name +
" is " + apple.fruit_plu)
公共值和缺省值
在上面的例子中,'fruit_plu'和'fruit_name'两个关键字都被认为是公共类成员,尽管'fruit_plu'是唯一一个前面有访问修饰符的成员。记住,当你没有指定一个类成员的范围时,它可以从类的外部被访问。
私有修改器
一个类的private 方法或数据成员不能从其类的外部访问。你可以使用private 关键字来设置它的范围。当一个私有成员的范围被限制在它的类中时,只有同一类中的其他方法可以访问它:
class Fruit {
private fruit_plu: number = 4129;
fruit_name: string
}
let apple = new Fruit();
// running this console.log should return an error because it is inaccessible
console.log("\n\n The PLU code for this fruit is " + apple.fruit_plu)
在上面的例子中,试图访问fruit_plu 成员将返回一个编译器错误,因为它的作用域已经被设置为private 。你仍然能够访问fruit_name 成员,因为它的作用域默认为public 。
受保护的修改器
受保护的访问修饰符的工作原理与私有访问修饰符类似,但有一个主要的例外。一个类的受保护方法或数据成员可以被该类本身访问,也可以被任何从该类派生的子类访问。使用TypeScript,你可以在同一个类中使用构造器关键字来声明一个公共属性和一个受保护的属性。这些是参数属性,可以让你同时声明一个构造函数参数和一个类成员。
注意:TypeScript类型系统的一个注意事项是,私有和保护范围只在运行时类型检查中强制执行。
带有访问修改器的继承
现在你对继承和访问修饰符有了基本的了解,我们可以演示这两个概念如何被用来修改对一个类的某些成员的访问,而不是其他成员。
在下面的例子中,你会看到所有类成员的范围都可以随时访问。你应该能够执行这个程序而不返回任何错误。
然而,我们也包括了改变不同类成员范围的代码,作为注释。试着取消对代码不同部分的注释,看看不同的访问修改器是如何执行访问的!你还可以看到TypeScript返回了什么样的错误:
//
// Base class
class Car {
protected makeAndModel: string;
private year: number = 0
// constructor of Car
public constructor() {
}
//getter of make and model
public getMakeAndModel (): string {
return this.makeAndModel;
}
//setter of make and model
public setMakeAndModel(make: string) {
this.makeAndModel = make;
}
//getter of manufacture year
public getYear (): number {
return this.year;
}
//setter of manufacture year
public setYear(year: number) {
this.year = year;
}
}
// Derived class
class Tesla extends Car {
//public data member, for the car location.
public location: string
//constructor of Tesla class
constructor() {
super();
super.makeAndModel = 'Tesla X'
}
/** Uncommenting the following will give error, because */
/** year is defined private (instead of public or protected) */
/*
getYear (): number {
return this.year;
}
*/
}
// create objects of each class.
const tesla = new Tesla()
const car = new Car()
//setYear is public hence can be accessed from derived class
tesla.setYear(2022)
tesla.location = 'New York'
console.log("\n\nMake and model of car: " + tesla.getMakeAndModel())
console.log("\n\nLocation: "+ tesla.location);
// Uncommenting the following code should give error because
//location is first defined in the derived class
//car.location = "San Francisco"
// Uncommenting the following code should give error because
//year is a private variable.
//tesla.year = 2001;
继承和访问修改器
接口和类型别名
类型别名
在TypeScript中,有两种方法可以为你的数据定义类型:类型别名和接口。
类型别名是使用type 关键字声明的,并且用于明确地用一个名字(别名)来注释一个类型。类型别名可以用来表示原始数据类型,如字符串或布尔值,但它们也可以用来表示object 类型,tuples ,以及更多!
与接口不同,类型别名不能被多次声明,并且在创建后不能被改变。
注意:有趣的事实!TypeScript编译器在一个叫做转译的过程中把TypeScript完全转换为JavaScript代码。这是为了确保任何和所有的JavaScript程序都与TypeScript编程语言完全兼容。
接口
接口是另一种定义对象的数据结构的方式。它是一个抽象的类型,告诉TypeScript编译器一个给定的对象可以有哪些属性。在TypeScript中,接口提供了一个对象声明属性、方法和事件的语法,但要由派生类来定义这些成员。与类型别名不同,你可以自由地在现有的接口上添加新的字段。
你可以使用interface 关键字来声明一个接口。在这个例子中,我们可以为一个苹果定义一个接口,其属性 "品种 "和 "颜色 "都是字符串:
interface Apple {
variety: string;
color: string
}
为了实现Apple 接口,我们只需给这些属性赋值:
interface Apple {
variety: string;
color: string
}
let newFruit: Apple = {
variety: "Opal",
color: "yellow",
};
console.log("\n\n We have a new type of apple in stock! It's " + newFruit.color + " and of the "
+ newFruit.variety + " variety.")
接口可以被认为是派生类必须遵循的数据结构的蓝图。在下面的例子中,我们将看看如何使用implements 关键字用一个类来实现一个接口:
// Interface as a blueprint
interface IStudent {
roll: number
stdName: string
// ? implies the data item is a derived class
// may or may contain
stdDOB?: string
// Method declaration (no body) in the interface
getFathersName():string;
}
// A class implementing interface
class BSStudent implements IStudent {
roll: number
stdName: string
fathersName: string
getFathersName():string {
return this.fathersName
}
}
// Create an object of the class.
const std = new BSStudent ()
std.fathersName = 'Bob'
std.roll = 33
std.stdName = "John"
console.log("\n\n Student's name = "+std.stdName)
console.log("\n\n Father's name = "+std.fathersName)
console.log("\n\n Roll number = "+std.roll)
实现接口的类
接口和继承
就像类一样,我们可以通过继承从其他接口派生出一个接口。与类不同的是,一个接口可以从多个接口中扩展出来。
在下面的例子中,我们演示了接口ITeacherAndStudent 如何从IStudent 和ITeacher 接口中派生出来:
// Interface for students
interface IStudent {
roll: number
stdName: string
getFathersName():string;
}
// Interface for teachers
interface ITeacher {
id: number
name: string
}
// An interface derived from both students and teachers.
interface ITeacherAndStudent extends IStudent, ITeacher {
age: number
}
具有多重继承性的接口