oop 基本单位
基本场景:
面向过程的编程遇到问题经常会思考输入和输出,以及对数据的处理。
面向对象的编程则倾向于将数据和对数据的处理组织成一个模板, 当你需要解决问题的时候, 通过模板实例化出一个具体的对象让他去处理。
传统的 javascript 是基于对象的。 es 高级版本虽然加上了 class 的语法糖。 不过大多数前端并不会用 oop 的思维去编程,原因很多,别扭, 没必要, 习惯了函数式编程…..
typescript 借鉴很多 oop 语言,提供了比较多的对 oop 语法层面的支持, 一一来看。
// 类声明
class Greeter {
// 属性
greeting: string;
// 构造
constructor(message: string) {
this.greeting = message;
}
// 方法
greet() {
return "Hello, " + this.greeting;
}
}
// 实例化
let greeter = new Greeter("world");
public(defaule), protected, private[子类,外部都不可访问,可配合 get set 实现访问修改]
TypeScript使用的是结构型类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有 private或 protected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected 成员也使用这个规则。
修饰符修饰普通属性和方法时的可访问性:
修饰符 | 本类内部 | 子类 | 外部 |
---|---|---|---|
public | √ | √ | √ |
protected | √ | √ | × |
private | √ | × | × |
修饰符修饰构造时可实例化: ( const xx = new x())
修饰符 | 本类内部 | 子类 | 外部 |
---|---|---|---|
public | √ | √ | √ |
protected | √ | √ | × |
private | √ | 不可被继承 | × |
tip: 访问权限修饰符修饰还可以修饰构造参数,此时表示一种简写。
class A {
public a1: string;
protected a2: string;
private a3: string;
constructor(a1: string, a2: string, a3: string) {
this.a1 = a1;
this.a2 = a2;
this.a3 = a3;
}
}
class HA {
constructor(public a1: string,
protected a2: string,
private a3: string) { }
}
const a = new A('a', 'b', 'c');
const ha = new HA('d', 'e', 'f');
console.log(a, ha); // A { a1: "a", a2: "b", a3: "c" } HA { a1: "d", a2: "e", a3: "f" }
注意点:
- “set” 访问器不能具有返回类型批注。
- 存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。
- 这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
很多语言中推崇这样一种规范: 虽有的属性都设计成 private, 然后使用 getter/setter 实现存取。
有人说了你这不是多此一举么?确实,对于大多数形如这样的存取器确实在 【访问方式】上和设计成 public 没有区别。
class P {
constructor(private _a: number) {}
get a(): number {
return this._a;
}
set a(_a: number) {
this._a = _a;
}
}
const p = new P(1);
console.log(p.a, p.a = 10, p.a); // 1, 10, 10
但实际上是完全不同的理念,怎么理解呢? 就好像别人想知道你的年收入,他有两种方案。
这时候呢如果你是方案一, 则他就直接知道了。 但如果设计成方案二, 你就可以根据问的人的熟悉度不同而返回一个处理过的结果给对应的人。这个处理过程则由你决定策略。
你会怎么设计呢?
所谓的静态成员,在使用上表现为不用实例化就能使用的成员。
理解起来也很简单。在 js 中,想要实例化一个对象,方法之一是创建一个 function 作为模板,然后 new 去调用这个 function 就能产生实例, function 中所有挂载在 this 上的成员,都会成为实例化的对象的成员。
同时呢,函数除了作为模板外(类似于类), 在 js 中也是一个对象。 所以就能在对象上挂载一些操作和属性,这也就是 js 中’类’的静态成员。
function Dog(name, age) {
this.name = name;
this.age = age;
}
// 成员方法
Dog.prototype.gatekeeper = function () {
console.log(`${this.name}: go away!`);
}
// 静态方法
Dog.legCount = 4;
Dog.run = function () {
console.log(`running dog by ${Dog.legCount} legs`);
// 静态方法中的 this 即是类对象本身。被 java 先入为主了, 一直以为静态方法中不能访问 this。
console.log(`running dog by ${this.legCount} legs`);
}
Dog.run(); // running dog by 4 legs
new Dog('dahuang', 4).gatekeeper(); // dahuang: go away!
tsc 实现这样一个基本结构代码如下:
静态成员存在于类本身上面而不是类的实例上.
静态成员可以被访问权限修饰符修饰, 表现和实例成员一致。
静态成员也可以被继承,继承方式、表现和实例成员一致。
不能实例化
抽象类很重要的一个价值在于兼容接口变化,如果极端情况下接口扩展了,所有的子类直接实现接口的话,就悲剧了。所有子类都需要扩展。但是如果有一层抽象类的话, 就可以在这一层做通用的实现方案。
TODO 如何修改或者包装类的实例成员? 如果能拿到实例成员,才是真的强大。
作为动态语言,类也能作为参数和返回值进行传递,所以反射什么的,都是瞎 jb 折腾 😂😂
类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
这也是接口可以继承类的根本原因, 虽说我觉得这是个徒增复杂度的功能。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = { x: 1, y: 2, z: 3 };
let point2d: Point = { x: 1, y: 2 };
非常强大的功能
多态(Polymorphism)按字面的意思就是“多种状态”, 在面向对象语言中,接口的多种不同的实现方式即为多态, 多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
利用多态和接口配合可以写出非常抽象的上层实现。
[综合案例:接口定义规范+抽象类过度+实现类聚合复合]
但其实要理解一点和传统 oop 语言的差别: TypeScript 使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
也就是说 TS 的多态并不是非要依托于继承关系之上。 总之如果学过其他的 oop 多态 ,则理解起来有些费劲。