Harmony os------ArkTS 语言笔记(四):类、对象、接口和抽象类
这一篇主要整理 ArkTS 里的 类 / 对象 / 接口 / 抽象类 。
写的时候我尽量按"实际写代码时会遇到的问题"来组织
一、从一个最普通的类开始
最标准的 ArkTS 类声明长这样:
ts
class Person {
name: string = '';
surname: string = '';
constructor(n: string, sn: string) {
this.name = n;
this.surname = sn;
}
fullName(): string {
return this.name + ' ' + this.surname;
}
}
let p = new Person('John', 'Smith');
console.info(p.fullName());
几个要点:
class引入一个新类型 :Person- 里面可以有:
- 字段(
name,surname) - 构造函数(
constructor) - 方法(
fullName())
- 字段(
- 使用
new创建实例:new Person(...)
这部分和 TypeScript 基本是一致的。
二、字段:实例字段、静态字段和"必须初始化"这件事
2.1 实例字段 vs 静态字段
实例字段:每个对象各有一份。
class Person {
name: string = '';
age: number = 0;
constructor(n: string, a: number) {
this.name = n;
this.age = a;
}
getName(): string {
return this.name;
}
}
let p1 = new Person('Alice', 25); // p1 有自己的 name/age
let p2 = new Person('Bob', 28); // p2 有自己的 name/age
静态字段:属于类本身,所有实例共享一份。
class Person {
static numberOfPersons = 0;
constructor() {
Person.numberOfPersons++;
}
}
console.info(Person.numberOfPersons);
访问方式:
- 实例字段:
实例.字段 - 静态字段:
类名.字段
个人习惯:
- "统计类信息、全局配置、缓存"放静态字段
- "跟具体对象相关的属性"用实例字段
2.2 ArkTS 为什么强制"字段必须初始化"?
ArkTS 的要求是:
字段要么在声明时 初始化,要么在构造函数里初始化。
不这么做,会怎样?看这个例子(错误写法):
class Person {
name: string; // 这里其实是 undefined
setName(n: string): void {
this.name = n;
}
getName(): string {
return this.name; // 这里假装一定是 string
}
}
let jack = new Person();
// 从头到尾都没给 jack.name 赋过值
jack.getName().length; // 运行时直接炸:name is undefined
问题关键点:
name实际上可能是undefined;- 但类型标成了
string,骗过了类型系统; - 最终在
getName().length这里炸掉。
ArkTS 的思路是:尽量在编译期就暴露问题,不要把炸弹留到运行时。
改成 ArkTS 推荐写法:
class Person {
name: string = ''; // 明确初始化为 ''
setName(n: string): void {
this.name = n;
}
getName(): string { // 现在真的保证是 string,不是 undefined
return this.name;
}
}
let jack = new Person();
jack.getName().length; // 0,正常执行
如果字段确实有可能缺失,就把这个可能性写在类型上:
class Person {
name?: string; // 类型:string | undefined
setName(n: string): void {
this.name = n;
}
// 错误示例:返回值不能只写 string
getNameWrong(): string {
return this.name; // 编译器会报错
}
getName(): string | undefined {
return this.name;
}
}
let jack = new Person();
// 编译期就会阻止这种写法:
jack.getName().length; // ❌ 编译失败
// 正确写法:加上可选链或其他判断
jack.getName()?.length; // ✅ 编译通过,没有运行时异常
我自己的理解:
ArkTS 不想让你偷懒。
要么一开始就给个安全默认值,要么在类型上诚实地承认:"它可能是 undefined"。
2.3 getter / setter:做"受控属性"
有些字段不希望被随便改,希望在赋值时做校验,这就是 getter/setter 的场景。
class Person {
name: string = '';
private _age: number = 0;
get age(): number {
return this._age;
}
set age(x: number) {
if (x < 0) {
throw Error('Invalid age argument');
}
this._age = x;
}
}
let p = new Person();
console.info(p.age); // 0
p.age = -42; // 抛异常
特点:
- 对外访问方式还是
p.age/p.age = ... - 但内部可以加校验、日志、派发事件等逻辑
三、方法:实例方法、静态方法、继承和重写
3.1 实例方法
实例方法依赖具体对象的状态来计算结果。
class RectangleSize {
private height: number = 0;
private width: number = 0;
constructor(height: number, width: number) {
this.height = height;
this.width = width;
}
calculateArea(): number {
return this.height * this.width;
}
}
let square = new RectangleSize(10, 10);
square.calculateArea(); // 100
3.2 静态方法
静态方法属于类本身,一般用于"与具体实例无关的工具行为"。
class Cl {
static staticMethod(): string {
return 'this is a static method.';
}
}
console.info(Cl.staticMethod());
特点:
- 通过类名调用:
Cl.staticMethod() - 静态方法里只能直接访问静态成员,不访问某个具体实例的字段
3.3 继承:extends + implements
类的定义可以同时:
-
extends一个基类; -
implements若干接口。class Person {
name: string = '';
private _age = 0;get age(): number { return this._age; }}
class Employee extends Person {
salary: number = 0;calculateTaxes(): number { return this.salary * 0.42; }}
实现接口:
interface DateInterface {
now(): string;
}
class MyDate implements DateInterface {
now(): string {
return 'now';
}
}
小总结:
- 继承(extends):继承"实现 + 状态",可以复用代码。
- 实现(implements):只继承"约定",必须自己实现方法。
3.4 super:访问父类构造和方法
class RectangleSize {
protected height: number = 0;
protected width: number = 0;
constructor(h: number, w: number) {
this.height = h;
this.width = w;
}
draw() {
/* 画边框 */
}
}
class FilledRectangle extends RectangleSize {
color = '';
constructor(h: number, w: number, c: string) {
super(h, w); // 调用父类构造函数
this.color = c;
}
draw() {
super.draw(); // 先画边框
/* 再填充颜色 */
}
}
注意:
- 在派生类构造函数中,
super(...)必须是第一条语句。 - 重写方法时,如果想保留父类部分逻辑,就调用
super.xxx()。
3.5 方法重写(override 的思想)
子类可以用同名方法覆盖父类实现,只要:
-
参数列表一致;
-
返回类型相同或是父类返回类型的子类型。
class RectangleSize {
area(): number {
return 0;
}
}class Square extends RectangleSize {
private side: number = 0;area(): number { return this.side * this.side; }}
3.6 方法重载签名(类里的"多态调用")
和函数重载一样,方法也可以写多个重载签名 + 一个实现:
class C {
foo(x: number): void; /* 第一个签名 */
foo(x: string): void; /* 第二个签名 */
foo(x: number | string): void { /* 实现签名 */
// ...
}
}
let c = new C();
c.foo(123); // 使用第一个签名
c.foo('aa'); // 使用第二个签名
注意:
- 重载签名的参数列表不能完全相同;
- 实现签名必须能覆盖所有前面的重载情况(通常用联合类型)。
四、构造函数:初始化对象状态
4.1 基本构造函数
class Point {
x: number = 0;
y: number = 0;
}
let p = new Point(); // 没有自定义构造函数时,系统给你一个默认的
如果你自己写了构造函数,就可以定制初始化逻辑:
class RectangleSize {
width: number = 0;
height: number = 0;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
}
4.2 派生类构造函数:必须先 super(...)
class RectangleSize {
constructor(width: number, height: number) {
// ...
}
}
class Square extends RectangleSize {
constructor(side: number) {
super(side, side);
}
}
这块和 TS/ES 的规则一样:
子类构造函数里,在访问 this 之前一定要先调用 super(...)。
4.3 构造函数重载签名
写法和方法重载很像:
class C {
constructor(x: number) /* 第一个签名 */
constructor(x: string) /* 第二个签名 */
constructor(x: number | string) { /* 实现签名 */
// ...
}
}
let c1 = new C(123);
let c2 = new C('abc');
同样的规则:签名参数列表不能一模一样。
五、可见性修饰符:public / private / protected
默认可见性是 public。手动标时有三种:
5.1 public(公有)
- 任何地方都可以访问(只要能访问到这个类)。
5.2 private(私有)
-
只能在类内部访问,外部和子类都不行。
class C {
public x: string = '';
private y: string = '';set_y(new_y: string) { this.y = new_y; // ✅ 类内部访问私有字段 }}
let c = new C();
c.x = 'a'; // ✅
c.y = 'b'; // ❌ 编译错误:'y' 是私有的
5.3 protected(受保护)
-
和
private类似,但子类中可以访问。class Base {
protected x: string = '';
private y: string = '';
}class Derived extends Base {
foo() {
this.x = 'a'; // ✅ protected 子类可访问
this.y = 'b'; // ❌ 编译错误:private 只能在 Base 内部访问
}
}
简单记法:
- 只给自己用:
private- 自己和子类用:
protected- 对外开放:
public
六、对象字面量、Record 和"结构化数据"
6.1 用对象字面量初始化类类型
class C {
n: number = 0;
s: string = '';
}
let c: C = { n: 42, s: 'foo' };
这里有一个前提:上下文要能推断出类型是 C,否则 ArkTS 不知道这个字面量应该匹配谁。
还可以这样用:
class C {
n: number = 0;
s: string = '';
}
function foo(c: C) {}
let c: C;
c = { n: 42, s: 'foo' }; // 利用变量类型
foo({ n: 42, s: 'foo' }); // 利用参数类型
function bar(): C {
return { n: 42, s: 'foo' }; // 利用返回类型
}
数组中也一样:
class C {
n: number = 0;
s: string = '';
}
let cc: C[] = [
{ n: 1, s: 'a' },
{ n: 2, s: 'b' },
];
归纳一下:
对象字面量本身是"长得像某个类型"的一坨数据,ArkTS 需要一个上下文来知道它应该被当成哪种类型。
6.2 Record<K, V>:用键值映射来表达表格数据
Record<K, V> 本质就是:键类型为 K,值类型为 V 的一个对象类型。
let map: Record<string, number> = {
'John': 25,
'Mary': 21,
};
map['John']; // 25
K可以是string或number(不包括 BigInt)V可以是任意类型
结合复杂类型:
interface PersonInfo {
age: number;
salary: number;
}
let map: Record<string, PersonInfo> = {
'John': { age: 25, salary: 10 },
'Mary': { age: 21, salary: 20 },
};
这在做"名字 → 配置""ID → 对象信息"等映射时非常自然。
七、抽象类:不能直接 new,只能被继承
7.1 抽象类基本概念
带 abstract 的类就是抽象类,不能直接实例化。
abstract class X {
field: number;
constructor(p: number) {
this.field = p;
}
}
let x = new X(666); // ❌ 编译错误:不能创建抽象类实例
但可以继承它:
abstract class Base {
field: number;
constructor(p: number) {
this.field = p;
}
}
class Derived extends Base {
constructor(p: number) {
super(p);
}
}
let x = new Derived(666); // ✅
7.2 抽象方法
抽象方法只有声明,没有实现,只能放在抽象类里。
class Y {
abstract method(p: string);
// ❌ 编译错误:抽象方法只能在 abstract class 里
}
正确写法:
abstract class Base {
abstract method(p: string): void;
}
class Derived extends Base {
method(p: string): void {
console.info(p);
}
}
八、接口:行为的"协议"
接口是对"应该有什么属性/方法"的约定。
interface Style {
color: string; // 属性
}
interface AreaSize {
calculateAreaSize(): number; // 方法声明
someMethod(): void;
}
实现接口的类:
interface AreaSize {
calculateAreaSize(): number;
someMethod(): void;
}
class RectangleSize implements AreaSize {
private width: number = 0;
private height: number = 0;
someMethod(): void {
console.info('someMethod called');
}
calculateAreaSize(): number {
this.someMethod();
return this.width * this.height;
}
}
8.1 接口属性:字段 vs getter/setter
接口里的属性可以写成:
interface Style {
color: string;
}
也可以拆成 getter + setter:
interface Style {
get color(): string;
set color(x: string);
}
类实现接口时也有两种写法:
interface Style {
color: string;
}
class StyledRectangle implements Style {
color: string = '';
}
或者用 getter/setter 形式:
interface Style {
color: string;
}
class StyledRectangle implements Style {
private _color: string = '';
get color(): string {
return this._color;
}
set color(x: string) {
this._color = x;
}
}
8.2 接口继承接口
接口可以继承其他接口:
interface Style {
color: string;
}
interface ExtendedStyle extends Style {
width: number;
}
ExtendedStyle 拥有 color + width 两个属性。
九、抽象类 vs 接口:什么时候用谁?
官方给出的差异可以总结成这几条(我顺便加一点自己的理解):
-
继承数量
-
一个类只能
extends一个抽象类; -
但可以
implements多个接口:class Bird extends Animal implements CanFly, CanSwim {
// ...
}
-
-
静态成员
-
接口里不能有静态方法和静态代码块;
-
抽象类里可以:
interface MyInterface {
static staticMethod(): void; // ❌ 不允许
static { console.info('static'); }; // ❌ 不允许
}abstract class MyAbstractClass {
static staticMethod(): void { console.info('static'); } // ✅
static { console.info('static initialization block'); } // ✅
}
-
-
是否允许方法实现
-
接口:完全抽象,只能有方法声明,不能写实现;
-
抽象类:既可以有抽象方法,也可以有写好实现的普通方法。
abstract class MyAbstractClass {
func(): void { console.info('func'); } // ✅ 可以有实现
}interface MyInterface {
func(): void { console.info('func'); } // ❌ 接口不能有实现
}
-
-
构造函数
-
抽象类可以有构造函数;
-
接口不能有构造函数。
abstract class MyAbstractClass {
constructor() {} // ✅
}interface MyInterface {
constructor(); // ❌ 不允许
}
-
我自己的经验总结:
- 如果你需要共享一部分实现 + 统一抽象 + 需要构造/静态成员 → 用抽象类。
- 如果你只是想定义一组"行为约定",然后让不同类自由地各自实现 → 用接口。
- 很多时候,两者是配合使用的:
- "一个抽象父类 + 若干接口 + 多个具体子类"。