1# ArkTS语言介绍 2 3ArkTS是一种为构建高性能应用而设计的编程语言。ArkTS在继承TypeScript语法的基础上进行了优化,以提供更高的性能和开发效率。 4 5随着移动设备在人们的日常生活中变得越来越普遍,许多编程语言在设计之初没有考虑到移动设备,导致应用的运行缓慢、低效、功耗大,针对移动环境的编程语言优化需求也越来越大。ArkTS是专为解决这些问题而设计的,聚焦于提高运行效率。 6 7目前流行的编程语言TypeScript是在JavaScript基础上通过添加类型定义扩展而来的,而ArkTS则是TypeScript的进一步扩展。TypeScript深受开发者的喜爱,因为它提供了一种更结构化的JavaScript编码方法。ArkTS旨在保持TypeScript的大部分语法,为现有的TypeScript开发者实现无缝过渡,让移动开发者快速上手ArkTS。 8 9ArkTS的一大特性是它专注于低运行时开销。ArkTS对TypeScript的动态类型特性施加了更严格的限制,以减少运行时开销,提高执行效率。通过取消动态类型特性,ArkTS代码能更有效地被运行前编译和优化,从而实现更快的应用启动和更低的功耗。 10 11与JavaScript的互通性是ArkTS语言设计中的关键考虑因素。鉴于许多移动应用开发者希望重用其TypeScript和JavaScript代码和库,ArkTS提供了与JavaScript的无缝互通,使开发者可以很容易地将JavaScript代码集成到他们的应用中。这意味着开发者可以利用现有的代码和库进行ArkTS开发。 12 13为了确保应用开发的最佳体验,ArkTS提供对方舟开发框架ArkUI的声明式语法和其他特性的支持。由于此部分特性不在既有TypeScript的范围内,因此我们在《ArkUI支持》一章中提供了详细的ArkUI示例。 14 15本教程将指导开发者了解ArkTS的核心功能、语法和最佳实践,使开发者能够使用ArkTS高效构建高性能的移动应用。 16 17## 基本知识 18 19### 声明 20 21ArkTS通过声明引入变量、常量、函数和类型。 22 23#### 变量声明 24 25以关键字`let`开头的声明引入变量,该变量在程序执行期间可以具有不同的值。 26 27```typescript 28let hi: string = 'hello'; 29hi = 'hello, world'; 30``` 31 32#### 常量声明 33 34以关键字`const`开头的声明引入只读常量,该常量只能被赋值一次。 35 36```typescript 37const hello: string = 'hello'; 38``` 39 40对常量重新赋值会造成编译时错误。 41 42#### 自动类型推断 43 44由于ArkTS是一种静态类型语言,所有数据的类型都必须在编译时确定。 45 46但是,如果一个变量或常量的声明包含了初始值,那么开发者就不需要显式指定其类型。ArkTS规范中列举了所有允许自动推断类型的场景。 47 48以下示例中,两条声明语句都是有效的,两个变量都是`string`类型: 49 50```typescript 51let hi1: string = 'hello'; 52let hi2 = 'hello, world'; 53``` 54 55### 类型 56 57#### `Number`类型 58 59ArkTS提供`number`和`Number`类型,任何整数和浮点数都可以被赋给此类型的变量。 60 61数字字面量包括整数字面量和十进制浮点数字面量。 62 63整数字面量包括以下类别: 64 65* 由数字序列组成的十进制整数。例如:`0`、`117`、`-345` 66* 以0x(或0X)开头的十六进制整数,可以包含数字(0-9)和字母a-f或A-F。例如:`0x1123`、`0x00111`、`-0xF1A7` 67* 以0o(或0O)开头的八进制整数,只能包含数字(0-7)。例如:`0o777` 68* 以0b(或0B)开头的二进制整数,只能包含数字0和1。例如:`0b11`、`0b0011`、`-0b11` 69 70浮点字面量包括以下: 71 72* 十进制整数,可为有符号数(即,前缀为“+”或“-”); 73* 小数点(“.”) 74* 小数部分(由十进制数字字符串表示) 75* 以“e”或“E”开头的指数部分,后跟有符号(即,前缀为“+”或“-”)或无符号整数。 76 77示例: 78 79```typescript 80let n1 = 3.14; 81let n2 = 3.141592; 82let n3 = .5; 83let n4 = 1e10; 84 85function factorial(n: number): number { 86 if (n <= 1) { 87 return 1; 88 } 89 return n * factorial(n - 1); 90} 91``` 92 93#### `Boolean`类型 94 95`boolean`类型由`true`和`false`两个逻辑值组成。 96 97通常在条件语句中使用`boolean`类型的变量: 98 99```typescript 100let isDone: boolean = false; 101 102// ... 103 104if (isDone) { 105 console.log ('Done!'); 106} 107``` 108 109#### `String`类型 110 111`string`代表字符序列;可以使用转义字符来表示字符。 112 113字符串字面量由单引号(')或双引号(")之间括起来的零个或多个字符组成。字符串字面量还有一特殊形式,是用反向单引号(\`)括起来的模板字面量。 114 115```typescript 116let s1 = 'Hello, world!\n'; 117let s2 = 'this is a string'; 118let a = 'Success'; 119let s3 = `The result is ${a}`; 120``` 121 122#### `Void`类型 123 124`void`类型用于指定函数没有返回值。 125此类型只有一个值,同样是`void`。由于`void`是引用类型,因此它可以用于泛型类型参数。 126 127```typescript 128class Class<T> { 129 //... 130} 131let instance: Class <void> 132``` 133 134#### `Object`类型 135 136`Object`类型是所有引用类型的基类型。任何值,包括基本类型的值(它们会被自动装箱),都可以直接被赋给`Object`类型的变量。 137 138#### `Array`类型 139 140`array`,即数组,是由可赋值给数组声明中指定的元素类型的数据组成的对象。 141数组可由数组复合字面量(即用方括号括起来的零个或多个表达式的列表,其中每个表达式为数组中的一个元素)来赋值。数组的长度由数组中元素的个数来确定。数组中第一个元素的索引为0。 142 143以下示例将创建包含三个元素的数组: 144 145```typescript 146let names: string[] = ['Alice', 'Bob', 'Carol']; 147``` 148 149#### `Enum`类型 150 151`enum`类型,又称枚举类型,是预先定义的一组命名值的值类型,其中命名值又称为枚举常量。 152使用枚举常量时必须以枚举类型名称为前缀。 153 154```typescript 155enum ColorSet { Red, Green, Blue } 156let c: ColorSet = ColorSet.Red; 157``` 158 159常量表达式可以用于显式设置枚举常量的值。 160 161```typescript 162enum ColorSet { White = 0xFF, Grey = 0x7F, Black = 0x00 } 163let c: ColorSet = ColorSet.Black; 164``` 165 166#### `Union`类型 167 168`union`类型,即联合类型,是由多个类型组合成的引用类型。联合类型包含了变量可能的所有类型。 169 170```typescript 171class Cat { 172 // ... 173} 174class Dog { 175 // ... 176} 177class Frog { 178 // ... 179} 180type Animal = Cat | Dog | Frog | number 181// Cat、Dog、Frog是一些类型(类或接口) 182 183let animal: Animal = new Cat(); 184animal = new Frog(); 185animal = 42; 186// 可以将类型为联合类型的变量赋值为任何组成类型的有效值 187``` 188 189可以用不同的机制获取联合类型中特定类型的值。 190示例: 191 192```typescript 193class Cat { sleep () {}; meow () {} } 194class Dog { sleep () {}; bark () {} } 195class Frog { sleep () {}; leap () {} } 196 197type Animal = Cat | Dog | Frog | number 198 199let animal: Animal = new Frog(); 200if (animal instanceof Frog) { 201 let frog: Frog = animal as Frog; // animal在这里是Frog类型 202 animal.leap(); 203 frog.leap(); 204 // 结果:青蛙跳了两次 205} 206 207animal.sleep (); // 任何动物都可以睡觉 208``` 209 210#### `Aliases`类型 211 212`Aliases`类型为匿名类型(数组、函数、对象字面量或联合类型)提供名称,或为已有类型提供替代名称。 213 214```typescript 215type Matrix = number[][]; 216type Handler = (s: string, no: number) => string; 217type Predicate <T> = (x: T) => Boolean; 218type NullableObject = Object | null; 219``` 220 221### 运算符 222 223#### 赋值运算符 224 225赋值运算符`=`,使用方式如`x=y`。 226 227复合赋值运算符将赋值与运算符组合在一起,其中`x op = y`等于`x = x op y`。 228 229复合赋值运算符列举如下:`+=`、`-=`、`*=`、`/=`、`%=`、`<<=`、`>>=`、`>>>=`、`&=`、`|=`、`^=`。 230 231#### 比较运算符 232 233| 运算符| 说明 | 234| -------- | ------------------------------------------------------------ | 235| `==` | 如果两个操作数相等,则返回true。 | 236| `!=` | 如果两个操作数不相等,则返回true。 | 237| `>` | 如果左操作数大于右操作数,则返回true。 | 238| `>=` | 如果左操作数大于或等于右操作数,则返回true。 | 239| `<` | 如果左操作数小于右操作数,则返回true。 | 240| `<=` | 如果左操作数小于或等于右操作数,则返回true。 | 241 242#### 算术运算符 243 244一元运算符为`-`、`+`、`--`、`++`。 245 246二元运算符列举如下: 247 248| 运算符| 说明 | 249| -------- | ------------------------ | 250| `+` | 加法 | 251| `-` | 减法 | 252| `*` | 乘法 | 253| `/` | 除法 | 254| `%` | 除法后余数| 255 256#### 位运算符 257 258| 运算符 | 说明 | 259| --------- | ------------------------------------------------------------ | 260| `a & b` | 按位与:如果两个操作数的对应位都为1,则将这个位设置为1,否则设置为0。| 261| `a \| b` | 按位或:如果两个操作数的相应位中至少有一个为1,则将这个位设置为1,否则设置为0。| 262| `a ^ b` | 按位异或:如果两个操作数的对应位不同,则将这个位设置为1,否则设置为0。| 263| `~ a` | 按位非:反转操作数的位。 | 264| `a << b` | 左移:将a的二进制表示向左移b位。| 265| `a >> b` | 算术右移:将a的二进制表示向右移b位,带符号扩展。| 266| `a >>> b` | 逻辑右移:将a的二进制表示向右移b位,左边补0。| 267 268#### 逻辑运算符 269 270| 运算符 | 说明| 271| ---------- | ----------- | 272| `a && b` | 逻辑与 | 273| `a \|\| b` | 逻辑或 | 274| `! a` | 逻辑非 | 275 276### 语句 277 278#### `If`语句 279 280`if`语句用于需要根据逻辑条件执行不同语句的场景。当逻辑条件为真时,执行对应的一组语句,否则执行另一组语句(如果有的话)。 281`else`部分也可能包含`if`语句。 282 283`if`语句如下所示: 284 285```typescript 286if (condition1) { 287 // 语句1 288} else if (condition2) { 289 // 语句2 290} else { 291 // else语句 292} 293``` 294 295条件表达式可以是任何类型。但是对于`boolean`以外的类型,会进行隐式类型转换: 296 297```typescript 298let s1 = 'Hello'; 299if (s1) { 300 console.log(s1); // 打印“Hello” 301} 302 303let s2 = 'World'; 304if (s2.length != 0) { 305 console.log(s2); // 打印“World” 306} 307``` 308 309#### `Switch`语句 310 311使用`switch`语句来执行与`switch`表达式值匹配的代码块。 312 313`switch`语句如下所示: 314 315```typescript 316switch (expression) { 317 case label1: // 如果label1匹配,则执行 318 // ... 319 // 语句1 320 // ... 321 break; // 可省略 322 case label2: 323 case label3: // 如果label2或label3匹配,则执行 324 // ... 325 // 语句23 326 // ... 327 break; // 可省略 328 default: 329 // 默认语句 330} 331``` 332 333如果`switch`表达式的值等于某个label的值,则执行相应的语句。 334 335如果没有任何一个label值与表达式值相匹配,并且`switch`具有`default`子句,那么程序会执行`default`子句对应的代码块。 336 337`break`语句(可选的)允许跳出`switch`语句并继续执行`switch`语句之后的语句。 338 339如果没有`break`语句,则执行`switch`中的下一个label对应的代码块。 340 341#### 条件表达式 342 343条件表达式由第一个表达式的布尔值来决定返回其它两个表达式中的哪一个。 344 345示例如下: 346 347```typescript 348condition ? expression1 : expression2 349``` 350 351如果`condition`的为真值(转换后为`true`的值),则使用`expression1`作为该表达式的结果;否则,使用`expression2`。 352 353示例: 354 355```typescript 356let isValid = Math.random() > 0.5 ? true : false; 357let message = isValid ? 'Valid' : 'Failed'; 358``` 359 360#### `For`语句 361 362`for`语句会被重复执行,直到循环退出语句值为`false`。 363 364`for`语句如下所示: 365 366```typescript 367for ([init]; [condition]; [update]) { 368 statements 369} 370``` 371 372`for`语句的执行流程如下: 373 3741、 执行`init`表达式(如有)。此表达式通常初始化一个或多个循环计数器。 3752、 计算`condition`。如果它为真值(转换后为`true`的值),则执行循环主体的语句。如果它为假值(转换后为`false`的值),则`for`循环终止。 3763、 执行循环主体的语句。 3774、 如果有`update`表达式,则执行该表达式。 3785、 回到步骤2。 379 380示例: 381 382```typescript 383let sum = 0; 384for (let i = 0; i < 10; i += 2) { 385 sum += i; 386} 387``` 388 389#### `For-of`语句 390 391使用`for-of`语句可遍历数组或字符串。示例如下: 392 393```typescript 394for (forVar of expression) { 395 statements 396} 397``` 398 399示例: 400 401```typescript 402for (let ch of 'a string object') { 403 /* process ch */ 404} 405``` 406 407#### `While`语句 408 409只要`condition`为真值(转换后为`true`的值),`while`语句就会执行`statements`语句。示例如下: 410 411```typescript 412while (condition) { 413 statements 414} 415``` 416 417示例: 418 419```typescript 420let n = 0; 421let x = 0; 422while (n < 3) { 423 n++; 424 x += n; 425} 426``` 427 428#### `Do-while`语句 429 430如果`condition`的值为真值(转换后为`true`的值),那么`statements`语句会重复执行。示例如下: 431 432```typescript 433do { 434 statements 435} while (condition) 436``` 437 438示例: 439 440```typescript 441let i = 0; 442do { 443 i += 1; 444} while (i < 10) 445``` 446 447#### `Break`语句 448 449使用`break`语句可以终止循环语句或`switch`。 450 451示例: 452 453```typescript 454let x = 0; 455while (true) { 456 x++; 457 if (x > 5) { 458 break; 459 } 460} 461``` 462 463如果`break`语句后带有标识符,则将控制流转移到该标识符所包含的语句块之外。 464 465示例: 466 467```typescript 468let x = 1 469label: while (true) { 470 switch (x) { 471 case 1: 472 // statements 473 break label // 中断while语句 474 } 475} 476``` 477 478#### `Continue`语句 479 480`continue`语句会停止当前循环迭代的执行,并将控制传递给下一个迭代。 481 482示例: 483 484```typescript 485let sum = 0; 486for (let x = 0; x < 100; x++) { 487 if (x % 2 == 0) { 488 continue 489 } 490 sum += x; 491} 492``` 493 494#### `Throw`和`Try`语句 495 496`throw`语句用于抛出异常或错误: 497 498```typescript 499throw new Error('this error') 500``` 501 502`try`语句用于捕获和处理异常或错误: 503 504```typescript 505try { 506 // 可能发生异常的语句块 507} catch (e) { 508 // 异常处理 509} 510``` 511 512下面的示例中`throw`和`try`语句用于处理除数为0的错误: 513 514```typescript 515class ZeroDivisor extends Error {} 516 517function divide (a: number, b: number): number{ 518 if (b == 0) throw new ZeroDivisor(); 519 return a / b; 520} 521 522function process (a: number, b: number) { 523 try { 524 let res = divide(a, b); 525 console.log('result: ' + res); 526 } catch (x) { 527 console.log('some error'); 528 } 529} 530``` 531 532支持`finally`语句: 533 534```typescript 535function processData(s: string) { 536 let error: Error | null = null; 537 538 try { 539 console.log('Data processed: ' + s); 540 // ... 541 // 可能发生异常的语句 542 // ... 543 } catch (e) { 544 error = e as Error; 545 // ... 546 // 异常处理 547 // ... 548 } finally { 549 if (error != null) { 550 console.log(`Error caught: input='${s}', message='${error.message}'`); 551 } 552 } 553} 554``` 555 556## 函数 557 558### 函数声明 559 560函数声明引入一个函数,包含其名称、参数列表、返回类型和函数体。 561 562以下示例是一个简单的函数,包含两个`string`类型的参数,返回类型为`string`: 563 564```typescript 565function add(x: string, y: string): string { 566 let z: string = `${x} ${y}`; 567 return z; 568} 569``` 570 571在函数声明中,必须为每个参数标记类型。如果参数为可选参数,那么允许在调用函数时省略该参数。函数的最后一个参数可以是rest参数。 572 573### 可选参数 574 575可选参数的格式可为`name?: Type`。 576 577```typescript 578function hello(name?: string) { 579 if (name == undefined) { 580 console.log('Hello!'); 581 } else { 582 console.log(`Hello, ${name}!`); 583 } 584} 585``` 586 587可选参数的另一种形式为设置的参数默认值。如果在函数调用中这个参数被省略了,则会使用此参数的默认值作为实参。 588 589```typescript 590function multiply(n: number, coeff: number = 2): number { 591 return n * coeff; 592} 593multiply(2); // 返回2*2 594multiply(2, 3); // 返回2*3 595``` 596 597### Rest参数 598 599函数的最后一个参数可以是rest参数。使用rest参数时,允许函数或方法接受任意数量的实参。 600 601```typescript 602function sum(...numbers: number[]): number { 603 let res = 0; 604 for (let n of numbers) 605 res += n; 606 return res; 607} 608 609sum() // 返回0 610sum(1, 2, 3) // 返回6 611``` 612 613### 返回类型 614 615如果可以从函数体内推断出函数返回类型,则可在函数声明中省略标注返回类型。 616 617```typescript 618// 显式指定返回类型 619function foo(): string { return 'foo'; } 620 621// 推断返回类型为string 622function goo() { return 'goo'; } 623``` 624 625不需要返回值的函数的返回类型可以显式指定为`void`或省略标注。这类函数不需要返回语句。 626 627以下示例中两种函数声明方式都是有效的: 628 629```typescript 630function hi1() { console.log('hi'); } 631function hi2(): void { console.log('hi'); } 632``` 633 634### 函数的作用域 635 636函数中定义的变量和其他实例仅可以在函数内部访问,不能从外部访问。 637 638如果函数中定义的变量与外部作用域中已有实例同名,则函数内的局部变量定义将覆盖外部定义。 639 640### 函数调用 641 642调用函数以执行其函数体,实参值会赋值给函数的形参。 643 644如果函数定义如下: 645 646```typescript 647function join(x: string, y: string): string { 648 let z: string = `${x} ${y}`; 649 return z; 650} 651``` 652 653则此函数的调用需要包含两个`string`类型的参数: 654 655```typescript 656let x = join('hello', 'world'); 657console.log(x); 658``` 659 660### 函数类型 661 662函数类型通常用于定义回调: 663 664```typescript 665type trigFunc = (x: number) => number // 这是一个函数类型 666 667function do_action(f: trigFunc) { 668 f(3.141592653589); // 调用函数 669} 670 671do_action(Math.sin); // 将函数作为参数传入 672``` 673 674### 箭头函数或Lambda函数 675 676函数可以定义为箭头函数,例如: 677 678```typescript 679let sum = (x: number, y: number): number => { 680 return x + y; 681} 682``` 683 684箭头函数的返回类型可以省略;省略时,返回类型通过函数体推断。 685 686表达式可以指定为箭头函数,使表达更简短,因此以下两种表达方式是等价的: 687 688```typescript 689let sum1 = (x: number, y: number) => { return x + y; } 690let sum2 = (x: number, y: number) => x + y 691``` 692 693### 闭包 694 695闭包是由函数及声明该函数的环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。 696 697在下例中,`z`是执行`f`时创建的`g`箭头函数实例的引用。`g`的实例维持了对它的环境的引用(变量`count`存在其中)。因此,当`z`被调用时,变量`count`仍可用。 698 699```typescript 700function f(): () => number { 701 let count = 0; 702 let g = (): number => { count++; return count; }; 703 return g; 704} 705 706let z = f(); 707z(); // 返回:1 708z(); // 返回:2 709``` 710 711### 函数重载 712 713我们可以通过编写重载,指定函数的不同调用方式。具体方法为,为同一个函数写入多个同名但签名不同的函数头,函数实现紧随其后。 714 715```typescript 716function foo(x: number): void; /* 第一个函数定义 */ 717function foo(x: string): void; /* 第二个函数定义 */ 718function foo(x: number | string): void { /* 函数实现 */ 719} 720 721foo(123); // OK,使用第一个定义 722foo('aa'); // OK,使用第二个定义 723``` 724 725不允许重载函数有相同的名字以及参数列表,否则将会编译报错。 726 727## 类 728 729类声明引入一个新类型,并定义其字段、方法和构造函数。 730 731在以下示例中,定义了`Person`类,该类具有字段`name`和`surname`、构造函数和方法`fullName`: 732 733```typescript 734class Person { 735 name: string = '' 736 surname: string = '' 737 constructor (n: string, sn: string) { 738 this.name = n; 739 this.surname = sn; 740 } 741 fullName(): string { 742 return this.name + ' ' + this.surname; 743 } 744} 745``` 746 747定义类后,可以使用关键字`new`创建实例: 748 749```typescript 750let p = new Person('John', 'Smith'); 751console.log(p.fullName()); 752``` 753 754或者,可以使用对象字面量创建实例: 755 756```typescript 757class Point { 758 x: number = 0 759 y: number = 0 760} 761let p: Point = {x: 42, y: 42}; 762``` 763 764### 字段 765 766字段是直接在类中声明的某种类型的变量。 767 768类可以具有实例字段或者静态字段。 769 770#### 实例字段 771 772实例字段存在于类的每个实例上。每个实例都有自己的实例字段集合。 773 774要访问实例字段,需要使用类的实例。 775 776```typescript 777class Person { 778 name: string = '' 779 age: number = 0 780 constructor(n: string, a: number) { 781 this.name = n; 782 this.age = a; 783 } 784 785 getName(): string { 786 return this.name; 787 } 788} 789 790let p1 = new Person('Alice', 25); 791p1.name; 792let p2 = new Person('Bob', 28); 793p2.getName(); 794``` 795 796#### 静态字段 797 798使用关键字`static`将字段声明为静态。静态字段属于类本身,类的所有实例共享一个静态字段。 799 800要访问静态字段,需要使用类名: 801 802```typescript 803class Person { 804 static numberOfPersons = 0 805 constructor() { 806 // ... 807 Person.numberOfPersons++; 808 // ... 809 } 810} 811 812Person.numberOfPersons; 813``` 814 815#### 字段初始化 816 817为了减少运行时的错误和获得更好的执行性能, 818ArkTS要求所有字段在声明时或者构造函数中显式初始化。这和标准TS中的`strictPropertyInitialization`模式一样。 819 820以下代码是在ArkTS中不合法的代码。 821 822```typescript 823class Person { 824 name: string // undefined 825 826 setName(n:string): void { 827 this.name = n; 828 } 829 830 getName(): string { 831 // 开发者使用"string"作为返回类型,这隐藏了name可能为"undefined"的事实。 832 // 更合适的做法是将返回类型标注为"string | undefined",以告诉开发者这个API所有可能的返回值。 833 return this.name; 834 } 835} 836 837let jack = new Person(); 838// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')" 839jack.getName().length; // 运行时异常:name is undefined 840``` 841 842在ArkTS中,应该这样写代码。 843 844```typescript 845class Person { 846 name: string = '' 847 848 setName(n:string): void { 849 this.name = n; 850 } 851 852 // 类型为'string',不可能为"null"或者"undefined" 853 getName(): string { 854 return this.name; 855 } 856} 857 858 859let jack = new Person(); 860// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')" 861jack.getName().length; // 0, 没有运行时异常 862``` 863 864接下来的代码展示了如果`name`的值可以是`undefined`,那么应该如何写代码。 865 866```typescript 867class Person { 868 name?: string // 可能为`undefined` 869 870 setName(n:string): void { 871 this.name = n; 872 } 873 874 // 编译时错误:name可以是"undefined",所以将这个API的返回值类型标记为string 875 getNameWrong(): string { 876 return this.name; 877 } 878 879 getName(): string | undefined { // 返回类型匹配name的类型 880 return this.name; 881 } 882} 883 884let jack = new Person(); 885// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')" 886 887// 编译时错误:编译器认为下一行代码有可能会访问undefined的属性,报错 888jack.getName().length; // 编译失败 889 890jack.getName()?.length; // 编译成功,没有运行时错误 891``` 892 893#### getter和setter 894 895setter和getter可用于提供对对象属性的受控访问。 896 897在以下示例中,setter用于禁止将`age`属性设置为无效值: 898 899```typescript 900class Person { 901 name: string = '' 902 private _age: number = 0 903 get age(): number { return this._age; } 904 set age(x: number) { 905 if (x < 0) { 906 throw Error('Invalid age argument'); 907 } 908 this._age = x; 909 } 910} 911 912let p = new Person(); 913p.age; // 输出0 914p.age = -42; // 设置无效age值会抛出错误 915``` 916 917在类中可以定义getter或者setter。 918 919### 方法 920 921方法属于类。类可以定义实例方法或者静态方法。静态方法属于类本身,只能访问静态字段。而实例方法既可以访问静态字段,也可以访问实例字段,包括类的私有字段。 922 923#### 实例方法 924 925以下示例说明了实例方法的工作原理。 926 927`calculateArea`方法通过将高度乘以宽度来计算矩形的面积: 928 929```typescript 930class RectangleSize { 931 private height: number = 0 932 private width: number = 0 933 constructor(height: number, width: number) { 934 // ... 935 } 936 calculateArea(): number { 937 return this.height * this.width; 938 } 939} 940``` 941 942必须通过类的实例调用实例方法: 943 944```typescript 945let square = new RectangleSize(10, 10); 946square.calculateArea(); // 输出:100 947``` 948 949#### 静态方法 950 951使用关键字`static`将方法声明为静态。静态方法属于类本身,只能访问静态字段。 952 953静态方法定义了类作为一个整体的公共行为。 954 955必须通过类名调用静态方法: 956 957```typescript 958class Cl { 959 static staticMethod(): string { 960 return 'this is a static method.'; 961 } 962} 963console.log(Cl.staticMethod()); 964``` 965 966#### 继承 967 968一个类可以继承另一个类(称为基类),并使用以下语法实现多个接口: 969 970```typescript 971class [extends BaseClassName] [implements listOfInterfaces] { 972 // ... 973} 974``` 975 976继承类继承基类的字段和方法,但不继承构造函数。继承类可以新增定义字段和方法,也可以覆盖其基类定义的方法。 977 978基类也称为“父类”或“超类”。继承类也称为“派生类”或“子类”。 979 980示例: 981 982```typescript 983class Person { 984 name: string = '' 985 private _age = 0 986 get age(): number { 987 return this._age; 988 } 989} 990class Employee extends Person { 991 salary: number = 0 992 calculateTaxes(): number { 993 return this.salary * 0.42; 994 } 995} 996``` 997 998包含`implements`子句的类必须实现列出的接口中定义的所有方法,但使用默认实现定义的方法除外。 999 1000```typescript 1001interface DateInterface { 1002 now(): string; 1003} 1004class MyDate implements DateInterface { 1005 now(): string { 1006 // 在此实现 1007 return 'now is now'; 1008 } 1009} 1010``` 1011 1012#### 父类访问 1013 1014关键字`super`可用于访问父类的实例字段、实例方法和构造函数。在实现子类功能时,可以通过该关键字从父类中获取所需接口: 1015 1016```typescript 1017class RectangleSize { 1018 protected height: number = 0 1019 protected width: number = 0 1020 1021 constructor (h: number, w: number) { 1022 this.height = h; 1023 this.width = w; 1024 } 1025 1026 draw() { 1027 /* 绘制边界 */ 1028 } 1029} 1030class FilledRectangle extends RectangleSize { 1031 color = '' 1032 constructor (h: number, w: number, c: string) { 1033 super(h, w); // 父类构造函数的调用 1034 this.color = c; 1035 } 1036 1037 draw() { 1038 super.draw(); // 父类方法的调用 1039 // super.height -可在此处使用 1040 /* 填充矩形 */ 1041 } 1042} 1043``` 1044 1045#### 方法重写 1046 1047子类可以重写其父类中定义的方法的实现。重写的方法必须具有与原始方法相同的参数类型和相同或派生的返回类型。 1048 1049```typescript 1050class RectangleSize { 1051 // ... 1052 area(): number { 1053 // 实现 1054 return 0; 1055 } 1056} 1057class Square extends RectangleSize { 1058 private side: number = 0 1059 area(): number { 1060 return this.side * this.side; 1061 } 1062} 1063``` 1064 1065#### 方法重载签名 1066 1067通过重载签名,指定方法的不同调用。具体方法为,为同一个方法写入多个同名但签名不同的方法头,方法实现紧随其后。 1068 1069```typescript 1070class C { 1071 foo(x: number): void; /* 第一个签名 */ 1072 foo(x: string): void; /* 第二个签名 */ 1073 foo(x: number | string): void { /* 实现签名 */ 1074 } 1075} 1076let c = new C(); 1077c.foo(123); // OK,使用第一个签名 1078c.foo('aa'); // OK,使用第二个签名 1079``` 1080 1081如果两个重载签名的名称和参数列表均相同,则为错误。 1082 1083### 构造函数 1084 1085类声明可以包含用于初始化对象状态的构造函数。 1086 1087构造函数定义如下: 1088 1089```typescript 1090constructor ([parameters]) { 1091 // ... 1092} 1093``` 1094 1095如果未定义构造函数,则会自动创建具有空参数列表的默认构造函数,例如: 1096 1097```typescript 1098class Point { 1099 x: number = 0 1100 y: number = 0 1101} 1102let p = new Point(); 1103``` 1104 1105在这种情况下,默认构造函数使用字段类型的默认值来初始化实例中的字段。 1106 1107#### 派生类的构造函数 1108 1109构造函数函数体的第一条语句可以使用关键字`super`来显式调用直接父类的构造函数。 1110 1111```typescript 1112class RectangleSize { 1113 constructor(width: number, height: number) { 1114 // ... 1115 } 1116} 1117class Square extends RectangleSize { 1118 constructor(side: number) { 1119 super(side, side); 1120 } 1121} 1122``` 1123 1124#### 构造函数重载签名 1125 1126我们可以通过编写重载签名,指定构造函数的不同调用方式。具体方法为,为同一个构造函数写入多个同名但签名不同的构造函数头,构造函数实现紧随其后。 1127 1128```typescript 1129class C { 1130 constructor(x: number) /* 第一个签名 */ 1131 constructor(x: string) /* 第二个签名 */ 1132 constructor(x: number | string) { /* 实现签名 */ 1133 } 1134} 1135let c1 = new C(123); // OK,使用第一个签名 1136let c2 = new C('abc'); // OK,使用第二个签名 1137``` 1138 1139如果两个重载签名的名称和参数列表均相同,则为错误。 1140 1141### 可见性修饰符 1142 1143类的方法和属性都可以使用可见性修饰符。 1144 1145可见性修饰符包括:`private`、`protected`和`public`。默认可见性为`public`。 1146 1147#### Public(公有) 1148 1149`public`修饰的类成员(字段、方法、构造函数)在程序的任何可访问该类的地方都是可见的。 1150 1151#### Private(私有) 1152 1153`private`修饰的成员不能在声明该成员的类之外访问,例如: 1154 1155```typescript 1156class C { 1157 public x: string = '' 1158 private y: string = '' 1159 set_y (new_y: string) { 1160 this.y = new_y; // OK,因为y在类本身中可以访问 1161 } 1162} 1163let c = new C(); 1164c.x = 'a'; // OK,该字段是公有的 1165c.y = 'b'; // 编译时错误:'y'不可见 1166``` 1167 1168#### Protected(受保护) 1169 1170`protected`修饰符的作用与`private`修饰符非常相似,不同点是`protected`修饰的成员允许在派生类中访问,例如: 1171 1172```typescript 1173class Base { 1174 protected x: string = '' 1175 private y: string = '' 1176} 1177class Derived extends Base { 1178 foo() { 1179 this.x = 'a'; // OK,访问受保护成员 1180 this.y = 'b'; // 编译时错误,'y'不可见,因为它是私有的 1181 } 1182} 1183``` 1184 1185### 对象字面量 1186 1187对象字面量是一个表达式,可用于创建类实例并提供一些初始值。它在某些情况下更方便,可以用来代替`new`表达式。 1188 1189对象字面量的表示方式是:封闭在花括号对({})中的'属性名:值'的列表。 1190 1191```typescript 1192class C { 1193 n: number = 0 1194 s: string = '' 1195} 1196 1197let c: C = {n: 42, s: 'foo'}; 1198``` 1199 1200ArkTS是静态类型语言,如上述示例所示,对象字面量只能在可以推导出该字面量类型的上下文中使用。其他正确的例子: 1201 1202```typescript 1203class C { 1204 n: number = 0 1205 s: string = '' 1206} 1207 1208function foo(c: C) {} 1209 1210let c: C 1211 1212c = {n: 42, s: 'foo'}; // 使用变量的类型 1213foo({n: 42, s: 'foo'}); // 使用参数的类型 1214 1215function bar(): C { 1216 return {n: 42, s: 'foo'}; // 使用返回类型 1217} 1218``` 1219 1220也可以在数组元素类型或类字段类型中使用: 1221 1222```typescript 1223class C { 1224 n: number = 0 1225 s: string = '' 1226} 1227let cc: C[] = [{n: 1, s: 'a'}, {n: 2, s: 'b'}]; 1228``` 1229 1230#### `Record`类型的对象字面量 1231 1232泛型`Record<K, V>`用于将类型(键类型)的属性映射到另一个类型(值类型)。常用对象字面量来初始化该类型的值: 1233 1234```typescript 1235let map: Record<string, number> = { 1236 'John': 25, 1237 'Mary': 21, 1238} 1239 1240map['John']; // 25 1241``` 1242 1243类型`K`可以是字符串类型或数值类型,而`V`可以是任何类型。 1244 1245```typescript 1246interface PersonInfo { 1247 age: number 1248 salary: number 1249} 1250let map: Record<string, PersonInfo> = { 1251 'John': { age: 25, salary: 10}, 1252 'Mary': { age: 21, salary: 20} 1253} 1254``` 1255 1256## 接口 1257 1258接口声明引入新类型。接口是定义代码协定的常见方式。 1259 1260任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。 1261 1262接口通常包含属性和方法的声明 1263 1264示例: 1265 1266```typescript 1267interface Style { 1268 color: string // 属性 1269} 1270interface AreaSize { 1271 calculateAreaSize(): number // 方法的声明 1272 someMethod(): void; // 方法的声明 1273} 1274``` 1275 1276实现接口的类示例: 1277 1278```typescript 1279// 接口: 1280interface AreaSize { 1281 calculateAreaSize(): number // 方法的声明 1282 someMethod(): void; // 方法的声明 1283} 1284 1285// 实现: 1286class RectangleSize implements AreaSize { 1287 private width: number = 0 1288 private height: number = 0 1289 someMethod(): void { 1290 console.log('someMethod called'); 1291 } 1292 calculateAreaSize(): number { 1293 this.someMethod(); // 调用另一个方法并返回结果 1294 return this.width * this.height; 1295 } 1296} 1297``` 1298 1299### 接口属性 1300 1301接口属性可以是字段、getter、setter或getter和setter组合的形式。 1302 1303属性字段只是getter/setter对的便捷写法。以下表达方式是等价的: 1304 1305```typescript 1306interface Style { 1307 color: string 1308} 1309``` 1310 1311```typescript 1312interface Style { 1313 get color(): string 1314 set color(x: string) 1315} 1316``` 1317 1318实现接口的类也可以使用以下两种方式: 1319 1320```typescript 1321interface Style { 1322 color: string 1323} 1324 1325class StyledRectangle implements Style { 1326 color: string = '' 1327} 1328``` 1329 1330```typescript 1331interface Style { 1332 color: string 1333} 1334 1335class StyledRectangle implements Style { 1336 private _color: string = '' 1337 get color(): string { return this._color; } 1338 set color(x: string) { this._color = x; } 1339} 1340``` 1341 1342### 接口继承 1343 1344接口可以继承其他接口,如下面的示例所示: 1345 1346```typescript 1347interface Style { 1348 color: string 1349} 1350 1351interface ExtendedStyle extends Style { 1352 width: number 1353} 1354``` 1355 1356继承接口包含被继承接口的所有属性和方法,还可以添加自己的属性和方法。 1357 1358## 泛型类型和函数 1359 1360泛型类型和函数允许创建的代码在各种类型上运行,而不仅支持单一类型。 1361 1362### 泛型类和接口 1363 1364类和接口可以定义为泛型,将参数添加到类型定义中,如以下示例中的类型参数`Element`: 1365 1366```typescript 1367class CustomStack<Element> { 1368 public push(e: Element):void { 1369 // ... 1370 } 1371} 1372``` 1373 1374要使用类型CustomStack,必须为每个类型参数指定类型实参: 1375 1376```typescript 1377let s = new CustomStack<string>(); 1378s.push('hello'); 1379``` 1380 1381编译器在使用泛型类型和函数时会确保类型安全。参见以下示例: 1382 1383```typescript 1384let s = new CustomStack<string>(); 1385s.push(55); // 将会产生编译时错误 1386``` 1387 1388### 泛型约束 1389 1390泛型类型的类型参数可以绑定。例如,`HashMap<Key, Value>`容器中的`Key`类型参数必须具有哈希方法,即它应该是可哈希的。 1391 1392```typescript 1393interface Hashable { 1394 hash(): number 1395} 1396class HasMap<Key extends Hashable, Value> { 1397 public set(k: Key, v: Value) { 1398 let h = k.hash(); 1399 // ...其他代码... 1400 } 1401} 1402``` 1403 1404在上面的例子中,`Key`类型扩展了`Hashable`,`Hashable`接口的所有方法都可以为key调用。 1405 1406### 泛型函数 1407 1408使用泛型函数可编写更通用的代码。比如返回数组最后一个元素的函数: 1409 1410```typescript 1411function last(x: number[]): number { 1412 return x[x.length - 1]; 1413} 1414last([1, 2, 3]); // 3 1415``` 1416 1417如果需要为任何数组定义相同的函数,使用类型参数将该函数定义为泛型: 1418 1419```typescript 1420function last<T>(x: T[]): T { 1421 return x[x.length - 1]; 1422} 1423``` 1424 1425现在,该函数可以与任何数组一起使用。 1426 1427在函数调用中,类型实参可以显式或隐式设置: 1428 1429```typescript 1430// 显式设置的类型实参 1431last<string>(['aa', 'bb']); 1432last<number>([1, 2, 3]); 1433 1434// 隐式设置的类型实参 1435// 编译器根据调用参数的类型来确定类型实参 1436last([1, 2, 3]); 1437``` 1438 1439### 泛型默认值 1440 1441泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。下面的示例展示了类和函数的这一点。 1442 1443```typescript 1444class SomeType {} 1445interface Interface <T1 = SomeType> { } 1446class Base <T2 = SomeType> { } 1447class Derived1 extends Base implements Interface { } 1448// Derived1在语义上等价于Derived2 1449class Derived2 extends Base<SomeType> implements Interface<SomeType> { } 1450 1451function foo<T = number>(): T { 1452 // ... 1453} 1454foo(); 1455// 此函数在语义上等价于下面的调用 1456foo<number>(); 1457``` 1458 1459## 空安全 1460 1461默认情况下,ArkTS中的所有类型都是不可为空的,因此类型的值不能为空。这类似于TypeScript的严格空值检查模式(`strictNullChecks`),但规则更严格。 1462 1463在下面的示例中,所有行都会导致编译时错误: 1464 1465```typescript 1466let x: number = null; // 编译时错误 1467let y: string = null; // 编译时错误 1468let z: number[] = null; // 编译时错误 1469``` 1470 1471可以为空值的变量定义为联合类型`T | null`。 1472 1473```typescript 1474let x: number | null = null; 1475x = 1; // ok 1476x = null; // ok 1477if (x != null) { /* do something */ } 1478``` 1479 1480### 非空断言运算符 1481 1482后缀运算符`!`可用于断言其操作数为非空。 1483 1484应用于空值时,运算符将抛出错误。否则,值的类型将从`T | null`更改为`T`: 1485 1486```typescript 1487class C { 1488 value: number | null = 1; 1489} 1490 1491let c = new C(); 1492let y: number; 1493y = c.value + 1; // 编译时错误:无法对可空值作做加法 1494y = c.value! + 1; // ok,值为2 1495``` 1496 1497### 空值合并运算符 1498 1499空值合并二元运算符`??`用于检查左侧表达式的求值是否等于`null`或者`undefined`。如果是,则表达式的结果为右侧表达式;否则,结果为左侧表达式。 1500 1501换句话说,`a ?? b`等价于三元运算符`(a != null && a != undefined) ? a : b`。 1502 1503在以下示例中,`getNick`方法如果设置了昵称,则返回昵称;否则,返回空字符串: 1504 1505```typescript 1506class Person { 1507 // ... 1508 nick: string | null = null 1509 getNick(): string { 1510 return this.nick ?? ''; 1511 } 1512} 1513``` 1514 1515### 可选链 1516 1517在访问对象属性时,如果该属性是`undefined`或者`null`,可选链运算符会返回`undefined`。 1518 1519```typescript 1520class Person { 1521 nick: string | null = null 1522 spouse?: Person 1523 1524 setSpouse(spouse: Person): void { 1525 this.spouse = spouse; 1526 } 1527 1528 getSpouseNick(): string | null | undefined { 1529 return this.spouse?.nick; 1530 } 1531 1532 constructor(nick: string) { 1533 this.nick = nick; 1534 this.spouse = undefined; 1535 } 1536} 1537``` 1538 1539**说明**:`getSpouseNick`的返回类型必须为`string | null | undefined`,因为该方法可能返回`null`或者`undefined`。 1540 1541可选链可以任意长,可以包含任意数量的`?.`运算符。 1542 1543在以下示例中,如果一个`Person`的实例有不为空的`spouse`属性,且`spouse`有不为空的`nickname`属性,则输出`spouse.nick`。否则,输出`undefined`: 1544 1545```typescript 1546class Person { 1547 nick: string | null = null 1548 spouse?: Person 1549 1550 constructor(nick: string) { 1551 this.nick = nick; 1552 this.spouse = undefined; 1553 } 1554} 1555 1556let p: Person = new Person('Alice'); 1557p.spouse?.nick; // undefined 1558``` 1559 1560## 模块 1561 1562程序可划分为多组编译单元或模块。 1563 1564每个模块都有其自己的作用域,即,在模块中创建的任何声明(变量、函数、类等)在该模块之外都不可见,除非它们被显式导出。 1565 1566与此相对,从另一个模块导出的变量、函数、类、接口等必须首先导入到模块中。 1567 1568### 导出 1569 1570可以使用关键字`export`导出顶层的声明。 1571 1572未导出的声明名称被视为私有名称,只能在声明该名称的模块中使用。 1573 1574**注意**:通过export方式导出,在导入时要加{}。 1575 1576```typescript 1577export class Point { 1578 x: number = 0 1579 y: number = 0 1580 constructor(x: number, y: number) { 1581 this.x = x; 1582 this.y = y; 1583 } 1584} 1585export let Origin = new Point(0, 0); 1586export function Distance(p1: Point, p2: Point): number { 1587 return Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)); 1588} 1589``` 1590 1591### 导入 1592 1593#### 静态导入 1594 1595导入声明用于导入从其他模块导出的实体,并在当前模块中提供其绑定。导入声明由两部分组成: 1596 1597* 导入路径,用于指定导入的模块; 1598* 导入绑定,用于定义导入的模块中的可用实体集和使用形式(限定或不限定使用)。 1599 1600导入绑定可以有几种形式。 1601 1602假设模块具有路径“./utils”和导出实体“X”和“Y”。 1603 1604导入绑定`* as A`表示绑定名称“A”,通过`A.name`可访问从导入路径指定的模块导出的所有实体: 1605 1606```typescript 1607import * as Utils from './utils' 1608Utils.X // 表示来自Utils的X 1609Utils.Y // 表示来自Utils的Y 1610``` 1611 1612导入绑定`{ ident1, ..., identN }`表示将导出的实体与指定名称绑定,该名称可以用作简单名称: 1613 1614```typescript 1615import { X, Y } from './utils' 1616X // 表示来自utils的X 1617Y // 表示来自utils的Y 1618``` 1619 1620如果标识符列表定义了`ident as alias`,则实体`ident`将绑定在名称`alias`下: 1621 1622```typescript 1623import { X as Z, Y } from './utils' 1624Z // 表示来自Utils的X 1625Y // 表示来自Utils的Y 1626X // 编译时错误:'X'不可见 1627``` 1628 1629#### 动态导入 1630应用开发的有些场景中,如果希望根据条件导入模块或者按需导入模块,可以使用动态导入代替静态导入。 1631import()语法通常称为动态导入dynamic import,是一种类似函数的表达式,用来动态导入模块。以这种方式调用,将返回一个promise。 1632如下例所示,import(modulePath)可以加载模块并返回一个promise,该promise resolve为一个包含其所有导出的模块对象。该表达式可以在代码中的任意位置调用。 1633 1634```typescript 1635let modulePath = prompt("Which module to load?"); 1636import(modulePath) 1637.then(obj => <module object>) 1638.catch(err => <loading error, e.g. if no such module>) 1639``` 1640 1641如果在异步函数中,可以使用let module = await import(modulePath)。 1642 1643```typescript 1644// say.ts 1645export function hi() { 1646 console.log('Hello'); 1647} 1648export function bye() { 1649 console.log('Bye'); 1650} 1651``` 1652 1653那么,可以像下面这样进行动态导入: 1654 1655```typescript 1656async function test() { 1657 let ns = await import('./say'); 1658 let hi = ns.hi; 1659 let bye = ns.bye; 1660 hi(); 1661 bye(); 1662} 1663``` 1664 1665更多的使用动态import的业务场景和使用实例见[动态import](arkts-dynamic-import.md) 1666 1667### 顶层语句 1668 1669模块可以包含除return语句外的任何模块级语句。 1670 1671如果模块包含主函数(程序入口),则模块的顶层语句将在此函数函数体之前执行。否则,这些语句将在执行模块的其他功能之前执行。 1672 1673### 程序入口 1674 1675程序(应用)的入口是顶层主函数。主函数应具有空参数列表或只有`string[]`类型的参数。 1676 1677```typescript 1678function main() { 1679 console.log('this is the program entry'); 1680} 1681``` 1682 1683## ArkUI支持 1684 1685本节演示ArkTS为创建图形用户界面(GUI)程序提供的机制。ArkUI基于TypeScript提供了一系列扩展能力,以声明式地描述应用程序的GUI以及GUI组件间的交互。 1686 1687 1688### ArkUI示例 1689 1690[MVVM应用示例](arkts-mvvm.md#mvvm应用示例)提供了一个完整的基于ArkUI的应用程序,以展示其GUI编程功能。 1691 1692有关ArkUI功能的更多详细信息,请参见ArkUI[基本语法概述](arkts-basic-syntax-overview.md)。 1693