TypeScript入门之类

434 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情

简介

传统的面向对象语言基本都是基于类的,JavaScript 基于原型的方式让开发者多了很多理解成本,在 ES6 之后,JavaScript 拥有了 class 关键字,虽然本质依然是构造函数,但是开发者已经可以比较舒服地使用 class 了。

TypeScript中,对类有了更一步的提升,比如添加了访问修饰符public、private、protected,有了抽象类的概念等等。

定义类

我们使用关键字class定义类。

class People {
  // 属性
  name: string
  
  constructor(name: string) {
    this.name = name
  }
  
  // 方法
  say() {
    console.log(this.name)
  }
}

在类中我们可以定义属性和方法,并且可以在构造函数中给属性赋初始值。

上面的例子我们首先定义了name属性,然后再在构造函数中给该属性赋值。如果不想这么麻烦的话,我们还可以简写。

直接定义在构造函数中就可以了。

class People {
  constructor(public name: string) {
    this.name = name
  }
  
  say() {
    console.log(this.name)
  }
}

我们知道,class其实就是function的语法糖。

console.log(typeof People) //function

那我们使用class定义类和之前的方法定义构造函数有什么区别呢?

let Animal = function (type) {
  // 定义属性和方法
  this.type = type;
  this.say = function () {
    console.log("say");
  };
};

// 在原型上定义属性和方法
Animal.prototype.info = '动物'
Animal.prototype.walk = function () {
  console.log("walk");
};

const a1 = new Animal("dog");
console.log(a1);

我们会发现,在构造函数里面定义的方法和属性都会挂载在实例上,如果需要挂载在原型上需要我们使用prototype显示定义。

image.png

我们再来看看class

class Animal {
  type: string;
  say: () => void;

  constructor(type: string) {
    this.type = type;
    this.say = function () {
      console.log("say");
    };
  }

  walk() {
    console.log("walk");
  }
}

const a1 = new Animal("dog");
console.log(a1);

我们可以发现,在constructor定义的属性和方法都会挂载在实例上,跟function是一样的。

但是定义在类中的方法,默认会被挂载到原型上,不再需要我们显示定义在prototype上。

image.png

继承

在之前js的继承是很麻烦的,有各种版本的继承,什么构造继承、原型继承、组合继承等等。不清楚的可以看看笔者前面写的都2022年了你不会还没搞懂JS原型和继承吧

ES6出现了class后,继承就相对简单啦。我们只需要使用extends关键字就能实现继承。

class People {
  name: string
  
  constructor(name: string) {
    this.name = name
  }
  
  say() {
    console.log(this.name)
  }
}

// 继承
class Child extends People {
  // 不显示定义构造函数 默认会调用父类构造函数
  
  // 如果显示定义了构造函数,就必须使用super调用父类构造函数
  // 父构造函数会比子类构造函数先运行
  constructor(name: string) {
    super(name)
  }
}

const c1 = new Child("randy")
c1.name // randy
c1.say() // randy

请注意,类不比接口,不能多继承,一次只能继承一个类。

下面我们来说说TS对类新增的访问修饰符。

访问修饰符

学过java的同学对访问修饰符肯定不陌生,在TS中概念和使用是类似的。

访问修饰符总共有省略、public、protected、private四种,只是省略和public是一样的。

public

public是公开的,也就是用public定义的属性和方法在外部是可以被访问到的。

public权限我们一般会把public省略,当然你写上也是一样的。

class People1 {
  // 公共属性
  name: string;

  constructor(name: string) {
    this.name = name;
  }
  
  // 公共方法
  say() {
    console.log("say");
  }
  // 公共方法
  public say1() {
    console.log(this.name + " say");
  }
}

const p1 = new People1('randy')
p1.name // randy
p1.say() // say
p1.say1() // randy say

protected

protected定义的属性或方法只能在本类或子类中访问到。

就算是本身实例或子实例也访问不到。

class People {
  name: string
  protected count: number
  
  constructor(name: string) {
    this.name = name
  }
  
  sayCount() {
    // 本类能访问到
    console.log(this.count)
  }
}

// 继承
class Child extends People {
  constructor(name: string) {
    super(name)
  }
  
  sayCount2() {
    // 子类能访问到
    console.log(this.count)
  }
}

const p1 = new People("demi")
p1.count // Error 访问不到

const c1 = new Child("randy")
c1.count // Error 访问不到

private

private是私有的意思,就是只能在本类中访问。

class People1 {
  name: string;
  private _sex: string;

  constructor(name: string) {
    this.name = name;
  }
}

const p1 = new People1('randy')
p1._sex // Error 属性“_sex”为私有属性,只能在类“People1”中访问

那想要私有属性能被设置或访问怎么办呢?这就需要用到我们的getset方法。当提供了get方法后我们就能访问,当提供了set方法后我们就能设置新值。

class People1 {
  name: string;
  private _sex: string;

  constructor(name: string) {
    this.name = name;
  }

  get sex() {
    return this._sex;
  }

  set sex(val) {
    this._sex = val;
  }
}

const p1 = new People1('randy')
p1.sex = 'male'
console.log(p1.sex) // male

静态属性和静态方法

我们知道,静态属性和方法是类所有的,只能使用类来访问,不能通过实例来访问。

ES6之前我们通过给构造函数直接定义属性和方法就是静态属性和静态方法。

