用大白话讲 TypeScript,两小时快速上手TypeScript (下) (4)

用大白话讲 TypeScript,两小时快速上手TypeScript (下)

start

5. 泛型

5.1 初见泛型

在翻看 vue3 源码的时候,我发现有一个这么一段代码:

interface Matchers<R, T> {
  toHaveBeenWarned(): R
  toHaveBeenWarnedLast(): R
  toHaveBeenWarnedTimes(n: number): R
}

上面的代码有什么是我们不熟悉的?

  1. 尖括号;
  2. 大写字母 T, 大写字母 R

不要被新奇的内容和陌生的名称吓到,不要慌,我们一起学习并掌握它。

5.2 泛型是什么?

在定义类型的时候,有这种需求场景:

有些时候希望函数返回值的类型参数类型相关的。

举个例子:

我有一个函数 demo,传入 a,返回 a。返回值的类型由传入参数的类型决定。我想在 ts 代码中,反映出:参数与返回值之间的类型关系。该如何实现呢?

function demo(a) {
  return a
}

比如:

  1. 传入一个数字类型的,我希望返回值是数字类型的。
  2. 传入字符串类型的,我希望返回值是字符串类型的;
  3. 传入一个满足某个接口的对象,我希望返回值也满足这个接口的类型。

我的思考:想要实现这个功能,最好有一个变量来代替这个未知的类型,然后在声明类型的时候,使用这个变量当做 类型名 去声明。

为了解决这个问题,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 教程–阮一峰–泛型的写法

说下我学习泛型的时候,觉得比较困惑的点。

  1. 不要被 泛型 这个名称吓到;

    不要被名字吓到,以为是什么很高深的东西。开个玩笑来理解它:它就是一个变化的类型,然后简称变形不好听,就叫泛型了

  2. 不要被 泛型中变量名 吓到;

    • 最初的时候,看别人写的泛型:<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 满足装饰器特征。我很好奇参数 a1a2 存储的是什么?以及 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;

对应的参数解释:

  1. value:所要装饰的值,某些情况下可能是undefined(装饰属性时)。

  2. context:上下文信息对象。

  3. 装饰器函数的返回值,是一个新版本的装饰对象,但也可以不返回任何值(void)。

context对象有很多属性,其中kind属性表示属于哪一种装饰,其他属性的含义如下。

  • kind:字符串,表示装饰类型,可能的取值有classmethodgettersetterfieldaccessor
  • name:被装饰的值的名称。
  • access:对象,包含访问这个值的方法,即存值器和取值器。
  • static: 布尔值,该值是否为静态元素。
  • private:布尔值,该值是否为私有元素。
  • addInitializer:函数,允许用户增加初始化逻辑。

个人理解:

装饰器的参数基本上都是和我们被修饰的内容有关。其中包含的参数名比较多,我们可以选择性记忆即可。

比如常见的三个:

  1. kind:表示装饰类型;
  2. name:被装饰的值的名称;
  3. 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

  • 感谢阅读,希望这篇博客对你有帮助。后续如果我再遇到疑惑的语法,还会在写博客做说明补充,加油ヾ(◍°∇°◍)ノ゙。

  • 这篇文章只是起点,不是终点,希望“我们”都不要停下学习的脚步。

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lazy_tomato

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值