什么是ArkTS

ArkTS是Harmony OS优选的主力开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,保持了TS的基本风格,同时通过规范定义强化开发期静态检查和分析,提升程序执行稳定性和性能。

从API version 10开始,ArkTS进一步通过规范强化静态检查和分析:

  • 强制使用静态类型: 静态类型是ArkTS最重要的特性之一。如果使用静态类型,那么程序中变量的类型就是确定的。同时,由于所有类型在程序实际运行前都是已知的,编译器可以验证代码的正确性,从而减少运行时的类型检查,有助于性能提升。
  • 禁止在运行时改变对象布局: 为实现最大性能,ArkTS要求在 程序执行期间不能更改对象布局
  • 限制运算符语义: 为获得更好的性能并鼓励开发者编写更清晰的代码,ArkTS限制了一些运算符的语义。比如,一元加法运算符只能作用于数字,不能用于其他类型的变量。
  • 不支持Structural typing: 对Structural typing的支持需要在语言、编译器和运行时进行大量的考虑和仔细的实现,当前ArkTS不支持该特性。根据实际场景的需求和反馈,我们后续会重新考虑。

ArkTS 在 ArkUI 中扩展的能力

  • 基本语法: ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。
  • 状态管理: ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。
  • 渲染控制: ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。

Typescript

TypeScript 由微软开发的自由和开源的编程语言。 是 JavaScript 的一个超集,支持 ECMAScript 6 标准

设计目标

TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。

语言特性

TypeScript 是一种给 JavaScript 添加特性的语言扩展。增加的功能包括:

  • 类型批注和编译时类型检查
  • 类型推断
  • 类型擦除
  • 接口
  • 枚举
  • Mixin
  • 泛型编程
  • 名字空间
  • 元组
  • Await

以及从ECMA 2015 反向移植而来的

  • 模型
  • lambda 函数的箭头语法
  • 可选参数以及默认参数

Javascript 与 Typescript 的区别

TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。

TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。

TypeScript 与 ArkTS 的区别

ArkTS在保持TypeScript(简称TS)基本语法风格的基础上,进一步通过规范强化静态检查和分析,使得在程序开发期能检测更多错误,提升程序稳定性,并实现更好的运行性能。

程序稳定性

显式初始化类的属性

动态类型语言,例如JavaScript(简称JS),可以使得开发者非常快速地编写代码,但是同时,它也使得程序容易在运行时产生非预期的错误。

// ----------------------------------------非严格模式下的TS代码---------------------------------------------------------
class Person {
  name: string // undefined
  
  setName(n: string): void {
    this.name = n
  }
  
  getName(): string {
  // 开发者使用"string"作为返回类型,这隐藏了name可能为"undefined"的事实。
  // 更合适的做法是将返回类型标注为"string | undefined",以告诉开发者这个API所有可能的返回值的类型。
    return this.name
  }
}

let buddy = new Person()
// 假设代码中没有对name的赋值,例如没有调用"buddy.setName('John')"
buddy.getName().length; // 运行时异常:name is undefined

// ----------------------------------------ArkTS要求属性显式初始化---------------------------------------------------------
class Person {
  name: string = ''
  
  setName(n: string): void {
    this.name = n
  }
  
  // 类型为"string",不可能为"null"或者"undefined"
  getName(): string {
    return this.name
  }
}

let buddy = new Person()
// 假设代码中没有对name的赋值,例如没有调用"buddy.setName('John')"
buddy.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 buddy = new Person()
// 假设代码中没有对name的赋值,例如没有调用"buddy.setName('John')"

// 编译时错误:编译器认为下一行代码有可能访问"undefined"的属性,报错
buddy.getName().length;  // 编译失败

buddy.getName()?.length; // 编译成功,没有运行时错误

程序性能

为了保证程序的正确性,动态类型语言不得不在运行时检查对象的类型。
例如:
JS不允许访问undefined的属性。但是检查一个值是否为undefined的唯一的办法是在运行时进行一次类型检查。所有的JS引擎都会做如下的事:
如果一个值不是undefined,那么可以访问其属性,否则抛出异常。现代JS引擎可以很好地对这类操作进行优化,但是总有一些运行时的检查是无法被消除的,这就使得程序变慢了。
由于TS总是先被编译成JS,所以在TS代码中,也会面临相同的问题。
ArkTS解决了这个问题。由于使能了静态类型检查,ArkTS代码将会被编译成方舟字节码文件,而不是JS代码。因此,ArkTS运行速度更快,更容易被进一步地优化。

