OpenHarmony 4.0 - 学习 ArkTS 语言
官方文档学习笔记,查漏补缺
个人理解的 ArkTS TypeScript JavaScript 三者之间的关系:TypeScript 是 JavaScript 的超集,ArkTS 是 TypeScript 的限制集。
.ets 和 .js 文件后缀改成 .ts 编译没有问题,反之则可能不行。.js 文件后缀改成 .ets 也不行。
ArkTS是OpenHarmony优选的主力应用开发语言。
ArkTS 在 TypeScript 基础上做了进一步扩展,保持了 TS 的基本风格,同时通过规范定义强化开发期静态检查和分析。
基本知识
声明
let const
自动类型推断:如果一个变量或常量的声明包含了初始值,那么开发者就不需要显式指定其类型。
类型
number boolean string
void 类型用于指定函数没有返回值。 此类型只有一个值,同样是void。由于void是引用类型,因此它可以用于泛型类型参数。
class Class<T> {
//...
}
let instance: Class <void>
Object 类型是所有引用类型的基类型。任何值,包括基本类型的值(它们会被自动装箱),都可以直接被赋给Object类型的变量。
Array 类型 是由可赋值给数组声明中指定的元素类型的数据组成的对象。
Enum类型
Enum 枚举类型,是预先定义的一组命名值的值类型,其中命名值又称为枚举常量。 使用枚举常量时必须以枚举类型名称为前缀。
enum Color { Red, Green, Blue }
let c: Color = Color.Red
常量表达式可以用于显式设置枚举常量的值。
enum Color { White = 0xFF, Grey = 0x7F, Black = 0x00 }
let c: Color = Color.Black
Union类型
union类型,即联合类型,是由多个类型组合成的引用类型。联合类型包含了变量可能的所有类型。
class Cat {
// ...
}
class Dog {
// ...
}
class Frog {
// ...
}
type Animal = Cat | Dog | Frog | number
// Cat、Dog、Frog是一些类型(类或接口)
let animal: Animal = new Cat()
animal = new Frog()
animal = 42
// 可以将类型为联合类型的变量赋值为任何组成类型的有效值
可以用不同的机制获取联合类型中特定类型的值。 示例:
class Cat { sleep () {}; meow () {} }
class Dog { sleep () {}; bark () {} }
class Frog { sleep () {}; leap () {} }
type Animal = Cat | Dog | Frog | number
let animal: Animal = new Cat()
if (animal instanceof Frog) {
let frog: Frog = animal as Frog// animal在这里是Frog类型
animal.leap()
frog.leap()
// 结果:青蛙跳了两次
}
animal.sleep () // 任何动物都可以睡觉
Aliases类型
Aliases类型为类型别名(数组、函数、对象字面量或联合类型)提供名称,或为已有类型提供替代名称。
type Matrix = number[][]
type Handler = (s: string, no: number) => string
type Predicate <T> = (x: T) => Boolean
type NullableObject = Object | null
运算符
位运算符
| 运算符 | 说明 |
|---|---|
a & b | 按位与:如果两个操作数的对应位都为1,则将这个位设置为1,否则设置为0。 |
a | b | 按位或:如果两个操作数的相应位中至少有一个为1,则将这个位设置为1,否则设置为0。 |
a ^ b | 按位异或:如果两个操作数的对应位不同,则将这个位设置为1,否则设置为0。 |
~ a | 按位非:反转操作数的位。 |
a << b | 左移:将a的二进制表示向左移b位。 |
a >> b | 算术右移:将a的二进制表示向右移b位,带符号扩展。 |
a >>> b | 逻辑右移:将a的二进制表示向右移b位,左边补0。 |
语句
if switch for while do-while break continue throw try-catch-finally
switch表达式的类型必须是number、enum或string;label必须是常量表达式或枚举常量值。
for-of语句可遍历数组或字符串。
function processData(s: string) {
let error: Error | null = null
try {
// ...
} catch (e) {
error = e as Error
// ...
} finally {
if (error != null) {
console.log(`Error caught: input='${s}', message='${error.message}'`)
}
}
}
函数
在函数声明中,必须为每个参数标记类型。如果参数为可选参数,那么允许在调用函数时省略该参数。函数的最后一个参数可以是rest参数。
可选参数的格式可为name?: Type。可选参数的另一种形式为设置的参数默认值 n: number = 1。
使用rest参数时,允许函数或方法接受任意数量的实参。function sum(...numbers: number[]): number {}
如果可以从函数体内推断出函数返回类型,则可在函数声明中省略标注返回类型。不需要返回值的函数的返回类型可以显式指定为void或省略标注。
函数类型通常用于定义回调:
type trigFunc = (x: number) => number // 这是一个函数类型
function do_action(f: trigFunc) {
f(3.141592653589) // 调用函数
}
do_action(Math.sin) // 将函数作为参数传入
函数重载
我们可以通过编写重载,指定函数的不同调用方式。具体方法为,为同一个函数写入多个同名但签名不同的函数头,函数实现紧随其后。
function foo(): void; /* 第一个函数定义 */
function foo(x: string): void; /* 第二个函数定义 */
function foo(x?: string): void { /* 函数实现 */
console.log(x)
}
foo() // OK,使用第一个定义
foo('aa') // OK,使用第二个定义
不允许重载函数有相同的名字以及参数列表,否则将会编译报错。
类
类声明引入一个新类型,并定义其字段、方法和构造函数。
定义类后,可以使用关键字new创建实例。或者,可以使用对象字面量创建实例:
class Point {
x: number = 0
y: number = 0
}
let p: Point = {x: 42, y: 42}
类可以具有实例字段或者静态字段。静态字段属于类本身,类的所有实例共享一个静态字段。
class Person {
name: string = '' // 实例字段
static numberOfPersons = 0 // 静态字段
constructor() {
Person.numberOfPersons++
}
}
console.log(Person.numberOfPersons)
ArkTS要求所有字段在声明时或者构造函数中显式初始化。
接下来的代码展示了如果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的属性,报错
console.log(jack.getName().length); // 编译失败
console.log(jack.getName()?.length); // 编译成功,没有运行时错误
setter和getter可用于提供对对象属性的受控访问。
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
}
}
类可以定义实例方法或者静态方法。静态方法属于类本身,只能访问静态字段。必须通过类名调用静态方法:
class Cl {
static staticMethod(): string {
return 'this is a static method.'
}
}
console.log(Cl.staticMethod())
继承
一个类可以继承另一个类(称为基类),并使用以下语法实现多个接口:
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 is now'
}
}
关键字super可用于访问父类的实例字段、实例方法和构造函数。在实现子类功能时,可以通过该关键字从父类中获取所需接口:
class Rectangle {
protected height: number = 0
protected width: number = 0
constructor (h: number, w: number) {
this.height = h
this.width = w
}
draw() {
/* 绘制边界 */
}
}
class FilledRectangle extends Rectangle {
color = ''
constructor (h: number, w: number, c: string) {
super(h, w) // 父类构造函数的调用
this.color = c
}
override draw() {
super.draw() // 父类方法的调用
// super.height -可在此处使用
/* 填充矩形 */
}
}
重写的方法可以用关键字override标记,以提高可读性。重写的方法必须具有与原始方法相同的参数类型和相同或派生的返回类型。
方法重载签名
通过重载签名,指定方法的不同调用。具体方法为,为同一个方法写入多个同名但签名不同的方法头,方法实现紧随其后。
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,使用第二个签名
如果未定义构造函数,则会自动创建具有空参数列表的默认构造函数。
如果派生类的构造函数函数体不以父类构造函数的显式调用开始,则派生类构造函数函数体隐式地以父类构造函数调用super()开始。
构造函数重载签名
我们可以通过编写重载签名,指定构造函数的不同调用方式。具体方法为,为同一个构造函数写入多个同名但签名不同的构造函数头,构造函数实现紧随其后。
class C {
constructor() /* 第一个签名 */
constructor(x: string) /* 第二个签名 */
constructor(x?: string) { /* 实现签名 */
console.log(x)
}
}
let c1 = new C() // OK,使用第一个签名
let c2 = new C('abc') // OK,使用第二个签名
可见性修饰符包括:private、protected和public。默认可见性为public。
private修饰的成员不能在声明该成员的类之外访问。
protected修饰符的作用与private修饰符非常相似,不同点是protected修饰的成员允许在派生类中访问。
对象字面量是一个表达式,可用于创建类实例并提供一些初始值。对象字面量只能在可以推导出该字面量类型的上下文中使用。
class C {
n: number = 0
s: string = ''
}
let c: C = {n: 42, s: 'foo'}
Record类型的对象字面量
泛型Record<K, V>用于将类型(键类型)的属性映射到另一个类型(值类型)。常用对象字面量来初始化该类型的值。
类型K可以是字符串类型或数值类型,而V可以是任何类型。
interface PersonInfo {
age: number
salary: number
}
let map: Record<string, PersonInfo> = {
'John': { age: 25, salary: 10},
'Mary': { age: 21, salary: 20}
}
接口
接口声明引入新类型。
任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。
// 接口:
interface Area {
calculateArea(): number // 方法的声明
someMethod(): void; // 方法的声明
}
// 实现:
class Rectangle implements Area {
private width: number = 0
private height: number = 0
someMethod(): void {
console.log('someMethod called')
}
calculateArea(): number {
this.someMethod() // 调用另一个方法并返回结果
return this.width * this.height
}
}
接口属性可以是字段、getter、setter或getter和setter组合的形式。
属性字段只是getter/setter对的便捷写法。以下表达方式是等价的:
interface Style {
color: string
}
interface Style {
get color(): string
set color(x: string)
}
实现接口的类也可以使用以下两种方式:
interface Style {
color: string
}
class StyledRectangle implements Style {
color: string = ''
}
interface Style {
color: string
}
class StyledRectangle implements Style {
private _color: string = ''
get color(): string { return this._color }
set color(x: string) { this._color = x }
}
接口可以继承其他接口,继承接口包含被继承接口的所有属性和方法,还可以添加自己的属性和方法。
interface Style {
color: string
}
interface ExtendedStyle extends Style {
width: number
}
泛型类型和函数
泛型类型和函数允许创建的代码在各种类型上运行,而不仅支持单一类型。
类和接口可以定义为泛型,将参数添加到类型定义中:
class Stack<Element> {
public pop(): Element {
// ...
}
public push(e: Element):void {
// ...
}
}
要使用类型Stack,必须为每个类型参数指定类型实参:
let s = new Stack<string>
s.push('hello')
编译器在使用泛型类型和函数时会确保类型安全。参见以下示例:
let s = new Stack<string>
s.push(55) // 将会产生编译时错误
泛型约束
泛型类型的类型参数可以绑定。例如,MyHashMap<Key, Value>容器中的Key类型参数必须具有哈希方法,即它应该是可哈希的。
interface Hashable {
hash(): number
}
class MyHashMap<Key extends Hashable, Value> {
public set(k: Key, v: Value) {
let h = k.hash()
// ...其他代码...
}
}
在上面的例子中,Key类型扩展了Hashable,Hashable接口的所有方法都可以为Key调用。
泛型函数
使用泛型函数可编写更通用的代码。比如返回数组最后一个元素的函数,如果需要为任何数组定义相同的函数,使用类型参数将该函数定义为泛型:
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]))
泛型默认值
泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。
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>()
空安全
可以为空值的变量定义为联合类型T | null。
非空断言运算符
后缀运算符! 可用于断言其操作数为非空。
应用于空值时,运算符将抛出错误。否则,值的类型将从T | null更改为T:
let x: number | null = 1
let y: number
y = x + 1 // 编译时错误:无法对可空值作加法
y = x! + 1 // ok
空值合并二元运算符?? 用于检查左侧表达式的求值是否等于null。a ?? b等价于三元运算符a != null ? a : b。
可选链运算符 ?.
模块
假设模块具有路径“./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 as Z, Y } from './utils'
Z // 表示来自Utils的X
Y // 表示来自Utils的Y
从TypeScript到ArkTS的适配规则
尽管ArkTS是基于TypeScript设计的,但一些TypeScript的特性被限制了。
比如:
- 不支持函数表达式,要使用箭头函数。
- 一元运算符
+、-和~仅适用于数值类型,否则会发生编译时错误。与TypeScript不同,ArkTS不支持隐式将字符串转换成数值,必须进行显式转换。 - 不支持
in运算符,如果仍需检查某些类成员是否存在,使用instanceof代替。 - 不支持解构赋值。
- 不支持解构变量声明。
- 不支持
for .. in。 - 不支持在函数内声明函数,改用lambda函数。
- 不支持在函数和类的静态方法中使用
this,只能在类的实例方法中使用this。 - 没有原型的概念,因此不支持在原型上赋值。
- 不支持
Function.apply、Function.bind和Function.call。 - 允许.ets文件
import.ets/.ts/.js文件源码, 不允许.ts/.js文件import.ets文件源码。
等等。