1. ArkTS 语言概述

ArkTS 是华为为 HarmonyOS 应用开发推出的一门现代化编程语言,它在 TypeScript 的基础上进行了一系列扩展和约束,专门为声明式 UI 开发和高性能运行进行了优化。ArkTS 既是 HarmonyOS 应用开发的主力语言,也是 OpenHarmony 生态中构建跨设备应用的核心技术之一。理解 ArkTS 的语言基础,是进入鸿蒙应用开发世界的第一步,也是后续掌握 ArkUI 声明式框架、状态管理、组件化开发等高级内容的重要前提。

从语言谱系上看,ArkTS 并不是一门从零设计的语言,而是建立在 TypeScript 生态之上的“超集加约束”语言。TypeScript 本身就是 JavaScript 的超集,它为 JavaScript 补充了静态类型系统、接口、泛型、装饰器等能力,使得大型应用开发变得更加可靠。ArkTS 在保留 TypeScript 大部分表达能力的同时,为了提高运行时性能、提升代码可预测性,以及更好地服务声明式 UI 范式,对 TypeScript 的部分动态特性进行了限制和裁剪。

ArkTS 的核心设计目标可以概括为三点。第一是高性能,ArkTS 放弃了 JavaScript 中一些难以静态优化的动态特性,例如任意类型的动态属性访问、原型链的随意修改等,使得编译器能够生成更高效的字节码和机器码。第二是低门槛,对于已经熟悉 TypeScript、JavaScript、Java 或 C++ 的开发者来说,ArkTS 的语法非常亲切,学习曲线相对平缓。第三是声明式友好,ArkTS 为 ArkUI 提供了语言级的支持,包括装饰器、状态管理和组件化语法,使得 UI 开发可以用更接近自然描述的方式完成。

需要特别说明的是,ArkTS 与 TypeScript 的关系不能简单理解为“完全兼容”。ArkTS 是 TypeScript 的一个严格子集,这意味着大部分 TypeScript 代码在经过调整后可以运行在 ArkTS 环境中,但并非所有 TypeScript 特性都被支持。例如,ArkTS 不支持动态属性访问、不支持通过字符串拼接动态构造对象、对联合类型和交叉类型的使用有更严格限制等。这些限制虽然会在初学阶段带来一些不适,但从长期看,它们帮助开发者在写代码时就规避了大量运行时错误,也让编译器有更多机会做静态优化。

在 HarmonyOS 应用开发中,ArkTS 承担了两大职责:一是编写业务逻辑,包括数据模型、网络请求、文件读写、事件处理等;二是构建声明式 UI,通过 ArkUI 框架描述界面结构、样式和交互。得益于这一统一语言的设计,开发者不需要在逻辑语言和 UI 模板语言之间来回切换,整个应用可以在同一种语言体系内完成,这在开发体验和工程效率上都有明显优势。

本节最后,我们梳理一下学习 ArkTS 的推荐路径:第一步掌握基本语法和类型系统,第二步理解函数、类、接口和泛型等语言特性,第三步学习装饰器和状态管理机制,第四步进入 ArkUI 声明式开发实践,第五步通过完整项目巩固所学。后续章节将按照这一路径逐步展开,每一部分都会配合可运行的代码示例,帮助读者把概念落实为具体可写的代码。

2. 开发环境准备

在正式开始书写 ArkTS 代码之前,我们需要先把开发环境搭建好。HarmonyOS 应用开发目前主要使用华为官方提供的 DevEco Studio 集成开发环境。DevEco Studio 基于 IntelliJ IDEA 平台构建,提供了工程管理、代码编辑、编译构建、模拟器运行、真机调试、性能分析等一整套能力,是 ArkTS 开发的首选工具。

安装 DevEco Studio 的系统要求如下:Windows 系统建议使用 64 位 Windows 10 及以上版本,macOS 建议使用 Apple Silicon 或 Intel 芯片的较新版本,内存建议 16GB 及以上以获得流畅体验,磁盘空间至少预留 30GB。安装包可以在华为开发者联盟官网下载,安装过程与常见的 JetBrains 系列 IDE 类似,按照安装向导逐步点击即可。

安装完成后,首次启动 DevEco Studio 会引导下载 HarmonyOS SDK。SDK 中包含了 API 版本对应的库文件、模拟器镜像、构建工具链等。新建工程时,需要选择目标设备类型,例如手机、平板、智慧屏、手表或车机,不同设备对应的 SDK 版本和 API 能力有所差异。对于初学者,建议选择手机设备,API 版本选择当前推荐的稳定版本即可。

一个标准的 ArkTS 工程目录结构大致如下:AppScope 目录存放应用级配置,例如 app.json5 中的包名、版本号、图标等信息;entry 目录是主模块,其中的 src/main/ets 目录存放 ArkTS 源码,src/main/resources 目录存放图片、字符串等资源文件;module.json5 是模块级配置文件,声明了模块的能力、权限和页面路由信息;build-profile.json5 用于配置编译选项和签名信息。

下面通过一个最简单的示例来感受工程结构。在 entry/src/main/ets/pages 目录下新建一个 Index.ets 文件,内容如下:

@Entry
@Component
struct Index {
  @State message: string = 'Hello ArkTS'