let Animal2 = function (type) {
  this.type = type;
};

// 定义静态属性和静态方法
Animal2.count = 1;
Animal2.say = () => {
  console.log("say");
};

const a2 = new Animal2("cat");
console.log(Animal2.count); // 1
console.log(Animal2.say()); // say
// console.log(a2.count); // Error
// console.log(a2.say()); // Error

class中,我们使用static来定义静态属性和静态方法。

class Animal2 {
  public type: string;

  public static count: number = 1;

  constructor(type: string) {
    this.type = type;
  }

  public static say() {
    console.log("say");
  }
}

const a2 = new Animal2("cat");
console.log(Animal2.count); // 1
console.log(Animal2.say()); // say
// console.log(a2.count); // Error
// console.log(a2.say()); // Error

上面的例子我们在static前面加上了public,其实你省略也是可以的。因为省略和public的权限是一样的。当然你还可以使用 propected、private来修饰,这个看具体应用场景。

只读属性

类似接口,我们还可以使用readoly来定义只读属性。

class People1 {
  public readonly num: number = 10;
}

const p1 = new People()
console.log(p1.num) // 10
p1.num = 100 // Error 无法分配到 "num" ,因为它是只读属性。

上面的例子我们在readonly前面加上了public,其实你省略也是可以的。因为省略和public的权限是一样的。当然你还可以使用 propected、private来修饰,这个看具体应用场景。

注意,我们只能使用readoly来定义只读属性,不能定义只读方法。

可选属性

类似接口,我们还可以使用?来定义可选属性。

class People {
  name: string;
  age?: number;

  constructor(name: string, age?: number, public sex?: string) {
    this.name = name;
    this.age = age;
  }
}

const p1 = new People("randy");
const p2 = new People("randy", 24);
const p3 = new People("randy", 24, "male");

抽象类

抽象类做为其它派生类的基类使用,它不能直接被实例化,不同于接口,抽象类可以包含成员的实现细节。但是接口是只能有定义,不能有实现。

抽象类用abstract修饰。在抽象类中我们可以使用abstract定义抽象属性和抽象方法。抽象属性和抽象方法是不需要我们具体去实现的,只要定义即可。

abstract class A1 {
  // 抽象属性
  abstract age: number;
  sex: string = "male";

  say() {
    console.log("say");
  }
  
  // 抽象方法
  abstract hi(): void;
  abstract hi2(num: number): number;
  // abstract hi2: (num: number) => number;
}

我们继承抽象类的时候,必须把里面的抽象属性和抽象方法全都实现,不然会报错。

class Son extends A1 {
  age: number;

  hi() {
    console.log("hi");
  }

  hi2(num: number) {
    return num + 1;
  }
}

const s1 = new Son();
console.log(s1.age);
console.log(s1.sex);
s1.say(); // say
s1.hi(); // hi
s1.hi2(1); // 2

接口实现

前面我们讲接口的时候有提到过一嘴,接口是可以被类实现的。

我们来看例子

interface Inter1 {
  name: string;

  hello(): void;
  // hello: () => void
}

我们使用implements关键字来实现接口。接口里面定义的属性和方法我们必须全部都实现,否则会报错。

class Son implements Inter1 {
  name: string;

  hello() {
    console.log("hello");
  }
}

const s1 = new Son();
console.log(s1.name);
s1.hello();

不比类只能继承一个,接口是可以被多实现的。

interface Inter2 {
  name2: string;

  hello2: () => void;
}

多实现,使用逗号分开

class Son implements Inter1, Inter2 {
  name: string;
  name2: string;

  hello() {
    console.log("hello");
  }
  hello2() {
    console.log("hello2");
  }
}

const s1 = new Son();
console.log(s1.name);
s1.hello();
console.log(s1.name2);
s1.hello2();

类、抽象类、接口的区别

说到这,我们学习了类、抽象类、接口,有些同学可能有点懵了,这三者到底都有什么区别呢?

下面笔者说说自己的见解。

类是对对象的抽象

类里面可以定义属性和方法,并且属性有值,方法有具体的实现,实例化出来是一个一个的具有属性和方法的对象。

抽象类是对类的抽象

抽象类不能被实例化。它里面既可以有抽象属性和抽象方法又可以有普通属性和普通方法。我们可以在抽象类中把通用行为定义成普通属性和普通方法。把一些灵活多变的属性和方法定义为抽象属性和抽象方法,在子类中根据不同场景去具体的实现。

抽象类可以被继承,只能单继承。

抽象类可以继承抽象类,并且不需要把抽象属性和方法实现。

抽象类可以继承普通类。

接口是对行为的抽象

接口不能被实例化。接口里面只能定义属性和方法,属性不能被赋值,方法不能有具体实现,相当于就只能有抽象。相比抽象类更加严格。

接口可以被类实现,并且可以多实现。

接口可以继承接口,并且支持多继承。

系列文章

TypeScript入门之环境搭建

TypeScript入门之数据类型

TypeScript入门之函数

TypeScript入门之接口

TypeScript入门之类

TypeScript入门之类型推断、类型断言、双重断言、非空断言、确定赋值断言、类型守卫、类型别名

TypeScript入门之泛型

TypeScript入门之装饰器

TypeScript入门之模块与命名空间

TypeScript入门之申明文件

TypeScript入门之常用内置工具类型

TypeScript入门之配置文件

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!