【ts】这都Vue3,还不学习TypeScript嘛?

·  阅读 2880

前言:当Vue3都要使用ts重构的时候,我们就应该知道ts的时代不远了,终究来说我们真的需要沉下心去怀抱ts的魅力,让自己的代码更加强健。

​ 首先我就爱喜欢一篇长文总结学习知识点,方便不说,也是为了统筹全局的学习,所以,篇幅和前面的文章一样,可能有点过于长,别介意哈,我不太喜欢分一二三四,一口气吃完,我也觉得挺香的哈。

归纳一下翻阅的来源

不多bb,肯定先是如何项目里面安装TypeScript

以React和Vue为例

React

首先并不是所有的库都有TypeScript的声明文件,所以为了需要安装TypeScript的声明文件,来实现对安装库的类型检查

  • 不多说先安装typescript【先装环境】
cnpm install --save-dev typescript
复制代码
  • ts代码编译成js代码【因本文方向,不多赘述,可看来源部分】
    • 经典的ts-loader+babel-loader

    • awesome-typescript-loader

    • babel-loader + @babel/preset-typescript

      新版本使用 babel 编译TS,babel 编译并不会读取tsconfig.json中的配置,我们需要将相关配置转移到 babel.config.js 或 .babelrcc

**#**类型声明包

cnpm install --save @types/react @types/react-dom
复制代码

Vue3

引流自己:Vue3+JSX+TS尝鲜

TypeScript开始

基础类型

布尔值Boolean、数字Number、字符串String、数组Array、元组Tuple【数组各元素类型不必相同,且已知元素数量与类型】、枚举Enum、任意类型Any、没有任何类型Void、Null 和 Undefined、永不存在的值的类型Never、独一无二Symbol

布尔值Boolean

let isDone: boolean = false;✔

*注意,使用构造函数 `Boolean` 创造的对象不是布尔值,返回的是一个 Boolean 对象
let isDones: boolean = new Boolean(fasle);✔

/*  Type 'Boolean' is not assignable to type 'boolean'. */
let createdByNewBoolean: boolean = new Boolean(1);❌
复制代码

数字Number

支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量【所有数字都是浮点数】

数据类型-数字 Number-NaN

let decLiteral: number = 6;✔
let hexLiteral: number = 0xf00d;✔

// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;✔

// ES6 中的八进制表示法
let octalLiteral: number = 0o744;✔

/* NaN不是数字,但是它的数据类型是数字 */
/* NaN是JavaScript之中唯一不等于自身的值,不等于任何值,包括它本身 */
let notANumber: number = NaN;✔

/* Infinity正无穷大与-Infinity负无穷,但是它的数据类型是数字 */
let infinityNumber: number = Infinity;✔
复制代码

字符串String

可以使用双引号( ")或单引号(')表示字符串

还可以使用模版字符串,它可以定义多行文本和内嵌表达式

let name: string = "bob";✔
let names: string = `Gene`;✔
复制代码

数组Array

/* 元素类型后面接上 [],表示由此类型元素组成的一个数组 */
let list: number[] = [1, 2, 3];✔
/*数组泛型,Array<元素类型>*/
let list: Array<number> = [1, 2, 3];✔
/* 混合数组类型 */
const arr: (number | string)[] = [1, "string", 2];
复制代码

元组Tuple

数组各元素类型不必相同,且已知元素数量与类型

/* 定义一个Tuple类型 */
let x: [string, number];
/* 初始化 */
x = ['hello', 10]; ✔
/* 这样子初始化是不正确的【类型不匹配】 */
x = [10, 'hello']; ❌
/* 错误的,number类型没有substr方法 */
x[1].substr(0,1) ❌
复制代码

访问一个越界的元素,会使用联合类型替代【联合类型是高级主题,我们会在后面讨论它】

let aaa: [string, number] = ['aaa', 5];
// 添加时不会报错
aaa.push(6);
// 打印整个元祖不会报错
console.log(aaa); // ['aaa',5,6];
// 打印添加的元素时会报错
console.log(aaa[2]); // error【元组是已知的数组】
复制代码

枚举Enum

对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字

枚举类型提供的一个便利是你可以由枚举的值得到它的名字

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}
复制代码
  • 数字枚举:默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号【全部都采用手动赋值】

    enum Direction {
      NORTH,
      SOUTH,
      EAST,
      WEST,
    }
    let direction: string = Direction[2];
    console.log(direction);  // 显示'SOUTH'因为上面代码里它的值是2
    复制代码
  • 字符串枚举:在 TypeScript 2.4 版本,允许我们使用字符串枚举

    • 每个成员都必须用字符串字面量【】

      /* ✔  */
      enum Direction {             
        NORTH = "NORTH",
        SOUTH = "SOUTH",
        EAST = "EAST",
        WEST = "WEST",
      }
      /* ✔  NORTH=0,SOUTH=1*/
      enum Direction {             
        NORTH,
        SOUTH,
        EAST = "EAST",
        WEST = "WEST",
      }
      /* ❌ */
      enum Direction {             
        NORTH,
        SOUTH,
        EAST = "EAST",
        WEST,
      }
      /* ✔  NORTH=0,SOUTH=1,WEST=9 */
      enum Direction {
        NORTH,
        SOUTH,
        EAST = 8,
        WEST,
      }
      复制代码
      • 字符串和数字混合枚举:最后一个必须是数字
      /* ✔ */
      enum Enum {
        A,
        B,
        C = "C",
        D = "D",
        E = 8,
        F,
      }
      /* ✔ */
      enum Char {
          // const member 常量成员:在编译阶段被计算出结果
          a,				 // 没有初始值
          b = Char.a,// 对常量成员的引用
          c = 1 + 3, // 常量表达式
        
          // computed member 计算成员:表达式保留到程序的执行阶段
          d = Math.random(),// 非常量表达式
          e = '123'.length,
          // 紧跟在计算成员后面的枚举成员必须有初始值
          f = 6,
          g
      }
      复制代码