  build() {
    Column() {
      Text(this.message)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这段代码展示了一个最基础的 ArkTS 页面:@Entry 装饰器表示该组件是页面入口,@Component 装饰器声明这是一个自定义组件,struct 定义组件结构,@State 声明响应式状态变量,build 方法负责描述 UI。虽然这里已经出现了装饰器和组件语法,但读者不必急于理解每一处细节,后续章节会逐一拆解。当前只需要确认环境能够正常编译和运行即可。

工程创建完成后,可以通过模拟器或真机运行。模拟器需要在 DevEco Studio 的设备管理器中创建,选择对应的设备类型和系统镜像进行下载。真机调试则需要开启开发者模式,并完成 USB 或无线连接配置。当屏幕上出现“Hello ArkTS”字样时,说明开发环境已经准备就绪,可以正式开始语言基础的学习。

3. 基础语法与程序结构

ArkTS 的基础语法与 TypeScript 高度一致,熟悉 TypeScript 或 JavaScript 的开发者可以快速上手。本节从程序的基本组成单位讲起,包括变量声明、常量、语句、注释和代码组织方式,帮助读者建立对 ArkTS 程序结构的整体认识。

ArkTS 程序由一条条语句组成,语句之间可以用分号分隔。与 JavaScript 类似,ArkTS 允许省略行尾分号,但为了代码清晰和避免潜在歧义,官方推荐显式书写分号。语句可以分组为代码块,代码块用一对花括号包裹,内部可以包含多条语句。函数体、类体、条件分支和循环体都是典型的代码块。

注释是程序中不可忽视的部分。ArkTS 支持两种注释方式:单行注释使用双斜杠,多行注释使用斜杠加星号包裹。良好的注释习惯可以显著提升代码的可维护性,官方建议在复杂逻辑处、公共接口处和业务规则处添加恰当注释。

变量声明是任何程序的基础。ArkTS 提供了 let 和 const 两种声明方式,以及传统的 var 关键字。现代 ArkTS 开发中强烈推荐使用 let 声明可变变量,使用 const 声明不可变常量。let 声明的变量具有块级作用域,只在声明它的代码块内有效;const 声明的常量必须在声明时完成初始化,并且初始化后不能重新赋值。需要注意的是,const 保证的是变量标识符与值的绑定关系不变,对于引用类型,其内部的属性仍然可能被修改。

下面是变量声明的基本示例:

let count: number = 10
const appName: string = 'HarmonyOS Demo'
let isActive: boolean = true
let userName = '张三' // 类型推断为 string

在 ArkTS 中,类型标注写在变量名之后、等号之前,用冒号连接。类型标注不是强制要求的,当初始值类型明确时,编译器可以自动推断类型;但在函数参数、公共接口和复杂对象中,显式标注类型能够提升可读性并帮助编译器做更严格的检查。

关于 var 关键字,ArkTS 虽然保留了它,但由于 var 存在变量提升和函数级作用域等历史设计问题,容易引发难以排查的 bug,官方明确建议避免使用 var,统一使用 let 和 const。这一规范也符合现代 JavaScript 和 TypeScript 的最佳实践。

标识符的命名规则也与主流语言一致:标识符可以由字母、数字、下划线和美元符号组成,但首字符不能是数字;标识符区分大小写;不能使用语言保留字作为标识符。ArkTS 社区普遍采用驼峰命名法,变量名和函数名使用小驼峰,例如 userName、getUserInfo;类名、接口名和组件名使用大驼峰,例如 UserModel、LoginPage。

语句表达式方面,ArkTS 支持算术表达式、比较表达式、逻辑表达式、赋值表达式和三元表达式。三元表达式可以在一行内完成简单的条件判断,例如:

let level: string = score >= 90 ? '优秀' : '普通'
let displayName: string = userName.length > 0 ? userName : '匿名用户'

代码组织方面,一个 .ets 文件通常由一个或多个导出声明组成。顶层可以定义变量、常量、函数、类、接口、枚举等,使用 export 关键字可以将其导出供其他文件导入使用。合理的代码组织应当遵循单一职责原则,把相关的数据模型、工具函数和业务逻辑划分到不同文件中,避免单个文件过于臃肿。

最后强调几个新手容易忽略的细节:赋值语句是一个表达式而不是语句的结束,它会产生赋值结果;分号在极少数跨行场景下的自动插入机制可能引发歧义,因此统一加分号是最稳妥的选择;空语句单独一个分号是合法的,但几乎总是意味着笔误。把握这些基础规则后,我们就可以继续深入类型系统的学习。

4. 数据类型体系

类型系统是 ArkTS 的核心内容,也是它与传统 JavaScript 最大的差异所在。ArkTS 采用静态类型系统,编译器在编译期就能检查出大部分类型错误,从而在应用运行之前消灭隐患。理解 ArkTS 的数据类型体系,是写出高质量代码的前提。ArkTS 的数据类型可以分为原始类型、对象类型和特殊类型三大类。

4.1 原始类型

原始类型包括 number、string、boolean、bigint、symbol 等。number 类型在 ArkTS 中既表示整数也表示浮点数,底层采用统一的数值类型实现,因此不存在单独的 int、float、double 等细分类型。string 类型表示文本数据,使用单引号或双引号包裹,也可以使用模板字符串。boolean 类型只有 true 和 false 两个取值。bigint 用于表示超出 number 安全范围的超大整数,书写时在数字末尾加 n。symbol 用于创建唯一标识符,常作为对象属性的键。

下面是原始类型的定义示例:

let age: number = 25
let price: number = 99.5
let title: string = 'ArkTS 学习笔记'
let template: string = `当前年龄:${age},价格:${price}`
let isLogin: boolean = false
let bigNumber: bigint = 9007199254740993n
let uniqueKey: symbol = Symbol('id')

模板字符串是字符串处理中非常实用的能力,使用反引号包裹,内部通过 ${} 嵌入表达式,表达式会在运行时被求值并转换为字符串。模板字符串还支持多行书写,这在拼接较长文本时非常方便。

4.2 对象类型

对象类型包括数组、元组、函数、类实例和普通对象。数组是最常用的集合类型,ArkTS 提供了两种数组声明方式:一种是类型加方括号,例如 number[];另一种是使用泛型接口 Array<number>。推荐使用前者,书写更简洁。数组提供了一系列常用方法,如 push、pop、map、filter、reduce、forEach 等,这些方法与 JavaScript 中的使用方式基本一致。

下面是数组的定义与常见操作示例:

let numbers: number[] = [1, 2, 3, 4, 5]
let names: Array<string> = ['华为', '荣耀', '鸿蒙']
numbers.push(6)
let doubled: number[] = numbers.map((item: number): number => item * 2)
let evenNumbers: number[] = numbers.filter((item: number): boolean => item % 2 === 0)
let total: number = numbers.reduce((acc: number, cur: number): number => acc + cur, 0)

元组用于表示固定长度且各元素类型已知的数组。元组在需要表达“一对数据”或“多字段但不值得单独建类”的场景非常有用。定义元组时,在方括号内按顺序标注每个位置的类型:

let point: [number, number] = [10, 20]
let userInfo: [string, number] = ['张三', 28]
let x: number = point[0]
let y: number = point[1]

需要提醒的是,ArkTS 对元组的越界访问会给出编译检查,这一点避免了传统 JavaScript 中访问越界得到 undefined 的隐患。在涉及固定结构数据时,优先使用元组可以让类型信息更加精确。

4.3 特殊类型

特殊类型包括 any、unknown、void、never、null、undefined 和联合类型等。any 表示任意类型,使用它等于放弃了类型检查,官方建议尽量避免使用 any,因为在静态类型语言中长期使用 any 会显著削弱类型系统的价值。unknown 是 any 的安全替代品,它同样可以接受任意类型的值,但在使用该值之前必须先进行类型收窄,否则无法直接操作,这从机制上保证了类型安全。

void 通常用作函数的返回值类型,表示函数没有返回值。never 表示永远不会有返回值的函数,例如总是抛出异常的函数或无限循环的函数。null 和 undefined 在 ArkTS 中是独立的类型,也分别是各自的值的类型。在默认开启严格空检查的情况下,null 和 undefined 不能直接赋值给其他类型,这一设计帮助开发者提前规避空值错误。

下面是特殊类型的使用示例:

function logMessage(msg: string): void {
  console.log(msg)
}

function throwError(msg: string): never {
  throw new Error(msg)
}

let dynamicValue: unknown = 'hello'
if (typeof dynamicValue === 'string') {
  console.log(dynamicValue.toUpperCase()) // 类型收窄后可以安全使用
}

联合类型允许一个变量同时接受多种类型,使用竖线分隔。联合类型在 ArkTS 中受到一定限制,通常要求联合的各成员之间具有可区分性,编译器在访问联合类型值时往往需要配合类型判断来完成收窄。下面是一个联合类型示例:

type Id = number | string

function printId(id: Id): void {
  if (typeof id === 'string') {
    console.log(`字符串 ID:${id}`)
  } else {
    console.log(`数字 ID:${id}`)
  }
}

类型别名通过 type 关键字可以为已有类型起一个新名字,提升代码可读性。类型别名可以指向原始类型、对象类型、联合类型等,是组织复杂类型的重要手段。与接口相比,类型别名更擅长表达联合类型和交叉类型,而接口更擅长表达对象结构并可被类实现和扩展,二者在实际开发中可以配合使用。

5. 运算符与表达式

运算符是构成表达式的基础元素,ArkTS 提供了丰富的运算符集合,涵盖算术运算、赋值运算、比较运算、逻辑运算、位运算、字符串运算和类型运算等。本节对各类运算符逐一讲解,并说明使用中的注意要点。

算术运算符包括加、减、乘、除、取余、自增和自减。加号除了进行数值加法外,还可以用于字符串拼接,当加号两侧有一侧是字符串时,另一侧会被转换为字符串后进行拼接。取余运算符用于求两个数相除后的余数,在处理循环周期、奇偶判断等场景非常常用。自增和自减运算符分为前置和后置两种形式,前置形式先运算后取值,后置形式先取值后运算,二者在单独成句时效果相同,但在表达式中混用时结果有明显差异,建议初学者谨慎混用。

赋值运算符包括基本赋值符和复合赋值符。复合赋值符将算术运算或位运算与赋值合并,例如 +=、-=、*=、/=、%= 等。复合赋值书写简洁,并且只对左侧变量求值一次,在性能上略优于展开形式,不过这种差异在现代编译器优化下通常可以忽略。

比较运算符用于比较两个值的大小或相等关系,结果始终是布尔值。相等比较分为严格相等和宽松相等,ArkTS 推荐始终使用严格相等 === 和严格不等 !==,因为它们不会进行隐式类型转换,能够避免因类型转换引发的意外结果。大小比较符号用于数值或字符串,字符串按字典序比较。需要特别注意的是,数值与字符串之间的比较在宽松场景下可能产生不符合直觉的结果,因此应始终保持比较两侧类型一致。

逻辑运算符包括与、或、非三种。逻辑与和逻辑或都采用短路求值机制:对于逻辑与,如果左侧为假,右侧表达式不会执行;对于逻辑或,如果左侧为真,右侧表达式不会执行。短路机制常被用来做条件守卫,例如在访问对象属性之前先判断对象是否存在。逻辑非是单目运算符,将操作数转换为布尔值后取反。

位运算符包括按位与、按位或、按位异或、按位非、左移、右移和无符号右移。位运算直接操作整数的二进制表示,在权限标记、颜色值处理、协议解析等底层场景非常高效。使用位运算时,操作数会先被转换为 32 位有符号整数,这一细节在处理超大整数时需要留意。

下面是运算符综合示例:

let a: number = 10
let b: number = 3
let sum: number = a + b        // 13
let remainder: number = a % b  // 1
let quotient: number = a / b   // 3.333...

let flag1: boolean = true
let flag2: boolean = false
let andResult: boolean = flag1 && flag2  // false
let orResult: boolean = flag1 || flag2   // true
let notResult: boolean = !flag1          // false

let bitAnd: number = a & b   // 2
let bitOr: number = a | b    // 11
let bitXor: number = a ^ b   // 9
let bitLeft: number = a << 2 // 40

此外,三元运算符可以视为 if-else 的表达式版本,适合在赋值语句中根据条件选择值。空值合并运算符 ?? 用于在左侧值为 null 或 undefined 时回退到右侧默认值。可选链运算符 ?. 用于在访问可能为空的对象的属性或方法时避免抛出空错误,如果中间的访问链任一环节为空,整个表达式返回 undefined 而不是报错。这些运算符在现代代码中非常常见,熟练使用它们可以让代码更加稳健和简洁。

运算符的优先级决定了表达式的求值顺序,完整的优先级表比较长,但在实际开发中,与其记忆复杂的优先级规则,不如在不确定时使用括号显式指定运算顺序。括号不仅能避免错误,也能提升代码的可读性,让阅读者一眼看清运算意图。官方编码规范也推荐在混合多种运算符时使用括号表达意图。

6. 流程控制语句

流程控制决定了程序执行的路径。ArkTS 提供了完整的流程控制语句,包括条件分支、循环和跳转语句。掌握流程控制是编写任何实际业务逻辑的基础,本节对各类语句的语法和使用场景进行详细说明。

6.1 条件分支

if-else 是最基本的条件分支语句,根据布尔表达式的真假决定执行哪个分支。if 后面可以跟随零个或多个 else if 分支以及一个可选的 else 分支。条件表达式必须求值为布尔值,ArkTS 不接受将任意值直接作为条件,这与某些弱类型语言中“非零即真”的惯例不同,需要特别注意。

下面是 if-else 的典型用法:

function getGrade(score: number): string {
  if (score >= 90) {
    return '优秀'
  } else if (score >= 80) {
    return '良好'
  } else if (score >= 60) {
    return '及格'
  } else {
    return '不及格'
  }
}

switch 语句适用于基于单个表达式的多分支判断。switch 的表达式与每个 case 后的值进行严格相等比较,匹配后从该 case 开始执行,直到遇到 break 或 switch 结束。ArkTS 中每个非空 case 分支通常需要显式 break 或 return,否则会发生贯穿执行。default 分支在没有任何 case 匹配时执行,是可选的。

下面是 switch 的典型用法:

function getWeekName(day: number): string {
  switch (day) {
    case 0:
      return '星期日'
    case 1:
      return '星期一'
    case 2:
      return '星期二'
    case 3:
      return '星期三'
    default:
      return '未知'
  }
}

switch 还可以配合枚举使用,让多分支逻辑更加清晰。相比连续多个 else if,当判断对象是同一个值的多个离散取值时,switch 的可读性更好,执行效率也更高。

6.2 循环语句

循环语句用于重复执行某段代码。ArkTS 支持 for 循环、while 循环、do-while 循环和 for-of 循环。for 循环是最常用的循环形式,由初始化表达式、条件表达式和步进表达式三部分组成,适合已知循环次数的场景。

下面是 for 循环示例:

let totalValue: number = 0
for (let i: number = 1; i <= 100; i++) {
  totalValue += i
}
console.log(`1 到 100 的和为:${totalValue}`)

while 循环在每次迭代前判断条件,条件为真则继续执行。do-while 循环则先执行一次循环体再判断条件,因此循环体至少执行一次。两者都适合循环次数不确定、依赖外部条件控制的场景。

for-of 循环用于遍历可迭代对象,最常见的应用是遍历数组。相比传统 for 循环通过索引访问,for-of 的写法更简洁,也不容易产生越界错误。如果遍历时还需要索引,可以配合 entries 方法使用。下面是示例:

let fruits: string[] = ['苹果', '香蕉', '橙子']
for (const fruit of fruits) {
  console.log(fruit)
}

for (const [index, fruit] of fruits.entries()) {
  console.log(`第 ${index} 个水果是 ${fruit}`)
}

6.3 跳转语句

跳转语句包括 break 和 continue。break 用于立即终止当前循环或 switch 分支,continue 用于跳过当前迭代的剩余部分,直接进入下一次迭代。二者可以配合标签使用来跳出多层嵌套循环,但标签语法会降低代码可读性,只有在确实需要从内层循环控制外层循环时才建议使用。

下面是一个结合 break 和 continue 的示例:

let searchNumber: number = 42
let found: boolean = false
let dataList: number[] = [10, 20, 42, 30, 42, 50]

for (let i: number = 0; i < dataList.length; i++) {
  if (dataList[i] % 2 !== 0) {
    continue // 跳过奇数
  }
  if (dataList[i] === searchNumber) {
    found = true
    break
  }
}
console.log(`是否找到:${found}`)

在掌握了流程控制之后,读者已经具备了编写基本算法逻辑的能力。但仅仅依靠基本的 if 和 for 还不够,代码的组织和复用需要借助函数和类,这正是接下来两章的主题。

7. 函数

函数是组织代码、实现复用的基本单元。ArkTS 中的函数既可以独立定义,也可以作为对象的方法,还可以作为参数传递和作为返回值返回,这体现了 ArkTS 对函数式编程风格的支持。理解函数的各种形式,是构建复杂应用的必备技能。

7.1 函数定义与调用

函数使用 function 关键字定义,包含函数名、参数列表、返回值类型和函数体。参数列表中的每个参数由名称和类型组成,多个参数用逗号分隔。返回值类型写在参数列表之后,如果函数没有返回值,使用 void 类型。函数体由花括号包裹,包含要执行的语句,可以使用 return 语句返回结果。

下面是函数定义的示例:

function add(a: number, b: number): number {
  return a + b
}

function greet(name: string): void {
  console.log(`你好,${name}`)
}

let result: number = add(3, 5)
greet('华为开发者')

函数也支持箭头函数语法,箭头函数更简洁,并且不绑定自己的 this,这一特性在回调函数中非常有用。箭头函数的语法为参数列表后跟箭头再跟函数体,当函数体只有一条表达式时,可以省略花括号和 return,直接写表达式。

let multiply = (a: number, b: number): number => a * b
let square = (x: number): number => {
  return x * x
}
let value: number = multiply(4, 5)

7.2 参数处理

ArkTS 函数的参数支持默认值和可选参数。默认参数在调用时如果未传入对应实参,就使用默认值;可选参数使用问号标记,表示该参数可以省略。在参数列表中,必选参数必须放在可选参数和默认参数之前。这一约束保证了参数绑定的确定性。

function createUser(name: string, age?: number, city: string = '深圳'): string {
  if (age !== undefined) {
    return `${name},${age} 岁,来自 ${city}`
  }
  return `${name},来自 ${city}`
}

console.log(createUser('张三', 28))
console.log(createUser('李四'))

剩余参数使用三个点表示,可以将任意数量的实参收集到一个数组中。剩余参数必须是参数列表中的最后一个参数。剩余参数常在工具函数中用于处理不定数量的输入。

function sumAll(...numbers: number[]): number {
  return numbers.reduce((acc: number, cur: number): number => acc + cur, 0)
}

console.log(sumAll(1, 2, 3, 4, 5))

7.3 函数作为值与高阶函数

在 ArkTS 中,函数是一等公民,可以赋值给变量、作为参数传递、作为返回值返回。接受函数作为参数或返回函数的函数称为高阶函数。高阶函数是函数式编程的基础,可以实现对行为的抽象和组合。

那么,如何为函数类型书写类型标注呢?函数类型使用箭头语法表示,参数类型写在括号内,返回值类型写在箭头后。例如 (a: number, b: number) => number 表示接受两个 number 参数并返回 number 的函数类型。

function operate(a: number, b: number, operation: (x: number, y: number) => number): number {
  return operation(a, b)
}

let additionResult: number = operate(8, 2, (x, y) => x + y)
let subtractionResult: number = operate(8, 2, (x, y) => x - y)
console.log(`加法结果:${additionResult},减法结果:${subtractionResult}`)

高阶函数在数组方法中体现得淋漓尽致。map、filter、forEach、reduce、sort、find 等方法都接受回调函数,通过传入不同的回调实现对数组的多样化处理。掌握高阶函数,可以让数据处理代码从命令式风格转向声明式风格,代码更紧凑也更易读。

7.4 函数重载

ArkTS 支持函数重载,即同一个函数名可以有多个不同的签名,编译器根据调用时的实参类型选择合适的签名。重载声明只写签名不写实现,真正的实现需要兼容所有重载签名。函数重载在需要为不同参数类型提供不同处理逻辑的公共接口中非常有用。

function parseValue(value: string): string
function parseValue(value: number): number
function parseValue(value: string | number): string | number {
  if (typeof value === 'string') {
    return value.trim()
  }
  return value * 2
}

console.log(parseValue('  text  '))
console.log(parseValue(21))

函数重载提升了 API 的灵活性,同时也保持了类型安全。在设计对外暴露的工具函数时,通过重载可以为不同输入提供明确且精确的返回类型,让调用方获得更好的类型提示和编译检查。

8. 类与面向对象

面向对象编程是组织大型应用代码的重要范式。ArkTS 在 TypeScript 的基础上完整支持类、继承、封装、多态等面向对象特性,并额外提供了访问修饰符的简写语法。理解类的各种机制,有助于构建结构清晰、易于扩展的业务模型。

8.1 类的定义与实例化

类使用 class 关键字定义,包含属性、构造函数和方法。属性声明时通常会标注类型并可以赋予初始值。构造函数名为 constructor,用于在创建实例时初始化对象状态。使用 new 关键字调用构造函数即可创建类的实例。下面是基础示例:

class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  introduce(): string {
    return `我是 ${this.name},今年 ${this.age} 岁。`
  }
}

let person: Person = new Person('张三', 30)
console.log(person.introduce())

在这里,this 指向当前实例。构造函数内部通过 this.name = name 将参数值赋给实例属性。方法内部的 this 同样指向调用该方法的实例。

8.2 访问修饰符

ArkTS 提供了 public、private、protected 三种访问修饰符。public 是默认修饰符,表示成员对外公开;private 表示成员只能在类内部访问;protected 表示成员可以在类内部和子类中访问。访问修饰符既限制编译期的可见性,也传达了类的设计意图。

构造函数参数还可以直接使用访问修饰符,这样在传入参数的同时自动创建并初始化同名属性,这是 ArkTS 中非常常用的简写语法:

class User {
  constructor(public name: string, private age: number) {
  }

  getAge(): number {
    return this.age
  }

  describe(): string {
    return `${this.name} 的年龄是 ${this.age}`
  }
}

let user: User = new User('李四', 25)
console.log(user.name)     // 可以访问 public 属性
console.log(user.getAge()) // 通过方法访问 private 属性

在上面的代码中,构造函数参数前的 public 和 private 表示这些参数将同时成为类的属性,其中 name 是公有属性,age 是私有属性。这种写法省去了在构造函数体内逐一赋值的样板代码,让类的定义更加简洁。

8.3 getter 与 setter

getter 和 setter 用于对属性的读写进行拦截和加工。getter 使用 get 关键字定义,在读取属性时触发;setter 使用 set 关键字定义,在写入属性时触发。通过 getter 和 setter,可以在对外保持简洁属性语法的同时,内部实现校验、转换和副作用逻辑。

class Product {
  private _price: number = 0

  get price(): number {
    return this._price
  }

  set price(value: number) {
    if (value < 0) {
      throw new Error('价格不能为负数')
    }
    this._price = value
  }
}

let product: Product = new Product()
product.price = 99
console.log(product.price)

需要注意的是,getter 和 setter 不能与同名的普通属性共存,通常的做法是使用下划线前缀的私有属性作为实际存储,通过 getter 和 setter 暴露逻辑名称。这样既保证了外部访问的直观性,又在内部保留了校验和控制能力。

8.4 继承

继承是面向对象的核心机制之一,子类通过 extends 关键字继承父类的属性和方法,并可以添加新的成员或覆盖父类的方法。在子类构造函数中,必须先调用 super 以初始化父类部分,然后才能访问 this。方法覆盖时,可以在子类方法中通过 super 调用父类的同名方法。

class Animal {
  constructor(protected name: string) {
  }

  makeSound(): string {
    return '发出声音'
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name)
  }

  makeSound(): string {
    return `${this.name} 汪汪叫`
  }
}

let dog: Dog = new Dog('旺财')
console.log(dog.makeSound())

继承建立了类型之间的层级关系,允许子类实例被赋值给父类类型的变量,这就是多态的基础。通过多态,程序可以针对父类类型编写通用逻辑,从而实现对不同子类的统一处理,显著提升代码的扩展性。

8.5 静态成员与抽象类

静态成员属于类本身而非实例,使用 static 关键字定义,通过类名访问。静态成员常用于工具方法、常量配置和共享状态。抽象类使用 abstract 关键字定义,不能直接实例化,只能作为基类被继承。抽象方法只有签名没有实现,必须在具体子类中被实现。

abstract class Shape {
  abstract getArea(): number

