TypeScript丨进阶(2)

Typescript优点之一:增强的oo,可以用更多设计模式,IoC,AOP...

class

js中,使用函数和基于原型的继承来创建可重用的组件
es6中,JavaScript程序员将能够使用基于类的面向对象的方式
ts中,允许开发者现在使用class更多的特性【public,private,protected...】,而不需要等到下个JavaScript版本

在构造函数里访问 this的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

js中,生成实例对象的传统方法是通过构造函数
function Dog(name) {
  this.name = name;
}
Dog.prototype.voice = () => {
  console.log("wang wang wang");
};
const dog = new Dog("yello dog");
dog.voice();
// 输出:wang wang wang
ts中,我们可以使用更多的特性,更好的oo编程

一个例子描述所有特性【public / private / protected / readonly / static / 存取器[get/set] / 继承 / 重写】

/*
  基类/超类
 */
class Animal {
  // 构造函数初始化赋值的两种写法

  // 1.
  // private what: string;
  // constructor(what: string) {
  //   this.what = what;
  // }

  // 2.
  constructor(private what: string) {}

  public eat() {
    console.log("i am an animal,i eat");
  }
}

/*
  派生类 / 子类
  ts规定子类必须要在构造函数写 super()
 */
class Dog extends Animal {
  public name: string;
  protected color: string;
  readonly legs: number;
  constructor(name: string = "", color: string = "") {
    super("dog");
    this.name = name;
    this.color = color;
    this.legs = 4;
  }
  public voice() {
    console.log("wang wang wang...");
  }
}

const dog = new Dog();

/*
  继承 Dog
 */
class YellowDog extends Dog {
  // 静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化
  static yellowDogNum: number = 0;

  public firstName: string = "";
  public lastName: string = "";

  // 存取器
  get fullName(): string {
    return this.firstName + "." + this.lastName;
  }

  set fullName(fullName: string) {
    if (fullName === "dog.king") {
      console.log("名字不能为dog.king");
    }
  }

  constructor(name: string, color: string) {
    super(name, color);

    YellowDog.yellowDogNum += 1;

    this.color = "yellow yellow yellow";
    // 在子类中可以访问父类的 protected 成员

    // this.what = "";
    // 在子类中不可以访问父类的 private 成员
    // error:Property 'what' is private and only accessible within class 'Animal'.

    console.log(this.getDogNums());
  }
  private getDogNums() {
    return `现在一共有${YellowDog.yellowDogNum}只黄狗`;
  }
  // 重写 父类 eat 方法
  public eat() {
    console.log("i am YelloDog,i eat");
    // 调用父类方法
    console.log("parent-eat:");
    super.eat();
  }
}

const yellowDog = new YellowDog("yellow dog", "yellow");
// 现在一共有1只黄狗
console.log(yellowDog);

yellowDog.legs = 5;
// 不能修改read-only属性
// error:Cannot assign to 'legs' because it is a read-only property.

console.log(yellowDog.what);
// 实例对象不可以访问 private 成员
// error:Property 'what' is private and only accessible within class 'Animal'.

console.log(yellowDog.color);
// 实例对象不可以访问 protected 成员
// error:Property 'what' is private and only accessible within class 'Animal'.

yellowDog.eat();
// 输出:i am YelloDog,i eat

yellowDog.firstName = "dog";
yellowDog.lastName = "queen";
console.log(yellowDog.fullName);
// 输出:dog.queen

yellowDog.fullName = "dog.king";
// 输出:名字不能为dog.king

const yellowDog2 = new YellowDog("yellow dog2", "yellow2");
// 输出:现在一共有2只黄狗
const yellowDog3 = new YellowDog("yellow dog3", "yellow3");
// 输出:现在一共有3只黄狗
抽象类

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现

abstract class Person {
  constructor(public name: string) {}
  printName(): void {
    console.log("Person name: " + this.name);
  }
  // 必须在派生类中实现
  abstract working(): void;
}