任意类型Any

任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型

let notSure: any = 4;✔
notSure = "maybe a string instead";✔
notSure = false; // 允许,可能是一个布尔  ✔
复制代码
  • 当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据

    let list: any[] = [1, true, "free"];✔
    
    list[1] = 100;✔
    复制代码
  • 对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查

Unknown 类型

所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型

any与unknown区别

let value: unknown;

value.foo.bar; // ❌
value.trim(); // ❌
复制代码
let value: any;

value.foo.bar; // ✔
value.trim(); // ✔
复制代码

没有任何类型Void

void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

/* 声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null */
let unusable: void = undefined;// ✔
let unusables: void = 'undefined';// ❌
复制代码

Null Undefined

TypeScript里,undefinednull两者各自有自己的类型分别叫做undefinednull。 和 void相似,它们的本身的类型用处不是很大

/* 我们不能再给这些变量赋值了 */
let u: undefined = undefined;
let n: null = null;
复制代码
  • nullundefined 是所有类型的子类型

    • 可以把 nullundefined 赋值给 number /string等类型的变量
  • 指定了--strictNullChecks 标记

    • nullundefined 只能赋值给 void 和它们各自的类型

      // tsconfig.json 
      {
          // 对 null 类型检查,设置为 false 就不会报错了
          // "strictNullChecks": true,              /* Enable strict null checks. *//
      }
      复制代码

永不存在的值的类型Never

  • never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

    // 返回never的函数必须存在无法达到的终点
    function error(message: string): never {
        throw new Error(message);
    }
    复制代码
  • never类型是任何类型的子类型,也可以赋值给任何类型【没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

  • 详细的检查

    function foo(x: string | number): boolean {
      if (typeof x === 'string') {
        return true;
      } else if (typeof x === 'number') {
        return false;
      }
    
      // 如果不是一个 never 类型,这会报错:
      // - 不是所有条件都有返回值 (严格模式下)
      // - 或者检查到无法访问的代码
      // 但是由于 TypeScript 理解 `fail` 函数返回为 `never` 类型
      // 它可以让你调用它,因为你可能会在运行时用它来做安全或者详细的检查。
      return fail('Unexhaustive');
    }
    
    function fail(message: string): never {
      throw new Error(message);
    }
    复制代码

独一无二Symbol

ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,它是 JavaScript 语言的第七种数据类型

一种类似于字符串或者是数字的数据类型

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分

ts中Symbol只能是字符串类型或者数字类型【在ts 不允许的传入对象,js中可以】

const s3 = Symbol({ a: 'a' })
console.log(s3) // Symbol([object Object]) ts 这么写会报错
复制代码

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

let myFavoriteNumber: string | number;
复制代码

辨析联合类型

考虑 SquareRectangle 的联合类型 ShapeSquareRectangle有共同成员 kind,因此 kind 存在于 Shape

interface Square {
  kind: 'square';
  size: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

type Shape = Square | Rectangle;
复制代码

如果你使用类型保护风格的检查(=====!=!==)或者使用具有判断性的属性(在这里是 kind),TypeScript 将会认为你会使用的对象类型一定是拥有特殊字面量的,并且它会为你自动把类型范围变小:

function area(s: Shape) {
  if (s.kind === 'square') {
    // 现在 TypeScript 知道 s 的类型是 Square
    // 所以你现在能安全使用它
    return s.size * s.size;
  } else {
    // 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle
    return s.width * s.height;
  }
}
复制代码

联系到Never

当我们再新增一种类型Circle给到Shape原代码优惠咋样呢

// 有人仅仅是添加了 `Circle` 类型
// 我们可能希望 TypeScript 能在任何被需要的地方抛出错误
interface Circle {
  kind: 'circle';
  radius: number;
}

type Shape = Square | Rectangle | Circle;
复制代码

代码变差

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height;
  }

  // 如果你能让 TypeScript 给你一个错误,这是不是很棒?
}
复制代码

你可以通过一个简单的向下思想,来确保块中的类型被推断为与 never 类型兼容的类型。例如,你可以添加一个更详细的检查来捕获错误:

function area(s: Shape) {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height;
  } else {
    // Error: 'Circle' 不能被赋值给 'never'
    const _exhaustiveCheck: never = s;
  }
}
复制代码

也可以通过 switch 来实现以上例子:

function area(s: Shape) {
  switch (s.kind) {
    case 'square':
      return s.size * s.size;
    case 'rectangle':
      return s.width * s.height;
    case 'circle':
      return Math.PI * s.radius ** 2;
    default:
      const _exhaustiveCheck: never = s;
  }
}
复制代码

接口【重要】

这些类型命名和为你的代码或第三方代码定义契约

它是对行为的抽象,而具体如何行动需要由类去实现。

interface Point {
     x: number;
     y: number;
      
     width?: number;/*可选属性*/
     
     readonly color: number;/*只读属性*/

     mySearch:{(source: string, subString: string, desc?:string): void;}/*函数类型*/
     