  static describe(): string {
    return '这是一个形状'
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super()
  }

  getArea(): number {
    return Math.PI * this.radius * this.radius
  }
}

let circle: Circle = new Circle(5)
console.log(circle.getArea())
console.log(Shape.describe())

抽象类适合在需要为多个子类定义统一模板、但又不希望父类被直接实例化的场景中使用。静态成员则适合承载与具体实例无关的逻辑。二者都是面向对象设计中构建类层次的重要手段。

9. 接口与类型系统进阶

接口是 ArkTS 类型系统的重要组成部分,用于描述对象的形状,即一个对象应该具有哪些属性和方法以及它们的类型。接口一方面为对象提供类型约束,另一方面也作为抽象契约供类实现。理解接口及其与类的协作,有助于设计出清晰、稳定的 API。

9.1 接口定义与对象形状

接口使用 interface 关键字定义,内部声明属性和方法的签名。接口中的属性可以标注为可选属性,使用问号表示;也可以标注为只读属性,使用 readonly 修饰。下面是接口定义与使用的示例:

interface PersonInfo {
  readonly id: number
  name: string
  age?: number
}

let info: PersonInfo = {
  id: 1001,
  name: '王五'
}
// info.id = 1002 // 编译错误:只读属性不可修改
info.age = 23

接口还可以描述方法签名,方法签名包括方法名、参数列表和返回值类型,但不包含实现。拥有方法的接口可以约束实现该接口的类必须提供对应的方法。

9.2 接口的实现与继承

类使用 implements 关键字实现接口,实现类必须提供接口中声明的所有成员的实现。一个类可以实现多个接口,用逗号分隔。接口之间也可以通过 extends 关键字继承,形成接口层次。下面是一个完整示例:

interface Moveable {
  move(distance: number): void
}

interface Flyable {
  fly(height: number): void
}

interface Bird extends Moveable, Flyable {
  readonly name: string
}

class Sparrow implements Bird {
  constructor(public readonly name: string) {
  }

