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,是一件很美好的事
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。