     StringArray:{[index: number]: string} /*可索引的类型 ❗ 需要注意的是 index 只能为 number 类型或 string 类型*/

	 [propName: string]: any;/*带有任意数量的其它属性*/
}

const reslute: mySearch = function (source, subString, desc = '') {
    //const sum: mySearch = 
    //function (source: string, subString: string, desc: string): void {}
    // ts类型系统默认推论可以不必书写上述类型定义
    console.log(source, subString ,desc)
}
复制代码

类interface

/* 定义接口 */
interface ComesFromString {
    name: string;
}
/* 定义一个类,接口给到new【constructor】*/
interface StringConstructable {
    new(n: string): ComesFromString;
}

function makeObj(n: StringConstructable) {
    return new n('hello!');
}

console.log(makeObj(MadeFromString).name);
复制代码

Interface 的继承

【**extends**是继承】

跟 class 一样,使用 extens 继承,更新新的形状,比方说继承接口并生成新的接口,这个新的接口可以设定一个新的方法检查。

interface PersonInfoInterface { // 1️⃣ 这里是第一个接口
    name: string
    age: number
    log?(): void
}

interface Student extends PersonInfoInterface { // 2️⃣ 这里继承了一个接口
    doHomework(): boolean // ✔️ 新增一个方法检查
}

interface Student extends PersonInfoInterface { // 2️⃣ 这里继承了一个接口
    doHomework(): boolean // ✔️ 新增一个方法检查
}

let Alice: Teacher = {
    name: 'Alice',
    age: 34,
    dispatchHomework() { // ✔️ 必须满足继承的接口规范
        console.log('dispatched')
    }
}

let oliver: Student = {
    name: 'oliver',
    age: 12,
    log() {
        console.log(this.name, this.age)
    },
    doHomework() { // ✔️ 必须满足继承的接口规范
        return true
    }
}
复制代码

继承类的 Interface

【**implements **是实现】

Interface 不仅能够继承 Interface 还能够继承类,再创建子类的过程中满足接口的描述就会必然满足接口继承的类的描述

⚠️ Child 接口继承了 Person 对 type 的描述,还定义了 Child 接口本身 log 的描述:

interface Person {
       // 声明变量
  name: string;
  age: number
  // 创建构造函数
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
interface Child extends Person { 
    // ❗️Child 接口继承自 Person 类,因此规范了 name 属性
    name:'萨维塔';
    log(): void
    // 这里其实有一个 type: string
}
复制代码

继承写法:

// 🥇 第一种写法【Girl派生类】【Child基类】
class Girl implements Child {
  // 声明变量
  name: '萨维塔';
  age: number;
  school: string;
  constructor(school: string) {
    this.school = school;
    this.name = '萨维塔';
    this.age = 20;
  }
  log() {} // 接口本身规范的
}
// 🥈 第二种写法
class Boy extends Person implements Child {
  name: '萨维塔';
  constructor() {
    super('萨维塔', 21);
    this.name = '萨维塔';
  }
  // 首先 extends 了 Person 类,然后还需满足 Child 接口的描述
  log() {}
}
复制代码

示例

修改原始类型

在 TypeScript 中,接口是开放式的,这意味着当你想使用不存在的成员时,只需要将它们添加至 lib.d.ts 中的接口声明中即可,TypeScript 将会自动接收它。注意,你需要在全局模块中做这些修改,以使这些接口与 lib.d.ts 相关联。我们推荐你创建一个称为 global.d.ts 的特殊文件。

  • Window添加属性

    interface Window {
      helloWorld(): void;
    }
    
    =======>
    // Add it at runtime
    window.helloWorld = () => console.log('hello world');
    
    // Call it
    window.helloWorld();
    复制代码

基于可维护性,我们推荐创建一个 global.d.ts 文件。然而,如果你愿意,你可以通过使用 declare global { /* global namespace */ },从文件模块中进入全局命名空间

  • lib.d.ts

    interface String {
      endsWith(suffix: string): boolean;
    }
    复制代码
  • global.d.ts

    declare global {
      interface String {
        endsWith(suffix: string): boolean;
      }
    }
    复制代码
    String.prototype.endsWith = function(suffix: string): boolean {
      const str: String = this;
      return str && str.indexOf(suffix, str.length - suffix.length) !== -1;
    };
    复制代码

泛型【难点】

我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

  • 泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

    • 示例

      你实现一个函数,函数的参数可以是任何值,返回值就是将参数原样返回,并且其只能接受一个参数,你会怎么做?

      type commonBoolean = (arg:boolean) => boolean;
      type commonString = (arg:string) => string;
      type commonNumber = (arg:number) => number;
      //.....
      const common:commonBoolean|commonString|commonNumber.... = () => {
      
      };
      复制代码

      一个笨的方法就像上面那样,也就是说 JS 提供多少种类型,就需要往上面加。【这不是我们的预期】

      还有一种方式是使用 any 这种“万能语法”

      type commonAny = (arg: any) => any;
      const common:commonAny = () => {};
      
      
      common("string").length; // ok
      common("string").toFixed(2); // ok
      common(null).toString(); // ok
      ...
      复制代码

      如果你使用 any 的话,怎么写都是 ok 的, 这就丧失了类型检查的效果。实际上我知道我传给你的是 string,返回来的也一定是 string,而 string 上没有 toFixed 方法,因此需要报错才是我想要的。也就是说我真正想要的效果是:当我用到id的时候,你根据我传给你的类型进行推导。比如我传入string,但是使用了 number 上的方法,你就应该报错。