  move(distance: number): void {
    console.log(`${this.name} 走了 ${distance} 米`)
  }

  fly(height: number): void {
    console.log(`${this.name} 飞到了 ${height} 米高`)
  }
}

let sparrow: Sparrow = new Sparrow('麻雀')
sparrow.move(10)
sparrow.fly(50)

接口与抽象类的区别值得辨析。接口描述“能做什么”,强调能力契约;抽象类描述“是什么”,强调类型层级和复用。一个类只能继承一个父类,但可以实现多个接口,因此当需要组合多个能力时,接口是更灵活的选择。

9.3 函数类型接口与索引签名

接口除了描述对象形状,还可以描述函数类型。函数类型接口定义了调用签名,表示实现该接口的值是一个可调用的函数。此外,接口还支持索引签名,用来描述通过索引访问属性的类型,例如字典结构的对象。

interface StringMap {
  [key: string]: string
}

let config: StringMap = {
  server: 'https://example.com',
  theme: 'dark'
}
console.log(config['server'])

索引签名在需要处理键名不确定的对象时非常有用,例如配置文件、缓存对象和数据处理中间结果。需要注意的是,索引签名不能与显式声明的属性产生类型冲突,编译器会检查二者的兼容性。

10. 泛型

泛型是编写可复用、类型安全代码的强大工具。通过在类型定义中引入类型参数,泛型让同一个函数、类或接口可以适用于多种类型,同时在使用时保持精确的类型信息,避免使用 any 带来的类型丢失。泛型是 ArkTS 类型系统高级特性的核心内容。

10.1 泛型函数

泛型函数使用尖括号声明类型参数,类型参数在函数签名中作为占位符使用。调用泛型函数时,可以显式传入类型实参,也可以让编译器根据实参自动推断。下面是泛型函数的典型示例:

function identity<T>(value: T): T {
  return value
}

let strValue: string = identity<string>('你好')
let numValue: number = identity<number>(123)
let autoValue: boolean = identity(true) // 自动推断为 boolean

泛型函数在工具函数中应用广泛,例如返回数组第一个元素的函数、交换两个变量值的函数、安全解析 JSON 的函数等。使用泛型后,函数可以自然地保留输入和输出之间的类型关系。

10.2 泛型约束

有时我们需要对泛型类型参数施加约束,限制它必须满足某个条件,例如必须拥有某个属性或必须继承自某个类型。泛型约束使用 extends 关键字表示,约束后就可以在函数体内安全地访问被约束的成员。

interface HasLength {
  length: number
}

function getLength<T extends HasLength>(value: T): number {
  return value.length
}

console.log(getLength('字符串'))
console.log(getLength([1, 2, 3]))
// console.log(getLength(123)) // 编译错误:number 没有 length 属性

泛型约束让泛型从“任意类型”收窄为“满足条件的类型”,在保留灵活性的同时,为函数体内部提供了可用的类型信息,这是泛型设计中非常重要的平衡手段。

10.3 泛型接口与泛型类

接口和类同样可以使用泛型。泛型接口常用于描述通用的数据结构契约,例如仓库接口、容器接口。泛型类则用于实现可复用的数据结构,例如栈、队列、链表和包装器等。

interface Repository<T> {
  getById(id: number): T | null
  save(entity: T): void
}

class Stack<T> {
  private items: T[] = []

  push(item: T): void {
    this.items.push(item)
  }

  pop(): T | undefined {
    return this.items.pop()
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1]
  }

  get size(): number {
    return this.items.length
  }
}

let numberStack: Stack<number> = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
console.log(numberStack.pop())

泛型接口和泛型类的意义在于,它们把“类型参数化”的思想从函数扩展到更大的代码结构上,让数据结构本身变得可复用。一个设计良好的泛型类可以被不同业务类型共享,从而大幅减少重复代码。

11. 枚举

枚举用于定义一组有名字的常量集合。当程序中有多个固定的、互相排斥的取值时,枚举比裸字符串或裸数字更能表达语义,也更容易受到编译器的检查和保护。ArkTS 支持数字枚举和字符串枚举两种形式。

11.1 数字枚举

数字枚举如果没有显式赋值,成员的值从 0 开始自动递增;也可以为某个成员指定数值,后续成员在此基础上递增。数字枚举常用于状态码、错误码、方向等取值连续的场景。

enum Direction {
  Up,     // 0
  Down,   // 1
  Left,   // 2
  Right   // 3
}

enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500
}

let direction: Direction = Direction.Up
console.log(direction) // 0

11.2 字符串枚举

字符串枚举要求每个成员都显式赋予字符串值。字符串枚举在调试时更有可读性,因为输出的是有意义的字符串而不是难以理解的数字。字符串枚举不支持自动递增。

enum ThemeMode {
  Light = 'light',
  Dark = 'dark',
  System = 'system'
}

let currentTheme: ThemeMode = ThemeMode.Dark
console.log(currentTheme) // 'dark'

