Nest(3):扫盲篇:TypeScript 类和装饰器

前言

先回顾下前文中介绍了哪些内容:

  • 使用 @nestjs/cli 创建和管理 Nest 应用
  • Hello, World 示例代码分析
  • Nest 基本概念:模块,控制器,服务
  • 常用的装饰器:@Module、@Controller、@Get、@Injectable
  • Nest 目录结构分析
  • @nest/cli 脚手架的命令

本文先不继续讲解 Nest 中的内容,而是打算介绍 TypeScript 中的两个语法:类和装饰器,帮助新手理解 Nest 中代码的写法。

如果你对 TypeScript 已经很熟悉,根据自己实际情况有选择的阅读即可。

类 Class

NestJS 支持面向对象编程,也支持函数式编程。但在实际开发中还是以面向对象为主,而面向对象又是和类紧密联系的,所以对于类的一些概念和语法,一定要熟练掌握。

ES6 推出了 class 类,本质是过去的构造函数的语法糖。TypeScript 中的类的用法和 ES 标准中的类大差不离,多了一些更加 OOP 的语法支持。、

下面是对类的一些常用语法的说明。

类成员

在 JS 中类的成员有两种,分别是成员属性成员方法。TypeScript 官方文档中会称之为字段(filed)和方法(method)。本文描述时会按照 JS 的习惯。

属性

在 TS 中声明类使用 class 关键字,如果实例有属性,则必须先声明其属性类型。如下,声明一个 Person 类:

class Person {
  // 声明类的属性,可以使用类型注解声明类型
  name: string;
  age: number;
    
  // 也可以省略,则默认是 any 类型
  address;
}

// 实例化
const person = new Person('kw', 18);
console.log(person.name);

初始值

类的属性可以设置初始值,既可以在声明时设置,也可以在构造函数中设置:

class Person {
  name: string = 'kw';
  age: number = 18;
}

class Person { 
  name: string;
  age: number;
    
  constructor() {
      this.name = 'kw';
      this.age = 18;
  }
}

--strictPropertyInitialization

这是 tsconfig.json 中的一个配置项,是否严格检查类的属性初始化操作。默认为 true,表示类的成员属性必须在声明时或者在构造函数中进行初始化操作。如果该选项开启了:

class Person { 
  // 正确,在声明时完成了初始化
  name: string = 'kw';
  // 正确,在构造函数中完成了初始化
  age: number;
  // 报错:Property 'address' has no initializer and is not definitely assigned in the constructor.
  address: string;
    
  constructor() {
      this.age = 18;
  }
}

有些场景下需要在别的地方进行属性的初始化,此时可以对属性应用非空断言:

class Person {
    name!: string;
}

此时虽然没有做初始化,但是编译器也不会报错了。

readonly 修饰符

使用 readonly 修饰类的成员属性后,该属性就变为了只读属性,只能读,不能修改。因此对于只读属性,必须在声明时,或者在构造函数中进行初始化,否则会报错。

class Person { 
  readonly name: string = 'kw';
  age: number;
}

构造方法

构造方法用于创建和初始化类的实例对象。使用 new 操作一个类时,就会触发这个类的构造方法的执行。

构造方法不是必须的,类有一个默认的空的构造方法。但是如果需要为类的实例设置不同的属性,则必须实现一个构造方法。

class Person { 
  name: string;
  age: number;

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

// 实例化
const person = new Person();
console.log(person.name);
console.log(person.age);

可访问性修饰符

修饰符用来限制类的成员属性和成员方法的可访问范围

假设有一个 A 类和继承自 A 的 B 类,则可见范围有三种:任意地方,A 类内部,B 类的内部。分别对应了三种修饰符:

  • public:默认的修饰符,表示成员是公有的,可在任意地方被访问
  • protected:受保护的修饰符,表示成员能在当前类的内部,子类的内部被访问到
  • private:私有修饰符,表示成员只能在类的内部被访问到

Nest(3):扫盲篇:TypeScript 类和装饰器_第1张图片

class A {
    // 默认为 public,等同于 public a
    a;
    // 受保护的属性,可以在当前类和子类中访问
    protected b;
    // 私有属性,只能在当前类内部访问
    private c;
    
    test() {
        console.log(this.c)
    }
}

class B extends A {
    test() {
        console.log(this.b)
    }
}

参数属性

参数属性(Parameter Properties)是一种能简化代码的语法糖。例如要声明一个类,要先声明成员属性,再写构造方法进行初始化:

class Person { 
  name: string;
  age: number;

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

const person = new Person('kw', 18)
console.log(person.name)
console.log(person.age)

当这个类有非常多的属性时,光这些初始化代码可能就要写几十行。

在 TypeScript 中,在构造函数的参数前加上一个可见性修饰符publicprivateprotected 或者 readonly,该参数就变为了参数属性,TypeScript 会将这些构造函数的参数转换为具有相同名称和值的类属性

上面的 Person 类就等同于这种写法:

class Person { 
  // 在声明类成员时 public 修饰符可以省略,但在使用参数属性时,不能省略
  constructor(public name: string, public age: number) {
    // 不需要函数体
  }
}

const person = new Person('kw', 18)
console.log(person.name)
console.log(person.age)

回顾下 AppController 控制器的代码:

export class AppController {
  constructor(private readonly appService: AppService) {}
}

现在再看这段代码是什么含义就很清晰了,它的完整写法是:

export class AppController {
  // appService 是私有属性且只读,需要在声明或者构造函数中完成初始化
  private readonly appService: AppService;
    