      为了解决上面的这些问题,我们使用泛型对上面的代码进行重构。和我们的定义不同,这里用了一个 类型 T,这个 T 是一个抽象类型,只有在调用的时候才确定它的值,这就不用我们复制粘贴无数份代码了。

      function common<T>(arg: T): T {
        return arg;
      }
      复制代码
  • T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外【如果在你的参数里,不止拥有一个泛型,你应该使用一个更语义化名称】,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;

  • V(Value):表示对象中的值类型;

  • E(Element):表示元素类型。

接口泛型

首先Person的接口,这个接口会被用来限制用户的注册表单:

interface Person {
  name: string;
  sex: boolean;
  age: number;
}
复制代码

现在需要做一个活动,需要用户填写手机号码来接收短信,很蠢的办法就是重新写一个新的类:

interface MarketPerson {
  name?: string;
  sex?: Sex;
  age?: number;
  phone: string;
}
复制代码

这里重复定义了类型

这明显不够优雅。如果 Person 字段很多呢?这种重复代码会异常多,不利于维护。 TS 的设计者当然不允许这么丑陋的设计存在:一种是集合操作,另一种是泛型

type MarketPerson = Person & { phone: string };
复制代码

添加了一个必填字段 phone,但是没有做到name, sex, age 选填,似乎集合操作做不到这一点呀,接下来就要用到泛型:

// 可以看成是上面的函数定义,可以接受任意类型。由于是这里的 “Type” 形参,因此理论上你叫什么名字都是无所谓的,就好像函数定义的形参一样。
type Partial<T> = { [K in keyof T]?: T[K] }
// 可以看成是上面的函数调用,调用的时候传入了具体的类型 Person
type PartialedPerson = Partial<Person>
复制代码

T代表着传入了Person,K代表键值,in 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被作为类型保护使用,keyof导出在条件块中的的变量类型

type MarketPerson = PartialedPerson & { phone: string };
复制代码
typeof 类型保护

如果你在一个条件块中使用typeof ,TypeScript 将会推导出在条件块中的的变量类型

function doSome(x: number | string) {
  if (typeof x === 'string') {
    // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
    console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上
    console.log(x.substr(1)); // ok
  }

  x.substr(1); // Error: 无法保证 `x` 是 `string` 类型
}
复制代码

可以用来表示一个对象中的所有 key 值

interface Person {
    name: string;
    age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number
复制代码
in类型保护

in 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被作为类型保护使用:

interface A {
  x: number;
}

interface B {
  y: string;
}

if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
复制代码

遍历枚举类型

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
复制代码
infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用

type ParamType<T> = T extends (param: infer P) => any ? P : T;
复制代码

infer P 表示待推断的函数参数

Textends (param: infer P) => any,则结果是 (param: infer P) => any 类型中的参数 P,否则返回为 T

interface User {
  name: string;
  age: number;
}

type Func = (user: User) => void;
复制代码
type Param = ParamType<Func>; // Param = User
type AA = ParamType<string>; // string
复制代码
误用的泛型

我见过开发者使用泛型仅仅是为了它的 hack。当你使用它时,你应该问问自己:你想用它来提供什么样的约束。如果你不能很好的回答它,你可能会误用泛型,如:

declare function foo<T>(arg: T): void;
复制代码

在这里,泛型完全没有必要使用,因为它仅用于单个参数的位置,使用如下方式可能更好:

declare function foo(arg: any): void;
复制代码
配合 axios 使用

通常情况下,我们会把后端返回数据格式单独放入一个 interface 里

// 请求接口数据
export interface ResponseData<T = any> {
  /**
   * 状态码
   * @type { number }
   */
  code: number;

  /**
   * 数据
   * @type { T }
   */
  result: T;

  /**
   * 消息
   * @type { string }
   */
  message: string;
}
复制代码
//当我们把 API 单独抽离成单个模块时:
// 在 axios.ts 文件中对 axios 进行了处理,例如添加通用配置、拦截器等
import Ax from './axios';

import { ResponseData } from './interface.ts';

export function getUser<T>() {
  return Ax.get<ResponseData<T>>('/somepath')
    .then(res => res.data)
    .catch(err => console.error(err));
}
复制代码

接着我们写入返回的数据类型 User,这可以让 TypeScript 顺利推断出我们想要的类型:

interface User {
  name: string;
  age: number;
}

async function test() {
  // user 被推断出为
  // {
  //  code: number,
  //  result: { name: string, age: number },
  //  message: string
  // }
  const user = await getUser<User>();
}
复制代码

函数泛型

function ids<T, U>(arg1: T, arg2: U): [T, U] {
  return [arg1, arg2];
}
复制代码

类泛型

class MyComponent extends React.Component<Props, State> {
   ...
}
复制代码

泛型类看上去与泛型接口差不多。 泛型类使用(<>)括起泛型类型,跟在类名后面:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
复制代码

GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
复制代码

泛型约束

loggingIdentity例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}
复制代码

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含.length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}
复制代码

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property
复制代码

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});
复制代码

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 给泛型加上约束。

interface ILengthwise {
  length: number;
}

function loggingIdentity<T extends ILengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
复制代码

其实泛型我们在 React 组件里也很常见(说不定大家觉得很眼熟了),用泛型确保了 React 组件的 Props 和 State 是类型安全的~

interface ICustomToolProps {
  // @TODO
}

