上一章节我们学习了【鸿蒙开发】第五章 ArkTS基础知识 - 声明变量、常量、类型和函数,大概了解ArkTS的基础类型和用法和ts的大致基本相同,与Java也极其相似。本章节我们继续学习ArkTS类和接口、泛型等相关知识,为后续鸿蒙应用开发夯实基础。
类声明引入一个新类型,并定义其字段
、方法
和构造函数
。
有两种方法创建实例:
// 常规的类创建与使用
class Person {
name: string = ''
surname: string = ''
constructor(n: string, sn: string) {
this.name = n
this.surname = sn
}
fullName(): string {
return this.name + ' ' + this.surname
}
}
// 1.通过new创建实例
let person = new Person('John', 'Smith')
console.log(person.fullName())
// 2.可以使用对象字面量创建实例,声明时要给属性赋值,给方法赋值等
let person2: Person = { name: 'John', surname: 'Smith', fullName(): string {return this.name + ' ' + this.surname} }}
console.log(person2 .fullName())
字段是直接在类中声明的某种类型的变量。
类可以具有实例字段
或者静态字段
。
实例字段存在于类的每个实例上。每个实例都有自己的实例字段集合。
实例字段就是上面我们类实例中的属性,如:
let person = new Person('John', 'Smith')
// person.name为实例字段
console.log(person.name)
使用关键字static
将字段声明为静态。静态字段属于类本身,类的所有实例共享一个静态字段。
要访问静态字段,需要使用类名:
class Person {
static sex:number = 0
}
// 通过类调用
console.log(Person.sex)
为了减少运行时的错误和获得更好的执行性能, ArkTS要求所有字段在声明时或者构造函数中显式初始化
。这和标准TS
中的strictPropertyInitialization模式
一样。
class Person {
// 可能为`undefined`
name ?: string
setName(n:string): void {
this.name = n
}
// 编译时错误:name可以是"undefined",所以将这个API的返回值类型标记为string
getNameWrong(): string {
return this.name
}
// 返回类型匹配name的类型
getName(): string | undefined {
return this.name
}
}
function main(){
let jack = new Person()
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
// 编译时错误:编译器认为下一行代码有可能会访问undefined的属性,报错
let length1 = jack.getName().length
// 编译成功,没有运行时错误,?.表示可以为undefined
let length2 = jack.getName()?.length
}
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()
console.log (p.age) // 将打印输出0
p.age = -42 // 设置无效age值会抛出错误
方法属于类。类可以定义实例方法
或者静态方法
。静态方法属于类本身,只能访问静态字段。而实例方法既可以访问静态字段,也可以访问实例字段,包括类的私有字段。
上面Person类中fullName()方法就是实例方法,必须通过类的实例调用实例方法。
let person = new Person('John', 'Smith')
// 通过类的实例调用实例方法
person.fullName()
使用关键字static
将方法声明为静态。静态方法属于类本身,只能访问静态字段。
静态方法定义了类作为一个整体的公共行为。所有实例都可以访问静态方法。必须通过类名调用静态方法:
class Person {
static staticMethod(): string {
return 'this is a static method.'
}
}
console.log(Person.staticMethod())
一个类可以继承另一个类(称为基类),可以实现多个接口。单继承类,多实现接口
。
继承类可以继承基类的字段和方法,但不继承构造函数。继承类可以新增定义字段和方法,也可以覆盖其基类定义的方法。
基类也称为“父类”或“超类”。继承类也称为“派生类”或“子类”。
包含implements
子句的类必须实现列出的接口中定义的所有方法,但使用默认实现定义的方法除外。
interface DateInterface {
now(): string;
}
class Person {
name: string = ''
surname: string = ''
constructor(n: string, sn: string) {
this.name = n
this.surname = sn
}
fullName(): string {
return this.name + ' ' + this.surname
}
}
class Employee extends Person implements DateInterface {
// 实现接口的now方法
now(): string {
throw new Error('Method not implemented.');
}
}
父类属性
和父类方法
都通过super
调用
class Employee extends Person implements DateInterface {
id: number = 0
constructor(n: string, sn: string) {
// 构造函数也通过super直接调用父类构造函数,
// 如果构造函数函数体不以父类构造函数的显式调用开始,
// 则构造函数函数体隐式地以父类构造函数调用super()开始。
super(n, sn)
}
sessionId(): string {
// 通过super访问父类属性
return super.name + this.id
}
// 方法重写
fullName(): string {
// 通过super访问父类方法
return super.fullName();
}
// 实现接口的now方法
now(): string {
throw new Error('Method not implemented.');
}
}
class Employee extends Person implements DateInterface {
// 方法重写
fullName(): string {
// 通过super访问父类方法
return super.fullName();
}
// 实现接口的now方法
now(): string {
throw new Error('Method not implemented.');
}
}
通过重载签名,指定方法的不同调用。具体方法为,为同一个方法写入多个同名但签名不同的方法头,方法实现紧随其后。构造方法重载也一样
。
class C {
foo(): void; /* 第一个签名 */
foo(x: string): void; /* 第二个签名 */
foo(x?: string): void { /* 实现签名 */
console.log(x)
}
}
let c = new C()
c.foo() // OK,使用第一个签名
c.foo('aa') // OK,使用第二个签名
如果两个重载签名的名称和参数列表均相同,则为错误。
接口声明引入新类型。接口是定义代码协定的常见方式。
任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态
。
接口通常包含属性和方法的声明
interface Style {
color: string
}
// 继承接口包含被继承接口的所有属性和方法
// 还可以添加自己的属性和方法。
interface Area extends Style {
width: number
height: number
// 方法的声明
someMethod(): void;
}
class Rectangle implements Area {
// 接口的属性必须实现
// 1.接口属性通过直接声明实现
width: number;
height: number;
// 2.接口属性通过getter、setter方法实现,等价于上面方法1
private _color: string = ''
get color(): string {
return this._color
}
set color(x: string) {
this._color = x
}
// 接口方法实现
someMethod(): void {
console.log('someMethod called')
}
}
泛型类型和函数
允许创建的代码在各种类型上运行,而不仅支持单一类型。
类和接口可以定义为泛型,将参数添加到类型定义中,如以下示例中的类型参数Element
:
class Stack<Element> {
public pop(): Element {
// ...
}
public push(e: Element):void {
// ...
}
}
要使用类型Stack
,必须为每个类型参数指定类型实参:
let s = new Stack<string>
s.push('hello')
typescript
let s = new Stack<string>
s.push('hello')
编译器在使用泛型类型和函数时会确保类型安全。参见以下示例:
let s = new Stack<string>
s.push(55) // 将会产生编译时错误
typescript
let s = new Stack<string>
s.push(55) // 将会产生编译时错误
泛型类型的类型参数可以绑定。例如,HashMap
容器中的Key类型参数必须具有哈希方法,即它应该是可哈希的。
interface Hashable {
hash(): number
}
class HasMap<Key extends Hashable, Value> {
public set(k: Key, v: Value) {
let h = k.hash()
// ...其他代码...
}
}
在上面的例子中,Key
类型扩展了Hashable
,Hashable
接口的所有方法都可以为key
调用。
使用泛型函数可编写更通用的代码。比如返回数组最后一个元素的函数:
function last(x: number[]): number {
return x[x.length - 1]
}
console.log(last([1, 2, 3])) // 输出:3
如果需要为任何数组定义相同的函数,使用类型参数将该函数定义为泛型:
function last<T>(x: T[]): T {
return x[x.length - 1]
}
现在,该函数可以与任何数组一起使用。
在函数调用中,类型实参可以显式或隐式设置:
// 显式设置的类型实参
console.log(last<string>(['aa', 'bb']))
console.log(last<number>([1, 2, 3]))
// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
console.log(last([1, 2, 3]))
typescript
// 显式设置的类型实参
console.log(last<string>(['aa', 'bb']))
console.log(last<number>([1, 2, 3]))
// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
console.log(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>()
typescript
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>()
程序
可划分为多组编译单元
或模块
。
每个模块都有其自己的作用域,即,在模块中创建的任何声明(变量、函数、类等)在该模块之外都不可见,除非它们被显式导出。
与此相对,从另一个模块导出的变量、函数、类、接口等必须首先导入到模块中。
可以使用关键字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
如果标识符列表定义了ident as alias
,则实体ident
将绑定在名称alias
下:
import { X as Z, Y } from './utils'
Z // 表示来自Utils的X
Y // 表示来自Utils的Y
X // 编译时错误:'X'不可见