Null-Safety

function notify(who: string, what: string) {
  console.log(`Dear ${who}, a message for you: ${what}`)
}

notify('Jack', 'You look great today')

// 大多数情况下notify 会接受两个string类型的变量作为输入,产生一个新的字符串。但是如果将一些特殊值作为输入,例如notify(null, undefined),会怎么样呢?
// 程序仍然会正常运行,输出预期值: Dear null,a message for you :undefined。
// 一切看起来正常,但是需要注意的是,为了保证该场景下程序的正确性,引擎总是在运行时进行类型检查,执行类似一下的伪代码。
function __internal_tostring(s: any): string {
  if (typeof s === 'string')
    return s
  if (s === undefined)
    return 'undefined'
  if (s === null)
    return 'null'
  // ...
}
// 现在想象一下,如果函数notify是某些复杂的负载场景中的一部分,而不仅仅是打印日志,那么在运行时执行像__internal_tostring的类型检查将会是一个性能问题。
// 如果可以确保在运行时,只有string类型的值(不会是其他值,例如null或者undefined) 可以被传入notify呢?在这种情况下,因为可以确保没有其他边界情况,像__internal_tostring的检查就是多余的了。对于这个场景这样的机制叫做 null-safety 也就是说,保证null不是一个合法的string类型变量的值。如果ArkTS有了这个特性,类型不符合的代码将无法编译。
function notify(who: string, what: string) {
  console.log(`Dear ${who}, a message for you: ${what}`)
}

notify('Jack', 'You look great today')
notify(null, undefined) // 编译时错误

TS通过打开编译选项strictNullChecks来实现此特性。但是TS是被编译成JS的,而JS没有这个特性,因此严格null检查只在编译时起作用。从程序稳定性和性能角度考虑,ArkTS将“null-safety”视为一个重要的特性。
这就是为什么ArkTS强制进行严格null检查,在ArkTS中,上面的代码总是编译报错。作为交换,这样的代码可以给ArkTS引擎带来更多的信息和有关值的类型保证,这有助于更好地优化性能。

.ets代码兼容性

在API version 10之前,ArkTS(.ets文件)完全采用了标准TS的语法。从API version 10 Release起,ArkTS的语法规则基于上述设计考虑进行了明确定义,同时,SDK增加了在编译流程中对.ets文件的ArkTS语法检查,通过编译告警或编译失败提示开发者适配新的ArkTS语法。

根据工程的compatibleSdkVersion,具体策略如下:

  • compatibleSdkVersion >= 10 为标准模式。在该模式下,对.ets文件,违反ArkTS语法规则的代码会导致工程编译失败,需要完全适配ArkTS语法后方可编译成功。

  • compatibleSdkVersion < 10 为兼容模式。在该模式下,对.ets文件,以warning形式提示违反ArkTS语法规则的所有代码。尽管违反ArkTS语法规则的工程在兼容模式下仍可编译成功,但是需要完全适配ArkTS语法后方可在标准模式下编译成功。

方舟运行时兼容TS/JS

在API version 11上,HarmonyOS SDK中的TypeScript版本为4.9.5,target字段为es2017。在应用中,开发者可以使用ECMA2017+的语法进行TS/JS开发。

  • 应用环境限制
  1. 强制使用严格模式
  2. 禁止使用eval()
  3. 禁止使用with(){}
  4. 禁止以字符串为代码创建函数
  • 与标准TS/JS的差异
    标准TS/JS中,JSON的数字格式,小数点后必须跟着数字,如2.e3这类科学计数法不被允许,报出SyntaxError。在方舟运行时中,允许使用这类科学计数法。

ArkTS介绍

ArkTS是一种为构建高性能应用而设计的编程语言。ArkTS在继承TypeScript语法的基础上进行了优化,以提供更高的性能和开发效率。

ArkTS的一大特性是它专注于低运行时开销。ArkTS对TypeScript的动态类型特性施加了更严格的限制,以减少运行时开销,提高执行效率。通过取消动态类型特性,ArkTS代码能更有效地被运行前编译和优化,从而实现更快的应用启动和更低的功耗。