class Workman extends Person {
  constructor() {
    super("工人");
  }

  working(): void {
    console.log("working 8 hours");
  }

  rest(): void {
    console.log("rest...");
  }
}

// 允许创建一个对抽象类型的引用
let workman: Person;


workman = new Person();
// 错误:不能创建一个抽象类的实例

// 允许对一个抽象子类进行实例化和赋值
workman = new Workman();
workman.printName();
workman.working();

workman.rest();
// 错误:方法在声明的抽象类中不存在
把类当做接口使用

因为类可以创建出类型,所以你能够在允许使用接口的地方使用类,
所以同名的 类和接口 也会被合并

class User {
  id?: string;
  age?: number;
  sleep?: () => void;
  run() {
    return "run";
  }
}
interface User {
  name: string;
  hobby: string;
  play: () => string[];
}
const user: User = {
  name: "zhangsan",
  hobby: "working",
  play() {
    return ["game", "ball"];
  },
  run() {
    return "running";
  }
};

装饰器 (Decorators)

在一些场景下我们需要额外的特性来支持标注或修改类及其成员。
装饰器为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
Javascript里的装饰器目前处在征集阶段,但在TypeScript里已做为一项实验性特性予以支持。

装饰器是一种特殊类型的声明,它能够被附加到类、方法、访问符、属性、参数上。
装饰器使用 @expression这种形式,esxpression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器要传入参数的话必须再包一层函数,然后 return 装饰器函数

先了解 Typescript 内置接口 PropertyDescriptor【属性描述符】
interface PropertyDescriptor {
  /* 
    configurable 键值为 true 时,
    能够重新定义属性,
    能够删除属性
    默认:false
  */
  configurable?: boolean;
  /*
    是否可枚举
    for..in
    Object.keys
    JSON.stringify
    Object.assign()
    默认 false
   */
  enumerable?: boolean;
  // 属性的值
  value?: any;
  /*
   当且仅当该属性的 writable 键值为 true 时,
   属性的值,才能被赋值运算符改变。 
   默认 false
   */
  writable?: boolean;

  get?(): any;
  set?(v: any): void;
}
类装饰器
interface Test {
  fn: () => void;
}
// 附加到 类 只能有一个参数,target,是一个构造函数
const Inject = (fn: () => void = () => {}) => (target: any) => {
  // 静态成员赋值方式
  // target.fn = fn;

  target.prototype.fn = fn;
};

@Inject(() => {
  console.log("my name is fn");
})
class Test {}
const test = new Test();
console.log(test.fn());
// 输出:my name is fn
方法装饰器
// 装饰器有参数
const makeFriends = (key: string) => {
  return (
    target: any,
    propertyKey: string,
    // 属性描述
    descriptor: PropertyDescriptor
  ) => {
    const origin = descriptor.value;
    descriptor.value = function (...args: any[]) {
      (this as any)[key] = true;
      origin.apply(this, args);
      (this as any)[key] = false;
    };
  };
};

interface Test {
  fn: () => void;
}

class Test {
  private _goOut: boolean;
  role: string;
  constructor() {
    this._goOut = false;
    this.role = "parent";
  }

  get goOut() {
    return this._goOut;
  }
  set goOut(val: boolean) {
    this._goOut = val;
    console.log("current-goOut :>> ", this._goOut);
  }

  @makeFriends("goOut")
  sayHello(name: string, age: number) {
    console.log("currthis.role :>> ", this.role);
    console.log(`hi,my name is ${name},i am ${age} years old`);
  }
}
const test = new Test();

test.sayHello("zhangsan", 10);

控制台输出

current-goOut :>>  true
currthis.role :>>  parent 
hi,my name is zhangsan,i am 10 years old 
current-goOut :>>  false

当有多个装饰器的时候。
装饰器会由上至下依次对装饰器表达式求值。
求值的结果会被当作函数,由下至上依次调用。

class Xx{
  @f
  @g
  xxx
}

等价于 