interface ICustomToolState {
  // @TODO
}

class CustomTool extends React.Component<ICustomToolProps, ICustomToolState> {
  // @TODO
}
复制代码

传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。 使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本

class Greeter {
    // 静态属性
    static cname: string = "Greeter";
    // 成员属性
    greeting: string;
    // 构造函数 - 执行初始化操作
    constructor(message: string) {
        this.greeting = message;
    }
    // 静态方法
    //静态方法中可以返回静态属性,,静态成员只能使用类名.静态成员的方式进行访问。
    static getClassName() {
      return "Class name is"+Greeter.cname;
    }
    // 构造函数 - 执行初始化操作
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");
复制代码

关于类中成员访问修饰符:

  1. ts类中成员(成员属性、成员方法)的访问修饰符,类似于java中类成员的访问修饰符,不同的是ts中默认是被public修饰。
  2. public :公有 在当前类里面、 子类 、类外面都可以访问
  3. protected:保护类型 在当前类里面、子类里面可以访问 ,在类外部没法访问
  4. private :私有 在当前类里面可以访问,子类、类外部都没法访问
  5. 属性如果不加修饰符 默认就是公有(public)

继承

类的继承:使用extends实现继承,通过super()或super.XX调用父类的构造方法或属性和普通方法

class Animal{
    name;//定义属性:这是es6的新写法,之前es6只能在constructor中通过this.name来定义
    constructor(name){
        this.name = name;
    }
    onLoad(){
        alert('000');
    }
    showName(){
        console.log(this.name);
    }
}
复制代码
super

类的继承:使用extends实现继承,通过super()或super.XX调用父类的构造方法或属性和普通方法

class Dog extends Animal{//若继承了父类,则构造方法中必须要调用父类的构造方法super(...)
    constructor(name){
        super(name);//调用父类的构造方法,将父类的name属性赋值
    }
    //覆盖父类showName()方法
    showName(){
        super.showName();//调用父类的方法,输出父类name属性的值
    }
}
复制代码
存取器

存取属性setter,getter:会拦截住name属性的取值和存值

class Animal2{
    // name;//注:如果写了存取值器,则不能再这里进行定义name,但是可以在构造器中this.name = name;
    constructor(name){
        this.name = name;
    }
    set name(name) {
        //这个方法只是会在name属性被设置值的时候出发,不是setName(),
        //因此不能在这里this.name = name;这样做可能胡不停调用到这个监听函数,导致栈溢出
        //this.name = name;//error:VM514:6 Uncaught RangeError: Maximum call stack size exceeded
        console.log('setter: ' + name);
    }
    get name(){
        //不能return 返回this.name
        console.log('getter...');
        return 'jack';
    }
}
let animal2 = new Animal2('熊二');//熊二,构造方法也会出发set name()函数
animal2.name = '光头强';//setter: 光头强
console.log(animal2.name);//getter... jack
复制代码

修饰符

默认为 public

注意到我们之前的很多代码里并没有使用 public来做修饰,因为在TypeScript里,成员都默认为 public,们可以自由的访问他们

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
复制代码
理解 private

当成员被标记成 private时,它就不能在声明它的类的外部访问【允许内部访问】:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.
复制代码
理解 protected

protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

复制代码

我们不能在 Person类外使用 name,但是我们仍然可以通过 Employee类的实例方法访问,因为 Employee是由 Person派生而来的

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());//类的实例方法访问name
console.log(howard.name); // 错误[不能直接访问]
复制代码

构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承c

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.
复制代码
readonly修饰符

使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化

函数

函数声明

有两种声明函数类型的方式:

let myAdd: (x: number, y: number) => number =
    function(x: number, y: number): number { return x + y; };
复制代码
let myAdd = function(x: number, y: number): number { return x + y; };
复制代码

剩余参数

在TypeScript里,你可以把所有参数收集到一个变量里,可以使用 arguments来访问所有传入的参数

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
复制代码

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组、

重载与重写

  • 重写(Override)

    子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写.

    class Animal {
        public eat() {
            console.log("这是一个吃的方法")
        }
    }
    class Dog extends Animal {
        public eat() {
            console.log("这是一个小狗吃的方法")
        }
    }
    let dog: Dog = new Dog()
    dog.eat()
    复制代码

    利用extends方法去继承父类的方法

  • 重载(Overload)

    允许一个函数接受不同数量或类型的参数时,作出不同的处理

    TypeScript 允许你声明函数重载。这对于文档 + 类型安全来说很实用

    function padding(all: number);
    function padding(topAndBottom: number, leftAndRight: number);
    function padding(top: number, right: number, bottom: number, left: number);
    // Actual implementation that is a true representation of all the cases the function body needs to handle
    function padding(a: number, b?: number, c?: number, d?: number) {
      if (b === undefined && c === undefined && d === undefined) {
        b = c = d = a;
      } else if (c === undefined && d === undefined) {
        c = a;
        d = b;
      }
      return {
        top: a,
        right: b,
        bottom: c,
        left: d
      };
    }
    复制代码

装饰器

Decorator 是 ES7 的一个新语法,目前仍处于第2阶段提案中,正如其“装饰器”的叫法所表达的,他通过添加@方法名可以对一些对象进行装饰包装然后返回一个被包装过的对象,可以装饰的对象包括:类,属性,方法等。

函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数

function now(){
	console.log(new Date())
}
复制代码

现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)

/* js */

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(now);
wrapped();