与JavaScript的互通性是ArkTS语言设计中的关键考虑因素。鉴于许多移动应用开发者希望重用其TypeScript和JavaScript代码和库,ArkTS提供了与JavaScript的无缝互通,使开发者可以很容易地将JavaScript代码集成到他们的应用中。这意味着开发者可以利用现有的代码和库进行ArkTS开发。

基本知识

声明

ArkTS通过声名引入变量、常量、函数和类型。

  • 变量声明:
// 以关键字 let 开头的声明引入变量,该变量在程序执行期间可以具有不同的值。
let hi : string = 'hello';
hi = 'hello world';
  • 常量声明:
// 已关键字const开头的声明引入只读常量,该常量只能被赋值一次。

const hello : string = 'hello';

//对常量的重新赋值,会导致编译时错误。
  • 自动类型推断:
    由于ArkTS是一种静态类型语言,所有数据的类型都必须在编译时确定。
    但是如果一个变量或者常量的声明包含了初始值,那么开发者就不需要显示指定其类型。
let hi1 : string = 'hello';
let hi2 = 'hello, world';
// 两条声明变量都是有效的。

类型

  • Number 类型

ArkTS提供number和Number类型,任何整数和浮点数都可以被赋给此类型的变量。
数字字面量包括整数字面量和十进制浮点数字面量。

  • Boolean 类型

boolean类型由true和false两个逻辑值组成,通常在条件语句中使用。

let isDone: boolean = false;

// ...

if (isDone) {
  console.log ('Done!');
}
  • String 类型

string代表字符序列;可以使用转义字符来表示字符。

// 字符串字面量由单引号(')或双引号(")之间括起来的零个或多个字符组成。字符串字面量还有一特殊形式,是用反向单引号(`)括起来的模板字面量。

let s1 = 'Hello, world!\n';
let s2 = 'this is a string';
let a = 'Success';
let s3 = `The result is ${a}`;
  • Viod 类型

void类型用于指定函数没有返回值。

// 此类型只有一个值,同样是void。由于void是引用类型,因此它可以用于泛型类型参数。
class Class<T> {
  //...
}
let instance: Class <void>
  • Object 类型

Object类型是所有引用类型的基类型。任何值,包括基本类型的值(它们会被自动装箱),都可以直接被赋给Object类型的变量。

  • Array 类型

array,即数组,是由可赋值给数组声明中指定的元素类型的数据组成的对象。

数组可由数组复合字面量(即用方括号括起来的零个或多个表达式的列表,其中每个表达式为数组中的一个元素)来赋值。
let names: string[] = ['Alice', 'Bob', 'Carol'];
  • Enum 类型

enum类型,又称枚举类型,是预先定义的一组命名值的值类型,其中命名值又称为枚举常量。

// 使用枚举常量时必须以枚举类型名称为前缀
enum ColorSet { Red, Green, Blue }
let c: ColorSet = ColorSet.Red;

// 常量表达式可以用于显式设置枚举常量的值
enum ColorSet { White = 0xFF, Grey = 0x7F, Black = 0x00 }
let c: ColorSet = ColorSet.Black;
  • Union 类型

union类型,即联合类型,是由多个类型组合成的引用类型。联合类型包含了变量可能的所有类型。

class Cat {
  name: string = 'cat';
  // ...
}
class Dog {
  name: string = 'dog';
  // ...
}
class Frog {
  name: string = '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;

function foo(animal: Animal) {
  if (animal instanceof Frog) {
    animal.leap();  // animal在这里是Frog类型
  }
  animal.sleep(); // Animal具有sleep方法
}
  • Aliases 类型(类型别名)

Aliases类型为匿名类型(数组、函数、对象字面量或联合类型)提供名称,或为已有类型提供替代名称。

type Matrix = number[][];
type Handler = (s: string, no: number) => string;
type Predicate <T> = (x: T) => boolean;
type NullableObject = Object | null;

运算符

  • 赋值运算符

赋值运算符=,使用方式如x=y。

复合赋值运算符将赋值与运算符组合在一起,其中x op = y等于x = x op y。

复合赋值运算符列举如下:+=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、|=、^=。