  constructor(appService: AppService) {
      this.appService = appService;
  }
}

类的其他语法

类还有一些其他语法,比如访问器,静态成员属性和方法,继承等等。这些内容如果还不熟悉,可以阅读其他文章博客或者文档,我们先学习上面这些,够用为主。

装饰器 Decorator

装饰器,看名字就知道是用起装饰作用的。它用来增强类(class)的功能,许多面向对象的语言都有这种语法。

虽然装饰器的历史悠久,但是由于 JS 这门语言本身很少有场景需要使用到装饰器,所以目前装饰器仍处于提案阶段,并且这一提,就是很多年。记得最早在 ES2015 中,装饰器语法就已经处于提案阶段了。

虽然 ES 标准的装饰器还未成为标准,但是 TS 中的装饰器可以大胆使用。

装饰器的本质就是函数,只不过使用形式上和普通函数有所不同:普通函数使用 () 调用,装饰器使用 @ 调用。普通函数可以在任意位置执行,装饰器只能用在类和类的成员身上

装饰器和装饰器工厂

先来看一个装饰器的最简单的例子。

function Fn(target: any) {
  let a = new target;
  a.say()
}

@Fn
class A {
  say() {
    console.log('hello')
  }
}

代码执行,打印 “hello”。

Fn 是一个装饰器函数,当被用在类上时,它所接收的参数就是该类。所以可以在装饰器内部,实例化 A 类型,并调用 a对象 的实例方法。

如果需要一些定制化的内容,想让装饰器接收一些其他的参数,就要用到装饰器工厂了。装饰器工厂就是一个返回装饰器的工厂函数。比如:

function FnFactory(name: string) {
    
  // 返回一个装饰器
  return function(target: any) {
      let a = new target;
      a.say(name)
  }
  
}

@FnFactory('kw')
class A {
  say(name: string) {
    console.log('hello, ', name)
  }
}

代码执行,打印 “hello, kw”。

注意区分装饰器和装饰器工厂的使用,普通的装饰器的用法是 @装饰器,装饰器工厂的用法需要带上圆括号,@装饰器()

类装饰器

根据所修饰对象的不同,装饰器具体可以分为:

  • 类装饰器
  • 属性装饰器
  • 方法装饰器
  • 存取器装饰器
  • 参数装饰器

上面的示例中,装饰器在类上调用,这就是类装饰器。类装饰器只有一个参数,就是被装饰的类

属性装饰器

应用在类的属性成员上的装饰器,可接收两个参数:

  • 如果是静态属性,则是类的构造方法;如果是实例属性,则是类的原型对象
  • 属性名
function Property() {
  return function (target: any, property: any) {
    console.log(target)
    console.log(property)
  }
}

class C {
  @Property()
  name!:string;
}

// {}
// name

方法装饰器

方法装饰器用的比较多,它接收3个参数:

  • 如果修饰的是静态成员,则为类的构造函数;如果修饰的是实例方法,则为类的原型对象
  • 方法名
  • 方法的属性描述符
function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;
    
  constructor(message: string) {
    this.greeting = message;
  }
 
  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

这个装饰器的作用是修改属性描述符的 enumerable 属性,设为 false,也就代表着该方法不能被枚举了。

存取器装饰器

用在访问器身上的装饰器,可接收参数和方法装饰器相同。

function configurable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }
 
  @configurable(false)
  get x() {
    return this._x;
  }
 
  @configurable(false)
  get y() {
    return this._y;
  }
}

该装饰器可以动态修改存取器的属性描述符的 configurable 属性,示例中将其设置为 false,表示不能被删除,也不能被修改。

参数装饰器

应用在构造方法或者实例方法的参数上的装饰器,它接收三个参数:

  • 如果修饰的是静态成员方法,则为类的构造函数;如果修饰的是构造方法或者实例方法,则为类的原型对象
  • 参数所在方法的名字
  • 参数在原函数的参数列表中的位置,也就是第几个参数
// 参数装饰器
function Param() {
  return function(target: any, param: string, index: number) {
    console.log(target)
    console.log(param)
    console.log(index)
  }
}

class Person {
  say(@Param() msg:string) {
    console.log(msg)
  }
}

// {}
// say
// 0

@Controller 和 @Get 装饰器

看了这么多装饰器,已经眼花缭乱了,来看一个实际的例子。

打开 app.controller.ts,修改为以下内容:

import { Controller, Get, Query } from '@nestjs/common';
import { AppService } from './app.service';

// 声明类为控制器,并为该模块的路由设置一个请求前缀 news
@Controller('news')
export class AppController {
  constructor(private readonly appService: AppService) {}

  // 和路由前缀拼接,处理 Get /news/list 请求
  @Get('list')
  // 参数装饰器,入参是请求中的 query 参数
  getHello(@Query('page') page) {
    return {
      code: 0,
      data: {
        list: [],
        page,
      },
    };
  }
}

打开浏览器,访问 localhost:3000/news/list?page=10

Nest(3):扫盲篇:TypeScript 类和装饰器_第2张图片

当然,Nest 中还有非常多的装饰器,后面也会继续介绍。

总结

本文没有继续介绍 NestJS 有关的内容,而是讲解了 TypeScript 中两个很重要的语法:类和装饰器,可以帮助新手理解 Nest 应用中的一些代码写法的含义。

毕竟不积跬步,无以至千里,打好这些基础,后面上手的也会更顺利。

你可能感兴趣的:(NestJS,typescript,node.js,Nest.js)