# 对象类型-类
本章详细讲解 TypeScript 基于 class 的对象类型限制。 在开始之前建议阅读 MDN class (opens new window) 了解类的基本使用。
# 类的申明
es6 新增了采用 class 语法定义类的功能。
在 TypeScript class 申明同时创建了基于该类的对象类型。
// 创建一个类,同时也创建基于此类对象类型 Greeter
class Greeter {
// 声明属性 greeting 为字符串类型
greeting: string;
// 申明构造器函数传入参数为 string 类型
constructor(message: string) {
this.greeting = message;
}
greet() {
return 'Hello, ' + this.greeting;
}
}
// 限制 greeter 变量为 Greeter 类型的实例
let greeter: Greeter;
greeter = new Greeter('world');
console.log(greeter.greet());
将上述内容复制到 index.ts 设定 target 为 es5 或 es6 看一下输出差异。
# 编译结果为 es5 语法,输出文件名为 index.es5.js 输出采用构造器函数表示类
tsc index.ts --target es5 --outFile index.es5.js
# 编译结果为 es6 语法,输出文件名为 index.es6.js 采用类语法,输出删除了加黑部分非 JavaScript 的语法
tsc index.ts --target es6 --outFile index.es6.js
注意之前示例中的标黑内容,叫做 字段申明 (opens new window) 目前处于 stage3 的阶段,你可以在字段申明时赋默认值。
class A {
// 字段申明,实例化时会采用次默认值
a: string = 'default';
constructor(a?: string) {
if (a) {
this.a = a;
}
}
}
let b: A = new A();
console.log(b); // 返回默认值 { a: 'default' }
b = new A('foo');
console.log(b); // 传入值替换默认值 { a: 'foo' }
# 访问控制
类的成员包含方法和属性。在面向对象编程中一个常见的概念,叫做访问控制 (opens new window),它用于控制类的成员在类,子类和对象实例中的访问权限。 TypeScript 引入了此特性。通常在属性或方法前采用 public, private, protected 来申明限制,它们对类,子类,和对象实例在访问属性或方法的限制如下表:
| 访问类型 | 类 | 子类 | 对象和子对象 |
|---|---|---|---|
| public | 可以引用属性和方法 | 可以 | 可以 |
| protected | 可以 | 可以 | 不可以 |
| private | 可以 | 不可以 | 不可以 |
在 JavaScript 中采用类语法申明的属性和方法均为 public 类型,由于 public 为默认行为不赘述, 下面着重讲解 protected,private 的使用
# protected
protected 限制属性和方法只能在类及对应的子类中访问。
class Parent {
// 限制属性 foo 只有类和子类才可访问
protected foo: string;
constructor() {
this.foo = 'foo';
}
// 限制方法 setFoo 只有类和子类才可访问
protected setFoo(str: string) {
this.foo = str;
}
}
class Children extends Parent {
bar: string;
constructor(bar: string) {
super();
this.bar = bar;
}
changeFoo(str: string) {
// 子类中访问 setFoo 方法合法
this.setFoo(str);
}
getFoo() {
// 子类中访问 foo 属性合法
return this.foo;
}
}
let parent0: Parent = new Parent();
let children: Children = new Children('bar');
// 报错, 由于 属性 foo 申明为 protected 属性,类和子类的实例均无法访问此属性
console.log(parent1.foo);
console.log(children.foo);
// 报错, 由于 方法 setFoo 申明为 protected 属性,类和子类的实例均无法访问此方法
parent1.setFoo('foo1');
children.setFoo('foo1');
// 合法,getFoo 为公有方法,通过 Children 类访问到了 foo 属性的值
console.log(children.getFoo());
// 合法,changeFoo 为公有方法,通过 Children 类调用 setFoo
console.log(children.changeFoo('foo1'), children.getFoo());
在 es6 中可以使用
# private
private 限制属性和方法只能在类中访问,子类和实例均不可以。
class Parent1 {
// 限制属性 foo 只有类中才可访问
private _foo: string;
constructor() {
this._foo = 'foo';
}
// 限制 log 方法只有类才可访问
private _log(str: string) {
console.log(`${new Date()} set foo:`, str);
}
//foo 为私有属性, 通过 protected 提供方法给子类设置
protected setFoo(str: string) {
this._foo = str;
//log 为私有函数只能在类中使用
this._log(str);
}
//foo 为私有属性, 通过 protected 提供方法给子类读取
protected getFoo() {
return this._foo;
}
}
class Children1 extends Parent1 {
bar: string;
constructor(bar: string) {
super();
this.bar = bar;
}
changeFoo(str: string) {
//foo 为 private,子类无法直接设置,必须使用 changeFoo 才可以设置
this.setFoo(str);
}
showFoo() {
//foo 为 private,子类无法直接获取,必须使用 getFoo 才可以读取
return this.getFoo();
}
}
let parent1: Parent1 = new Parent1();
let children1: Children1 = new Children1('bar');
// 报错,属性 foo 申明为 private 属性,类和子类的实例均无法访问此属性
console.log(parent1._foo);
console.log(children1._foo);
// 报错,方法 log 申明为 private 属性,类和子类的实例均无法访问此方法
parent1._log('foo1');
children1._log('foo1');
// 合法, changeFoo 为公有方法
children1.changeFoo('foo1');
// 合法 changeFoo 为公有方法,通过 Children 类调用 setFoo
console.log(children1.showFoo());
TIP
JavaScript 类没有 private 控制,默认风格是在方法或属性名前使用 _ 表示私有属性或方法,你可以在 TypeScript 保留此风格。
TypeScript 于 3.8 加入了进入提案状态的 private (opens new window) 语法。采用 # 标识和使用私有变量。
class Person {
#name: string;
constructor(name:string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let locke = new Person('Locke');
// 合法
locke.greet();
// 报错,私有属性无法在实例上访问
locke.#name;TIP
VSCode 参看 Using newer TypeScript versions (opens new window) 更新编辑器的 TypeScript。避免版本问题,导致不支持新特性。
更详细的使用参看 TypeScript 3.8 ECMAScript Private Fields (opens new window)
# 属性修饰
之前讲过可使用 属性修饰 限制属性为只读或可选,在申明类中也可使用此特性。
你可以结合访问控制与属性修饰设置类。
class Octopus1 {
// 属性为只读可选
readonly #name?: string;
readonly numberOfLegs: number = 8;
constructor(theName?: string) {
// 由于属性可选有值才添加
if (theName) {
this.#name = theName;
}
}
showName() {
console.log(this.#name)
}
}
let o2: Octopus1 = new Octopus1();
// 由于属性可选,输出为 { numberOfLegs: 8 } 无 name 属性
console.log(o2);
// 报错,私有属性无法访问
o2.#name;
// 报错, 属性并未创建, 无法访问
o2.showName();
o2 = new Octopus1('Paul');
console.log(o2); // 返回 { numberOfLegs: 8, name: 'Paul' }
// 合法, 使用函数访问私有属性
o2.showName();
//报错, 私有属性无法访问
console.log(o2.#name);
# 访问器属性
除了常规属性也支持 访问器属性 (opens new window)。实际上规范内部的 Object.__proto__ 属性就是一个访问器属性。
const fullNameMaxLength = 10;
class Employee {
/**
* 注意这里 ! 由于在严格模式, TypeScript 会要求所有属性必须初始化,若没有则会报错。
* 采用 ! 告知 TypeScript , _fullName 属性一定会有值避免检查报错
*
* */
private _fullName!: string;
get fullName(): string {
return this._fullName;
}
// 利用 setter 输入值长度进行检查
set fullName(newName: string) {
if (newName && newName.length > fullNameMaxLength) {
throw new Error(`fullname has a max length of ${fullNameMaxLength}`);
}
this._fullName = newName;
}
}
let employee = new Employee();
// 合法
employee.fullName = 'Tom';
console.log(employee.fullName);
// 运行时报错, 长度超过限制
employee.fullName = 'foo bar foo bar ';
TIP
示例中的 属性 !: 类型 叫做明确赋值断言(definite assignment assertion)。也是一种类型断言,详情见 Definite Assignment Assertions (opens new window)
对于只设置了 get 的属性,TypeScript 会自动推导为只读属性。
# 构造器的访问控制
由于类中的构造器也是函数所以同样支持 private,protected 的控制。
// 单例模式
class Singleton {
private static instance: Singleton;
// 阻止采用构造器实例化
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
let v = Singleton.getInstance();
// 报错, 不能采用 new 触发构造函数
let e = new Singleton();
# 静态属性和方法
JavaScript 支持在类之前申明 static 常见静态属性和方法。TypeScript 也可进行相应限制。
// 申明一个点的接口
interface Point {
x: number;
y: number;
}
// 申明一个坐标系类
class Grid {
// 设置静态属性 origin 为私有
private static origin = { x: 0, y: 0 };
// 提供重置 origin 的方法
static setOrigin(point: Point) {
Grid.origin = point;
}
// 计算两点距离
calculateDistanceFromOrigin(point: Point) {
let xDist = point.x - Grid.origin.x;
let yDist = point.y - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist);
}
}
let grid1 = new Grid();
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 坐标系偏移 5,5
Grid.setOrigin({ x: 5, y: 5 });
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 报错, 不能直接访问私有静态成员
Grid.origin = { x: 0, y: 0 };
你也可以尝试结合属性修饰限制静态属性。
# 实例类型和构造器类型
每当采用 class 语法申明一个类时, TypeScript 同时创建了该类对应的实例类型。若使用构造函数方式创建对象实例,如何申明类对应的构造函数类型?
可以采用 typeof <classname> 提取类的构造器类型。
class Greeter1 {
static PREFIX = '';
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
// 对象实例调用此方法
if (this.greeting) {
console.log(`${Greeter1.PREFIX} ${this.greeting}`);
} else {
// 非成员调用触发
console.log(`${Greeter1.PREFIX}`);
}
}
}
let greet1: Greeter1 = new Greeter1('morning');
greet1.greet();
// 申明一个工厂方法,由于此时需要返回 Greeter 的构造器所以采用 typeof Greeter 来申明返回类型
function GreetMaker(msg: string): typeof Greeter1 {
Greeter1.PREFIX = msg;
return Greeter1;
}
// 利用工厂方法, 创建 bye 的构造函数
let BeyGreeter: typeof Greeter1 = GreetMaker('Bye');
let greet2: Greeter1 = new BeyGreeter('tom');
greet2.greet();
# 总结
本小节我们学习了如何对类及其静态方法进行类型限制。知识点如下:
- 利用
class创建类的同时也创建了基于此类的类型 - 采用访问控制限制类,子类及对象实例的属性和方法
public默认行为,均可访问protected类和子类才可访问,实例无法访问private只有类才可访问
- 可以使属性修饰器
readonly或可选属性搭配访问空