枚举还支持反向映射,即通过枚举值获取枚举成员名,这一特性在数字枚举中可用。反向映射在需要序列化、反序列化枚举值的场景中非常方便。字符串枚举不提供反向映射。

在实际项目中,枚举常与 switch 语句配合,用于处理多种业务状态。相比直接使用字符串字面量,枚举提供了集中定义和编译期检查,当某个取值被误写时,编译器能够立即发现错误,而不是让错误潜伏到运行时。

12. 模块与文件组织

随着应用规模增长,把所有代码写在一个文件中显然不可维护。ArkTS 采用模块化机制来组织代码,通过 export 和 import 关键字实现跨文件共享。合理的模块划分是高可维护性工程的基础。

导出分为命名导出和默认导出。命名导出使用 export 关键字直接修饰声明,可以在一个文件中导出多个成员;默认导出使用 export default,每个文件只能有一个默认导出。导入时,命名导出使用花括号按名称导入,默认导出使用任意合法名称导入。

下面通过一个完整示例演示模块的用法。首先创建一个模型文件 user_model.ets:

export class UserModel {
  constructor(public id: number, public name: string) {
  }

  display(): string {
    return `用户:${this.name}`
  }
}

export function formatUserName(name: string): string {
  return name.trim().toUpperCase()
}

然后在另一个文件中导入并使用:

import { UserModel, formatUserName } from './user_model'

let userModel: UserModel = new UserModel(1, '张三')
console.log(userModel.display())
console.log(formatUserName('  李四  '))

导入路径支持相对路径,也支持基于模块别名的路径。在 HarmonyOS 工程中,还经常通过 import 引入 SDK 提供的系统模块,例如引入 router 模块进行页面跳转、引入 http 模块进行网络请求等。系统模块的导入路径通常以 @ohos 或 @kit 开头。

在组织项目代码时,建议按照职责划分目录,例如 pages 存放页面组件、model 存放数据模型、service 存放业务逻辑、utils 存放工具函数、common 存放公共常量。清晰的目录结构配合模块化导出导入,可以让团队协作开发更加有序。

尽量避免循环依赖,即模块 A 导入模块 B,同时模块 B 又导入模块 A 的情况。循环依赖会增加代码的耦合度,可能导致难以排查的初始化顺序问题。当发现两个模块需要互相引用时,通常意味着职责划分不够清晰,应当通过提取公共模块、引入接口抽象等方式解耦。

13. 装饰器

装饰器是 ArkTS 中极具特色的语言能力,也是 ArkUI 声明式开发的基础。装饰器本质上是一种特殊的声明,可以附加到类、方法、属性上,为它们添加元数据或修改其行为。在 ArkTS 中,装饰器使用 @ 符号前缀,例如 @State、@Prop、@Link、@Component 等。理解装饰器是进入 ArkUI 状态管理和组件化开发的关键。

13.1 装饰器的概念与语法

装饰器写在被装饰声明的前一行,可以接收参数。装饰器会在声明被定义时执行,由框架或运行时的装饰器实现负责处理。在 ArkTS 开发中,开发者通常使用框架提供的装饰器,而不是自行编写装饰器实现,因此理解装饰器的语义比理解其底层实现更为重要。

@Entry
@Component
struct LoginPage {
  @State username: string = ''
  @State password: string = ''

  build() {
    Column() {
      Text('登录页面')
    }
  }
}

在上面的代码中,@Entry 将组件标记为页面入口,@Component 将结构体标记为可复用组件,@State 将属性标记为组件内部响应式状态。这些装饰器共同定义了组件在 ArkUI 框架中的角色和行为。

13.2 常用状态管理装饰器

状态管理是 ArkTS 装饰器体系中最重要的应用。@State 用于声明组件内部状态,当状态变化时,依赖该状态的 UI 会自动刷新。@Prop 用于父组件向子组件单向传递数据,子组件不能修改 @Prop 数据。@Link 用于父组件和子组件之间的双向数据同步,任意一方的修改都会同步到另一方。

此外还有 @Provide 和 @Consume 用于跨层级传递状态,@Observed 和 @ObjectLink 用于观察对象属性的变化,@StorageLink 和 @StorageProp 用于与应用级别的持久化存储建立响应式连接。这些装饰器共同构成了 ArkUI 完整的状态管理体系,让复杂页面的数据流保持一致。

装饰器的使用有一套必须遵守的规则。例如,被 @State 修饰的属性必须在声明时初始化,@Prop 和 @Link 修饰的属性则不能本地初始化,而是从父组件接收初始值。装饰器修饰的属性类型也有要求,@State 支持原始类型、对象、数组和类实例等,但复杂类型的变化观察受到一定限制,需要配合正确的方式触发刷新。

13.3 装饰器与组件生命周期

除了状态装饰器,ArkTS 还通过装饰器和接口形式提供生命周期管理。组件生命周期包括 aboutToAppear、aboutToDisappear 等回调,页面生命周期包括 onPageShow、onPageHide、onBackPress 等回调。这些生命周期方法让开发者能够在组件出现、消失、页面可见性变化等关键节点执行初始化与清理逻辑。

装饰器是与 ArkTS 原生生态深度绑定的能力,传统 TypeScript 项目中的实验性装饰器在 ArkTS 中有自己的实现和约束。学习装饰器的最佳方式是在实际组件开发中反复练习,观察不同装饰器下状态变化与 UI 刷新的行为差异,逐步建立直觉。

14. 状态管理机制

状态管理是声明式 UI 开发的核心问题。在传统命令式 UI 中,开发者需要手动操作控件更新界面;而在 ArkUI 的声明式模型中,开发者只需描述 UI 与状态的对应关系,当状态改变时框架自动完成界面刷新。正确理解和使用状态管理,是写出高效、正确 ArkUI 应用的关键。

14.1 状态与 UI 的绑定

在 ArkUI 组件中,被状态管理装饰器标记的变量就是状态。build 方法中读取这些状态变量来描述 UI,框架会记录状态变量与 UI 节点之间的依赖关系。当状态变量的值发生改变时,框架只重新执行相关组件的 build 逻辑,更新受影响的 UI 部分,而不是粗暴地重建整个页面。这种细粒度的更新机制保证了界面刷新效率。

下面是一个计数器示例,展示了本地状态与 UI 的绑定:

@Entry
@Component
struct CounterPage {
  @State count: number = 0

  build() {
    Column({ space: 20 }) {
      Text(`当前计数:${this.count}`)
        .fontSize(24)
      Button('增加')
        .onClick(() => {
          this.count++
        })
      Button('重置')
        .onClick(() => {
          this.count = 0
        })
    }
    .width('100%')
    .padding(20)
  }
}

在这个示例中,点击“增加”按钮会修改 count 状态,框架检测到状态变化后自动刷新 Text 组件的显示内容。开发者不需要手动查找控件并设置文本,这正是声明式开发的便利之处。

14.2 父子组件状态传递

在实际开发中,界面往往由多层组件嵌套组成,状态需要在组件之间传递。父子组件之间的数据传递主要依靠 @Prop 和 @Link。@Prop 实现单向数据流,父组件的数据变化会传递给子组件,但子组件的修改不会影响父组件,适合展示型子组件。@Link 实现双向绑定,父子组件共享同一份数据源,适合需要子组件修改父组件状态的场景,如表单输入、开关切换等。

下面演示 @Link 的双向绑定:

@Component
struct ToggleItem {
  @Link isOn: boolean

  build() {
    Row() {
      Text('开关状态')
      Toggle({ type: ToggleType.Switch, isOn: this.isOn })
        .onChange((value: boolean) => {
          this.isOn = value
        })
    }
  }
}

@Entry
@Component
struct ParentPage {
  @State switchOn: boolean = false

  build() {
    Column() {
      ToggleItem({ isOn: this.switchOn })
      Text(`父组件状态:${this.switchOn}`)
    }
  }
}

使用 @Link 时,父组件在构造子组件时需要传入状态变量本身,并通过 $ 前缀传递引用,框架会自动建立双向同步关系。这个例子中,子组件内切换开关会同步修改父组件的 switchOn,父组件中的文本也随之更新。

14.3 复杂对象的状态观察

对于数组和对象,状态管理需要更细致的处理。@State 修饰的数组,在整体替换数组引用或修改某些特定索引时能够触发刷新,但某些原地修改操作可能无法被框架观察到。为了确保 UI 正确更新,推荐在修改数组时使用整体赋值或创建新数组的方式。

对于类对象,@Observed 和 @ObjectLink 提供了属性级观察能力。@Observed 装饰类,@ObjectLink 修饰子组件中接收的该类型属性,这样当对象内部属性变化时,依赖这些属性的 UI 也能局部刷新。下面是示意代码:

@Observed
class UserProfile {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

@Component
struct ProfileView {
  @ObjectLink profile: UserProfile

