先回顾下前文中介绍了哪些内容:
本文先不继续讲解 Nest 中的内容,而是打算介绍 TypeScript 中的两个语法:类和装饰器,帮助新手理解 Nest 中代码的写法。
如果你对 TypeScript 已经很熟悉,根据自己实际情况有选择的阅读即可。
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
修饰类的成员属性后,该属性就变为了只读属性,只能读,不能修改。因此对于只读属性,必须在声明时,或者在构造函数中进行初始化,否则会报错。
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 类的内部。分别对应了三种修饰符:
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 中,在构造函数的参数前加上一个可见性修饰符public
、private
、protected
或者 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;
}
}
类还有一些其他语法,比如访问器,静态成员属性和方法,继承等等。这些内容如果还不熟悉,可以阅读其他文章博客或者文档,我们先学习上面这些,够用为主。
装饰器,看名字就知道是用起装饰作用的。它用来增强类(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
看了这么多装饰器,已经眼花缭乱了,来看一个实际的例子。
打开 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 中还有非常多的装饰器,后面也会继续介绍。
本文没有继续介绍 NestJS 有关的内容,而是讲解了 TypeScript 中两个很重要的语法:类和装饰器,可以帮助新手理解 Nest 应用中的一些代码写法的含义。
毕竟不积跬步,无以至千里,打好这些基础,后面上手的也会更顺利。