TypeScript的学习实践二

748 阅读32分钟

建议可以配合TypeScript的学习实践一学习效果更佳哦💖💖

类型系统深⼊

  • 类型保护:我们通常在 JavaScript 中通过判断来处理⼀些逻辑,在 TypeScript 中这种条件语句块还有另外⼀个特性:根据判断逻辑的结果,缩⼩类型范围(有点类似断⾔),这种特性称为 类型保护
  • 触发条件:逻辑条件语句块:if、else、elseif等等或者特定的⼀些关键字typeof、instanceof、in等等
  • typeof:我们知道 typeof 可以返回某个数据的类型,在 TypeScript 在 if 、 else 代码块中能够把typeof 识别为类型保护,推断出适合的类型
function fn(a: string|number) {

    //这里的a变量的是可能是字符串,
    //但也有可能是数字,
    //所以ts告诉我们,你不能直接作为字符串去使用,有风险
    //类型“number”上不存在属性“substring”。
    // a.substring(1,2);

    
    //类型“string”上不存在属性“toFixed”。
    // a.toFixed(1);

    // 解决方式一:通过类型断言
    // (<string> a).substring(1);


    //解决方式二:缩小到指定范围在判断
    //这里使用  typeof 
    if (typeof a === 'string') {
        a.substring(1);
    } else {
        a.toFixed(1);
    }


}
  • instanceof: typeof 类似的, instanceof 也可以被 TypeScript 识别为类型保护
function myFn(a:Date|Array<any>){
    if(a instanceof Array){
        a.push(1)
    }else{
        a.getFullYear();
    }
}

  • in:in也可以被 TypeScript 识别为类型保护
interface IA{
    x:string,
    y:string
}
interface IB{
    name:number,
    age:number
}
function myFun(arg: IA | IB) {
    if('x' in arg){
        //ok
        arg.x

        //error:类型“IA”上不存在属性“name”。
        // arg.name
    }else{
        //ok
        arg.age

        //error:类型“IB”上不存在属性“x”。
        // arg.x
    }
}
  • 字⾯量类型保护:如果类型为字⾯量类型,那么还可以通过该字⾯量类型的字⾯值进⾏推断
interface IA{
    call:'IA',
    x:string,
    y:string
}
interface IB{
    call:'IB',
    name:number,
    age:number
}
function myFun(arg: IA | IB) {
    if(arg.call === 'IA'){
        //ok
        arg.x

        //error:类型“IA”上不存在属性“name”。
        // arg.name
    }else{
        //ok
        arg.age

        //error:类型“IB”上不存在属性“x”。
        // arg.x
    }
}
  • ⾃定义类型保护:有的时候,以上的⼀些⽅式并不能满⾜⼀些特殊情况,则可以⾃定义类型保护规则:(data is Element[]|NodeList 是⼀种类型谓词,格式为: xx is XX ,返回这种类型的函数就可以被 TypeScript 识别为类型保护)
// // 自定义类型保护 
// 假设数据是Element[]|NodeList都是可以调用forEach的类型
function canEach(data: any): data is Element[]|NodeList {
    //判断数据可不可以调用forEach方法 可以返回true
    return data.forEach !== undefined;
}

function fn23(elements: Element[]|NodeList|Element) {

    // //类型“Element”上不存在属性“forEach”。
    // if(elements.forEach){
    //     //elements上可能不存在forEach方法
    //     //未进行类型保护
    // }


    // 第一步 指定一个可以返回true 或 false 的函数
    // 第二步 指定条件(类型谓词):data is Element[]|NodeList 
    if ( canEach(elements) ) {
        elements.forEach( (element: Element) => {
            element.className = '';
        } )
    } else {
        elements.className = '';
    }

}

  • 类型操作:TypeScript 提供了⼀些⽅式来操作类型这种数据,但是需要注意的是,类型数据只能作为类型来使⽤,⽽不能作为程序中的数据,这是两种不同的数据,⼀个⽤在编译检测阶段,⼀个⽤于程序执⾏阶段
  • typeof:在 TypeScript 中, typeof 有两种作⽤分别是获取数据的类型和捕获数据的类型
//注意这里是let
let str1 ='lth'

//如果是let,把'string'做为值赋值给t
//这里的typeof起到了获取数据的类型的作用
let t=typeof str1


//如果是type(类型别名),把'string'做为类型
//type 声明的是类型名称,这个值只是在ts编译检测阶段使用
type myType = typeof str1;

//
let str2:myType ='lth1'

//这里的typeof起到了捕获数据的类型的作用
let str3:typeof str1='lth2'
  • keyof:获取类型的所有 key 的集合
interface Person {
    name: string;
    age: number;
};

//等同于type personKeys = "name" | "age" 
type personKeys = keyof Person;


---------------------------------


let p11 = {
    name: 'lth',
    age: 35
}


//等同于type PT = {name: string;age: number;}
//type 声明的是类型名称
type PT = typeof p11;


------------------------------------


let p11 = {
    name: 'lth',
    age: 35
}


//等同于type PT = {name: string;age: number;}
type PT = typeof p11;


// keyof只能针对类型操作   
// p11是一个值 得加上typeof
// 等同于k: "name" | "age"
function getPersonVal(k: keyof typeof p11) {
    return p11[k];
}
  • in:针对类型进⾏操作的话,内部使⽤的 for…in 对类型进⾏遍历,in 后⾯的类型值必须是 string 或者 number 或者 symbol
interface Person {
    name: number;
    age: number;
}
type personKeys = keyof Person;
type newPerson = {
    [k in personKeys]: number
}

