Harmony NEXT编程进阶:深入理解类、接口与泛型的实战指南
类
类声明引入一个新类型,并定义其字段、方法和构造函数。
实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象
在以下示例中,定义了Person类,该类具有字段name和surname、构造函数和方法fullName:
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.log(p.fullName());
class Point {
x: number = 0
y: number = 0
}
let p: Point = {x: 42, y: 42};
字段
直接在类中声明的变量。
包含实例字段和静态字段(使用static关键字修饰的字段),类的所有实例共享一个静态字段
//实例字段
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;
let p2 = new Person('Bob', 28);
p2.getName();
//静态字段
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;
let p2 = new Person('Bob', 28);
p2.getName();
字段初始化
为了减少运行时的错误和获得更好的执行性能,
ArkTS要求所有字段在声明时或者构造函数中显式初始化
//不合法形式
class Person {
name: string // undefined
setName(n:string): void {
this.name = n;
}
getName(): string {
// 开发者使用"string"作为返回类型,这隐藏了name可能为"undefined"的事实。
// 更合适的做法是将返回类型标注为"string | undefined",以告诉开发者这个API所有可能的返回值。
return this.name;
}
}
let jack = new Person();
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
jack.getName().length; // 运行时异常:name is undefined
//正确写法
class Person {
name: string = ''
setName(n:string): void {
this.name = n;
}
// 类型为'string',不可能为"null"或者"undefined"
getName(): string {
return this.name;
}
}
let jack = new Person();
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
jack.getName().length; // 0, 没有运行时异常
//如果name的值可以是undefined,那么应该如何写代码
class Person {
name?: string // 可能为`undefined`
setName(n:string): void {
this.name = n;
}
// 编译时错误:name可以是"undefined",所以将这个API的返回值类型标记为string
getNameWrong(): string {
return this.name;
}
getName(): string | undefined { // 返回类型匹配name的类型
return this.name;
}
}
let jack = new Person();
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
// 编译时错误:编译器认为下一行代码有可能会访问undefined的属性,报错
jack.getName().length; // 编译失败
jack.getName()?.length; // 编译成功,没有运行时错误
getter和setter
setter和getter可用于提供对对象属性的受控访问。
//setter用于禁止将age属性设置为无效值
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();
p.age; // 输出0
p.age = -42; // 设置无效age值会抛出错误
构造函数
类声明可以包含用于初始化对象状态的构造函数
如果未定义构造函数,则会自动创建具有空参数列表的默认构造函数,默认构造函数使用字段类型的默认值来初始化实例中的字段
constructor ([parameters]) {
// ...
}
class Point {
x: number = 0
y: number = 0
}
let p = new Point();
派生类的构造函数
构造函数函数体的第一条语句可以使用关键字super来显式调用直接父类的构造函数。
构造函数重载签名
我们可以通过编写重载签名,指定构造函数的不同调用方式。具体方法为,为同一个构造函数写入多个同名但签名不同的构造函数头,构造函数实现紧随其后。
//派生类的构造函数
class RectangleSize {
constructor(width: number, height: number) {
// ...
}
}
class Square extends RectangleSize {
constructor(side: number) {
super(side, side);
}
}
//构造函数重载签名
class C {
constructor(x: number) /* 第一个签名 */
constructor(x: string) /* 第二个签名 */
constructor(x: number | string) { /* 实现签名 */
}
}
let c1 = new C(123); // OK,使用第一个签名
let c2 = new C('abc'); // OK,使用第二个签名
方法
方法属于类。类可以定义实例方法或者静态方法。静态方法属于类本身,只能访问静态字段。而实例方法既可以访问静态字段,也可以访问实例字段,包括类的私有字段。
实例方法
class RectangleSize {
private height: number = 0
private width: number = 0
constructor(height: number, width: number) {
// ...
}
calculateArea(): number {
return this.height * this.width;
}
}
let square = new RectangleSize(10, 10);
square.calculateArea(); // 输出:100
静态方法
使用关键字static将方法声明为静态。静态方法属于类本身,只能访问静态字段。
静态方法定义了类作为一个整体的公共行为。
必须通过类名调用静态方法
class Cl {
static staticMethod(): string {
return 'this is a static method.';
}
}
console.log(Cl.staticMethod());
继承
一个类可以继承另一个类(称为基类),继承使用extends关键字,实现使用implements
使用以下语法实现多个接口:
继承类继承基类的字段和方法,但不继承构造函数。继承类可以新增定义字段和方法,也可以覆盖其基类定义的方法。
基类也称为“父类”或“超类”。继承类也称为“派生类”或“子类”。
class [extends BaseClassName] [implements listOfInterfaces] {
// ...
}
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;
}
}
//包含implements子句的类必须实现列出的接口中定义的所有方法,但使用默认实现定义的方法除外。
interface DateInterface {
now(): string;
}
class MyDate implements DateInterface {
now(): string {
// 在此实现
return 'now';
}
}
父类访问
关键字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.height -可在此处使用
/* 填充矩形 */
}
}
方法重写
子类可以重写其父类中定义的方法的实现。重写的方法必须具有与原始方法相同的参数类型和相同或派生的返回类型。
class RectangleSize {
// ...
area(): number {
// 实现
return 0;
}
}
class Square extends RectangleSize {
private side: number = 0
area(): number {
return this.side * this.side;
}
}
方法重载签名
通过重载签名,指定方法的不同调用。具体方法为,为同一个方法写入多个同名但签名不同的方法头,方法实现紧随其后。
注意:两个重载签名的名称和参数列表不能都相同
class C {
foo(x: number): void; /* 第一个签名 */
foo(x: string): void; /* 第二个签名 */
foo(x: number | string): void { /* 实现签名 */
}
}
let c = new C();
c.foo(123); // OK,使用第一个签名
c.foo('aa'); // OK,使用第二个签名
修饰符
类的方法和属性都可以使用可见性修饰符。
可见性修饰符包括:private、protected和public。默认可见性为public。
Public(公有)
public修饰的类成员(字段、方法、构造函数)在程序的任何可访问该类的地方都是可见的。
Private(私有)
private修饰的成员不能在声明该成员的类之外访问
Protected(受保护)
protected修饰符的作用与private修饰符非常相似,不同点是protected修饰的成员允许在派生类中访问
class C {
public x: string = ''
private y: string = ''
set_y (new_y: string) {
this.y = new_y; // OK,因为y在类本身中可以访问
}
}
let c = new C();
c.x = 'a'; // OK,该字段是公有的
c.y = 'b'; // 编译时错误:'y'不可见
class Base {
protected x: string = ''
private y: string = ''
}
class Derived extends Base {
foo() {
this.x = 'a'; // OK,访问受保护成员
this.y = 'b'; // 编译时错误,'y'不可见,因为它是私有的
}
}
接口
接口声明引入新类型。接口是定义代码协定的常见方式。
任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。
接口通常包含属性和方法的声明
接口用来定义一个抽象类型,它不包含数据,但可以定义类型的行为。一个类型如果声明实现某接口,并且实现了该接口中所有的成员,就被称为实现了该接口。
接口的成员可以包含:
- 成员函数
- 操作符重载函数
- 成员属性
这些成员都是抽象的,要求实现类型必须拥有对应的成员实现。
interface Style {
color: string // 属性
}
// 接口:
interface AreaSize {
calculateAreaSize(): number // 方法的声明
someMethod(): void; // 方法的声明
}
// 实现:
class RectangleSize implements AreaSize {
private width: number = 0
private height: number = 0
someMethod(): void {
console.log('someMethod called');
}
calculateAreaSize(): number {
this.someMethod(); // 调用另一个方法并返回结果
return this.width * this.height;
}
}
接口与类区别:
(1)接口有点象一个规范、一个协议,是一个抽象的概念;
(2)而类则是实现了这个协议,满足了这个规范的具体实体,是一个具体的概念。
(3)从程序角度,简单理解,接口就是函数声明,类就是函数实现。需要注意的是同一个声明可能有很多种实现。
泛型
泛型类型和函数允许创建的代码在各种类型上运行,而不仅支持单一类型。
类和接口可以定义为泛型,将参数添加到类型定义中
class CustomStack<Element> {
public push(e: Element):void {
// ...
}
}
//要使用类型CustomStack,必须为每个类型参数指定类型实参
let s = new CustomStack<string>();
s.push('hello');
泛型函数
使用泛型函数可编写更通用的代码。比如返回数组最后一个元素的函数
function last(x: number[]): number {
return x[x.length - 1];
}
last([1, 2, 3]); // 3
//泛型函数 为任何数组定义相同的函数,使用类型参数将该函数定义为泛型
function last<T>(x: T[]): T {
return x[x.length - 1];
}
// 显式设置的类型实参
last<string>(['aa', 'bb']);
last<number>([1, 2, 3]);
// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
last([1, 2, 3]);
泛型默认值
泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。下面的示例展示了类和函数的这一点。
class SomeType {}
interface Interface <T1 = SomeType> { }
class Base <T2 = SomeType> { }
class Derived1 extends Base implements Interface { }
// Derived1在语义上等价于Derived2
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }
function foo<T = number>(): T {
// ...
}
foo();
// 此函数在语义上等价于下面的调用
foo<number>();
语法问题
空值定义null
let x: number = null; // 编译时错误
let y: string = null; // 编译时错误
let z: number[] = null; // 编译时错误
//可以为空值的变量定义为联合类型T | null。
let x: number | null = null;
x = 1; // ok
x = null; // ok
if (x != null) { /* do something */ }
模块
程序可划分为多组编译单元或模块。
每个模块都有其自己的作用域,即,在模块中创建的任何声明(变量、函数、类等)在该模块之外都不可见,除非它们被显式导出。
与此相对,从另一个模块导出的变量、函数、类、接口等必须首先导入到模块中。
一句话总结:一个软件有多个模块module,每个模块的作用域不共享,如果要使用需要导出导入。
导出
可以使用关键字export导出顶层的声明。
未导出的声明名称被视为私有名称,只能在声明该名称的模块中使用。
注意:通过export方式导出,在导入时要加{}。
export class Point {
x: number = 0
y: number = 0
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
export let Origin = new Point(0, 0);
export function Distance(p1: Point, p2: Point): number {
return Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));
}
导入
静态导入
导入声明用于导入从其他模块导出的实体,并在当前模块中提供其绑定。导入声明由两部分组成:
- 导入路径,用于指定导入的模块;
- 导入绑定,用于定义导入的模块中的可用实体集和使用形式(限定或不限定使用)。
导入绑定可以有几种形式。
假设模块具有路径“./utils”和导出实体“X”和“Y”。
//导入绑定* as A表示绑定名称“A”,通过A.name可访问从导入路径指定的模块导出的所有实体
import * as Utils from './utils'
Utils.X // 表示来自Utils的X
Utils.Y // 表示来自Utils的Y
//导入绑定{ ident1, ..., identN }表示将导出的实体与指定名称绑定,该名称可以用作简单名称
import { X, Y } from './utils'
X // 表示来自utils的X
Y // 表示来自utils的Y
//如果标识符列表定义了代号,则实体类将绑定在代号下
import { X as Z, Y } from './utils'
Z // 表示来自Utils的X
Y // 表示来自Utils的Y
X // 编译时错误:'X'不可见
动态导入
应用开发的有些场景中,如果希望根据条件导入模块或者按需导入模块,可以使用动态导入代替静态导入。
import()语法通常称为动态导入dynamic import,是一种类似函数的表达式,用来动态导入模块。以这种方式调用,将返回一个promise。
如下例所示,import(modulePath)可以加载模块并返回一个promise,该promise resolve为一个包含其所有导出的模块对象。该表达式可以在代码中的任意位置调用。
如果在异步函数中,可以使用let module = await import(modulePath)。
let modulePath = prompt("Which module to load?");
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)
// say.ts
export function hi() {
console.log('Hello');
}
export function bye() {
console.log('Bye');
}
async function test() {
let ns = await import('./say');
let hi = ns.hi;
let bye = ns.bye;
hi();
bye();
}
动态导入更多场景:文档中心
导入HarmonyOS SDK的开放能力
HarmonyOS SDK提供的开放能力(接口)也需要在导入声明后使用。可直接导入接口模块来使用该模块内的所有接口能力,例如:
import UIAbility from '@ohos.app.ability.UIAbility';
从HarmonyOS NEXT Developer Preview 1版本开始引入Kit概念。SDK对同一个Kit下的接口模块进行了封装,开发者在示例代码中可通过导入Kit的方式来使用Kit所包含的接口能力。其中,Kit封装的接口模块可查看SDK目录下Kit子目录中各Kit的定义。
通过导入Kit方式使用开放能力有三种方式:
- 方式一:导入Kit下单个模块的接口能力。例如:
import { UIAbility } from '@kit.AbilityKit';
- 方式二:导入Kit下多个模块的接口能力。例如:
import { UIAbility, Ability, Context } from '@kit.AbilityKit';
- 方式三:导入Kit包含的所有模块的接口能力。例如:
import * as module from '@kit.AbilityKit';
其中,“module”为别名,可自定义,然后通过该名称调用模块的接口。
第三种导入方式一般不会使用,开发中需要遵循最小引入原则,用什么导什么。
关键字
this
关键字this只能在类的实例方法中使用。
示例
class A {
count: string = 'a'
m(i: string): void {
this.count = i;
}
}
使用限制:
- 不支持this类型
- 不支持在函数和类的静态方法中使用this
示例
class A {
n: number = 0
f1(arg1: this) {} // 编译时错误,不支持this类型
static f2(arg1: number) {
this.n = arg1; // 编译时错误,不支持在类的静态方法中使用this
}
}
function foo(arg1: number) {
this.n = i; // 编译时错误,不支持在函数中使用this
}
关键字this的指向:
- 调用实例方法的对象
- 正在构造的对象
类声明引入一个新类型,并定义其字段、方法和构造函数。
实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象
在以下示例中,定义了Person类,该类具有字段name和surname、构造函数和方法fullName:
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.log(p.fullName());
class Point {
x: number = 0
y: number = 0
}
let p: Point = {x: 42, y: 42};
字段
直接在类中声明的变量。
包含实例字段和静态字段(使用static关键字修饰的字段),类的所有实例共享一个静态字段
//实例字段
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;
let p2 = new Person('Bob', 28);
p2.getName();
//静态字段
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;
let p2 = new Person('Bob', 28);
p2.getName();
字段初始化
为了减少运行时的错误和获得更好的执行性能,
ArkTS要求所有字段在声明时或者构造函数中显式初始化
//不合法形式
class Person {
name: string // undefined
setName(n:string): void {
this.name = n;
}
getName(): string {
// 开发者使用"string"作为返回类型,这隐藏了name可能为"undefined"的事实。
// 更合适的做法是将返回类型标注为"string | undefined",以告诉开发者这个API所有可能的返回值。
return this.name;
}
}
let jack = new Person();
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
jack.getName().length; // 运行时异常:name is undefined
//正确写法
class Person {
name: string = ''
setName(n:string): void {
this.name = n;
}
// 类型为'string',不可能为"null"或者"undefined"
getName(): string {
return this.name;
}
}
let jack = new Person();
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
jack.getName().length; // 0, 没有运行时异常
//如果name的值可以是undefined,那么应该如何写代码
class Person {
name?: string // 可能为`undefined`
setName(n:string): void {
this.name = n;
}
// 编译时错误:name可以是"undefined",所以将这个API的返回值类型标记为string
getNameWrong(): string {
return this.name;
}
getName(): string | undefined { // 返回类型匹配name的类型
return this.name;
}
}
let jack = new Person();
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
// 编译时错误:编译器认为下一行代码有可能会访问undefined的属性,报错
jack.getName().length; // 编译失败
jack.getName()?.length; // 编译成功,没有运行时错误
getter和setter
setter和getter可用于提供对对象属性的受控访问。
//setter用于禁止将age属性设置为无效值
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();
p.age; // 输出0
p.age = -42; // 设置无效age值会抛出错误
构造函数
类声明可以包含用于初始化对象状态的构造函数
如果未定义构造函数,则会自动创建具有空参数列表的默认构造函数,默认构造函数使用字段类型的默认值来初始化实例中的字段
constructor ([parameters]) {
// ...
}
class Point {
x: number = 0
y: number = 0
}
let p = new Point();
派生类的构造函数
构造函数函数体的第一条语句可以使用关键字super来显式调用直接父类的构造函数。
构造函数重载签名
我们可以通过编写重载签名,指定构造函数的不同调用方式。具体方法为,为同一个构造函数写入多个同名但签名不同的构造函数头,构造函数实现紧随其后。
//派生类的构造函数
class RectangleSize {
constructor(width: number, height: number) {
// ...
}
}
class Square extends RectangleSize {
constructor(side: number) {
super(side, side);
}
}
//构造函数重载签名
class C {
constructor(x: number) /* 第一个签名 */
constructor(x: string) /* 第二个签名 */
constructor(x: number | string) { /* 实现签名 */
}
}
let c1 = new C(123); // OK,使用第一个签名
let c2 = new C('abc'); // OK,使用第二个签名
方法
方法属于类。类可以定义实例方法或者静态方法。静态方法属于类本身,只能访问静态字段。而实例方法既可以访问静态字段,也可以访问实例字段,包括类的私有字段。
实例方法
class RectangleSize {
private height: number = 0
private width: number = 0
constructor(height: number, width: number) {
// ...
}
calculateArea(): number {
return this.height * this.width;
}
}
let square = new RectangleSize(10, 10);
square.calculateArea(); // 输出:100
静态方法
使用关键字static将方法声明为静态。静态方法属于类本身,只能访问静态字段。
静态方法定义了类作为一个整体的公共行为。
必须通过类名调用静态方法
class Cl {
static staticMethod(): string {
return 'this is a static method.';
}
}
console.log(Cl.staticMethod());
继承
一个类可以继承另一个类(称为基类),继承使用extends关键字,实现使用implements
使用以下语法实现多个接口:
继承类继承基类的字段和方法,但不继承构造函数。继承类可以新增定义字段和方法,也可以覆盖其基类定义的方法。
基类也称为“父类”或“超类”。继承类也称为“派生类”或“子类”。
class [extends BaseClassName] [implements listOfInterfaces] {
// ...
}
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;
}
}
//包含implements子句的类必须实现列出的接口中定义的所有方法,但使用默认实现定义的方法除外。
interface DateInterface {
now(): string;
}
class MyDate implements DateInterface {
now(): string {
// 在此实现
return 'now';
}
}
父类访问
关键字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.height -可在此处使用
/* 填充矩形 */
}
}
方法重写
子类可以重写其父类中定义的方法的实现。重写的方法必须具有与原始方法相同的参数类型和相同或派生的返回类型。
class RectangleSize {
// ...
area(): number {
// 实现
return 0;
}
}
class Square extends RectangleSize {
private side: number = 0
area(): number {
return this.side * this.side;
}
}
方法重载签名
通过重载签名,指定方法的不同调用。具体方法为,为同一个方法写入多个同名但签名不同的方法头,方法实现紧随其后。
注意:两个重载签名的名称和参数列表不能都相同
class C {
foo(x: number): void; /* 第一个签名 */
foo(x: string): void; /* 第二个签名 */
foo(x: number | string): void { /* 实现签名 */
}
}
let c = new C();
c.foo(123); // OK,使用第一个签名
c.foo('aa'); // OK,使用第二个签名
修饰符
类的方法和属性都可以使用可见性修饰符。
可见性修饰符包括:private、protected和public。默认可见性为public。
Public(公有)
public修饰的类成员(字段、方法、构造函数)在程序的任何可访问该类的地方都是可见的。
Private(私有)
private修饰的成员不能在声明该成员的类之外访问
Protected(受保护)
protected修饰符的作用与private修饰符非常相似,不同点是protected修饰的成员允许在派生类中访问
class C {
public x: string = ''
private y: string = ''
set_y (new_y: string) {
this.y = new_y; // OK,因为y在类本身中可以访问
}
}
let c = new C();
c.x = 'a'; // OK,该字段是公有的
c.y = 'b'; // 编译时错误:'y'不可见
class Base {
protected x: string = ''
private y: string = ''
}
class Derived extends Base {
foo() {
this.x = 'a'; // OK,访问受保护成员
this.y = 'b'; // 编译时错误,'y'不可见,因为它是私有的
}
}
接口
接口声明引入新类型。接口是定义代码协定的常见方式。
任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。
接口通常包含属性和方法的声明
接口用来定义一个抽象类型,它不包含数据,但可以定义类型的行为。一个类型如果声明实现某接口,并且实现了该接口中所有的成员,就被称为实现了该接口。
接口的成员可以包含:
- 成员函数
- 操作符重载函数
- 成员属性
这些成员都是抽象的,要求实现类型必须拥有对应的成员实现。
interface Style {
color: string // 属性
}
// 接口:
interface AreaSize {
calculateAreaSize(): number // 方法的声明
someMethod(): void; // 方法的声明
}
// 实现:
class RectangleSize implements AreaSize {
private width: number = 0
private height: number = 0
someMethod(): void {
console.log('someMethod called');
}
calculateAreaSize(): number {
this.someMethod(); // 调用另一个方法并返回结果
return this.width * this.height;
}
}
接口与类区别:
(1)接口有点象一个规范、一个协议,是一个抽象的概念;
(2)而类则是实现了这个协议,满足了这个规范的具体实体,是一个具体的概念。
(3)从程序角度,简单理解,接口就是函数声明,类就是函数实现。需要注意的是同一个声明可能有很多种实现。
泛型
泛型类型和函数允许创建的代码在各种类型上运行,而不仅支持单一类型。
类和接口可以定义为泛型,将参数添加到类型定义中
class CustomStack<Element> {
public push(e: Element):void {
// ...
}
}
//要使用类型CustomStack,必须为每个类型参数指定类型实参
let s = new CustomStack<string>();
s.push('hello');
泛型函数
使用泛型函数可编写更通用的代码。比如返回数组最后一个元素的函数
function last(x: number[]): number {
return x[x.length - 1];
}
last([1, 2, 3]); // 3
//泛型函数 为任何数组定义相同的函数,使用类型参数将该函数定义为泛型
function last<T>(x: T[]): T {
return x[x.length - 1];
}
// 显式设置的类型实参
last<string>(['aa', 'bb']);
last<number>([1, 2, 3]);
// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
last([1, 2, 3]);
泛型默认值
泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。下面的示例展示了类和函数的这一点。
class SomeType {}
interface Interface <T1 = SomeType> { }
class Base <T2 = SomeType> { }
class Derived1 extends Base implements Interface { }
// Derived1在语义上等价于Derived2
class Derived2 extends Base<SomeType> implements Interface<SomeType> { }
function foo<T = number>(): T {
// ...
}
foo();
// 此函数在语义上等价于下面的调用
foo<number>();
语法问题
空值定义null
let x: number = null; // 编译时错误
let y: string = null; // 编译时错误
let z: number[] = null; // 编译时错误
//可以为空值的变量定义为联合类型T | null。
let x: number | null = null;
x = 1; // ok
x = null; // ok
if (x != null) { /* do something */ }
模块
程序可划分为多组编译单元或模块。
每个模块都有其自己的作用域,即,在模块中创建的任何声明(变量、函数、类等)在该模块之外都不可见,除非它们被显式导出。
与此相对,从另一个模块导出的变量、函数、类、接口等必须首先导入到模块中。
一句话总结:一个软件有多个模块module,每个模块的作用域不共享,如果要使用需要导出导入。
导出
可以使用关键字export导出顶层的声明。
未导出的声明名称被视为私有名称,只能在声明该名称的模块中使用。
注意:通过export方式导出,在导入时要加{}。
export class Point {
x: number = 0
y: number = 0
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
export let Origin = new Point(0, 0);
export function Distance(p1: Point, p2: Point): number {
return Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));
}
导入
静态导入
导入声明用于导入从其他模块导出的实体,并在当前模块中提供其绑定。导入声明由两部分组成:
- 导入路径,用于指定导入的模块;
- 导入绑定,用于定义导入的模块中的可用实体集和使用形式(限定或不限定使用)。
导入绑定可以有几种形式。
假设模块具有路径“./utils”和导出实体“X”和“Y”。
//导入绑定* as A表示绑定名称“A”,通过A.name可访问从导入路径指定的模块导出的所有实体
import * as Utils from './utils'
Utils.X // 表示来自Utils的X
Utils.Y // 表示来自Utils的Y
//导入绑定{ ident1, ..., identN }表示将导出的实体与指定名称绑定,该名称可以用作简单名称
import { X, Y } from './utils'
X // 表示来自utils的X
Y // 表示来自utils的Y
//如果标识符列表定义了代号,则实体类将绑定在代号下
import { X as Z, Y } from './utils'
Z // 表示来自Utils的X
Y // 表示来自Utils的Y
X // 编译时错误:'X'不可见
动态导入
应用开发的有些场景中,如果希望根据条件导入模块或者按需导入模块,可以使用动态导入代替静态导入。
import()语法通常称为动态导入dynamic import,是一种类似函数的表达式,用来动态导入模块。以这种方式调用,将返回一个promise。
如下例所示,import(modulePath)可以加载模块并返回一个promise,该promise resolve为一个包含其所有导出的模块对象。该表达式可以在代码中的任意位置调用。
如果在异步函数中,可以使用let module = await import(modulePath)。
let modulePath = prompt("Which module to load?");
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)
// say.ts
export function hi() {
console.log('Hello');
}
export function bye() {
console.log('Bye');
}
async function test() {
let ns = await import('./say');
let hi = ns.hi;
let bye = ns.bye;
hi();
bye();
}
动态导入更多场景:文档中心
导入HarmonyOS SDK的开放能力
HarmonyOS SDK提供的开放能力(接口)也需要在导入声明后使用。可直接导入接口模块来使用该模块内的所有接口能力,例如:
import UIAbility from '@ohos.app.ability.UIAbility';
从HarmonyOS NEXT Developer Preview 1版本开始引入Kit概念。SDK对同一个Kit下的接口模块进行了封装,开发者在示例代码中可通过导入Kit的方式来使用Kit所包含的接口能力。其中,Kit封装的接口模块可查看SDK目录下Kit子目录中各Kit的定义。
通过导入Kit方式使用开放能力有三种方式:
- 方式一:导入Kit下单个模块的接口能力。例如:
import { UIAbility } from '@kit.AbilityKit';
- 方式二:导入Kit下多个模块的接口能力。例如:
import { UIAbility, Ability, Context } from '@kit.AbilityKit';
- 方式三:导入Kit包含的所有模块的接口能力。例如:
import * as module from '@kit.AbilityKit';
其中,“module”为别名,可自定义,然后通过该名称调用模块的接口。
第三种导入方式一般不会使用,开发中需要遵循最小引入原则,用什么导什么。
关键字
this
关键字this只能在类的实例方法中使用。
示例
class A {
count: string = 'a'
m(i: string): void {
this.count = i;
}
}
使用限制:
- 不支持this类型
- 不支持在函数和类的静态方法中使用this
示例
class A {
n: number = 0
f1(arg1: this) {} // 编译时错误,不支持this类型
static f2(arg1: number) {
this.n = arg1; // 编译时错误,不支持在类的静态方法中使用this
}
}
function foo(arg1: number) {
this.n = i; // 编译时错误,不支持在函数中使用this
}
关键字this的指向:
- 调用实例方法的对象
- 正在构造的对象