  • 比较运算符
运算符 说明
=== 如果两个操作数严格相等(对于不同类型的操作数认为是不相等的),则返回true。
!== 如果两个操作数严格不相等(对于不同类型的操作数认为是不相等的),则返回true。
== 如果两个操作数相等,则返回true。
!= 如果两个操作数不相等,则返回true。
> 如果左操作数大于右操作数,则返回true。
>= 如果左操作数大于或等于右操作数,则返回true。
< 如果左操作数小于右操作数,则返回true。
<= 如果左操作数小于或等于右操作数,则返回true。
  • 算术运算符

一元运算符为-、+、–、++。
二元运算符:

运算符 说明
+ 加法
- 减法
* 乘法
/ 除法
% 除法后余数
  • 位运算符
运算符 说明
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。
  • 逻辑运算符
运算符 说明
a && b 逻辑与
a
! a 逻辑非

语句

  • if 语句

if语句用于需要根据逻辑条件执行不同语句的场景。当逻辑条件为真时,执行对应的一组语句,否则执行另一组语句(如果有的话)。
else部分也可能包含if语句。

if (condition1) {
  // 语句1
} else if (condition2) {
  // 语句2
} else {
  // else语句
}

// 条件表达式可以是任何类型。但是对于boolean以外的类型,会进行隐式类型转换:
let s1 = 'Hello';
if (s1) {
  console.log(s1); // 打印“Hello”
}

let s2 = 'World';
if (s2.length != 0) {
  console.log(s2); // 打印“World”
}
  • Swich 语句

使用switch语句来执行与switch表达式值匹配的代码块。

switch (expression) {
  case label1: // 如果label1匹配,则执行
    // ...
    // 语句1
    // ...
    break; // 可省略
  case label2:
  case label3: // 如果label2或label3匹配,则执行
    // ...
    // 语句23
    // ...
    break; // 可省略
  default:
    // 默认语句
}

// 如果switch表达式的值等于某个label的值,则执行相应的语句。
// 如果没有任何一个label值与表达式值相匹配,并且switch具有default子句,那么程序会执行default子句对应的代码块。
// break语句(可选的)允许跳出switch语句并继续执行switch语句之后的语句。
// 如果没有break语句,则执行switch中的下一个label对应的代码块。
  • 条件表达式

条件表达式由第一个表达式的布尔值来决定返回其它两个表达式中的哪一个。

condition ? expression1 : expression2

// 如果condition的为真值(转换后为true的值),则使用expression1作为该表达式的结果;否则,使用expression2。

let isValid = Math.random() > 0.5 ? true : false;
let message = isValid ? 'Valid' : 'Failed';
  • For 语句

for语句会被重复执行,直到循环退出语句值为false。

for ([init]; [condition]; [update]) {
  statements
}
// 语句执行流程
// 1. 执行init表达式(如有)。此表达式通常初始化一个或多个循环计数器。
// 2. 计算condition。如果它为真值(转换后为true的值),则执行循环主体的语句。如果它为假值(转换后为false的值),则for循环终止。
// 3. 执行循环主体的语句。
// 4. 如果有update表达式,则执行该表达式。
// 5. 回到步骤2。

let sum = 0;
for (let i = 0; i < 10; i += 2) {
  sum += i;
}
  • For-of 语句

使用for-of语句可遍历数组或字符串。

for (forVar of expression) {
  statements
}

for (let ch of 'a string object') {
  /* process ch */
}
  • While 语句

只要condition为真值(转换后为true的值),while语句就会执行statements语句。

while (condition) {
  statements
}

let n = 0;
let x = 0;
while (n < 3) {
  n++;
  x += n;
}
  • Do-while语句

如果condition的值为真值(转换后为true的值),那么statements语句会重复执行。

do {
  statements
} while (condition)

let i = 0;
do {
  i += 1;
} while (i < 10)
  • Break 语句

使用break语句可以终止循环语句或switch。

let x = 0;
while (true) {
  x++;
  if (x > 5) {
    break;
  }
}
// 如果break语句后带有标识符,则将控制流转移到该标识符所包含的语句块之外。

let x = 1;
label: while (true) {
  switch (x) {
    case 1:
      // statements
      break label; // 中断while语句
  }
}
  • Continue 语句

continue语句会停止当前循环迭代的执行,并将控制传递给下一个迭代。

let sum = 0;
for (let x = 0; x < 100; x++) {
  if (x % 2 == 0) {
    continue;
  }
  sum += x;
}
  • Throw 和 Try 语句
// throw语句用于抛出异常或错误:
throw new Error('this error')
// try语句用于捕获和处理异常或错误:
try {
  // 可能发生异常的语句块
} catch (e) {
  // 异常处理
}

class ZeroDivisor extends Error {}

function divide (a: number, b: number): number{
  if (b == 0) throw new ZeroDivisor();
  return a / b;
}

function process (a: number, b: number) {
  try {
    let res = divide(a, b);
    console.log('result: ' + res);
  } catch (x) {
    console.log('some error');
  }
}

支持finally语句:

function processData(s: string) {
  let error: Error | null = null;

  try {
    console.log('Data processed: ' + s);
    // ...
    // 可能发生异常的语句
    // ...
  } catch (e) {
    error = e as Error;
    // ...
    // 异常处理
    // ...
  } finally {
    if (error != null) {
      console.log(`Error caught: input='${s}', message='${error.message}'`);
    }
  }
}

函数

函数声明

函数声明引入一个函数,包含其名称、参数列表、返回类型和函数体。

function add(x: string, y: string): string {
  let z: string = `${x} ${y}`;
  return z;
}
// 在函数声明中,必须为每个参数标记类型。如果参数为可选参数,那么允许在调用函数时省略该参数。函数的最后一个参数可以是rest参数。

可选参数

// 可选参数的格式可为name?: Type。
function hello(name?: string) {
  if (name == undefined) {
    console.log('Hello!');
  } else {
    console.log(`Hello, ${name}!`);
  }
}
// 可选参数的另一种形式为设置的参数默认值。如果在函数调用中这个参数被省略了,则会使用此参数的默认值作为实参。
function multiply(n: number, coeff: number = 2): number {
  return n * coeff;
}
multiply(2);  // 返回2*2
multiply(2, 3); // 返回2*3

Rest参数

函数的最后一个参数可以是rest参数。使用rest参数时,允许函数或方法接受任意数量的实参。

function sum(...numbers: number[]): number {
  let res = 0;
  for (let n of numbers)
    res += n;
  return res;
}

sum(); // 返回0
sum(1, 2, 3); // 返回6

返回类型

如果可以从函数体内推断出函数返回类型,则可在函数声明中省略标注返回类型。

// 显式指定返回类型
function foo(): string { return 'foo'; }

// 推断返回类型为string
function goo() { return 'goo'; }

// 不需要返回值的函数的返回类型可以显式指定为void或省略标注。这类函数不需要返回语句。
function hi1() { console.log('hi'); }
function hi2(): void { console.log('hi'); }

函数的作用域

函数中定义的变量和其他实例仅可以在函数内部访问,不能从外部访问

如果函数中定义的变量与外部作用域中已有实例同名,则函数内的局部变量定义将覆盖外部定义。

函数类型

// 函数类型通常用于定义回调:

type trigFunc = (x: number) => number // 这是一个函数类型

function do_action(f: trigFunc) {
   f(3.141592653589); // 调用函数
}

do_action(Math.sin); // 将函数作为参数传入

箭头函数(又名Lambda函数)

// 函数可以定义为箭头函数
let sum = (x: number, y: number): number => {
  return x + y;
}

// 箭头函数的返回类型可以省略;省略时,返回类型通过函数体推断。
//表达式可以指定为箭头函数,使表达更简短,因此以下两种表达方式是等价的:

let sum1 = (x: number, y: number) => { return x + y; }
let sum2 = (x: number, y: number) => x + y

闭包

闭包是由函数及声明该函数的环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。

// f函数返回了一个闭包,它捕获了count变量,每次调用z,count的值会被保留并递增。

function f(): () => number {
  let count = 0;
  let g = (): number => { count++; return count; };
  return g;
}

let z = f();
z(); // 返回:1
z(); // 返回:2

函数重载

我们可以通过编写重载,指定函数的不同调用方式。具体方法为,为同一个函数写入多个同名但签名不同的函数头,函数实现紧随其后。

function foo(x: number): void;            /* 第一个函数定义 */
function foo(x: string): void;            /* 第二个函数定义 */
function foo(x: number | string): void {  /* 函数实现 */
}

foo(123);     //  OK,使用第一个定义
foo('aa'); // OK,使用第二个定义

类声明引入一个新类型,并定义其字段、方法和构造函数。

// 定义了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;
  }
}