// 等同 [k in 'name'|'age']: number;
// 也可以写成[k in keyof Person]: number;
/**
 * 相当于
 * type newPerson = {
 *      name: number;
 *      age: number;
 * }
 */

  • 类型兼容:我们先来看一下这个例子体会一下类型兼容:这种基于结构⼦类型的类型系统是基于组成结构的,只要具有相同类型的成员,则两种类型即为兼容的
class Person {
   name: string;
   age: number; 
 }
 
 class Cat {
   name: string;
   age: number; 
 }
 
 function fn(p: Person) { console.log(p.name) }
 
 let xiao = new Cat();
 // 因为 Cat 类型的结构与 Person 类型的结构相似,所以它们是兼容的
 fn(xiao);
  • 在上面例子中我们无法利用ts编译器智能的告诉我们类底下的属性到底是属于哪个类的,我们可以可以用类与接口(implement)来改造一下上面的例子:
//定义一个接口告知必须实现
interface IFly {
    fly(): void;
}

class Person implements IFly {
    name: string;
    age: number;

    study() {

    }

    fly() {}
}

class Cat implements IFly {
    name: string;
    age: number;

    catchMouse() {}

    fly() {}
}

let p1 = new Person();
let c1 = new Cat();

function fn1(arg: Person) {
    arg.name;
    // arg.xx  是有智能提示的
}


//结构满足代码不出错就可以成功即在使用上没有问题
fn1( p1 )

//下一行报错
//fn1( c1 )

function fn2( arg: IFly ) {
    arg.fly();
    
}

fn2(p1);
fn2(c1);

泛型

  • 为什么要使用泛型:许多时候,标注的具体类型并不能确定,比如一个函数的参数类型
  • 所谓的泛型,就是给可变(不定)的类型定义变量(参数)
  • 例子一:
//  尖括号
function getVal<T>(obj: T, k: keyof T) {
    return obj[k];
}

let obj1 = {
    x: 1,
    y: 2
}
let obj2 = {
    username: 'lth',
    age: 35
}


//传入<>中的是类型不是具体值
getVal<typeof obj1>( obj1, 'x' );
getVal<typeof obj2>( obj2, 'age' );

//发生了类型推导写法也没错
getVal( obj2, 'age' );
  • 例子二:
abstract class Component<T1, T2> {

    props: T1;
    state: T2;

    constructor(props: T1) {
        this.props = props;
    }
    abstract render(): string;

}

interface IMyComponentProps {
    val: number;
}
interface IMyComponentState {
    x: number;
}

//泛型类型“Component<T1, T2>”需要 2 个类型参数。
class MYComponent extends Component<IMyComponentProps, IMyComponentState>{
    constructor(props:IMyComponentProps){
        super(props)

        this.state={
            x:1
        }
    }
    
    //必须实现抽象类中的所有抽象方法
    render(){
        console.log(this.props.val,'有提示了props下有val');
        console.log(this.state.x,'有提示了state下有x');
        return 'myComponent'
    }
}

//对象文字可以只指定已知属性,并且“x”不在类型
let myComponents=new MYComponent({val:2});

  • 泛型接口:我们来看一下泛型与接口结合的例子:
interface IResponseData<T> {
    code: number;
    message?: string;
    //接口中定义泛型 IResponseUserData或者IResponseArticleData
    data: T;  
}

// 用户接口
interface IResponseUserData {
    id: number;
    username: string;
    email: string;
}
// 文章接口
interface IResponseArticleData {
    id: number;
    title: string;
    author: IResponseUserData;
}

//函数中使用泛型
async function getData<U>(url: string) {
    let response = await fetch(url);
    //data是promise类型的
    let data: Promise<IResponseData<U>> = await response.json();
    return data;
}

(async function() {

    let userData = await getData<IResponseUserData>('/user');
    //满足此接口的特征有智能提示
    userData.data.id;


    //当调用getData函数时
    //确定泛型接口的U
    //即ResponseArticleData或者IResponseUserData

    let articleData = await getData<IResponseArticleData>('/article');
    //满足此接口的特征有智能提示
    articleData.data.author;

})()

模块系统

  • 模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能
  • 从基于 Node.js 的服务端 commonjs 模块化,到前端基于浏览器的 AMDCMD 模块化,再到 ECMAScript2015 开始原生内置的模块化, JavaScript 的模块化方案和系统日趋成熟。
  • TypeScript 也是支持模块化的,而且它的出现要比 ECMAScript模块系统标准化要早,所以在 TypeScript 中即有对 ECMAScript 模块系统的支持,也包含有一些自己的特点
  • 无论是那种模块化规范,重点关注:保证模块独立性的同时又能很好的与其它模块进行交互

  • CommonJS
  • 导出模块内部数据:通过 module.exportsexports 对象导出模块内部数据
let a = 1;
let b = 2;

//module.exports和exports.value=xx可以同时存在
module.exports = {
    x: a,
    y: b
}

//通过module.exports={}会发生覆盖
// console.log(module,'1');
exports.a=1
exports.b=2

//{ a: 1, b: 2 }
//通过exports.value=xx能够不断添加
console.log(exports);
  • 导入外部模块数据:通过 require 函数导入外部模块数据
let b = require('./b');
//{ a: 1, b: 2 }
// { x: 1, y: 2 }
console.log(b);

  • AMD
  • 因为 CommonJS 规范一些特性(基于文件系统,同步加载),它并不适用于浏览器端,所以另外定义了适用于浏览器端的规范,浏览器并没有具体实现该规范的代码,我们可以通过一些第三方库来解决,这里引入requireJS
  • 在html中引入CDN<script data-main="js/a" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
  • 通过一个 define 方法来定义一个模块,在该方法内部模拟模块独立作用域;导出模块内部数据通过 return 导出模块内部数据;导入外部模块数据通过前置依赖列表导入外部模块数据
  • 写法一:
//导出
//写法一
define(function(require, exports, module) {
    let a = 1;
    let b = 2;

    //写法一
    return {
        x: a,
        y: b
    }

} );

//导入
//写法一
define(['./b'], function(b) {
    ////写法一的引入对应着写法一的导出
    //{x: 1, y: 2}
    console.log(b);
});


//在html中引入requirejs并指定入口文件
<script src="./libs/require.min.js" data-main="./js/a.js"></script>
  • 写法二:
//写法二导出
define(function(require, exports, module) {
    let a = 1;
    let b = 2;


    //写法二:
    module.exports === exports;  // true

    exports.x = a;
    exports.y = b;

    module.exports = {
        x: a,
        y: b
    }
} );


//写法二导入
define(function(require, exports, module) {
    // console.log(b);


    //写法二的引入对应着写法二的导出
    //{x: 1, y: 2} "通过第二种方式引入b"
    let b = require('./b');
    console.log(b,'通过第二种方式引入b');
});


//在html中引入requirejs并指定入口文件
<script src="./libs/require.min.js" data-main="./js/a.js"></script>

  • UMD:严格来说,UMD 并不属于一套模块规范,它主要用来处理 CommonJSAMDCMD 的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行。随着 Node.js 的流行,前端和后端都可以基于 JavaScript 来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。
  • 我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD 就是一种解决方式
//定义一个函数并且立即执行
(function(root, factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        //在node.js环境下可以使用
        module.exports = factory();
    } else if (typeof define === "function" && define.amd) {
        //在浏览器环境下可以使用
        define( factory );
    } else {
        //暴露在全局  可能是window  global
        root.lth = factory;
    }
})(this, function() {
    let a = 1;
    let b = 2;

    return {
        x: a,
        y: b
    }
})

  • ESM:从 ECMAScript2015/ECMAScript6 开始,JavaScript 原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js 也有了支持,所以未来基于 JavaScript 的程序无论是在前端浏览器还是在后端 Node.js 中,都会逐渐的被统一
  • 独立模块作用域:一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式 下,即:'use strict'
  • script 标签需要声明 type="module" 不要用本地协议即file://去打开文件
  • 导出模块内部数据:使用 export 语句导出模块内部数据
  • 我们先来看导出:
//导出

let a = 1;
let b = 2;
let c = 3;
let d = 4;


// export可以写多个
export {a as x,b as y}
export {c,d}


// export default智能使用一个
// export default a + b;
export default {
    a:2,
    name:function(){
        return 'name'
    }
}
  • 我们在来看导入:
//导入

import v, {x, y,c,d} from './b.js';

// x:1 y:2 v.a:2 v.name:name
console.log('x:'+x, 'y:'+y, 'v.a:'+v.a,'v.name:'+v.name());
console.log(c,d); //3 4


-----------------------------------------
//在html页面中引入总依赖文件和协商type='module'
<script src="./js/a.js" type="module"></script>
  • 在上面的例子中使用 import 语句导入模块,这种方式称为:静态导入。静态导入方式不支持延迟加载,import 必须在模块的最开始
  • 此外,还有一个类似函数的动态 import(),它不需要依赖 type="module" 的 script 标签。关键字 import 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise
document.onclick = function() {

    import('./b.js').then(data => {
        // console.log(data);
        console.log(data.x, data.y, data.default)
    });

}

TypeScript 的模块系统

  • 虽然早期的时候,TypeScript 有一套自己的模块系统实现,但是随着更新,以及 JavaScript 模块化的日趋成熟,TypeScriptESM 模块系统的支持也是越来越完善
  • 模块:无论是 JavaScript 还是 TypeScript 都是以一个文件作为模块最小单元
  • 任何一个包含了顶级 import 或者 export 的文件都被当成一个模块
  • 相反的一个文件不带有顶级的 import 或者 export ,那么它的内容就是全局可见的
  • 全局模块:如果一个文件中没有顶级 import 或者 export ,那么它的内容就是全局的,整个项目可见的
// a.ts
let a1 = 100;
let a2 = 200;

// b.ts
// ok, 100
console.log(a1);
  • 不推荐使用全局模块,因为它会容易造成代码命名冲突(全局变量污染)

  • 文件模块:任何一个包含了顶级 import 或者 export 的文件都会当做一个模块,在 TypeScript 中也称为外部模块。
  • 模块语法TypeScriptESM 语法类似,使用 export 导出模块内部数据使用 import 导入外部模块数据
  • 模块编译TypeScript 编译器也能够根据相应的编译参数,把代码编译成指定的模块系统使用的代码
  • TypeScripttsconfig.jsmodule 配置选项是用来指定生成哪个模块系统的代码,可设置的值有:"none""commonjs""amd""udm""es6"/"es2015/esnext""System"
  • target=="es3" or "es5":默认使用 commonjs,其它情况,默认 es6

  • 模块导出默认值的问题:如果一个模块没有默认导出
// m1.ts
export let obj = {
  x: 1
}


//如果没有默认导出的话即没有用到default
/**
 * export default {
    x:a1
}
* 
*/
  • 则在引入该模块的时候,需要使用下列一些方式来导入
// main.ts
// error: 提示 m1 模块没有默认导出
import v from './m1'

// 可以简单的使用如下方式
import {obj} from './m1'
console.log(obj.x)
// or
import * as m1 from './m1'
console.log(m1.obj.x)
  • 此时的tsconfig.js的配置如下:

{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es6",
        "module": "commonjs"
        // "module": "amd"
    },
    "include": [
        "./src/**/*"
    ]
}

  • 加载非 TS 文件:有的时候,我们需要引入一些 js 的模块,比如导入一些第三方的使用 js 而非 ts 编写的模块,默认情况下 tsc 是不对非 ts 模块文件进行处理的
  • 我们可以通过 "allowJs": true选项开启该特性
// m1.js
export default 100;
// main.ts
import m1 from './m1.js'

  • ESM 模块中的默认值问题, 在 ESM 中模块可以设置默认导出值
export default 'lth';
  • 但是在 CommonJSAMD 中是没有默认值设置的,它们导出的是一个对象(exports),在 TypeScript 中导入这种模块的时候会出现 模块没有默认导出的错误提示
module.exports.obj = {
    x: 100
}
  • 简单一些的解决方法:
import * as m from './m1.js'
  • 通过tsconfig.js添加配置的解决方法:
{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es6",
        "module": "commonjs",
        "allowJs": true,
        //设置为:`true`,允许从没有设置默认导出的模块中默认导入。
        "allowSyntheticDefaultImports": true,
        //设置为:`true`,则在编译的同时生成一个 `__importDefault` 函数,用来处理具体的 `default` 默认导出
        "esModuleInterop": true
    },
    "include": [
        "./src/**/*"
    ]
}

//注意:以上设置只能当 `module` 不为 `es6+` 的情况下有效

  • 以模块的方式加载 JSON 格式的文件:TypeScript 2.9+ 版本添加了一个新的编译选项:resolveJsonModule,它允许我们把一个 JSON 文件作为模块进行加载
  • 在tsconfig.js配置resolveJsonModule设置为:true ,可以把 json 文件作为一个模块进行解析
{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es6",
        "module": "commonjs",
        "resolveJsonModule": true
    },
    "include": [
        "./src/**/*"
    ]
}
  • 接着就可以以模块的方式加载 JSON 格式的文件
//**data.json**
{
    "name": "lth",
    "age": 35,
    "gender": "男"
}

//**ts文件**
import * as userData from './data.json';
console.log(userData.name);

  • 模块解析策略:
  • 模块解析是指编译器在查找导入模块内容时所遵循的流程。
  • 根据模块引用是相对的还是非相对的,模块导入会以不同的方式解析。
  • 相对导入是以 /./../ 开头的引用
// 导入根目录下的 m1 模块文件
import m1 from '/m1'
// 导入当前目录下的 mods 目录下的 m2 模块文件
import m2 from './mods/m2'
// 导入上级目录下的 m3 模块文件
import m3 from '../m3'
  • 所有其它形式的导入被当作非相对的
import m1 from 'm1'
  • 在typescript中可以在tsconfig.js中配置模块解析策略。为了兼容不同的模块系统(CommonJSESM),TypeScript 支持两种不同的模块解析策略:NodeClassic,当 --module 选项为:AMDSystemES2015 的时候,默认为 Classic ,其它情况为 Node
 "moduleResolution": "classic"
  • Classic 模块解析策略是 TypeScript 以前的默认解析策略,它已经被新的 Node 策略所取代,现在使用该策略主要是为了向后兼容

  • TypeScript 模块解析策略TypeScript 现在使用了与 Node.js 类似的模块解析策略,但是 TypeScript 增加了其它几个源文件扩展名的查找(.ts.tsx.d.ts),同时 TypeScriptpackage.json 里使用字段 types 来表示 main 的意义

  • 命名空间:在 TS 中,exportimport 称为 外部模块,TS 中还支持一种内部模块 namespace,它的主要作用只是单纯的在文件内部(模块内容)隔离作用域
namespace k1 {
    let a = 10;
    export var obj = {
        a
    }
}

namespace k2 {
    let a = 20;
    console.log(k1.obj);
}

装饰器

  • 装饰器(Decorators)TypeScript 中是一种可以在不修改类代码的基础上通过添加标注的方式来对类型进行扩展的一种方式
  • 装饰器本质就是一个函数,通过特定语法在特定的位置调用装饰器函数即可对数据(类、方法、甚至参数等)进行扩展
  • 在使用装饰器语法时需要在tsconfig.json中启用装饰器特性,即设置experimentalDecorators: true
  • 下面的例子是使用装饰器装饰在类上面的方法:
  • 实现的需求是希望在调用该类的方法的时候记录一下这个方法使用到的数据日志信息并打印在控制台中
//第一步:定义类的方法并在类的方法上使用装饰器

class M {


    //使用方式是在你希望去装饰的方法或者其他的前面去调用它
    //装饰器不仅能使用在类的方法上面还可以使用在类中的其他位置使用
    @log 
    static add(a: number, b: number) {
        return a + b;
    }



    @log
    static sub(a: number, b: number) {
        return a - b;
    }
}

//第二步:自定义装饰器函数
function log(target: Function, name: string, descriptor: PropertyDescriptor) {
	
    //获取原函数方法
    let fn = descriptor.value;
    
    
    //重构原函数方法注入一些自定义拓展的行为
    descriptor.value = function(a: number, b: number) {
    
        //这里是为了存储并调用原方法的行为
        //达到不破坏原方法的行为的目的
        let result = fn(a, b);
    	
        //调用原函数时注入一些自定义拓展的行为1
    	console.log('这是新的方法')
        //调用原函数时注入一些自定义拓展的行为2
        console.log('日志:', {
          name,
          a,
          b,
          result
        });
		
        
        //需要把结果return出去否则该函数的返回值永远是undefined
        return result;
    }
}