f(g(xxx))  
访问符/属性/参数 修饰器
// appendStr.ts
/**
 * 访问符
 * @desc 追加字符串
 */
const appendStr = (apdStr: string = "") => (
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  const origin = descriptor.set;
  descriptor.set = function (val: string = "") {
    origin!.call(this, val + apdStr);
  };
};

// initProp.ts
/**
 * 属性
 * @desc 初始化值
 */
const initProp = (val: string = "") => (target: any, propertyKey: string) => {
  target[propertyKey] = val;
};

// log.ts
/**
 * 参数
 */
const log = (target: any, propertyKey: string, parameterIndex: number) => {
  console.log("参数位置", parameterIndex);
};

// index.ts
class Test {
  private _a: string;

  @initProp("hello")
  public b?: string;

  constructor() {
    this._a = "";
  }

  get a() {
    return this._a;
  }
  @appendStr("haha")
  set a(val: string) {
    this._a = val;
  }

  hello(@log c: string) {}
}

// 实例
const test = new Test();

// 访问符测试
test.a = "xixi";
console.log(test.a);
// 输出:xixihaha

// 属性测试
console.log(test.b);
// 输出:hello

AOP(Aspect Oriented Programming)

主要实现的目的是针对业务处理过程中的切面进行提取,所面对的是处理过程中某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。

AOP是对OOP的一个横向的补充,主要作用是把一些业务无关的功能抽离,例如日志打印、统计数据、安全控制、异常处理等。这些功能都与核心业务无关,但又随处可见。将其抽离出来用动态插入的方式嵌入到各业务逻辑中。让业务模块变得比较干净、不受污染,同时功能点能够得到很好的复用,给模块解耦。

前置通知 、后置通知 、环绕通知

如:

before(前置通知)
场景:
提交表单时,我们想要先通过一系列的验证,然后再执行表单提交
系统统计用户点击某个按钮的次数

around(环绕通知)
场景:提交表单的时候,点击提交按钮。我想要在提交前把按钮禁用掉,然后提交成功后【ajax请求后】把按钮启用。

环绕通知例子

ThorttleButton.ts

/**
 * @desc 按钮节流开关,场景:用在异步提交表单的时候,防止用户重复提交
 * 1开 0关
 * @param prop
 */
const ThorttleButton = (prop: string) => (
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
): any => {
  const origin = descriptor.value;
  descriptor.value = async function(...args: any[]) {
    (this as any)[prop] = false;
    await origin.apply(this, args);
    (this as any)[prop] = true;
  };
};
export default ThorttleButton;

调用

<!-- ... -->
<button :disabled="postBtn">提交</button>
<!-- ... -->

import ThorttleButton from '@/utils/decorate/ThorttleButton';
// ...
postBtn: boolean = true;
// ...

@ThorttleButton('postBtn')
async postForm() {
  await this.$request.post('/postData');
}

// ...

IoC(Inversion of Control)

  • 控制反转思想
  • 依赖倒置原则(Dependence Inversion Principle)
  • 依赖注入(Dependency Injection)是具体实现
  • 高层次的模块不应该依赖于低层次的模块
  • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
  • 面向接口编程
  • 可以解耦具有依赖关系的对象,降低开发维护成本

假设我们要实现一个 App,App有几个模块
停车场模块,统计停车场共有几辆车
用户功能,显示停车场的所有用户

于是我们开始编写模块

car.ts

class Car {
  constructor() {}
  getNum() {
    const num = 1000;
    console.log("初始化显示所有车辆数:", num);
    return num;
  }
}
export default Car;

user.ts

class User {
  constructor() {}
  getAll() {
    const users = Array.apply(Array, Array(1000)).map((_, i) => ({
      id: i + 1,
      name: "张三" + i
    }));
    console.log("初始化显示所有用户:", users);
    return users;
  }
}
export default User;

App.ts

import Car from "./car";
import User from "./user";

class App {
  car: Car;
  user: User;
  advertising: Advertising;
  constructor() {
    this.car = new Car();
    this.user = new User();

    this.init();
  }