  build() {
    Column() {
      Text(this.profile.name)
      Text(`${this.profile.age} 岁`)
    }
  }
}

状态管理的设计原则可以总结为:明确每个状态的拥有者,尽量保持单向数据流,将状态提升到需要共享它的最小公共祖先,避免不必要的全局状态。遵循这些原则,可以让应用的状态流转清晰可控,减少难以追踪的副作用。

15. 异步编程

现代应用离不开异步操作,无论是网络请求、文件读写、定时任务还是动画执行,都需要在异步模型中处理。ArkTS 继承了 TypeScript 的异步编程能力,支持回调函数、Promise 和 async/await 三种主要方式。理解异步编程是开发真实应用的基本功。

15.1 回调函数

回调函数是最基础的异步处理方式,将后续逻辑封装成函数,在异步操作完成时调用。回调的优点是简单直接,缺点是多个连续异步操作会形成层层嵌套的“回调地狱”,导致代码可读性急剧下降。

function fetchData(callback: (result: string) => void): void {
  setTimeout(() => {
    callback('数据加载完成')
  }, 1000)
}

fetchData((result: string) => {
  console.log(result)
})

在 ArkTS 的系统 API 中,大量传统接口使用回调函数风格,例如网络请求的 http.createHttp().request() 接收 success 和 fail 回调。虽然现代代码更多转向 Promise 和 async/await,但理解回调仍是阅读和使用系统 API 的基础。

15.2 Promise

Promise 是异步操作的标准化表示,代表一个可能在未来完成或失败的操作。Promise 对象有三种状态:pending 进行中、fulfilled 已成功、rejected 已失败。通过 then 方法注册成功回调,catch 方法注册失败回调,finally 方法注册无论成败都会执行的回调。

function delayMessage(message: string, delay: number): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    if (delay < 0) {
      reject(new Error('延迟时间不能为负数'))
      return
    }
    setTimeout(() => {
      resolve(message)
    }, delay)
  })
}

delayMessage('第一步完成', 500)
  .then((result: string) => {
    console.log(result)
    return delayMessage('第二步完成', 500)
  })
  .then((result: string) => {
    console.log(result)
  })
  .catch((error: Error) => {
    console.error(error.message)
  })

Promise 的链式调用解决了回调地狱问题,让异步流程按顺序书写,错误也可以通过统一的 catch 集中处理。此外,Promise 还提供 all、race 等静态方法,支持并行执行多个异步任务。

15.3 async 与 await

async/await 是建立在 Promise 之上的语法糖,让异步代码的书写方式接近同步代码,极大提升了可读性。async 关键字标记函数为异步函数,函数内部可以使用 await 等待 Promise 完成。await 会暂停当前函数的执行,直到 Promise 敲定,然后返回成功值或抛出失败异常。

async function executeTasks(): Promise<void> {
  try {
    let firstResult: string = await delayMessage('第一步完成', 500)
    console.log(firstResult)
    let secondResult: string = await delayMessage('第二步完成', 500)
    console.log(secondResult)
  } catch (error) {
    console.error('任务执行失败')
  }
}

executeTasks()

async 函数的返回值会自动包装为 Promise,因此调用方可以继续使用 await 或 then 处理其结果。async/await 配合 try/catch 让错误处理与同步代码保持一致,是目前 ArkTS 异步编程的首选写法。

在 ArkTS 中,部分系统 API 提供了 Promise 版本,例如网络请求可以使用 await 方式调用。对于只提供回调的 API,可以手动包装为 Promise 以统一代码风格。掌握回调、Promise 和 async/await 三种方式的转换关系,能够从容应对各种异步场景。

16. 错误处理与异常

健壮的应用必须妥善处理错误。ArkTS 提供了完整的异常处理机制,包括抛出异常、捕获异常和自定义错误类型。良好的错误处理策略可以让应用在遇到异常时优雅降级,而不是直接崩溃。

抛出异常使用 throw 关键字,可以抛出任意值,但最佳实践是抛出 Error 对象或其子类实例。Error 对象包含 message 属性描述错误信息,可以通过 new Error('描述') 创建。捕获异常使用 try/catch/finally 结构,try 块中放置可能出错的代码,catch 块处理错误,finally 块放置无论是否出错都必须执行的清理代码。

function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('除数不能为零')
  }
  return a / b
}

function safeDivide(a: number, b: number): string {
  try {
    let result: number = divide(a, b)
    return `结果:${result}`
  } catch (error) {
    return `计算出错:${(error as Error).message}`
  } finally {
    console.log('本次计算结束')
  }
}

console.log(safeDivide(10, 2))
console.log(safeDivide(10, 0))

自定义错误类型可以通过继承 Error 类实现,从而为不同错误场景提供更精确的类型信息。自定义错误可以携带额外的业务字段,例如错误码、上下文数据等,便于上层按类型分别处理。

class BusinessError extends Error {
  constructor(message: string, public code: number) {
    super(message)
    this.name = 'BusinessError'
  }
}

function validateAge(age: number): void {
  if (age < 0 || age > 150) {
    throw new BusinessError('年龄超出合法范围', 4001)
  }
}

try {
  validateAge(200)
} catch (error) {
  if (error instanceof BusinessError) {
    console.log(`业务错误 ${error.code}:${error.message}`)
  }
}

错误处理的一个常见误区是过度使用 try/catch 包裹大段代码,导致错误被笼统吞掉。推荐的做法是:在边界处集中捕获错误,例如异步任务入口、用户触发的操作入口;在内部函数中抛出具象的错误信息,交由上层统一处理。这样既能保证错误不丢失,又能让代码保持清爽。

此外,异步函数的错误通过 Promise 的 reject 传播,使用 await 时可以直接用 try/catch 捕获,这与同步错误处理在写法上保持一致。对于未捕获的 Promise 拒绝,应当设置统一的兜底处理,避免静默失败。ArkTS 鼓励开发者在关键异步路径上显式处理 reject,保证应用的可靠性。

17. ArkTS 与 TypeScript 的差异

虽然 ArkTS 以 TypeScript 为基础,但二者并不完全等同。了解 ArkTS 相对 TypeScript 做了哪些限制和调整,对于已经熟悉 TypeScript 的开发者尤为重要,也是从 TypeScript 平滑过渡到 ArkTS 的关键。

首先是动态特性的限制。TypeScript 允许通过索引签名任意访问对象属性,允许使用 obj[key] 形式的动态属性访问,也允许通过构造字符串动态拼出属性名。ArkTS 出于性能优化考虑,不支持这些动态属性访问方式,所有属性访问必须是静态可确定的。这一限制意味着开发者需要在编码阶段就把对象结构定义清楚,而不是依赖运行时动态行为。

其次是联合类型和类型收窄的要求更严格。TypeScript 中联合类型非常自由,可以访问联合成员的公共属性。ArkTS 对联合类型的支持较为保守,往往需要显式的类型判断或类型断言后,才能安全访问特定成员。这种严格性虽然增加了少量书写成本,但换来了更可靠的类型保证。

在泛型方面,ArkTS 与 TypeScript 的使用方式基本一致,但在某些复杂泛型场景下存在约束,例如对泛型函数嵌套调用、条件类型、映射类型等高级类型特性的支持有所限制。开发者应当在 ArkTS 中优先使用简单、明确的泛型,避免过度依赖 TypeScript 的高级类型体操。

在装饰器方面,ArkTS 的装饰器与 TypeScript 的实验性装饰器不完全相同,而是围绕 ArkUI 生态做了专门设计。ArkTS 的装饰器主要用于组件、状态和生命周期管理,使用方式和语义与框架绑定。传统 TypeScript 中通用装饰器的部分写法在 ArkTS 中并不适用,需要按照 ArkTS 的官方规范使用。

此外,ArkTS 对 any 的使用保持警惕,官方强烈建议避免使用 any,因为 any 会绕过类型检查,削弱 ArkTS 静态类型系统的价值。在 TypeScript 中,any 常被作为快速开发的临时手段,但在 ArkTS 工程中应当始终优先使用精确类型或 unknown 加类型收窄。

理解这些差异后,从 TypeScript 转向 ArkTS 的开发者应当注意:养成静态定义对象结构的习惯,避免动态拼属性;在联合类型场景中先收窄再使用;把装饰器当作 ArkUI 专属能力来学习,而不是套用 TypeScript 旧经验。把握这些要点,就能快速适应 ArkTS 的编程范式。

18. ArkUI 声明式开发基础

ArkUI 是 HarmonyOS 的声明式 UI 开发框架,而 ArkTS 是书写 ArkUI 代码的语言。理解 ArkUI 的基本范式,是把语言知识转化为界面开发能力的关键一步。本节介绍 ArkUI 的核心概念,包括组件、布局、样式和事件处理。

18.1 组件与 build 方法

在 ArkUI 中,界面由组件构成。每个自定义组件都是一个用 @Component 装饰的结构体,内部包含状态属性和 build 方法。build 方法描述该组件要渲染的界面内容,使用声明式语法嵌套组织系统组件和自定义组件。

系统组件是框架提供的原子组件,包括 Text、Button、Image、TextInput、List、Scroll、Column、Row 等。每个系统组件都有对应的构造参数和属性方法。构造参数在组件名后的括号内传入,属性方法通过点号链式调用设置,例如 .fontSize() 设置字号、.backgroundColor() 设置背景色、.onClick() 注册点击事件。

18.2 布局组件

布局是界面的骨架。Column 是垂直布局容器,子组件按从上到下的顺序排列;Row 是水平布局容器,子组件按从左到右的顺序排列;Stack 是叠加布局容器,子组件按先后顺序叠放;Grid 是网格布局容器,适合规则排列的卡片和图标。布局容器同样拥有属性方法,可以设置间距、对齐方式、内边距等。