//第三步:调用类的方法
let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
  • 注意点:就算不去调用这个类方法也可以输出target, name, descriptor,这说明了在类被创建的过程中就可以执行装饰器了,并不是类中的方法被执行了才能执行装饰器
  • 注意点二:装饰器函数在装饰类的方法时,装饰器函数的参数分别是:target参数是被装饰的方法所属的类,这是所属Function;装饰器函数中name参数是被装饰的方法的名称,这是是add和sub;装饰器函数中descriptor参数是描述符
  • 我们可以观察到在不破坏原先类的的方法的结构上我们通过装饰器注入了新的自定义行为:
  • 装饰器的更多使用场景(先来看一下例子):
  • 装饰器 是一个函数,它可以通过 @装饰器函数 这种特殊的语法附加在 方法访问符(get、set)属性参数 上,对它们进行包装,然后返回一个包装后的目标对象方法访问符属性参数
  • 装饰器工作是在类的构建阶段,而不是使用阶段。即在类的构建阶段装饰器就可以工作(使用)了。
function 装饰器1() {}
...装饰器函数...

@装饰器1:类装饰器
class MyClass {
  
  @装饰器2:属性装饰器
  a: number;
  
  @装饰器3:属性装饰器
  static property1: number;
  
  @装饰器4:访问器装饰器
  get b() { 
    return 1; 
  }
  
  @装饰器5:访问器装饰器
  static get c() {
    return 2;
  }
  
  @装饰器6:方法装饰器
  @装饰器5 :参数装饰器
  public method1(@装饰器5 x: number) {
    //todo...
  }
  
  @装饰器7:方法装饰器
  //静态方法直接通过类来调用而不是通过实例对象调用且该方法不会被实例继承
  public static method2() {}
}
  • 类装饰器:
  • 类装饰器的应用目标是在类的构造函数上
  • 类装饰器只有一个参数即类的构造函数
//在2.装饰器细节.ts文件中书写代码
//多个装饰器对类进行拓展


function d1(target: Function) {
    // console.log(typeof target, target,'1');
    // function和class MyClass {...}
}


@d1
@d11
class MyClass {

}

  • 属性装饰器:
  • 属性装饰器的应用目标是在类的属性上
  • 属性装饰器有两个参数,第二个参数是属性名称,第一个参数得先区分是类属性还是实例属性,如果是类属性该第一参数的意思是类的构造函数(即本身这个类),如果是实例属性该第一参数的意思是类的原型对象(即通过new出来的对象)
function d2(target: any, name: string) {
    console.log(typeof target, name);
	
    
    //实例对象
    //object a  ---> a: number;  
	
    //构造函数类
    //function property1 --->static property1: number;


    //所以填target填any   name是属性的名称
}


class MyClass {
    @d2
    //这里是类(构造函数上)的属性
    static property1: number;

    @d2
    //这里是实例对象上的属性
    a: number;

}

  • 访问器装饰器:
  • 访问器装饰器的应用目标是在类的访问器(getter、setter)上
  • 访问器装饰器有三个参数,第二个参数是属性名称,第三个参数是方法描述符对象,第一个参数得先区分是类属性还是实例属性,如果是类属性该第一参数的意思是类的构造函数(即本身这个类),如果是实例属性该第一参数的意思是类的原型对象(即通过new出来的对象)
function d3(target: any, name: string, descriptor: PropertyDescriptor) {
    console.log(typeof target, name, descriptor);
}

class MyClass {
    @d3
    //访问器装饰器 实例对象上的访问器
    get b() { 
         return 1; 
    }

    @d3
    //类(构造函数上)的访问器
    static get c() {
         return 2;
    }

}

  • 方法装饰器:
  • 方法装饰器的应用目标是在类的方法上
  • 方法装饰器有三个参数,第二个参数是方法名称,第三个参数是方法描述符对象,第一个参数得先区分是类属性还是实例属性,如果是类属性该第一参数的意思是类的构造函数(即本身这个类),如果是实例属性该第一参数的意思是类的原型对象(即通过new出来的对象)
function d4(target: any, name: string, descriptor: PropertyDescriptor) {
    console.log(typeof target, name, descriptor);
}

class MyClass {

    @d4
    //实例方法
    public method1(x: number, y: number) {}


    @d4
    //静态方法
    public static method2(x: number,y: number) {}

}

  • 参数装饰器:
  • 参数装饰器的应用目标是在参数上
  • 参数装饰器有三个参数,第二个参数是方法名称,第三个参数是参数在函数参数列表中的索引,第一个参数得先区分是类属性还是实例属性,如果是类属性该第一参数的意思是类的构造函数(即本身这个类),如果是实例属性该第一参数的意思是类的原型对象(即通过new出来的对象)
function d5(target: any, name: string, index: number) {
//name是当前参数所在的方法的名称(method1、method2)
    console.log(typeof target, name, index);

}

class MyClass {

    public method1(@d5 x: number, @d5 y: number) {}


    public static method2(@d5 x: number, @d5 y: number) {}

}

  • 装饰器的执行顺序(可多对一):

1.添加在实例上的装饰器都比添加在静态上的装饰器早执行,添加在类上的装饰器最后执行。2.添加在实例上的装饰器中属性装饰器 ==早于=> 访问符装饰器 ==早于=> 参数装饰器 ==早于=> 方法装饰器 3.添加在静态上的装饰器中属性装饰器 ==早于=> 访问符装饰器 ==早于=> 参数装饰器 ==早于=> 方法装饰器 4.最后是添加在类上的装饰器执行

装饰器工厂

  • 如果我们需要给装饰器在执行过程中传入一些参数的时候,就可以使用装饰器工厂来实现,改变同一装饰器的不同行为。
  • 情况一:执行过程中没有传入参数