> Starting
> Sun Sep 27 2020 15:07:44 GMT+0800 (中国标准时间)
> Finished

复制代码

方法装饰器

declare type MethodDecorator = <T>(
  target: Object, 
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>) =>
    TypedPropertyDescriptor<T> | void;
复制代码

方法装饰器接受三个参数:

  1. target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. propertyKey —— 属性的名称。
  3. descriptor —— 方法的属性描述符。
function logFunc(params: string) {
    return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
        // target === HelloWordClass.prototype
        // propertyName === "sayHello"
        // propertyDesciptor === Object.getOwnPropertyDescriptor(HelloWordClass.prototype, "sayHello")

        console.log(params);
        // 被装饰的函数
        const method = descriptor.value;
        //...args: any[]--->接受参数以及类型不确定,可见函数的剩余参数
        descriptor.value = function (...args: any[]) {
            let start = new Date().valueOf();
            // 将 sayHello 的参数列表转换为字符串
            args = args.map(arg => String(arg));
            console.log('参数args = ' + args);
            try {
                // // 调用 sayHello() 并获取其返回值
                return method.apply(this, args)
            } finally {
                let end = new Date().valueOf();
                console.log(`start: ${start} end: ${end} consume: ${end - start}`)
            }
        };
        return descriptor;
    }
}
复制代码

对方法进行装饰:

class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    private nameVar: string | undefined;

    @logFunc('log装饰器')
    sayHello(name: string) {
        console.log(name + ' sayHello');
    }
}
let pHello = new HelloWordClass();
pHello.sayHello('zzb');
>>>>>>>
log装饰器
我是构造函数
参数args = zzb
zzb sayHello
start: 1574331292433 end: 1574331292434 consume: 1
复制代码

参数装饰器

declare type ParameterDecorator = 
(target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
复制代码

参数装饰器会接收三个参数:

  1. target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. propertyKey —— 属性的名称。
  3. parameterIndex —— 参数数组中的位置。
function logParameter(target: any, propertyName: string, index: number) {
    // 为相应方法生成元数据键,以储存被装饰的参数的位置
    const metadataKey = `log_${propertyName}_parameters`;
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    } else {
        target[metadataKey] = [index];
    }
}


class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    private nameVar: string | undefined;

    sayHello(@logParameter name: string) {
        console.log(name + ' sayHello');
    }
}
let pHello = new HelloWordClass();
pHello.sayHello('zzb');
复制代码

类的装饰器

  • 装饰器函数参数只有一个,并且参数(target)是被装饰类的构造函数
  • 装饰器返回的值会起作用,会直接作为最终类定义export
@classDecoratorcc
class MyTestableClass {
  property = "property";
  hello: string;
  constructor(m: string) {
		this.hello = m;
    }
}

function classDecorator<T extends { new (...args: any[]): {} }>(
 constructor: T
    ) {
 		return class extends constructor {
            newProperty = "new property";
            hello = "override";
      	};
 }

const greeter = new Greeter("world");
console.log({ greeter }, greeter.hello);
复制代码

访问器装饰器accessors

访问器,不过是类声明中属性的读取访问器和写入访问器。访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 成员的名字。
  • 成员的属性描述符。
 class Person {
      _name: string;
      constructor(name: string) {
        this._name = name;
      }
      @Enumerable
       get name() {
         return this._name;
      }
    }
// 访问器装饰器
  function Enumerable(
      target: any,
      propertyKey: string,
      descriptor: PropertyDescriptor
 ) {
 //make the method enumerable
      descriptor.enumerable = true;
  }
复制代码

运行之后:

let person = new Person("Diana");
console.log("-- looping --");
for (let key in person) {
	console.log(key + " = " + person[key]);
}

>>>  -- looping --
>>> _name=Diana
>>> name=Diana
复制代码

如果上面 get 不添加Enumerable的话,那么 for in 只能出来_name _name = Diana

类型断言

将一个联合类型断言为其中一个类型

/* 联合类型 */
let msg:string | number;

const message:number =(num:number){
	return num+10
}

message(msg)//错误msg联合类型还存在string的情况

/* 假如我们能够明确在某一时刻msg的类型是number */
/* 我们可以进行断言告诉ts她就是number */
message(msg as number)
复制代码

TS工具类型

在 TypeScript 中默认内置了很多工具泛型,能够合理灵活的使用这些工具,可以使我们的类型定义更加灵活,严谨。

Partial

它用来将 T 中的所有的属性都变成可选的

type Partial<T> = {
  [P in keyof T]?: T[P];
};
复制代码

示例

interface IFoo {
  a: number
  b: number
}
const foo: Partial<IFoo> = { a: 2 } // 正确,因为 IFoo 的属性都已经变成了可选状态
复制代码

Required

作用正好和的Partial相反,是将 T 中的所有属性都变成必选的状态

type Required<T> = {
  [P in keyof T]-?: T[P];
};
复制代码

示例

interface IFoo {
  a?: number
  b?: number
}
// 错误,因为 IFoo 的属性已经被变成了必选的状态
const foo: Required<IFoo> = { 
  a: 2
}
复制代码

Readonly

是将一个类型的所有成员变为只读的状态

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
复制代码

示例

interface IFoo {
  name: string
  age: number
}
const foo: Readonly<IFoo> = {
  name: 'cxc',
  age: 22,
}

foo.name = 'xiaoming' // 错误,因为 name 仅是只读的
foo.age = 20 // 错误,因为 age 也仅是只读的
复制代码

Pick