// 定义后,可以使用关键字 new 创建实例:
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};

字段

字段是直接在类中声明的某种类型的变量。

类可以具有实例字段或者静态字段。

实例字段

实例字段存在于类的每个实例上。每个实例都有自己的实例字段集合。

// 要访问实例字段,需要使用类的实例。

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();
静态字段

使用关键字static将字段声明为静态。静态字段属于类本身,类的所有实例共享一个静态字段。

// 要访问静态字段,需要使用类名:

class Person {
  static numberOfPersons = 0;
  constructor() {
     // ...
     Person.numberOfPersons++;
     // ...
  }
}

Person.numberOfPersons;
字段初始化

为了减少运行时的错误和获得更好的执行性能,

ArkTS要求所有字段在声明时或者构造函数中显式初始化。这和标准TS中的strictPropertyInitialization模式一样。

// 在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

// ArkTS中,应该这样写代码
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值会抛出错误

在类中可以定义getter或者setter。

方法

方法属于类。类可以定义实例方法或者静态方法。静态方法属于类本身,只能访问静态字段。而实例方法既可以访问静态字段,也可以访问实例字段,包括类的私有字段。

实例方法
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;
  }
}
// calculateArea方法通过将高度乘以宽度来计算矩形的面积。
// 必须通过类的实例调用实例方法:

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());
继承

