📖TypeScript 速通教程 @尚硅谷
TS - 概述
- TypeScript 是 JavaScript 的超集。
- 它对 JS 进行了扩展,向 JS 中引入了类型的概念,并添加了许多新的特性。
- TS 代码需要通过编译器编译为 JS,然后再交由 JS 解析器执行。
- TS 完全兼容 JS,换言之,任何的 JS 代码都可以直接当成 TS 使用。
静态类型检查
- 静态类型检查,即在代码运行前进行检查,发现代码的错误或不合理之处,减少运行时异常的出现几率。
- TypeScript 的核心就是静态类型检查,即将运行时的错误前置。
TS - 编译
浏览器无法直接运行 .ts 文件,因此需要将 .ts 文件编译为 .js 文件。
编译 - 命令行
Step1. npm install -g typescript
Step2. tsc <.js 文件路径>
tsc
是全局安装 typescript 后暴露的命令,是 typescript compiler 的简写,用于将 .ts 文件编译为 .js 文件
编译 - 自动化
Step1. npm install -g typescript
Step2. tsc --init
创建了
tsconfig.json
配置文件,规定了将 .ts 文件编译为 .js 文件的方式。可以在配置文件中设置"noEmitOnError": true
,使得编译出错时不生成 .js 文件。
Step3. tsc --watch
监视当前目录下的所有 .ts 文件的变化,并根据
tsconfig.json
将其自动编译为 .js 文件。--watch
后也可以加具体的文件路径,表示只监视特定的文件。
TS - 类型声明
-
解释:类型声明给变量设置了类型,使得变量只能存储某种类型的值。
-
语法
1
2
3
4
5
6
7let 变量: 类型;
let 变量: 类型 = 值;
function fn(参数: 类型, 参数: 类型): 类型{
...
}
TS - 类型推断
解释:当对变量的声明和赋值是同时进行的,TS 编译器会自动判断变量的类型
JS|TS - 类型总览
- JavaScript 中的数据类型:①string ②number ③boolean ④null ⑤undefined ⑥bigint ⑦symbol ⑧object
- TypeScript 中的数据类型
- JS 所有类型
- TS 新增类型:①any ②unknown ③never ④void ⑤tuple ⑥enum ⑦字面量
- TS 用于自定义类型的关键字:①type ②interface
- 注意事项:String 和 string 都是 TypeScript 中所接受的类型,不过前者是包装对象类型,一般不建议使用;后者是原始类型,推荐使用。
- 原始类型:内存占用空间小,处理速度快。
- 包装对象:内存占用空间多,但可以调用方法或访问属性。
TS - 常用类型
any
- 解释:任意类型,即一旦将变量类型限制为
any
,那么就意味着放弃对该变量的类型检查。1
2
3
4
5
6
7
8// 显示 any
let a: any
// 隐式 any
let b
// 可以给 any 类型的变量赋任何值,不会警告
b = 100
b = 'hello'
b = false - 注意:
any
类型的变量,可以赋值给任意类型的变量,从而造成污染。1
2
3
4
5let a: any
a = 100
let b: string
b = a // 无警告
unknown
- 解释:未知类型,可以理解为类型安全的
any
。该类型会强制开发者在使用之前对变量进行类型检查(if 判断或类型断言),否则会进行警告。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let a: unknown
a = 100
a = 'hello'
a = false
let x: string
x = a // 警告:不能将类型 unknown 分配给类型 string
// 方式一:if 判断,让 ts 知道 unknown 变量的具体类型
if(typeof a === 'string'){
x = a
}
// 方式二:类型断言,用于指定 unknown 变量的具体类型
x = a as string // 类型断言写法 1
x = <string>a // 类型断言写法 2 - 注意:读取
any
类型变量的任何属性都不会报错,而unknown
正相反。
never
解释:不是任何类型,即 never
类型的变量不能有值。
- 不用
never
去限制变量,无意义。 - TypeScript 会根据代码执行情况,推断出某些永远无法执行的变量类型为
never
。1
2
3
4
5
6
7
8let a: string
a = 'hello'
if (typeof a === 'string'){
console.log(a.toUpperCase())
} else {
console.log(a) // 这里的 a 会被推断为 never 类型,因为没有值符合这里的逻辑
} never
也可以用于限制函数的返回值类型,当 ①程序无法正常执行(报错)②程序永远无法停止(死循环)时,其返回值类型就可以是never
。1
2
3function throwError(message: string): never {
throw new Error(message)
}
void
- 解释:空类型,常用于函数返回值的类型声明,表示 ①函数不返回任何值,②调用者也不应该依赖其返回值进行任何操作。其中,
undefined
是void
唯一接受的一种返回值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 隐式返回 undefined - 1(无警告)
function print(msg: string): void{
console.log(msg)
}
// 隐式返回 undefined - 2(无警告)
function print(msg: string): void{
console.log(msg)
return
}
// 显示返回 undefined - 3(无警告)
function print(msg: string): void{
console.log(msg)
return undefined
}
let res = print("hello world")
if(res) console.log("可以获取函数的 void 返回值吗?") // 警告 - 注意:如果函数的返回值被声明为
void
,那么- 语法上,函数可以返回
undefined
,显式还是隐式返回无所谓。 - 语义上,函数调用者不应该关心函数返回的值,也不应依赖返回值进行任何操作。
- 语法上,函数可以返回
object
object
:所有非原始类型,包括对象、函数、数组等。Object
:所有可以调用 Object 方法的类型(除了 undefined 和 null 的任何值)。- 注意:object 和 Object 在实际开发中使用频率极低。
声明对象类型
1 | // 声明(一般)对象类型的必选属性 |
声明函数类型
1 | // 声明函数类型,包括参数及返回值 |
声明数组类型
1 | // 声明数组类型 - 方式 1(类型[]) |
tuple
解释:特殊的数组类型,可以存储固定数量的元素,并且每个元素的类型是已知的,且可以不同。元组用于精确描述一组值的类型,?
表示可选元素。
1 | // 两个元素,第一个必须是 string 类型,第二个必须是 number 类型 |
enum
-
解释:
enum
可以定义一组命名常量,用于增强代码的可读性和可维护性。枚举分为数字枚举、字符串枚举、常量枚举。 -
数字枚举:最常见的枚举类型。数字枚举的成员的值是数字,默认会自动递增;数字枚举存在反向映射。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27enum Sex {
Male,
Female
}
/*
Sex 被编译为,
var Sex;
(function (Sex) {
Sex[Sex["Male"] = 0] = "Male";
Sex[Sex["Female"] = 1] = "Female";
})(Sex || (Sex = {}));
*/
function recruitPolicy(sex: Sex): void {
if (sex === Sex.Male) {
console.log('height >= 185cm and weight <= 80kg');
} else if (sex === Sex.Female) {
console.log('height >= 165cm and weight <= 70kg');
} else {
console.log('invalid params');
return;
}
console.log('eyesight >= 3.8');
console.log('salary = 3000');
}
recruitPolicy(Sex.Female); -
字符串枚举:枚举成员的值是字符串,不存在反向映射。
1
2
3
4
5
6
7
8
9
10
11
12
13enum Sex {
Male = 'male',
Female = 'female'
}
/*
Sex 被编译为,
var Sex;
(function (Sex) {
Sex["Male"] = "male";
Sex["Female"] = "female";
})(Sex || (Sex = {}));
*/ -
常量枚举:特殊的枚举类型,使用
const enum
关键字定义,在编译时会被内联,避免生成额外的代码。编译时内联:即 TypeScript 在编译时,会将枚举成员引用替换为它们的实际值,这样就不会生成额外的枚举对象,从而减少代码量,并提高运行时性能。
1
2
3
4
5
6const enum Sex {
Male = 'male',
Female = 'female'
}
console.log(Sex.Male)被编译为,
1
2;
console.log("male" /* Sex.Male */);
type
- 解释:
type
关键字用于为任意类型创建别名。 - 基本使用:
type 别名 = 类型
1
2
3
4
5// digit 类型是 number 类型的别名
type digit = number
let price: digit
price = 100 - 联合类型:
type 别名 = 类型1 | 类型2 | 类型3
,表示变量的值可以是若干类型之一。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23type Status = number | string
type State = "Success" | "Error" | "Unkown"
function printHttpStatus(status: Status) {
let state: State;
if (status.toString().startsWith('2')) {
console.info(status);
state = 'Success';
}
else if (status.toString().startsWith('4')) {
console.error(status);
state = "Error";
}
else {
console.log("unknown status");
state = "Unkown"
}
return state;
}
const state404 = printHttpStatus("404");
const state200 = printHttpStatus("200");
console.log(`404 is ${state404}, 200 is ${state200}`) - 交叉类型:
type 别名 = 类型1 & 类型2 & 类型3
,表示变量的值必须同时满足若干类型。交叉类型常用于对象类型,用于合并所有对象类型的成员。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26type Address = {
country: string;
province: string;
city: string;
district: string;
code: string
}
type BasicInfo = {
name: string;
age: number;
school: string;
}
type Person = Address & BasicInfo;
const lee: Person = {
name: "lee",
age: 22,
school: "NU",
country: "CN",
province: "SX",
city: "XX",
district: "JJ",
code: "000000"
}
type + void 存在的问题
- 问题描述:当使用类型声明限制函数返回值为 void 时,TypeScript 不会严格要求函数返回空(即允许函数返回 undefined 之外的值)。
1
2
3
4
5
6
7
8
9
10
11
12type NoReturnFunc = (...args: String[]) => void;
const printf: NoReturnFunc = (msg) => {
console.log(msg);
return 0;
}
const funcCode = printf("Hello World");
console.log(funcCode); // 0
if (funcCode == 0) // 警告:This comparison appears to be unintentional because the types 'void' and 'number' have no overlap.
console.log("success!") - 原因解释:TS 这样设计是为了确保像如下的代码成立,避免类型限制影响箭头函数的简写语法。
1
2
3
4
5
6
7
8const src = [1, 2, 3];
const dst = [0];
src.forEach(el => dst.push(el));
// forEach 的回调函数返回值类型是 void,表示不关心返回值。
// 这里使用的箭头函数虽然返回了 dst.push(el) 的结果(即 dst 的最新长度),但 TypeScript 将其处理为 void 类型。
// 这是为了避免因为不必要的返回值引发错误。
// 同时,void 类型的函数返回值不能被用于比较或赋值,否则会报错。
TS - 类
定义 | 重写
TS 中的类与 JS 相比,有以下注意事项,
- 属性必须预先声明
- 重写父类方法时推荐使用
override
关键字,可以增加语法提示,避免出错
1 | class Person { |
属性修饰符
1 | class Person { |
属性的简写
语法:constructor(修饰符 属性名: 类型, 修饰符 属性名: 类型, ...)
属性简写必须包含修饰符,但是当属性是继承于父类/抽象类时,不能包含修饰符。
1 | class Person1 { |
TS - 抽象类
- 解释:抽象类是一种无法被实例化的类,专门用于定义类的结构和行为,类中可以写抽象方法,也可以写具体实现。
- 功能:抽象类主要用于为其派生类提供一个基础结构,要求其派生类必须实现其中的抽象方法。
- 语法:使用
abstract
关键字修饰类,则为抽象类;修饰方法,则为抽象方法。 - 适用场景:①定义通用接口 ②提供基础实现 ③确保关键实现 ④共享代码和逻辑
1 | // 包裹 - 抽象类 |
TS - 接口
- 解释:接口(interface)是一种定义结构的方式,用于为类、对象、函数等规定一种契约,从而确保代码的一致性和类型安全。但是注意:interface 只能定义结构,不能包含任何实现。
- 命名规范:如
IPerson
或PersonInterface
。 - 适用场景:①定义对象的格式 ②类的契约 ③自动合并
定义 | 使用
类结构
类使用 implements
关键字实现已有的接口,规范类的实现。
1 | interface IPerson { |
对象结构
接口可以看作一个类型,可以使用 :接口名
的方式,限制对象的类型。
1 | interface IUser { |
函数结构
接口可以看作一个类型,也可以使用 :接口名
的方式,限制函数的类型。
1 | interface IAdd { |
继承
接口可以通过 extends
关键字实现继承。
1 | interface IPerson { |
自动合并
同名接口的重复定义并不会导致接口覆盖,而是自动合并。
1 | interface IPerson { |
与 type 和抽象类的区别
TS - 泛型
-
解释:泛型允许在定义函数、类或接口时,使用类型参数来表示未指定的类型,这些参数在具体使用时才被指定为具体的类型。泛型可以让同一段代码适用于多种类型,同时仍保持类型的安全性。
-
使用
-
泛型函数
1
2
3
4
5
6
7function log<T>(msg: T): T {
console.log(msg);
return msg;
}
log<number>(100);
log<string>('Hello World'); -
多个泛型
1
2
3
4
5
6
7function log<T, U>(msg1: T, msg2: U): T | U {
console.log(msg1, msg2)
return Date.now() % 2 ? msg1 : msg2
}
log<number, string>(100, 'big number');
log<string, boolean>('happy now', false); -
泛型接口
1
2
3
4
5
6
7
8
9
10
11interface IPerson<T> {
name: string
age: number
extraInfo: T
}
let p: IPerson<string> = {
name: "Jack",
age: 19,
extraInfo: "Male"
} -
泛型约束
1
2
3
4
5
6
7
8
9
10interface IPerson {
name: string
age: number
}
function logPerson<T extends IPerson>(info: T): void {
console.log(`name=${info.name}, age=${info.age}`);
}
logPerson({ name: "Jack", age: 19 }) -
泛型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Person<T> {
constructor(
public name: string,
public age: number,
public extraInfo: T
) { }
}
interface IJobInfo {
title: string,
company: string
}
const p = new Person<IJobInfo>("Jack", 19, { title: "web", company: "ccd" })
-
TS - 类型声明文件
-
解释:类型声明文件是 TypeScript 中的一种特殊文件,通常以
.d.ts
作为扩展名。其主要用于为现有的 JavaScript 代码提供类型信息,使得 TypeScript 能够在使用 JavaScript 库或模块时进行类型检查和提示。 -
语法:使用
declare
关键字,为现有代码提供类型信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// demo.js
export function add(a, b) {
return a + b;
}
export function mul(a, b) {
return a * b;
}
// demo.d.ts,为现有的同名 .js 文件中的代码添加类型信息
declare function add(a: number, b: number): number;
declare function mul(a: number, b: number): number;
export {add, mul};
// index.ts
import { add, mul } from "./demo.js"
const x = add(2, 4); // 会有类型提示
const y = mul(4,5); // 会有类型提示
console.log(x, y);
TS - 装饰器
概述
-
装饰器本质是一种特殊的函数,它可以对类、属性、方法、参数进行扩展,同时能让代码更简洁。
-
装饰器自 2015 年在 ECMAScript-6 中被提出到现在,已将近 10 年。
-
截止目前,装饰器依然是实验性特性,需要开发者手动调整配置来开启装饰器支持。
-
装饰器有五种:类装饰器、属性装饰器、方法装饰器、访问器装饰器、参数装饰器。
-
虽然 TypeScript 5.0 中可以直接使用类装饰,但为了确保其他装饰器可用,现阶段使用时,仍建议使用
experimentalDecorators
配置来开启装饰器支持。
类装饰器
-
解释:类装饰器是一个应用在类声明上的函数,可以为类添加额外的功能或逻辑。
-
语法:类装饰器的定义及使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/* 类装饰器 - 定义 :一个可接受目标类为参数的函数 */
/* 注意-1:target 参数就是要被装饰的类 */
/* 注意-2:类装饰器可以返回一个新的类,替换掉被装饰的类;否则,被装饰的类不会被替换 */
function ClassDecoratorDemo(target: Function) {
console.log(`类装饰器被调用,收到的参数为 ${target}`);
}
/* 类装饰器 - 使用:通过 @装饰器名 的方式将类装饰器应用于类 */
/* 注意:类装饰器的调用时机为类定义阶段,类实例化之前 */
// @ClassDecoratorDemo <==> ClassDecoratorDemo(Person)
class Person {
constructor(public name: string, public age: number) {
console.log(`类构造函数被调用`);
}
} -
构造类型声明:在 TypeScript 中,
Function
类型可以表示诸如普通函数、箭头函数、方法等等,但是Function
类型的函数并非都可以使用new
关键字实例化(如箭头函数),因此我们需要定义一个新的类型用于限制类装饰器中的target
参数类型,该类型称之为构造类型,共有以下两种方式声明构造类型。-
声明构造类型
1
2
3
4
5
6
7
8
9
10
11
12/* 仅声明构造类型 */
/*
- new 表示 该类型可以使用 new 关键字进行实例化,即该类型是构造类型
- ...args 表示 该类型可以接受任意数量的参数
- any[] 表示 该类型可以接受任意类型的参数
- {} 表示 该类型的返回类型是对象(非 null、非 undefined)
*/
type Constructor = new (...args: any[]) => {};
function ClassDecoratorDemo(target: Constructor) {}
class Person {} -
声明构造类型 + 指定静态属性
1
2
3
4
5
6
7
8
9
10
11/* 声明构造类型 + 指定静态属性 */
type Constructor = {
new (...args: any[]): {}; // 构造类型签名
layer: string; // 静态属性 layer(即指定的构造类型必须要有 layer 静态属性)
};
function ClassDecoratorDemo(target: Constructor) {}
class Person {
static layer = "elite";
}
-
-
应用举例-1:定义一个装饰器,实现
Person
实例调用toString
时返回JSON.stringify
的执行结果。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/* 自定义构造类型 */
type Constructor = new (...args: any[]) => {};
/* 自定义类装饰器,用于新增属性和方法 */
function ToStringDecorator(target: Constructor) {
// 修改 Person 的 toString 方法
target.prototype.toString = function () {
return JSON.stringify(this); // this 指向调用 toString 方法的 Person 实例
};
// “密封” Person 的原型对象,此时 ①阻止添加新属性 ②阻止删除现有属性 ③保持现有属性的可配置性
Object.seal(target.prototype);
}
/* 使用类装饰器 */
class Person {
constructor(public name: string, public age: number) {}
}
const tom = new Person("Tom", 18);
console.log(tom.toString()); -
应用举例-2:设计一个
LogTime
装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法,用于读取创建时间。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35/* 自定义构造类型 */
type Constructor = new (...args: any[]) => {};
/* User 类型约束 */
interface User {
createdAt: Date;
getCreatedAt: () => string; // 等价于 getCreatedAt(): string
}
/* 自定义类装饰器,用于新增属性和方法 */
function LogTime<T extends Constructor>(target: T) {
return class extends target {
public createdAt: Date; // 实例创建时间
constructor(...args: any[]) {
super(...args); // 调用父类的构造函数
this.createdAt = new Date();
}
getCreatedAt() {
return `该对象创建于 ${this.createdAt}`;
}
};
}
/* 使用类装饰器 */
class User {
constructor(public name: string, public age: number) {}
}
const tom = new User("Tom", 18);
console.log(JSON.stringify(tom));
console.log(tom.getCreatedAt());
console.log(tom.createdAt);
装饰器工厂
-
解释:装饰器工厂是一个返回装饰器的函数,可以为装饰器添加参数,从而更灵活地控制装饰器的行为。
-
应用举例:定义一个
LogInfo
类装饰器工厂,实现Person
实例可以调用introduce
方法,且introduce
调用内容输出的次数由LogInfo
接收的参数决定。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/* 构造类型 */
type Constructor = new (...args: any[]) => {};
/* Person 类型约束 */
interface Person {
introduce: () => void; // 等价于 introduce(): void
}
/* 类装饰器工厂 */
function DecoratorFactory(repetition: number) {
return function (target: Constructor) {
// 向实例的原型上添加方法 introduce
target.prototype.introduce = function () {
for (let i = 0; i < repetition; i++) {
console.log(`My name is ${this.name}, and I'm ${this.age} years old.`);
}
};
};
}
/* 使用类装饰器 */
5) (
class Person {
constructor(public name: string, public age: number) {}
}
const tom = new Person("tom", 20);
tom.introduce();
装饰器组合
解释:一个类可以组合使用多个装饰器或装饰器工厂,其执行顺序为 ① 由上到下执行所有的装饰器工厂,依次获取到对应的装饰器 ② 由下到上执行所有的装饰器。
1 | /* 自定义构造类型 */ |
上述代码执行输出结果为
1 | test2工厂 |
属性装饰器
-
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* 属性装饰器
* @param target 对于静态属性,该参数为类;对于实例属性,该参数为类的原型对象
* @param propertyKey 属性名
*/
function FieldDecoratorDemo(target: object, propertyKey: string) {
console.log(target, propertyKey);
}
class Person {
/* 属性装饰器的使用 */
public name: string; // 等价于 FieldDecoratorDemo(prototype, name)
public age: number; // 等价于 FieldDecoratorDemo(prototype, age)
public static layer = "elite"; // 等价于 FieldDecoratorDemo(Person, layer)
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
} -
属性遮蔽:以下代码,如果片段
(2)
在片段(3)
上边,那么实例化Person
时,this.name = name
创建实例属性name
,而this.age = age
则是调用原型对象上的setter
,用于修改全局变量__age
,即属性遮蔽。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// (1)
class Person {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// (2)
let __age = 130;
Object.defineProperty(Person.prototype, "age", {
get() {
return __age;
},
set(newAge) {
__age = newAge;
},
});
// (3)
const p = new Person("Tom", 19);
console.log(p.age); -
应用举例:定义一个
State
属性装饰器,用于监视属性的修改。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70/**
* 用于监听实例属性修改的属性装饰器
* @param target 原型对象(类的 prototype)
* @param propertyKey 被装饰的属性名
*/
function State(target: object, propertyKey: string) {
// 定义一个私有属性名,用于存储实际的值
const key = `__${propertyKey}`;
// 在原型对象上定义属性的 getter 和 setter
Object.defineProperty(target, propertyKey, {
get() {
// getter 用于获取存储在实例上的私有属性值
return this[key]; // `this` 指向实例对象
},
set(v) {
// setter 用于监控属性值的修改并更新存储的值
console.log(`实例属性 ${propertyKey} 被修改了: ${this[key]} --> ${v}`);
this[key] = v; // 更新存储的私有属性值
},
enumerable: true, // 属性可枚举
configurable: true, // 属性描述符可配置
});
}
class Person {
public name: string; // 定义实例属性 name
age: number; // 为 age 属性添加装饰器,使其成为响应式属性
constructor(name: string, age: number) {
this.name = name; // 初始化实例属性 name
this.age = age; // 通过 setter 初始化 age 属性,本质上是设置私有属性 __age 的值
}
}
// 创建实例并验证功能
const p1 = new Person("Tom", 10); // 输出: 实例属性 age 被修改了: undefined --> 10
const p2 = new Person("Jack", 19); // 输出: 实例属性 age 被修改了: undefined --> 19
console.log(p1, p2); // 输出: Person { name: 'Tom', __age: 10 } Person { name: 'Jack', __age: 19 }
/* ------------------------------------------------------------------------------- */
/* 上述代码可以完全等价为下述代码 */
class Animal {
public name: string;
public age: number;
/* 响应式实现-1,等同上述的属性装饰器 */
private __age: number = 0;
constructor(name: string, age: number) {
/* 响应式实现-2,监视 age 属性的修改,其中 age 属性存放在类的原型对象上,被代理的数据存储在实例对象上 */
Object.defineProperty(Animal.prototype, "age", {
get() {
return this.__age;
},
set(v) {
console.log(`实例属性 age 被修改了:${this.__age} --> ${v}`);
this.__age = v;
},
enumerable: true,
configurable: true,
});
this.name = name;
this.age = age;
}
}
const a1 = new Animal("Happy", 20);
const a2 = new Animal("Lucky", 12);
console.log(a1, a2);补充 Object.defineProperty 的使用
-
功能:
Object.defineProperty()
在给定的对象上定义一个新属性或修改一个现有属性,同时返回修改后的对象。 -
语法:
Object.defineProperty(obj, prop, descriptor)
obj
:给定的对象prop
:要定义或修改的属性名,可以是 string 或Symbol
descriptor
:属性描述符,用于控制要定义或修改的属性的行为
-
返回值:定义或修改了指定属性后的给定对象。
-
使用说明
- 属性描述符(property descriptor)可以分为数据描述符(data descriptor)和访问器描述符(accessor descriptor),对应的属性可以称之为数据属性和访问器属性。
- 属性描述符是一个对象,其中数据描述符所特有的配置项为
value
和writable
,访问器描述符所特有的配置项为get
和set
,二者所共有的配置项为enumerable
和configurable
。 - 一个属性描述符只能是数据描述符或访问器描述符二者其一。
- 如果一个属性描述符不包含
value
、writable
、get
、set
属性,则该属性描述符被看作数据描述符。 - 如果一个属性描述符同时包含 【
value
或writable
】和【get
或set
】,则会报错。
- 属性描述符是一个对象,其中数据描述符所特有的配置项为
- 默认情况下,
Object.defineProperty()
定义的属性是不可写(not writable)、不可枚举(not enumerable)、不可配置(not configurable)的。而通过object.propertyName=propertyValue
这样的方式定义的属性是可写、可枚举、可配置的。 Object.defineProperty()
本质上是通过 JavaScript 的内在方法(internal method)[[DefineOwnProperty]]
实现的,与另一个 JavaScript 的内在方法[[Set]]
无关,因此,通过该方法修改一个现有属性不会导致该属性的setter
被调用。
- 属性描述符(property descriptor)可以分为数据描述符(data descriptor)和访问器描述符(accessor descriptor),对应的属性可以称之为数据属性和访问器属性。
-
descriptor
配置(都是可选的!)-
共有的配置项(shared keys)
-
configurable
:属性的类型是否可以切换(数据属性 ⇔ 访问器属性);属性是否可以被删除;该属性的属性描述符的其他配置项是否可以被修改。默认值为false
。特殊情况:对于数据描述符而言,如果配置了
writable: true
,那么无论如何,该属性的值都可以被修改,同时writable
配置项可以被修改为false
。 -
enumerable
:属性是否可枚举(如for...in
、Object.keys
)。默认值为false
。
-
-
数据描述符的配置项
value
:属性对应的值,可以是任意合法的 JavaScript 值。默认值为undefined
。writable
:属性是否可以通过赋值操作符(assignment operator)被修改。默认值为false
。
-
访问器描述符的配置项
get
:属性的getter
,访问该属性时调用,不接受参数,同时将其返回值视作属性值。默认值为undefined
。set
:属性的setter
,对该属性赋值时调用,接受一个参数(即赋的新值)。默认值为undefined
。
注意事项:在
get
或set
方法被调用时,函数中的this
指向的是访问或修改该属性的对象。需要注意的是,由于 JavaScript 的继承机制,this
指向的不一定是属性所定义的对象。
-
-
方法装饰器
-
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
* 方法装饰器
* @param target 对于静态方法,该参数是类;对于实例方法,该参数是类的原型对象
* @param propertyKey 方法名
* @param descriptor 方法的描述对象,其 value 属性是被装饰的方法
*/
function MethodDecoratorDemo(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target, propertyKey, descriptor);
}
class Person {
constructor(public name: string, public age: number) {}
speak() {
// 等价于 MethodDecoratorDemo(prototype, speak, descriptor)
console.log(`Hi, my name is ${this.name}, and my age is ${this.age}`);
}
static isAdult(age: number) {
// 等价于 MethodDecoratorDemo(Person, isAdult, descriptor)
return age >= 18;
}
} -
应用举例:定义一个
Logger
方法装饰器,用于在方法执行前和执行后,追加一些额外的逻辑;一个Validate
方法装饰器,用于验证数据。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66/**
* 用于在实例方法执行前后追加一些额外逻辑的方法装饰器
* @param target 类的原型对象
* @param propertyKey 方法名
* @param descriptor 方法描述对象
*/
function Logger(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
/* 1. 缓存原始方法 */
const original = descriptor.value;
/* 2. 替换原始方法 */
descriptor.value = function (...args: any[]) {
console.log(`正在准备执行${propertyKey}方法······`);
const result = original.call(this, ...args);
console.log(`${propertyKey}方法已执行完毕!`);
return result;
};
}
/**
* 用于验证静态方法参数合法性的方法装饰器工厂
* @param maxValue 最大参数值
* @returns 方法装饰器
*/
function Validate(maxValue: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
/* 1. 缓存原始方法 */
const original = descriptor.value;
/* 2. 替换原始方法 */
descriptor.value = function (...args: any[]) {
if (args[0] > maxValue)
throw new Error(
`非法的参数,所传入参数值 ${args[0]} 大于最大允许传入值为 ${maxValue}`
);
return original.call(this, ...args);
};
};
}
class Person {
constructor(public name: string, public age: number) {}
speak(repetition: number) {
for (let i = 0; i < repetition; i++) {
console.log(`My name is ${this.name}, and my age is ${this.age}`);
}
}
150) (
static isAdult(age: number) {
return age >= 18;
}
}
const p = new Person("Tom", 23);
p.speak(5);
console.log(Person.isAdult(149));
console.log(Person.isAdult(200));补充 Function.prototype.apply/call/bind 的使用
Function.prototype.call()
:以给定的this
值和逐个提供的参数调用该函数。- 语法:
call(thisArg[, arg1, arg2, /* …, */ argN])
- 参数
thisArg
:调用func
时要使用的this
值。非严格模式下,null
和undefined
被替换为全局对象,原始值被转换为对象。arg1, arg2, /* …, */ argN
:可选的若干函数参数。
- 返回值:使用指定的
this
值和若干可选参数调用函数后的结果。
- 语法:
Function.prototype.apply()
:以给定的this
值和以数组(或类数组对象)提供的参数调用该函数。- 语法:
apply(thisArg, argsArray)
- 参数
thisArg
:调用func
时要使用的this
值。非严格模式下,null
和undefined
被替换为全局对象,原始值被转换为对象。argsArray
:可选的类数组对象,用于指定调用func
时的参数。
- 返回值:使用指定的
this
值和可选的类数组对象提供的参数调用函数后的结果。
- 语法:
Function.prototype.bind()
:根据原始函数创建一个新函数,同时指定该新函数的this
值和初始参数。- 语法:
bind(thisArg, arg1, arg2, /* …, */ argN)
- 参数
thisArg
:调用新函数时要使用的this
值。非严格模式下,null
和undefined
被替换为全局对象,原始值被转换为对象。如果新函数被当作构造函数使用,则该参数会被忽略。arg1, arg2, /* …, */ argN
:调用新函数时,插入到传入新函数中的参数前的可选的初始参数。
- 返回值:使用指定的
this
值和可选的初始参数创建的新函数。
- 语法:
访问器装饰器
-
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* 访问器装饰器
* @param target 对于静态访问器,该参数是类;对于实例访问器,该参数是类的原型对象
* @param propertyKey 访问器名称
* @param descriptor 访问器的描述对象,对应 get 和 set 属性是被装饰的访问器方法
*/
function AccessorDecoratorDemo(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target, propertyKey, descriptor);
}
class Person {
constructor(public name: string, public age: number) {}
get school() {
return "XDU";
}
static get address() {
return "Xi'an";
}
} -
应用举例:对
Weather
类的temp
属性的set
访问器进行限制,允许设置的取值范围为[-50, 50]
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/**
* 生成限制数值范围的访问器装饰器工厂
* @param min 最低取值
* @param max 最高取值
* @returns 限制数值范围的访问器装饰器
*/
function RangeValidate(min: number, max: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
/* 1. 缓存原始的 setter */
const original = descriptor.set;
/* 2. 替换 setter,加入范围验证逻辑 */
descriptor.set = function (value: number) {
if (value < min || value > max)
throw new Error(
`${propertyKey} 的取值范围应该在 ${min} 和 ${max} 之间`
);
if (original) original.call(this, value);
};
};
}
class Weather {
private _temp: number;
constructor(temp: number) {
this._temp = temp;
}
50, 50) set temp(value: number) { (-
this._temp = value;
}
get temp() {
return this._temp;
}
}
const w = new Weather(20);
参数装饰器
-
语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 参数装饰器
* @param target 修饰静态方法的参数时,该参数是类;修饰实例方法的参数时,该参数是类的原型对象
* @param propertyKey 参数所在方法的名称
* @param parameterIndex 参所在方法的参数列表中的索引(0-based)
*/
function ArgumentDecoratorDemo(
target: object,
propertyKey: string,
parameterIndex: number
) {
console.log(target, propertyKey, parameterIndex);
}
class Person {
constructor(public name: string) {}
speak(message: string) {
console.log(`${this.name} say: ${message}`);
}
} -
应用举例:定义方法装饰器
Validate
,同时搭配参数装饰器NotNumber
,来对指定方法的参数类型进行限制。NotNumber
限制指定参数不能是数值类型,Validate 来验证使用NotNumber
装饰器的参数是否满足条件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49/**
* 记录类型不为 number 的参数的参数装饰器
* @param target 类的原型对象
* @param propertyKey 参数所在的方法名
* @param parameterIndex 参数在方法参数列表中的索引
*/
function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
/* 存储方法 propertyKey 中所有限制类型不为数字的参数索引 */
const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
notNumberArr.push(parameterIndex);
target[`__notNumber_${propertyKey}`] = notNumberArr;
}
/**
* 验证是否所有使用了 NotNumber 参数装饰器的参数都符合条件
* @param target 类的原型对象
* @param propertyKey 方法名
* @param descriptor 方法的描述对象
*/
function Validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
/* 缓存原始方法 */
const originalMethod = descriptor.value;
/* 替换原始方法 */
descriptor.value = function (...args: any[]) {
const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
notNumberArr.forEach((index) => {
if (typeof args[index] === "number")
throw new Error(`第 ${index} 个参数不能为 number`);
});
return originalMethod.call(this, ...args);
};
}
class Person {
constructor(public name: string) {}
speak(message: any) {
console.log(`${this.name} say: ${message}`);
}
}
const p = new Person("Tom");
p.speak("Hello");
p.speak(1);