从 T 中将某个的 K 取出来,并生成一个新的类型

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
复制代码

示例

interface IFoo {
  a: number
  b: number,
  c: number
}

// 正确,使用 Pick 生成的新类型确实只包含 a 属性 c属性
const foo: Pick<IFoo, 'a' | 'c'> = {
  a: 2,
  c: 6
}

复制代码

Exclude

从 T 中排除掉所有包含的 U 属性

type Exclude<T, U> = T extends U ? never : T;
复制代码

示例

type TFoo = Exclude<1 | 2, 1 | 3>
/* 去除了1,3---》只剩下1  */
const foo: TFoo = 2 // 正确
const foo: TFoo = 3 // 错误,因为 TFoo 中不包含 3
复制代码

Extract

它的作用正好和上面的Exclude相反,T 中提取出所有包含的 U 属性值【交集】

type Extract<T, U> = T extends U ? T : never;
复制代码

示例

type TFoo = Extract<1 | 2, 1 | 3>

type TFoo = 1
复制代码

Record

生成一个属性为 K,类型为 T 的类型集合x

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
复制代码

示例一

type Foo = Record<'a', string>

const foo: Foo = {a: '1'} // 正确
const foo: Foo = {b: '1'} // 错误,因为 key 不为 a
复制代码

示例二:可以用Record来处理另外一种场景

interface Foo {
  a: string
}
interface Bar {
  b: string
}
/* 把Foo和Bar两个类型的 key 合并到一起,并给它们重新指定成 number 类型 */
type Baz = Record<keyof Foo | keyof Bar, number>
==>
interface Baz {
  a: number
  b: number
}
复制代码

ReturnType

得到一个函数的返回值类型c

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
复制代码

示例

type Func = (value: number) => string

const foo: ReturnType<Func> = '1'
复制代码

ReturnType获取到Func的返回值类型为string,所以,foo也就只能被赋值为字符串了。

NonNullable

它的作用是去除 T 中包含的null或者undefined

type NonNullable<T> = T extends null | undefined ? never : T;cc
复制代码

示例

type TFoo = 1 | null | undefined
let foo: NonNullable<TFoo> = 1 // 正确
foo = null // 错误,因为这个值已经被去除cc
复制代码

Parameters

获取一个函数的参数类型,返回的是包含一组类型的数组

type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;
复制代码

示例

type Func = (user: string,age:number) => void

type Param = Parameters<Func>
>>>>>Param: [user:string,age:number]
let p: Param = ['1',2] // 正确
p = ['1', '2'] // 错误,第二个是number类型
复制代码

ConstructorParameters

获取一个类的构造函数参数类型,并以数组的形式返回,类似于Parameters

type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;
复制代码

示例

class Foo {
  constructor(x: string, y: number){
    console.log(x, y)
  }
}
const foo: ConstructorParameters<typeof Foo> = ['1', 2]
复制代码

Omit

用来忽略 T 中的 K 属性c

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
复制代码

示例

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type Param = Omit<Todo, 'title'>;
>>>>
    Param:
    {
        description: string;
        completed: boolean;
    }
/* 忽略了title属性 */
const param:Param={ 
        description: ‘描述’;
        completed: false’;
	}
复制代码

非内置工具类型

DeepReadonly

DeepReadonly用来深度遍历 T,并将其所有属性变成只读类型【使用递归】

type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }
复制代码

示例

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  message: {
    name: string;
    content:string
  }
}
type msg=DeepReadonly<Todo>
/* 所有属性都是只读的 */
复制代码

可以类推:

/*所有类型非必填 */
type DeepReadonly<T> = {[P in keyof T]?: DeepReadonly<T[P]> }
...
复制代码

ConvertNumberToString【联合类型不适宜】

用来将number转换为string类型

type ConvertNumberToString<T> = {
  [K in keyof T]: T[K] extends number ? string : T[K]}
复制代码

示例

interface Todo {
  title: number;
  description: string;
  completed: boolean;
  message: {
    name: number;
    content: string;
  };
}

type msg = ConvertNumberToString<Todo>;


const param: msg = {
  title: '20',//正确,number被转变成string
  description: 'string',
  completed: false,
  message: {
    name: 20,//正确,因为非深度遍历缘故,类型还是number
    content: 'string',
  },
}
复制代码

进阶深度遍历:

type ConvertDeepNumberToString<T> = {
  [K in keyof T]: T[K] extends number ? string : ConvertDeepNumberToString<T[K]>}
复制代码

示例

interface Todo {
  title: number;
  description: string;
  completed: boolean;
  message: {
    name: number;
    content: string;
  };
}

type msg = ConvertNumberToString<Todo>;


const param: msg = {
  title: '20',//正确,number被转变成string
  description: 'string',
  completed: false,
  message: {
    name: '20',//正确,因为深度遍历缘故,number被转变成string
    content: 'string',
  },
}
复制代码

类推:用来将string转换为 number类型外加是否深度 等等等

ValueOf

ValueOfkeyof相对应。取出指定类型的所有 value

type ValueOf<T> = T[keyof T]
复制代码

示例

interface Todo {
  title: number;
  description: string;
  completed: boolean;
}

const aa: keyof Todo = 'title';

/* 可以是所有类型 */
const bb: ValueOf<Todo>=20;
const bb: ValueOf<Todo>='20';
const bb: ValueOf<Todo>=false;
复制代码

Mutable

用来将所有属性的readonly移除【浅层】

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}
复制代码