一个类可以继承另一个类(称为基类),并使用以下语法实现多个接口:

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';
  }
}
// -------------------------------------------------------示例----------------------------------------------------------------
// 定义接口
interface DateInterface {
  now(): string;
}

// 提供默认实现的类
class DefaultDate implements DateInterface {
  now(): string {
    return new Date().toISOString();
  }
}

// 继承默认实现的类
class CustomDate extends DefaultDate {
  // 不需要重新实现 now 方法,因为 DefaultDate 已经提供了默认实现
}

// 测试
const customDate = new CustomDate();
console.log(customDate.now()); // 输出当前时间的 ISO 字符串
父类访问

关键字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,使用第二个签名

// 如果两个重载签名的名称和参数列表均相同,则为错误。

构造函数

类声明可以包含用于初始化对象状态的构造函数

// 构造函数定义如下:
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,使用第二个签名

// 如果两个重载签名的名称和参数列表均相同,则为错误。

可见性修饰符

类的方法和属性都可以使用可见性修饰符。

可见性修饰符包括:private、protected和public。默认可见性为public。

Public(公有)

public修饰的类成员(字段、方法、构造函数)在程序的任何可访问该类的地方都是可见的。

Private(私有)

private修饰的成员不能在声明该成员的类之外访问

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'不可见

Protected(受保护)

protected修饰符的作用与private修饰符非常相似,不同点是protected修饰的成员允许在派生类中访问

class Base {
  protected x: string = '';
  private y: string = '';
}
class Derived extends Base {
  foo() {
    this.x = 'a'; // OK,访问受保护成员
    this.y = 'b'; // 编译时错误,'y'不可见,因为它是私有的
  }
}

对象字面量

对象字面量是一个表达式,可用于创建类实例并提供一些初始值。它在某些情况下更方便,可以用来代替new表达式。

// 对象字面量的表示方式是:封闭在花括号对({})中的'属性名:值'的列表。

class C {
  n: number = 0;
  s: string = '';
}

let c: C = {n: 42, s: 'foo'};

// ArkTS是静态类型语言,如上述示例所示,对象字面量只能在可以推导出该字面量类型的上下文中使用。其他正确的例子
class C {
  n: number = 0;
  s: string = '';
}

function foo(c: C) {}

// let 关声明一个变量c ,C 指定了变量 c 的类型为 C,这意味着 c 只能存储符合 C 类型的对象。
let c: C

// 变量赋值 --- c 的类型被声明为 C,因此这个对象必须包含 C 类中定义的所有属性,并且这些属性的类型必须匹配
c = {n: 42, s: 'foo'};

// 函数调用 --- 调用 foo 函数时,传入了一个对象 {n: 42, s: 'foo'},这个对象符合 C 类型的要求
foo({n: 42, s: 'foo'});