//执行过程中没有传入参数
function log(target:Function,name:string,descriptor: PropertyDescriptor){
    let value = descriptor.value;
    descriptor.value=function(x: number, y: number) {
        console.log('注入新行为');
        let result = value(x, y);
        return result;
    }
}



class M {

    @log
    static add(x: number, y: number) {
        return x + y;
    }

	@log
    static sub(x: number, y: number) {
        return x - y;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

  • 情况二:执行过程中有传入参数:我希望通过传入的参数可以控制我需要在执行的位置显示数据信息
function log(type: string) {
    return function (target: Function, name: string, descriptor: PropertyDescriptor) {

      let value = descriptor.value;
      descriptor.value = function(x: number, y: number) {
          let result = value(x, y);


          //todo1...
          console.log({
              type,
              name,
              x,
              y,
              result
          });

          //todo2....
          //....根据传入的参数的不同执行不同的行为.....


          return result;
      }

  }
}

class M {
    @log('log')  
    //我希望通过传入的参数可以控制我需要在执行的位置显示数据信息
    //模拟在控制台上显示数据信息
    static add(x: number, y: number) {
        return x + y;
    }

    @log('storage')  
    //模拟在localStorage中显示数据信息
    //函数加上括号本应返回undefined但是我们需要让其返回一个函数
    static sub(x: number, y: number) {
        return x - y;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

元数据

  • 装饰器 函数中 ,我们可以拿到 方法访问符属性参数 的基本信息,如它们的名称,描述符等,但是我们想获取更多信息就需要通过另外的方式来进行:元数据
  • 例子一:我们在使用装饰器函数(@log())时如果在不传入参数的情况还想要能够使用默认的参数这个默认的参数写在另一个装饰器函数(@L('storage'))中(这里是写在了类装饰器上),也就是说我们想要做到在不向装饰器函数(@log())传入参数的情况下,在这个装饰器函数((@log())中去使用在另一个装饰器函数(@L('storage'))中埋下的默认参数
@L('storage')
class M {
    @log()
    static add(x: number, y: number) {
        return x + y;
    }

    @log('log')
    static sub(x: number, y: number) {
        return x - y;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
  • 例子一注意点:我们想要做到在使用方法装饰器@log()时尽管不传入参数,也可以使用默认的参数。这里是使用在类装饰器@L('storage')中传入的参数,即'storage'
  • 例子一注意点:我们为了让@log()装饰器函数中可以拿到'storage',我们可以在@L('storage')装饰器函数中将该参数数据放在类的原型上
function L(type: string) {
    return function(target: Function) {
        //target是类(构造函数的情况下)
        //type可以放在类本身上  也可以放在类的原型上
        //放在类上可以通过target.type拿到
        //放在原型链上可以通过target.prototype.type拿到
        
        //把数据挂在类上面  放在类的什么地方上  
        //存在原型链上
        target.prototype.type = type;
    }
}
  • 例子一注意点:现在@log()中的参数可传可不传,我们需要使用怎么在@log()装饰器函数中拿到挂载在原型上的数据(这里是‘storage’),我们需要注意到@L('storage')是类装饰器,而@log()是方法装饰器,方法装饰器`@log()是比类装饰器@L('storage')先执行的。在取值时要注意取值的地方,避免拿到undefined.
  • 例子一注意点:在@log()装饰器函数中我们可以通过target参数来拿到@L('storage')设置在原型上的默认参数,可是target参数有两种情况,一种是类型为function(类构造函数),一种是类型为object(实例对象),因此如果target的类型为function,则通过target.prototype.type拿到默认参数,否则通过target.type拿到默认参数。
//现在@log()中的参数可传可不传
function log(type?: string) {
    //发现规律: target要么是类要么是实例
  return function (target: any, name: string, descriptor: PropertyDescriptor) {

      // log 方法装饰器 是比 L 类装饰器先执行的
      // 因此得在  descriptor.value = function(){}内去拿type
      // 该函数调用肯定是比装饰器更晚执行得 避免拿到undefined

    let value = descriptor.value;
    descriptor.value = function(x: number, y: number) {

      //target是类(构造函数的情况下)
      //type可以放在类本身上  也可以放在类的原型上
      //放在类上可以通过target.type拿到
      //放在原型链上可以通过target.prototype.type拿到


      //如果target是一个对象(实例对象)  
      //target.constructor.type
      //target.constructor --->原型



      let result = value(x, y);
      //如果有参数
      let _type = type;


     //如果不传入参数
      if (!_type) {
          _type = typeof target === 'function' ? target.prototype.type : target.type;
      }

      console.log({
        //使用L(type: string)的默认参数
        //还是使用@log('log')的type(这里是'log')
          type: _type, 
          name,
          x,
          y,
          result
      });

      return result;
      }
  }
}

  • 元数据的概念: 用来描述数据的数据,在我们的程序中,对象 等都是数据,它们描述了某种数据,另外还有一种数据,它可以用来描述 对象,这些用来描述数据的数据就是 元数据
  • 定义元数据:我们可以 方法 等数据定义元数据,元数据会被附加到指定的 方法 等数据之上,但是又不会影响 方法 本身的代码
  • 我们现在使用第三方库reflect-metadata来学习元数据。官网网址是reflect-metadata。即npm install reflect-metadata(tsconfig.js中配置是"target": "es5")
  • 我们在tsconfig.js中的配置如下:
{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es5",
        "strictNullChecks": true,
        "noImplicitAny": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "watch":true
    },
    "include": ["./src/**/*"]
}
  • 我们使用第三方库reflect-metadata来学习元数据,先来看一下下面的这个例子:(通过 Reflect.defineMetadata 方法调用来添加 元数据和通过Reflect.getMetadata(metadataKey, target, propertyKey)获取定义好的元数据)
  • Reflect.defineMetadata的使用:第一个参数:metadataKey:meta 数据的 key;第二个参数:metadataValue:meta 数据的 值;第三个参数:target:meta 数据附加的目标;第四个参数:propertyKey:对应的 property key
  • 使用Reflect.defineMetadata时前两个参数是必写的,后两个参数根据场景而定。
  • Reflect.getMetadata(metadataKey, target, propertyKey)的使用:参数的含义与 defineMetadata 对应
  • 除了通过 Reflect.defineMetadata 方法调用来添加 元数据,还可以通过 @Reflect.metadata 装饰器来添加 元数据
  • 下面的例子中用到了Reflect.defineMetadataReflect.getMetadata()
//暴露全局Reflect对象
import 'reflect-metadata';


class A {

    //静态方法
    public static method1() {
    }


    //实例方法
    public method2() {
    }
}

//装饰器函数无法对实例对象去定义元数据
//得通过Reflect.defineMetadata来给实例对象定义元数据
let obj = new A;


Reflect.defineMetadata( 'n', 1, A ); //给类去定义元数据
Reflect.defineMetadata( 'n', 2, obj ); //给实例去定义元数据


// //区分到底是静态方法还是实例方法(写法不一样!!!)
Reflect.defineMetadata( 'n', 3, A, 'method1' ); //给类底下的方法去定义元数据
Reflect.defineMetadata( 'n', 4, obj, 'method2' );//给实例对象底下的方法去定义元数据


//通过`Reflect.getMetadata(metadataKey, target, propertyKey)`获取metadataValue

console.log(Reflect.getMetadata('n', A));
console.log(Reflect.getMetadata('n', obj));


console.log(Reflect.getMetadata('n', A, 'method1'));
console.log(Reflect.getMetadata('n', obj, 'method2'));


//执行结果是 1 -> 2 ->  3 ->  4
  • 下面我们通过 @Reflect.metadata 装饰器来添加 元数据,并通过Reflect.getMetadata()获取定义好的元数据,例子如下:
//暴露全局Reflect对象
import 'reflect-metadata';

@Reflect.metadata('n', 1)
class A {
    @Reflect.metadata('n', 2)
    //静态方法
    public static method1() {
    }

    @Reflect.metadata('n', 4)
    //实例方法
    public method2() {
    }
}



//装饰器函数无法对实例对象去定义元数据
//得通过Reflect.defineMetadata来给实例对象定义元数据
let obj = new A;

//得通过Reflect.defineMetadata来给实例对象定义元数据
Reflect.defineMetadata( 'n', 2, obj ); //给实例去定义元数据



//通过`Reflect.getMetadata(metadataKey, target, propertyKey)`
//获取metadataValue
console.log(Reflect.getMetadata('n', A));
console.log(Reflect.getMetadata('n', obj));


console.log(Reflect.getMetadata('n', A, 'method1'));
console.log(Reflect.getMetadata('n', obj, 'method2'));


//执行结果是 1 -> 2 ->  3 ->  4
  • 我们回顾一下之前的例子一的写法:
function L(type: string) {
    return function(target: Function) {
        target.prototype.type = type;
    }
}

//现在@log()中的参数可传可不传
function log(type?: string) {
    //发现规律: target要么是类要么是实例
  return function (target: any, name: string, descriptor: PropertyDescriptor) {

    let value = descriptor.value;
    descriptor.value = function(x: number, y: number) {

      let result = value(x, y);
      //如果有参数
      let _type = type;


     //如果不传入参数
      if (!_type) {
          _type = typeof target === 'function' ? target.prototype.type : target.type;
      }

      console.log({
        //使用L(type: string)的默认参数
        //还是使用@log('log')的type(这里是'log')
          type: _type, 
          name,
          x,
          y,
          result
      });

      return result;
      }
  }
}
  • 我们可以使用元数据来改写例子一:(改造方式一):使用了Reflect.defineMetadata('type', type, target);和Reflect.getMetadata(...)
import 'reflect-metadata';

function L(type: string) {
    return function(target: Function) {
    	// 我们不在直接的挂载在原型上
        // target.prototype.type = type;
        // 通过定义元数据的方式装饰在类(构造函数上)
        // 注意这里的target是类(构造函数)
        // 在@log()装饰器函数中target可能是实例
        // 如果是实例获取元数据的方式是与是类获取元数据的方式是不同的
        Reflect.defineMetadata('type', type, target);
    }
}


function log(type?: string) {
    return function (target: any, name: string, descriptor: PropertyDescriptor) {


    let value = descriptor.value;
    descriptor.value = function(x: number, y: number) {
        let result = value(x, y);
        let _type = type;
        if (!_type) {

            if (typeof target === 'function') {
                //target是类的情况
                //获取定义好的元数据
                _type = Reflect.getMetadata('type', target);
            } else {
                //实例对象
                //这里的第二个参数是target.constructor
                //target.constructor指向类(构造函数)
                _type = Reflect.getMetadata('type', target.constructor);
            }
        }

        console.log({
            type: _type,
            name,
            x,
            y,
            result
        });

        return result;
    }
}
}


@L('storage')
class M {
    @log()
    static add(x: number, y: number) {
        return x + y;
    }

    @log('log')
    static sub(x: number, y: number) {
        return x - y;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
  • 我们可以使用元数据来改写例子一:(改造方式二):使用了@Reflect.metadata('type', 'storage');和Reflect.getMetadata(...)
import 'reflect-metadata';

// function L(type: string) {
//     return function(target: Function) {
//         // target.prototype.type = type;
//         Reflect.defineMetadata('type', type, target);
//     }
// }

function log(type?: string) {
    return function (target: any, name: string, descriptor: PropertyDescriptor) {
        let value = descriptor.value;
        descriptor.value = function(x: number, y: number) {
            let result = value(x, y);
            let _type = type;
            if (!_type) {
                if (typeof target === 'function') {
                    _type = Reflect.getMetadata('type', target);
                } else {
                    _type = Reflect.getMetadata('type', target.constructor);
                }
            }
    
            console.log({
                type: _type,
                name,
                x,
                y,
                result
            });
    
            return result;
        }
    
    }
}

@Reflect.metadata('type', 'storage')
class M {
    @log()
    static add(x: number, y: number) {
        return x + y;
    }

    @log('log')
    static sub(x: number, y: number) {
        return x - y;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

使用 emitDecoratorMetadata

  • tsconfig.json 中有一个配置 emitDecoratorMetadata,开启该特性,typescript 会在编译之后自动给 方法访问符属性参数 添加如下几个元数据
  • 先来看下面的例子在来感受使用emitDecoratorMetadata的好处:
function f(){
    return function(target: any, name: string, descriptor: PropertyDescriptor){

        //通过下面的方法只能打印出形参的长度 
        console.log(descriptor.value.length);
        
        //如果我们还想要知道形参的类型呢?
        //如果我们还想要知道返回值类型呢?
        //如果我们需要通过类型判断做相应的处理控制呢?
        //比如判定传进来的参数是string类型时做拼接等等的处理控制
        //还可能需要判定返回值的类型后做相应的处理控制等等
        //我们怎么解决这些需求呢?
        //做法是加上`emitDecoratorMetadata`的配置
    }
}


class B {
    name: string;

    constructor(a:string) {

    }


    //如果直接些@f报错
    //“f”收到的参数过少,无法在此处充当修饰器。
    //你是要先调用它,然后再写入 "@f()" 吗?
    //因为我们写的是function f(){return function(...){...}}
    //得写成@f()
    
    //需求场景:
    //假设有一个方法装饰器@f()
    //如果我想知道这个方法有多少个参数 
    //每一个参数(形参)的类型是什么
    //该函数的返回值类型是什么
    //如何知道呢
    //如果不知道
    //我们想要通过类型去做相应的处理控制岂不是无法做到
    @f()
    method1(a: string, b: number): string {
        
        console.log('method1');
        
        return 'a'
    }

  • 我们加上emitDecoratorMetadata的配置后,会在编译之后自动给 方法访问符属性参数 添加如下几个元数据:
  • 自动添加的第一个是design:type表示当前被装饰目标的类型;成员属性:属性的标注类型;成员方法:Function 类型
  • 自动添加的第二个是design:paramtypes;成员方法:方法形参列表的标注类型;类:构造函数形参列表的标注类型
  • 自动添加的第三个是design:returntype;成员方法:函数返回值的标注类型
  • 我们可以观察到添加emitDecoratorMetadata的配置后在编译后的js文件中自动的给 方法访问符属性参数 添加了元数据。
  • 再来看下面的例子:我们还是需要引入第三方库reflect-metadata来我们获取元数据。
import 'reflect-metadata'


function f(){
    return function(target: any, name: string, descriptor: PropertyDescriptor){

      //成功打印形参的长度
      console.log(descriptor.value.length);



      //引入import 'reflect-metadata'后


      //以method2举例说明
      //成功打印出当前被装饰目标的类型
      //[Function: Function]
      console.log( Reflect.getMetadata('design:type', target, name) );

      //以method2举例说明
      //成功打印出方法形参列表的标注类型
      //[ [Function: String], [Function: Number] ]
      console.log( Reflect.getMetadata('design:paramtypes', target, name) );

      //以method2举例说明
      //成功打印出函数返回值的标注类型
      //[Function: String]
      console.log( Reflect.getMetadata('design:returntype', target, name) );

    }
}


class B {
    name: string;

    constructor() {

    }


    @f()
    method2(x?: number) {
        console.log(x,'i am number');
    }
}


let b = new B();
b.method2();
  • 现在我们可以得到方法有多少个参数、每一个参数的类型是什么、方法的返回值的类型是什么,于是我们可以做一些处理控制,让装饰器函数针对不同的类型有不同的行为:
function f(){
    return function(target: any, name: string, descriptor: PropertyDescriptor){


      let _t = Reflect.getMetadata('design:paramtypes', target, name)[0];
      console.log(_t,'_t'); 

      
      
      console.log(typeof _t,'ty_t');
      
      
      let value=descriptor.value; //原函数
      
      
      if(_t === Number){
          //[Function: Number] '_t'   针对method2
          console.log('你标注的是一个数字类型');
          value(100)
      }

      if(_t === String){
          //[Function: String] '_t'  针对method1
          console.log('你标注的是一个字符串类型');
          value('i am ironman')
      }
      if(_t === Date){
          //[Function: Date] '_t'  针对method3
          console.log('你标注的是一个Date类型');
          value(new Date())
      }
  }
}


class B {
    name: string;

    constructor() {

    }

    @f()
    method1(a: string, b: number): string {
        
        console.log('i am string');
        
        return 'string'
    }


    @f()
    method3(x?: Date) {
        console.log(x,'i am Date');
    }


    @f()
    method2(x?: number) {
        console.log(x,'i am number');
    }
}


let b = new B();
b.method2();
  • 该tsconfig.js的配置如下:
{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es5",
        "strictNullChecks": true,
        "noImplicitAny": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "watch":true
    },
    "include": ["./src/**/*"]
}