示例

interface Todo {
  readonly title: number;
  readonly description: string;
  readonly completed: boolean;
  message: {
    readonly name: number;
  };
}

const msg: Mutable<Todo> = {
  title: 20,
  description: 'string',
  completed: false,
  mss: {
    dd:20
  }
};
cc.mss.dd = 20;//错误,dd是只读属性
cc.title=20//正确
复制代码

深度遍历

type Mutable<T> = {
  -readonly [P in keyof T]: Mutable<T[P]>;
};
复制代码

类推:去除问号可选属性

type MutableZX<T> = {
  [P in keyof T]-?: Mutable<T[P]>;
};
复制代码

重构难以维护的代码

所有的想法都是为了构建一个健壮的可维护的代码

代码例子

添加参数

描述🍏:如果一个函数能够不需要任何参数能够解决你的问题(包括使用其他的函数,),这当然是绝佳的。但是在我们日常开发中需要经常为函数添加参数。

// ① 函数参数 (理论上少于等于2个)
function createMenu(title:string, body:string, buttonText:string, cancellable:boolean) {
  //...
}
复制代码

重构代码:

// ② 超过2个参数 使用对象统一传递
interface IMenuConfig {
  title: string;
  body: string;
  buttonText: string;c
  cancellable: boolean;
}
const menuConfig: IMenuConfig = {
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true,
};

function createMenu(menuConfig: IMenuConfig) {}
复制代码
合并条件表达式

描述🍏:一系列条件测试,都得到相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数

初始代码:

//例子1
const conditional:number=()=>{
	if (m_seniority < 2)
        return 0;
    if (m_monthsDisabled >12)
        return  0;
    if (m_isPartTime)
        return 0;
    //compute the disability amount
    return 1;
}
//例子2
const conditional:number=()=>{
	if (onVacation())
    {
        if (lengthOfService() > 10)
            return 1;
    }
    return  0.5;
}
复制代码

重构代码:

//例子1
const conditional:number=()=>{
	if (Disability())
        return 0;
    //compute the disability amount
    return 1;
}

const Disability:boolean=()=>{
    return  (m_seniority < 2 || m_monthsDisabled >12 || m_isPartTime);
}
//例子2
const conditional:number=()=>{
	return (onVacation() && lengthOfService() > 10) ?  1 : 0.5 ;
}
复制代码
合并重复的条件片段

描述🍏:一组条件表达式的所有分支都执行了相同的某段代码。你应该将这段代码搬移到表达式外面。这样,代码才能更清楚地表明哪些东西随条件变化而变化、哪些东西保持不变。

作法:

  • 鉴别出「执行方式不随条件变化而变化」的代码。
    • 如果这些共通代码位于条件式起始处,就将它移到条件式之前
    • 如果这些共通代码位于条件式尾端,就将它移到条件式之后
  • 如果这些共通代码位于条件式中段,就需要观察共通代码之前或之后的代码 是否改变了什么东西。如果的确有所改变,应该首先将共通代码向前或向后 移动,移至条件式的起始处或尾端,再以前面所说的办法来处理。
  • · 如果共通代码不止一条语句,你应该首先使用以Extract Method 将共通 代码提炼到一个独立函数中,再以前面所说的办法来处理。
if (isSpecialDeal()) {
     total = price * 0.95;
     send();
}else {
     total = price * 0.98;
      send();
 }
复制代码

重构代码:

if (isSpecialDeal())
    total = price * 0.95;
else
    total = price * 0.98;
send();
复制代码
以查询取代临时变量

描述🍏:我们都知道临时变量都是暂时的,而且只能在所属的函数中使用。所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替换为一个查询,那么同一个类中所有函数都将可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写出更清晰的代码

做法:

  • 找出只被赋值依次的临时变量(如果被使用多次,考虑将其分割成多个变量,对应在后续重构方法中)。
  • 编译(确保该临时变量的确只被赋值一次)
  • 对该临时变量实施“内联临时变量”重构方法。
  • 将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立的函数中。(日后你可能会发现还有好多地方需要使用它,那时放松对它的保护也很容易;确保提炼出的函数无任何副作用,即该c函数不修改任何对象内容)

简单的函数开始:

function getPrice():number {
	const basePrice:number = _quantity * _itemPrice;
	let  discountFactor;
	if (basePrice > 5000)
		discountFactor = 0.95;
	else
		discountFactor = 0.98;
	return basePrice * discountFactor;
}
复制代码

重构提取提示变量:

function getPrice():number {
	const basePrice:number = _quantity * _itemPrice;

	if (basePrice > 5000)
		return  0.95 * basePrice;
	else
		return  0.98 * basePrice;
}
复制代码

重构:将这个basePrice表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用

function getPrice():number {
	if (basePrice() > 5000)
		return  0.95 * basePrice();
	else
		return  0.98 * basePrice();
}
//独立到函数
function basePrice(){
		return  _quantity * _itemPrice;
}
复制代码

我们常常使用临时变量保存循环中的累加信息。在这种情况下,整个循环都可以被提炼为一个独立的函数,这可以减少原函数中几行循环逻辑代码。该手法的使用可能会让你担心性能上的问题。就像和其它性能问题一样,我们现在不用管它,因为十有八九根本没有造成任何影响。就算真的出问题了,你也可以在优化时期解决它。代码组织良好,你也会发现更有效的优化方案;如果没有进行重构,好的优化方案就可能与你失之交臂。

下一期直接分析Vue3源码

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改