下面是一个结合多种布局的示例:

@Entry
@Component
struct LayoutDemo {
  build() {
    Column({ space: 10 }) {
      Row({ space: 8 }) {
        Text('左')
        Text('中')
        Text('右')
      }
      Stack() {
        Text('底层内容')
        Text('顶层内容')
          .fontSize(20)
          .fontColor(Color.Red)
      }
      Grid() {
        ForEach([1, 2, 3, 4], (item: number) => {
          GridItem() {
            Text(`${item}`)
              .width(60)
              .height(60)
              .textAlign(TextAlign.Center)
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr 1fr')
    }
    .width('100%')
    .padding(16)
  }
}

18.3 列表与条件渲染

列表渲染使用 ForEach 组件,它接受数据源、子项生成函数和可选的键生成函数。ForEach 会根据数据源为每个元素创建对应的 UI 子项。条件渲染使用 if/else 语句,在 build 方法中根据状态决定渲染哪个分支。由于 build 方法会在状态变化时重新执行,条件渲染能够响应式地显示或隐藏界面元素。

下面演示列表渲染与条件渲染:

@Entry
@Component
struct ListDemo {
  @State tasks: string[] = ['学习 ArkTS', '练习 ArkUI', '完成项目']

  build() {
    Column() {
      if (this.tasks.length === 0) {
        Text('暂无任务')
          .fontSize(18)
      } else {
        List() {
          ForEach(this.tasks, (task: string) => {
            ListItem() {
              Text(task)
                .padding(12)
            }
          })
        }
      }
    }
  }
}

18.4 事件处理与样式

事件处理方法为组件注册交互回调,常用事件包括 onClick 点击事件、onChange 值变化事件、onAppear 组件出现事件等。事件回调通常是箭头函数,内部可以修改状态变量,从而触发界面刷新。样式通过属性方法设置,涵盖字体、颜色、尺寸、边距、边框、阴影、透明度等。

声明式开发的核心心智模型是“UI 是状态的函数”。开发者的职责从手动更新控件,转变为正确定义状态、正确描述状态与 UI 的映射关系。一旦建立了这个心智模型,界面开发会变得非常直观,状态越清晰,界面逻辑就越简单。

19. 页面路由与导航

一个完整的应用通常由多个页面组成,页面之间的跳转与返回是基础导航能力。HarmonyOS 提供了 router 模块用于页面路由管理,开发者可以在 ArkTS 中通过导入 router 模块实现页面跳转、参数传递和返回操作。

页面跳转使用 router.pushUrl 方法,传入目标页面路径。页面路径在资源目录的 main_pages.json 中注册,通常对应 pages 目录下的 .ets 文件。pushUrl 接受一个配置对象,url 字段指定目标页面,params 字段可以携带参数。

import router from '@ohos.router'

@Entry
@Component
struct HomePage {
  build() {
    Column({ space: 12 }) {
      Text('首页')
        .fontSize(24)
      Button('跳转到详情页')
        .onClick(() => {
          router.pushUrl({
            url: 'pages/DetailPage',
            params: {
              articleId: 123,
              title: 'ArkTS 教程'
            }
          })
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

目标页面通过 router.getParams 方法获取传递过来的参数。返回上一页使用 router.back 方法。页面栈遵循后进先出原则,pushUrl 将新页面压入栈顶,back 将栈顶页面弹出。

import router from '@ohos.router'

@Entry
@Component
struct DetailPage {
  private articleId: number = 0
  private title: string = ''

  aboutToAppear(): void {
    let params = router.getParams() as Record<string, Object>
    if (params !== null && params !== undefined) {
      this.articleId = params['articleId'] as number
      this.title = params['title'] as string
    }
  }

  build() {
    Column({ space: 12 }) {
      Text(this.title)
        .fontSize(24)
      Text(`文章 ID:${this.articleId}`)
      Button('返回')
        .onClick(() => {
          router.back()
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

路由传参时需要注意参数对象的类型处理,getParams 返回的是通用对象,通常需要类型断言转换为具体类型。此外,HarmonyOS 还提供了页面导航的新范式 Navigation 组件,支持更复杂的导航栈管理和跨设备导航,在较新版本中逐渐成为推荐的导航方案。对于初学者,先掌握 router 的基本用法足以应对大多数简单场景。

20. 数据持久化基础

应用运行过程中产生的数据,如果需要在应用重启后仍然保留,就必须进行持久化存储。HarmonyOS 提供了多种持久化方案,包括 Preferences 轻量级键值存储、关系型数据库、文件读写等。本节介绍 Preferences 的基本用法,它是存储简单配置和用户偏好最便捷的方式。

Preferences 以键值对形式存储数据,适合保存登录状态、主题选择、用户设置等轻量数据。使用 Preferences 需要先导入相关模块,通过 getPreferences 获取实例,然后使用 get 和 put 方法读写数据,最后调用 flush 方法将内存中的修改落盘。

import preferences from '@ohos.data.preferences'

class PreferenceHelper {
  private static pref: preferences.Preferences | null = null

  static async init(context: Context): Promise<void> {
    PreferenceHelper.pref = await preferences.getPreferences(context, 'app_settings')
  }

  static async saveTheme(theme: string): Promise<void> {
    if (PreferenceHelper.pref === null) {
      return
    }
    await PreferenceHelper.pref.put('theme', theme)
    await PreferenceHelper.pref.flush()
  }

  static async getTheme(): Promise<string> {
    if (PreferenceHelper.pref === null) {
      return 'light'
    }
    return await PreferenceHelper.pref.get('theme', 'light') as string
  }
}

上面的示例封装了一个 Preferences 工具类,提供初始化、保存主题和读取主题的方法。get 方法的第二参数是默认值,当键不存在时返回默认值。put 方法修改内存数据,flush 方法确保数据写入持久化存储。

对于结构化程度更高的数据,关系型数据库是更合适的选择。HarmonyOS 的关系型数据库基于 SQLite,支持 SQL 语句操作,适合存储用户记录、订单信息等结构化数据。文件读写则适用于图片、日志、配置文本等非结构化内容。选择持久化方案时,应根据数据规模、结构复杂度和访问模式综合判断。

21. 网络请求实践

网络通信是现代应用的常见需求。HarmonyOS 提供了 http 模块用于发起 HTTP 请求,开发者可以使用它调用后端接口获取和提交数据。掌握网络请求的基本写法,是开发真实业务应用的重要一步。

使用 http 模块的第一步是创建 http 请求对象,然后设置请求方法和参数,调用 request 方法发起请求,在回调中处理响应。现代 ArkTS 代码更推荐使用 Promise 和 async/await 风格,让网络代码与整体异步策略保持一致。

import http from '@ohos.net.http'

async function requestData(url: string): Promise<string> {
  let httpRequest = http.createHttp()
  try {
    let response = await httpRequest.request(
      url,
      {
        method: http.RequestMethod.GET,
        connectTimeout: 10000,
        readTimeout: 10000
      }
    )
    if (response.responseCode === 200) {
      return response.result as string
    }
    throw new Error(`请求失败,状态码:${response.responseCode}`)
  } finally {
    httpRequest.destroy()
  }
}

在上面的代码中,request 方法返回 Promise,因此可以配合 await 等待结果。响应对象包含 responseCode 状态码、result 响应体和 header 响应头。请求结束后应调用 destroy 方法释放资源,通常放在 finally 中确保执行。

实际工程中,网络请求往往需要统一封装,集中处理基础地址、请求头、超时、错误提示和响应解析等公共逻辑。通过封装网络工具类,业务代码只需调用简洁的业务方法即可完成数据获取,提升代码复用性和可维护性。

网络请求涉及权限配置,需要在 module.json5 中声明 ohos.permission.INTERNET 权限。此外,网络请求属于耗时操作,应该放在异步任务中执行,避免阻塞 UI 线程。结合前面学习的 async/await 和错误处理,可以写出健壮、清晰的网络请求代码。

22. 组件化开发与代码组织

随着页面复杂度增加,把所有 UI 都写在一个组件中会变得难以维护。组件化开发是将界面拆分为多个职责单一、可复用的自定义组件,通过组合构建完整页面。这是 ArkUI 开发中的重要工程实践。

自定义组件的定义方式与页面组件类似,使用 @Component 装饰 struct。与页面组件不同的是,自定义组件通常不是 @Entry 入口,而是作为子组件被其他组件引用。组件通过属性接收外部数据,通过事件回调向外部传递交互结果。良好定义的组件接口让组件可以被安全地复用。

下面演示一个可复用的卡片组件:

@Component
struct InfoCard {
  @Prop title: string
  @Prop description: string

  build() {
    Column({ space: 6 }) {
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
      Text(this.description)
        .fontSize(14)
        .fontColor(Color.Gray)
    }
    .width('100%')
    .padding(16)
    .borderRadius(12)
    .backgroundColor(Color.White)
  }
}

父组件可以在 build 方法中直接引用该组件,并传入属性值:

@Entry
@Component
struct ComponentDemoPage {
  build() {
    Column({ space: 12 }) {
      InfoCard({ title: '组件化', description: '将界面拆分为可复用的自定义组件' })
      InfoCard({ title: '状态管理', description: '用状态驱动 UI 自动更新' })
      InfoCard({ title: '模块化', description: '按职责划分文件与目录' })
    }
    .padding(16)
  }
}

组件化设计的关键在于确定合理的拆分粒度。拆分过细会导致组件过多、传参繁琐;拆分不足则组件职责混杂、难以复用。通常以“单一职责”和“复用频率”作为拆分依据:一个组件负责一个明确的界面职责,如果一个片段在多处出现,就应当抽取为独立组件。

代码组织与组件化相辅相成。每个自定义组件通常存放在独立文件中,相关组件按功能归入同一目录。公共组件放入 components 目录,业务页面放入 pages 目录。清晰的目录结构让项目导航和协作更加高效。

23. 性能优化要点

虽然 ArkTS 和 ArkUI 在底层做了大量优化,但应用性能仍然在很大程度上取决于开发者的代码质量。了解性能优化的关键要点,有助于开发出流畅、省电、响应迅速的应用。

状态管理是性能优化的重要领域。应尽量缩小状态的影响范围,将状态声明在使用它的最小组件中,避免顶层大状态导致大面积组件刷新。对于需要频繁更新的复杂列表,考虑使用 LazyForEach 实现懒加载,按需渲染可见项,显著降低长列表的内存和渲染开销。

数组和对象操作也应关注性能。在处理大量数据时,避免在循环中进行不必要的对象创建和高频函数调用。数组的多次链式操作在数据量不大时无伤大雅,但在海量数据场景下应考虑合并处理逻辑,减少中间数组的产生。

异步任务和资源释放同样影响性能。网络请求结束后要及时销毁请求对象,定时器不再使用时应立即清理,避免内存泄漏。对于需要重复执行的耗时任务,评估是否需要移到后台任务或使用 Web Worker,避免阻塞主线程导致界面卡顿。

渲染层面,减少不必要的重绘和布局计算。为组件设置稳定的键值有助于 ForEach 复用已有节点,避免全量重建。合理使用缓存和按需渲染策略,对图片加载、列表滚动等高频场景进行有针对性的优化。

性能优化应当建立在测量之上,而不是盲目猜测。HarmonyOS 提供了性能分析工具,可以观察 CPU、内存、渲染帧率等指标。先定位真正的性能瓶颈,再有针对性地优化,才能用更小的代价获得更好的效果。过早的过度优化反而会牺牲代码可读性,得不偿失。

24. 常见问题与调试技巧

在学习和开发 ArkTS 应用的过程中,遇到问题是常态。掌握常见问题的排查思路和调试技巧,可以显著提升开发效率。本节整理一些高频问题及其解决思路。

编译错误是最常见的问题类型。类型不匹配、缺少导入、装饰器使用不当都会导致编译失败。遇到编译错误时,应当仔细阅读编译器输出的错误信息,通常它会指明出错文件和行号,并给出简短的错误原因。多数情况下,错误信息本身已经包含了足够的排查线索。

运行时崩溃往往与空值访问、数组越界、类型断言错误有关。启用严格空检查可以在编译期拦截大部分空值问题。对于运行时的偶发错误,可以通过 try/catch 捕获异常并记录日志,结合日志定位出错位置。

UI 不刷新是声明式开发中常见的困惑。如果修改了变量但界面没有变化,大概率是因为变量没有被状态管理装饰器修饰,或者使用了无法被观察到的原地修改方式。检查变量是否用 @State、@Link 等装饰器声明,并确认修改方式能够触发观察机制。

调试工具方面,DevEco Studio 提供了 Log 日志输出、断点调试、变量观察等功能。console.log 是最快捷的日志手段,适合快速确认程序执行路径和变量值。断点调试适合深入分析复杂逻辑,可以单步执行、查看调用栈和变量状态。

遇到难以解决的问题时,可以按照“最小化复现、查阅文档、搜索社区、提问求助”的顺序推进。官方文档是最权威的参考资料,社区中也有很多经验分享。提问时提供完整的报错信息、关键代码片段和已尝试的解决方法,往往能更快得到有效帮助。

25. 完整实战项目:待办事项应用

学习语言的最终目的是完成实际项目。本节通过一个完整的待办事项应用,把前面学习的语言基础、状态管理、组件化和持久化知识串联起来。这个项目虽然规模不大,但涵盖了 ArkTS 开发的核心流程。

待办事项应用的需求如下:用户可以添加待办任务,任务列表展示所有待办项,点击任务可以标记完成,支持删除任务,应用重启后任务数据仍然保留。下面逐步实现这个应用。

首先定义任务数据模型,并建立 Preferences 持久化工具。数据模型包含任务内容和完成状态两个字段:

class TodoItem {
  constructor(public content: string, public done: boolean = false) {
  }
}

接着实现主页面,使用 @State 管理任务列表,@Link 或回调处理任务状态变化。完成状态的切换需要更新列表数据,使用整体替换数组的方式确保刷新:

@Entry
@Component
struct TodoPage {
  @State todos: TodoItem[] = []
  @State inputText: string = ''

  addTodo(): void {
    if (this.inputText.trim() === '') {
      return
    }
    this.todos.push(new TodoItem(this.inputText.trim()))
    this.todos = [...this.todos]
    this.inputText = ''
  }

  toggleTodo(index: number): void {
    let updated = [...this.todos]
    updated[index] = new TodoItem(updated[index].content, !updated[index].done)
    this.todos = updated
  }

  deleteTodo(index: number): void {
    let updated = [...this.todos]
    updated.splice(index, 1)
    this.todos = updated
  }

  build() {
    Column({ space: 12 }) {
      Text('待办事项')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)

      Row({ space: 8 }) {
        TextInput({ placeholder: '输入新任务', text: this.inputText })
          .layoutWeight(1)
          .onChange((value: string) => {
            this.inputText = value
          })
        Button('添加')
          .onClick(() => {
            this.addTodo()
          })
      }
      .width('100%')

      if (this.todos.length === 0) {
        Text('暂无任务,点击上方添加')
          .fontSize(16)
          .fontColor(Color.Gray)
          .margin({ top: 40 })
      } else {
        List() {
          ForEach(this.todos, (item: TodoItem, index: number) => {
            ListItem() {
              Row() {
                Text(item.content)
                  .fontSize(16)
                  .fontColor(item.done ? Color.Gray : Color.Black)
                  .decoration({ type: TextDecorationType.LineThrough, color: Color.Gray })
                  .layoutWeight(1)
                Button(item.done ? '撤销' : '完成')
                  .onClick(() => {
                    this.toggleTodo(index)
                  })
                Button('删除')
                  .onClick(() => {
                    this.deleteTodo(index)
                  })
              }
              .width('100%')
              .padding(12)
            }
          }, (item: TodoItem, index: number) => `${item.content}-${index}`)
        }
      }
    }
    .padding(16)
    .width('100%')
    .height('100%')
  }
}

在上面的应用中,addTodo 创建新任务并整体替换数组触发刷新;toggleTodo 切换任务完成状态,同时替换任务对象和数组;deleteTodo 从数组中删除指定项。界面层通过 ForEach 渲染任务列表,条件渲染处理空列表提示。

为了持久化,可以在 aboutToAppear 中从 Preferences 加载已保存的任务,在任务列表变化时保存到 Preferences。对于本示例,重点展示语言基础上的状态与 UI 联动,持久化部分可以结合前面章节的工具类扩展实现。通过这个完整项目,读者可以体会到 ArkTS 从语法到界面的完整开发链路。

26. 总结与学习路线建议

到这里,我们从 ArkTS 的基本语法出发,逐步走过了类型系统、流程控制、函数、面向对象、接口、泛型、枚举、模块、装饰器、状态管理、异步编程、错误处理,一直到 ArkUI 声明式开发、路由导航、数据持久化、网络请求和组件化工程实践,最后通过一个完整项目完成了一次从语言到应用的贯通。

回顾整个学习路径,ArkTS 的学习可以划分为四个层次。第一层是语法与类型基础,这是后续一切内容的基石,需要反复练习直到熟练。第二层是语言进阶特性,包括泛型、装饰器、异步编程等,它们决定了代码的质量边界。第三层是框架应用能力,包括 ArkUI 组件、状态管理和系统模块使用,这是把语言能力转化为实际产品的桥梁。第四层是工程实践素养,包括组件化、模块化、性能优化和调试能力,它们决定了项目能否长期健康演进。

对于初学者,建议按照“先模仿、再理解、后创造”的顺序推进。先跟着示例代码敲一遍,让代码跑起来;然后逐行理解每一处语法和设计意图,弄懂背后的原理;最后尝试修改和扩展项目,加入自己的想法。动手实践远比被动阅读更能加深理解。

学习资源方面,华为官方文档是最系统、最权威的参考资料,涵盖了语言规范、API 说明和最佳实践。HarmonyOS 开发者社区提供了大量实战案例和经验分享。开源项目是观察真实工程代码的窗口,阅读优秀的开源项目可以快速提升工程意识和代码品味。

ArkTS 作为 HarmonyOS 生态的核心语言,它的价值正在随着鸿蒙设备的普及而不断增长。掌握 ArkTS 语言基础,不仅意味着掌握了一门编程语言,更意味着打开了进入万物互联时代的开发之门。希望这篇详解能够成为你 ArkTS 学习之路上的可靠起点,祝学习顺利,代码无 Bug。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