用大白话讲 TypeScript,两小时快速上手TypeScript (下)
start
- 上篇文章介绍了
ts
的一些基础内容,本文继续再介绍ts
后续内容。 - 上篇文章点此直达:用大白话讲 TypeScript,两小时快速上手TypeScript (上)
5. 泛型
5.1 初见泛型
在翻看 vue3
源码的时候,我发现有一个这么一段代码:
interface Matchers<R, T> {
toHaveBeenWarned(): R
toHaveBeenWarnedLast(): R
toHaveBeenWarnedTimes(n: number): R
}
上面的代码有什么是我们不熟悉的?
- 尖括号;
- 大写字母
T
, 大写字母R
。
不要被新奇的内容和陌生的名称吓到,不要慌,我们一起学习并掌握它。
5.2 泛型是什么?
在定义类型的时候,有这种需求场景:
有些时候希望函数返回值的类型与参数类型是相关的。
举个例子:
我有一个函数 demo
,传入 a
,返回 a
。返回值的类型由传入参数的类型决定。我想在 ts
代码中,反映出:参数与返回值之间的类型关系。该如何实现呢?
function demo(a) {
return a
}
比如:
- 传入一个数字类型的,我希望返回值是数字类型的。
- 传入字符串类型的,我希望返回值是字符串类型的;
- 传入一个满足某个接口的对象,我希望返回值也满足这个接口的类型。
我的思考:想要实现这个功能,最好有一个变量来代替这个未知的类型,然后在声明类型的时候,使用这个变量当做 类型名 去声明。
为了解决这个问题,TypeScript
就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。
那泛型怎么写呢?我个人理解:尖括号中定义变量名,使用这个变量名当做类型名去声明类型。
举个例子:
// 1. 定义一个泛型,这个泛型有一个变量 T, 然后这个接口中,name的类型就是由传入的 T 决定
interface tomato<T> {
name: T
}
// 2. 传入string字符串类型。使用接口tomato的时候,我们的name可以输入'lazyTomato' (字符串类型)
var a: tomato<string> = {
name: 'lazyTomato',
}
// 3. 传入number数字类型,使用接口tomato的时候,我们的name可以输入123 (数字类型)
var b: tomato<number> = {
name: 123,
}
// 4. 传入一个对象,使用接口tomato的时候,我们的name为满足对应条件的对象
var c: tomato<{
like: string; age: number }> = {
name: {
like: '吃番茄',
age: 18,
},
}
个人理解
泛型的写法,在不同的场景下有很多种。简化看一下,其实是:用尖括号定义变量,然后使用变量去声明类型,仅此而已。
泛型的作用,能让我们更加灵活的声明类型,但是也一定程度上增加了阅读难度。
更详细的介绍:TypeScript 教程–阮一峰–泛型的写法
说下我学习泛型的时候,觉得比较困惑的点。
-
不要被 泛型 这个名称吓到;
不要被名字吓到,以为是什么很高深的东西。开个玩笑来理解它:
它就是一个变化的类型,然后简称变形不好听,就叫泛型了
-
不要被 泛型中变量名 吓到;
-
最初的时候,看别人写的泛型:
<T>
,<Roansmaso>
,也是各种复杂的变量名,以为是某种API,感觉很陌生和晦涩难懂。 -
现在告诉自己,尖括号中就是变量而已。
-
5.3 小试牛刀
我们再回头看一下,5.1 中的泛型。
interface Matchers<R, T> {
toHaveBeenWarned(): R
toHaveBeenWarnedLast(): R
toHaveBeenWarnedTimes(n: number): R
}
既然我们知道泛型是什么了,我们结合上述的代码,尝试定义一个符合对应要求的变量。
interface Matchers<R, T> {
toHaveBeenWarned(): R // 函数返回值的类型是 R
toHaveBeenWarnedLast(): R // 函数返回值的类型是 R
toHaveBeenWarnedTimes(n: number): R // 函数的参数类型是艺术字,函数返回值的类型是 R
}
let obj: Matchers<string, number> = {
toHaveBeenWarned: function () {
return '111'
},
toHaveBeenWarnedLast() {
return '222'
},
toHaveBeenWarnedTimes(num) {
return num + '233313'
}
}
/* 1. T 没有用到,所以颜色偏灰; 2. R 我传入了一个字符串,则这个接口中的函数返回值都是字符串类型; 3. toHaveBeenWarned,toHaveBeenWarnedLast 后者是函数的简写形式,为了好理解,我分别用两种形式写的案例。 */
6. 装饰器
在 cocos create3.x
中,默认的代码模板是这样的
import {
_decorator, Component } from 'cc';
const {
ccclass, property } = _decorator;
@ccclass('HelloWorld')
export class HelloWorld extends Component {
@property
serializableDummy = 0;
}
有一个困惑我非常久的代码,@ccclass
, @property
是什么?
先给自己看的懂的内容加入注释:
import {
_decorator, Component } from 'cc'; // 从 cc 中引入内容
const {
ccclass, property } = _decorator; // 解构的方式,得到两个变量 ccclass, property
@ccclass('HelloWorld') // 未知
export class HelloWorld extends Component {
// 导出一个HelloWorld类,该类继承自 Component
@property // 未知
serializableDummy = 0;
}
/* 猜想: 1. @ccclass('HelloWorld') 能接括号,ccclass很大可能是函数。 2. decorator 英译: 装饰器 */
6.1 什么是 装饰器?
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
装饰器(Decorator)用来增强 JavaScript 类(class)的功能。
个人理解:装饰器可以拓展类的功能。
关于装饰器的版本
TypeScript 从早期开始,就支持装饰器。但是,装饰器的语法后来发生了变化。ECMAScript 标准委员会最终通过的语法标准,与 TypeScript 早期使用的语法有很大差异。
目前,TypeScript 5.0 同时支持两种装饰器语法。标准语法可以直接使用,传统语法需要打开--experimentalDecorators 编译参数。
个人理解:早期 JS 原生并不支持装饰器,此时的 TS 支持装饰器语法。后来 JS 原生支持了装饰器(目前处于第三阶段),此时 TS 的装饰器和 JS 原生的装饰器实现方式有差异,然后为了适配,TS 推出新版本标准语法装饰器。历史版本称为:旧版(或传统)。
6.2 如何定义装饰器?
6.2.1 装饰器特征
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@
,后面是一个表达式。
(2)@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
不要怕,接下来和我一起图文示例,从0到1。
6.2.2 尝试定义一个装饰器
我比较喜欢番茄,那我们就定义一个 番茄(tomato
)装饰器。用来装饰:demo
类。
@tomato // 编辑器会提示报错:找不到名称 tomato
class demo {
}
继续看装饰器语法上的定义,@
后的表达式必须是一个函数(或者执行后可以得到一个函数)。我们继续完善上面的代码
@tomato // 编辑器会提示报错:“tomato”收到的参数过少,无法在此处充当修饰器。你是要先调用它,然后再写入 "@tomato()" 吗?
class demo {
}
function tomato() {
console.log('tomato函数执行1')
}
提示 tomato
收到的参数过少,无法充当修饰器(装饰器)。这里的参数具体数量我们不清楚,我尝试给 tomato
加一个形参 a1
。
@tomato // 编辑器会提示报错:The runtime will invoke the decorator with 2 arguments, but the decorator expects 1.
class demo {
}
function tomato(a1) {
console.log('tomato函数执行1', a1)
}
The runtime will invoke the decorator with 2 arguments, but the decorator expects 1. 英译:运行时将使用2个参数调用decorator,但decorator使用1个参数。 意思:就是需要两个参数
根据提示,我们继续补充一个参数 a2
,此时代码就没有报错了。
@tomato
class demo {
}
function tomato(a1, a2) {
console.log('tomato函数执行1', a1, a2)
}
6.2.3 tsc 初体验
截止到目前,我们定义的 tomato
满足装饰器特征。我很好奇参数 a1
,a2
存储的是什么?以及 tomato
又是如何能影响 class
的功能呢?
目前主流的浏览器环境不支持运行装饰器语法的 JS。而且 TS 语法不能直接在浏览器中运行,如果想验证我的疑问,需要借助编译器
tsc
,将我们的ts
代码降级编译一下。正好我们可以熟悉一下
tsc
编译的.ts
文件的过程。下面请跟我来:
首先全局安装 typescript
。
npm i typescript -g
为了方便理解,我微调一下 装饰器 tomato
的打印,并保存文件为 1.ts
。
1.ts
@tomato
class demo {
}
function tomato(a1, a2) {
console.log('tomato函数执行了')
console.log('a1: ', a1)
console.log('a2: ', a2)
}
此时可以使用 tsc 文件名.ts
,编译对应文件。此时会得到同名的 .js
文件。
我编译出来的文件
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) {
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {
});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {
};
for (var p in contextIn) context[p] = p === "access" ? {
} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) {
if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? {
get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
return Object.defineProperty(f, "name", {
configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
};
var demo = function () {
var _classDecorators = [tomato];
var _classDescriptor;
var _classExtraInitializers = [];
var _classThis;
var demo = _classThis = /** @class */ (function () {
function demo_1() {
}
return demo_1;
}());
__setFunctionName(_classThis, "demo");
(function () {
var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
__esDecorate(null, _classDescriptor = {
value: _classThis }, _classDecorators, {
kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
demo = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, {
enumerable: true, configurable: true, writable: true, value: _metadata });
__runInitializers(_classThis, _classExtraInitializers);
})();
return demo = _classThis;
}();
function tomato(a1, a2) {
console.log('tomato函数执行了');
console.log('a1: ', a1);
console.log('a2: ', a2);
}
执行结果:
6.2.4 装饰器的参数
结合我们上述执行的代码截图,我们来熟悉一下装饰器的参数。
装饰器函数的类型定义如下:
type Decorator = (
value: DecoratedValue,
context: {
kind: string;
name: string | symbol;
addInitializer?(initializer: () => void): void;
static?: boolean;
private?: boolean;
access: {
get?(): unknown;
set?(value: unknown): void;
};
}
) => void | ReplacementValue;
对应的参数解释:
-
value
:所要装饰的值,某些情况下可能是undefined
(装饰属性时)。 -
context
:上下文信息对象。 -
装饰器函数的返回值,是一个新版本的装饰对象,但也可以不返回任何值(void)。
context
对象有很多属性,其中kind
属性表示属于哪一种装饰,其他属性的含义如下。
kind
:字符串,表示装饰类型,可能的取值有class
、method
、getter
、setter
、field
、accessor
。name
:被装饰的值的名称。access
:对象,包含访问这个值的方法,即存值器和取值器。static
: 布尔值,该值是否为静态元素。private
:布尔值,该值是否为私有元素。addInitializer
:函数,允许用户增加初始化逻辑。
个人理解:
装饰器的参数基本上都是和我们被修饰的内容有关。其中包含的参数名比较多,我们可以选择性记忆即可。
比如常见的三个:
kind
:表示装饰类型;name
:被装饰的值的名称;addInitializer
:函数,允许用户增加初始化逻辑。
图解说明:
6.2.5 装饰器如何影响 class
为了验证 装饰器如何影响 class的,我们编写如下代码:
@tomato
class demo {
constructor() {
console.log('demo开始实例化了')
}
}
function tomato(value, context) {
console.log('装饰器执行了')
if (context.kind === 'class') {
// 1. 判断是不是修饰的类
value.prototype.greet = function () {
// 2. 给类的原型上增加方法。
console.log('你好')
}
}
}
var d = new demo()
d.greet() // 你好
编译后执行
个人理解:
- 装饰器会是一个函数(或者返回一个函数);
- 在实例化类的时候,装饰器会先执行;
- 装饰器在执行时,扩充了
demo
原型上的方法;
7. enmu (枚举)
enmu
英译:枚举;
很多时候我们需要,对多个成员作区分。
举个例子:
定义性别的时候,区分男,女;
定义方向的时候,区分上,下,左,右;
在代码里面如何体现的呢?用中文肯定不行,所以需要有一个英文来表示这些类型。
// 编译前
enum Sex {
Male, // 男
Famale, // 女
}
// 编译后
let Sex = {
Male: 0, // 男
Famale:1, // 女
};
enmu
结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。
个人理解:
enmu
枚举,当需要对某些成员
作区分,不关心成员
的值的时候,我们就可以考虑用枚举去表示这些成员
。让我们更加关注成员
本身。
8. 数组
8.1 普通数组
// 1. 声明只能存储数字
let arr: number[] = [1, 2, 3]
// 2. 声明 存储数字或者字符串
let arr2: (number | string)[] = [1, 2, '3']
// 3. 使用 type 定义别名,简写
type t1 = number | string
let arr3: t1[] = [1, 2, '3']
// 4. 使用 TypeScript 内置的 Array 接口
let arr4: Array<number> = [1, 2, 3]
8.2 元组 (tuple)
在TypeScript(TS)中,元组(Tuple)是一种特殊的数据结构,用于表示具有固定数量和类型的有序元素集合。
元组可以看作是一个固定长度的数组。
// 元组
let t: [number] = [1]
t = [1, 2]
9. 总结
学习到这里,给自己绘制一个思维导图,重新梳理一下目前已经学习的内容。
10. 参考文章
end
-
感谢阅读,希望这篇博客对你有帮助。后续如果我再遇到疑惑的语法,还会在写博客做说明补充,加油ヾ(◍°∇°◍)ノ゙。
-
这篇文章只是起点,不是终点,希望“我们”都不要停下学习的脚步。
文章评论