  init() {
    this.car.getNum();
    this.user.getAll();
  }
}

export default App;

index.ts

import App from "./App";

new App();

上线稳定后,产品经理准备新增广告【advertising】功能了
于是我们新增了

advertising.ts【新增】

class Advertising {
  constructor() {}
  getAll() {
    const users = Array.apply(Array, Array(3)).map((_, i) => ({
      id: i + 1,
      name: "轮播广告" + i
    }));
    console.log("初始化显示所有广告:", users);
    return users;
  }
}
export default Advertising;

App.ts【修改】

import Car from "./car";
import User from "./user";
// 新增
import Advertising from "./advertising";

class App {
  car: Car;
  user: User;
  advertising: Advertising;
  constructor() {
    this.car = new Car();
    this.user = new User();
    // 新增
    this.advertising = new Advertising();

    this.init();
  }

  init() {
    this.car.getNum();
    this.user.getAll();
    // 新增
    this.advertising.getAll();
  }
}

export default App;

分析:
从App来看,会迭代更新,模块会越来越多
从IoC来看,this.car 和 this.user ...,都还是对 “具体实现” 的依赖。违背了 IoC思想的准则,需要进一步抽象 App 模块。
从编码上来看,这种不完全符合 “开闭原则” 的方式很容易产生额外的bug。

我们来改造一下

改造App.ts

import { TModule } from "./modules.type";

interface App {
  [prop: string]: any;
}

class App {
  static modules: TModule[] = [];
  constructor() {
    App.modules.forEach((module) => {
      module.init(this);
    });
  }
  // 依赖注入
  static inject(module: TModule) {
    App.modules.push(module);
  }
}

export default App;

新增了 modules.type.ts 文件,用来声明约束需要注入的module

import Car from "./car";
//import User from "./user";
//import Advertising from "./advertising";

export type TModule = Car

改造car.ts

import App from "./App";
class Car {
  constructor() {}
  init(app: App){
    app.car = this;
  }
  getNum() {
    const num = 1000;
    console.log("初始化显示所有车辆数:", num);
    return num;
  }
}
export default Car;

在index.ts里

import App from "./App";
import Car from "./car";

App.inject(new Car());

const app = new App();

(app.car as Car).getNum();
// 初始化显示所有车辆数:1000
还没完,我们要更灵活,更优雅

我们实现一个类装饰器,用于实例化所依赖的低层模块,并将其注入到容器(App)内

新增 decrotate.inject.ts

import { TModule } from "./modules.type";

const Inject = (modules: TModule[]) => (
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  modules.forEach((module: TModule) => {
    module.init(target);
  });
};
export default Inject;

改造car.ts

import App from "./App";
class Car {
  constructor() {}
  init(app: App){
    // app.car = this;

    // 要用prototype赋值,因为装饰器执行的顺序在App实例化之前
    app.prototype.car = this;
  }
  getNum() {
    const num = 1000;
    console.log("初始化显示所有车辆数:", num);
    return num;
  }
}
export default Car;

改造App.ts

import Inject from "./decrotate.inject";
import Car from "./car";

@Inject([new Car()])
class App {
  constructor() {}
}

export default App;
总结

这其实就是 IoC 思想中对 “面向接口编程,而不要面向实现编程”,App不关心模块具体实现了什么,只要满足对 接口 init 的约定就可以了。
App 模块此时应该称之为“容器”比较合适了,跟业务已经没有任何关系了,它仅仅只是提供了一些方法来辅助管理注入的依赖和控制模块如何执行。
控制反转(Inversion of Control)是一种思想,依赖注入(Dependency Injection)则是这一思想的一种具体实现,这里的 App 则是辅助依赖管理的一个容器。

TypeScript 拥有的特性可以让我们对oo体验更好,个人认为对于 Java/C# 的后端小伙伴来说,Node + TypeScript + typescript-ioc,是一件很美好的事

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容