// 返回值 --- 函数体中返回了一个对象 {n: 42, s: 'foo'},这个对象符合 C 类型的要求
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'}];
Record类型的对象字面量

泛型Record<K, V>用于将类型(键类型)的属性映射到另一个类型(值类型)。

// 常用对象字面量来初始化该类型的值:

let map: Record<string, number> = {
  'John': 25,
  'Mary': 21,
}

map['John']; // 25

// 类型K可以是字符串类型或数值类型,而V可以是任何类型。

interface PersonInfo {
  age: number;
  salary: number;
}
let map: Record<string, PersonInfo> = {
  'John': { age: 25, salary: 10},
  'Mary': { age: 21, salary: 20}
}

泛型

什么是泛型

泛型允许你在定义函数、接口和类时,使用占位符(类型参数)来表示将来会被具体类型替换的类型。这些类型参数在实际使用时会被具体的类型所替换。

泛型函数

定义泛型函数

function identity<T>(arg: T): T {
  return arg;
}

使用泛型函数

let output = identity<string>('myString'); // 指定类型参数为 string
console.log(output); // 输出: myString

let output2 = identity(10); // 类型推断,自动推断类型参数为 number
console.log(output2); // 输出: 10

泛型函数详解:点击前往

泛型接口

定义泛型接口

interface Box<T> {
  value: T;
}

let box: Box<number> = { value: 10 };
console.log(box.value); // 输出: 10

let box2: Box<string> = { value: 'Hello' };
console.log(box2.value); // 输出: Hello

泛型类

定义泛型类

class Container<T> {
  private data: T[];

  constructor() {
    this.data = [];
  }

  add(item: T): void {
    this.data.push(item);
  }

  get(index: number): T {
    return this.data[index];
  }

  getAll(): T[] {
    return this.data;
  }
}

let numberContainer = new Container<number>();
numberContainer.add(10);
numberContainer.add(20);
console.log(numberContainer.get(0)); // 输出: 10
console.log(numberContainer.getAll()); // 输出: [10, 20]

let stringContainer = new Container<string>();
stringContainer.add('Hello');
stringContainer.add('World');
console.log(stringContainer.get(0)); // 输出: Hello
console.log(stringContainer.getAll()); // 输出: ['Hello', 'World']

泛型约束

定义泛型约束

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 现在我们知道 arg 有一个 length 属性
  return arg;
}

loggingIdentity({ length: 10, value: 'Hello' }); // 合法
// loggingIdentity(10); // 错误,10 没有 length 属性

泛型工具类

Partial

// 将类型 T 的所有属性变为可选。
interface User {
  name: string;
  age: number;
}

type PartialUser = Partial<User>; // { name?: string; age?: number; }

Required

// 将类型 T 的所有属性变为必选。
interface User {
  name?: string;
  age?: number;
}

type RequiredUser = Required<User>; // { name: string; age: number; }

Readonly

// 将类型 T 的所有属性变为只读。
interface User {
  name: string;
  age: number;
}

type ReadOnlyUser = Readonly<User>; // { readonly name: string; readonly age: number; }

Record<K, T>

// 创建一个对象类型,其键的类型为 K,值的类型为 T。
type PersonInfo = {
  age: number;
  salary: number;
};

type PersonMap = Record<string, PersonInfo>;

let map: PersonMap = {
  'John': { age: 25, salary: 10 },
  'Mary': { age: 21, salary: 20 },
};

泛型类型默认参数

// 你可以在定义泛型时为类型参数提供默认类型。
function createArray<T = string>(length: number, value: T): T[] {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(value);
  }
  return result;
}

let strings = createArray(3, 'x'); // ['x', 'x', 'x']
let numbers = createArray(3, 10); // [10, 10, 10]

泛型条件类型

// 泛型条件类型允许你根据类型条件来选择不同的类型。
type TypeName<T> =
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends Function ? 'function' :
  'object';

type T1 = TypeName<string>; // 'string'
type T2 = TypeName<number>; // 'number'
type T3 = TypeName<() => void>; // 'function'
type T4 = TypeName<string[]>; // 'object'

总结

泛型是 TypeScript 中一个非常强大的特性,可以显著提高代码的复用性和灵活性。通过使用泛型,你可以在定义函数、接口和类时,延迟类型的具体化,从而编写更加通用和灵活的代码。

链接下文:ArkTS入门(二)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