TypeScript 是面向对象类的编程。
简而言之,就是程序中所有的操作,都是通过对象来完成的。计算机程序本质是对现实事物的抽象。一个人,一辆车,一只狗,这些都是对象,而这些对象发出的动作就是对象的方法,某些特征就是对象的属性。比如一个人,他的身高,长相,姓名,年龄等这些在对象中都是他的属性,而他发出的动作,走,跑,说话等这些在对象中都是他的方法。
类其实就是对象的模型,通过类可以来创建对象。对象的属性和方法,要在类中明示的表示出来。
下面定义一个简单的类:
class Person {
name: string = 'xiaobai';
static age: number = 18;
say() {
console.log('hello');
}
static run() {
console.log('run');
}
}
let person = new Person()
上面定义了一个简单的类,通过 new 关键字,实例化成对象。name 是实例对象。
static 指的是类的静态属性,用类名访问,无需创建对象,比如 Person.age
。但是实例对象访问不到。静态方法与静态属性使用方式相同。
为什么需要它呢?
当我们在创建类的时候,通常都不可能只实例化一个对象,通常会实例化多个对象。
// 这样创建出来的实例对象都是相同的属性值和方法
class Person {
name: string = 'xiaobai';
say() {
console.log('hello');
}
}
let person = new Person()
constructor 这个构造函数在每次 new 实例化对象,立即执行。
class Person {
name: string;
constructor(name: string) {
this.name = name;
console.log(this); // 它指向的是per这个实例化的对象
}
}
let per1 = new Person('xiaobai1');
let per2 = new Person('xiaobai2');
let per3 = new Person('xiaobai3');
在每次实例化对象完成之后,在 constructor 函数里面会生成一个 this 这个 this 指向的就是实例化产生的对象。
通过this.name
添加属性,就相当于往实例化的对象里面添加属性。这样我们再把实例化时传的参数带进去,这样就会创建出不同属性值的对象了。方法的话,就是哪个实例对象调用方法,方法里面的this就指向谁。
class Dog {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
say() {
console.log('汪汪');
}
}
class Cat {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
say() {
console.log('喵喵');
}
}
我们分别创建一个 Dog 和 Cat 类,我们发现它们都有相同的属性和方法,这样的代码我们重复写了两遍,如果我们再去创建一个牛,蛇,猪等更多的类,这样就开始做重复的工作了。
因此我们使用继承来简化它:
class Animal {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
say() {
console.log('叫');
}
}
class Dog extends Animal {
run() {
console.log(`${this.name}在跑`);
}
// 重写父类方法
say() {
console.log('汪汪')
}
}
class Cat extends Animal {
// 重写父类方法
say() {
console.log('喵喵')
}
}
let dog = new Dog('小白', 3);
console.log(dog); // {name: "小白", age: 3}
dog.say(); // 汪汪
let cat = new Cat('小黑', 2);
console.log(cat); // {name: "小黑", age: 2}
dog.say(); // 喵喵
从上面代码可以看出,我们使用了 extends
关键字来继承 Animal
这个类,从而使 Dog
, Cat
这两个类,都拥有了 Animal
类上的方法。这大大简化了我们的代码量,而且不用在去做重复的工作。
继承的子类,不仅有拥有父类的方法,还能有自己的方法,比如,子类 Dog 中的 run 方法。
如果继承的子类中添加了与父类相同的方法,子类的方法将覆盖父类的方法。这个子类方法覆盖掉父类的方法的形式,称为方法的重写
。
先写一段代码:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
say() {
console.log('叫');
}
}
class Dog extends Animal {
age: number;
constructor(name: string, age: number) {
super(name); // 调用父类的构造函数
this.age = age;
}
say() {
super.say() // 调用父类的方法
}
}
let dog = new Dog('小白', 3);
console.log(dog);
在子类的方法中使用 super 表示当前类的父类。
在子类的添加新的属性,如果在子类中写了新的构造函数,子类构造函数必须对父类构造函数进行调用。
有的时候,我们并不需要父类来实例化创建对象,父类是一个超类,只用来存放公用的属性和方法,用来继承使用。这个时候就用到了抽象类,抽象类只能用来继承,而不能用来实例化创建对象。
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
abstract say(): void;
}
class Dog extends Animal {
say() {
console.log('叫');
}
}
在 abstract 抽象类中,才能定义抽象方法,子类必须对抽象方法进行重写。抽象方法使用 abstract 开头,没有方法体。抽象类中,可以不写抽象方法。
类的修饰符 有三个 public
private
protected
公共属性,修饰的属性可以在任意位置进行访问(修改),包括继承的子类
属性前不设置修饰符时 默认为 public
class Person {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
let person = new Person('孙悟空', 18);
console.log(person); // 输出 {name: "孙悟空", age: 18}
person.name = '猪八戒';
person.age = 20;
console.log(person); // 此时输出 {name: "猪八戒", age: 20}
此时可以看出使用 public 修饰属性:
属性可以在实例化对象中任意被修改,这样使对象中的数据变得非常不安全。
私有属性,只能在类内部进行访问(修改)
此时我们将 Person 类中的,name属性改成 私有属性,结果如下:
这样属性被私有化,外部就不能 以对象点属性的方法去访问了,那么我们可以通过 向类中添加方法去访问(修改)里面的属性,如下:
class Person {
private name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
getName () {
return this.name;
}
setName (value: string) {
this.name = value;
}
}
let person = new Person('孙悟空', 18);
person.setName('猪八戒');
console.log(person.getName()); // 打印出 '猪八戒'
可以通过设置 setName 方法,在对象内部改变去修改 name 这个私有属性。
当然也可以直接直接使用 TS 提供的 getter , setter 方法,代码如下:
class Person {
private name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
get _name() {
return this.name;
}
set _name(value: string) {
this.name = value;
}
}
let person = new Person('孙悟空', 18);
person._name = '猪八戒';
console.log(person._name); // '猪八戒'
用private
可以做属性的封装,不直接去访问它的属性,通过 getter,setter 方法去访问。
受保护的属性,只能在当前类和子类中使用(修改),不能在实例化的对象中去访问。
接口用来定义一个类结构,用来定义一个类中应该哪些属性和方法,同时接口也可以当做类型声明去使用。
举个例子:
interface User {
name: string;
age: number;
}
interface User {
gender: string;
}
const USER1: User = {
name: '小明',
age: 18,
gender: '男'
}
console.log(USER1);
我们定义一个 user 对象的接口,声明的第一个接口中包括,name 和 number 属性,声明第二个接口包含 gender 属性,我们可以看到两个接口可以重复声明,并且两个接口中的属性检测会合并,我们再去使用这个接口时,就必须有 name age gender。
接口可以在定义类的时候去限制类的解构
接口定义类的特点:
定义类时,可以使类去实现一个接口,实现接口就是使类满足接口的要求
/**
* 接口实现类
*/
interface user {
name: string;
say(): void;
}
class user1 implements user {
name: string;
constructor(name: string) {
this.name = name;
}
say() {
console.log('hello');
}
}
由此可以看出,接口很像抽象类,但是也有很显著的区别:
本质上两者都是对类定义一个标准,使实现接口,或者继承抽象方法的类,必须去符合这个标准。
在定义函数或者类时,如果遇到类型不明确的就可以使用泛型。
当我们不明确函数传参是什么类型的时候,往往会使用 any 来表示任意类型,但是使用 any ,会关闭 TS 的类型检测,因此这里要用泛型来代替。
举个简单的代码案例:
// 使用any,关闭类型检测
function fn(a: any): any {
return a;
}
// 使用泛型,自动判断类型
function fn<T> (a: T): T {
return a;
}
fn<string>('hello'); // 也可以在调用的时候,自己指定类型
类使用泛型:
class User<T> {
name: T;
constructor(name: T) {
this.name = name;
}
}
let user = new User<string>('zhangsan');
any ,会关闭 TS 的类型检测,因此这里要用泛型来代替。
举个简单的代码案例:
// 使用any,关闭类型检测
function fn(a: any): any {
return a;
}
// 使用泛型,自动判断类型
function fn<T> (a: T): T {
return a;
}
fn<string>('hello'); // 也可以在调用的时候,自己指定类型
类使用泛型:
class User<T> {
name: T;
constructor(name: T) {
this.name = name;
}
}
let user = new User<string>('zhangsan');