Skip to content

Latest commit

 

History

History
853 lines (587 loc) · 23.4 KB

2.ArkTS基础语法.md

File metadata and controls

853 lines (587 loc) · 23.4 KB

2.ArkTS基础语法

背景

传统的Web开发使用的语言:

  • HTML: 控制页面元素
  • CSS: 控制布局和样式
  • JavaScript: 控制页面逻辑和数据状态

需要三种语言,而且语法不一样。

但是对华为来说Java又用不了,Web开发又太复杂,所以有了ArkTS。

为了确保应用开发的最佳体验,ArkTS提供对方舟开发框架ArkUI的声明式语法和其他特性的支持。由于此部分特性不在既有TypeScript的范围内。

但是对于Android和iOS的开发者,都会想到,如果有前端开发这一套,效率能行吗?

别忘了,鸿蒙还提供了方舟编译器(ArkCompiler),ArkCompiler AOT优化 : 隐藏继承优化、循环高级优化、过程空间优化、性能提升20%。 随着移动设备在人们的日常生活中变得越来越普遍,许多编程语言在设计之初没有考虑到移动设备,导致应用的运行缓慢、低效、功耗大,针对移动环境的编程语言优化需求也越来越大。ArkTS是专为解决这些问题而设计的,聚焦于提高运行效率。 ArkTS的一大特性是它专注于低运行时开销。ArkTS对TypeScript的动态类型特性施加了更严格的限制,以减少运行时开销,提高执行效率。通过取消动态类型特性,ArkTS代码能更有效地被运行前编译和优化,从而实现更快的应用启动和更低的功耗。

ArkTS

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

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

目前流行的编程语言TypeScript是在JavaScript基础上通过添加类型定义扩展而来的,而ArkTS则是TypeScript的进一步扩展。 TypeScript深受开发者的喜爱,因为它提供了一种更结构化的JavaScript编码方法。ArkTS旨在保持TypeScript的大部分语法,为现有的TypeScript开发者实现无缝过渡,让移动开发者快速上手ArkTS。

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

从API version 10开始,ArkTS进一步通过规范强化静态检查和分析,对比标准TS的差异可以参考从TypeScript到ArkTS的适配规则:

  • 强制使用静态类型:静态类型是ArkTS最重要的特性之一。如果使用静态类型,那么程序中变量的类型就是确定的。同时,由于所有类型在程序实际运行前都是已知的,编译器可以验证代码的正确性,从而减少运行时的类型检查,有助于性能提升。

  • 禁止在运行时改变对象布局:为实现最大性能,ArkTS要求在程序执行期间不能更改对象布局。

  • 限制运算符语义:为获得更好的性能并鼓励开发者编写更清晰的代码,ArkTS限制了一些运算符的语义。比如,一元加法运算符只能作用于数字,不能用于其他类型的变量。

  • 不支持Structural typing:对Structural typing的支持需要在语言、编译器和运行时进行大量的考虑和仔细的实现,当前ArkTS不支持该特性。根据实际场景的需求和反馈,我们后续会重新考虑。

当前,ArkTS在TypeScript的基础上,匹配ArkUI框架,并扩展了声明式UI、状态管理等能力,的主要扩展了如下能力:

  • 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。

  • 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活的利用这些能力来实现数据和UI的联动。

  • 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。

声明

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

变量

let hi: string = 'hello'
hi = 'hello, world'

由于ArkTS是一种静态类型语言,所有数据的类型都必须在编译时确定。

但是,如果一个变量或常量的声明包含了初始值,那么开发者就不需要显式指定其类型。ArkTS规范中列举了所有允许自动推断类型的场景。

let hi1: string = 'hello'
let hi2 = 'hello, world'

常量

const hello: string = 'hello'

number类型

ArkTS提供number和Number类型,任何整数和浮点数都可以被赋给此类型的变量。

数字字面量包括整数字面量和十进制浮点数字面量。

整数字面量包括以下类别:

  • 由数字序列组成的十进制整数。例如:0、117、-345
  • 以0x(或0X)开头的十六进制整数,可以包含数字(0-9)和字母a-f或A-F。例如:0x1123、0x00111、-0xF1A7
  • 以0o(或0O)开头的八进制整数,只能包含数字(0-7)。例如:0o777
  • 以0b(或0B)开头的二进制整数,只能包含数字0和1。例如:0b11、0b0011、-0b11

浮点字面量包括以下:

  • 十进制整数,可为有符号数(即,前缀为“+”或“-”);
  • 小数点(“.”)
  • 小数部分(由十进制数字字符串表示)
  • 以“e”或“E”开头的指数部分,后跟有符号(即,前缀为“+”或“-”)或无符号整数。
let n1 = 3.14
let n2 = 3.141592
let n3 = .5
let n4 = 1e10

function factorial(n: number): number {
  if (n <= 1) {
    return 1
  }
  return n * factorial(n - 1)
}

boolean类型

let isDone: boolean = false

string

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

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

void

void类型用于指定函数没有返回值。 此类型只有一个值,同样是void。由于void是引用类型,因此它可以用于泛型类型参数。

object

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

数组

// 方式1
let names: string[] = ['Alice', 'Bob', 'Carol']
let firstName = names[0]

// 方式2
let names: Array<string> = {'Jack', 'Rose'}
console.log(firstName)

两种方式都可以

for循环

当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的。
一些内置的类型如Array、Map、Set、string、等都具有可迭代性。

  • fori
  • for in
  • for of
let someArray = [1, 'string', false]
for (let item of someArray) {
    console.log(item)
}

for (let item in soneArray) {
    console.log(item)
}

元组

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let x: [string, number]
x = ['hello', 10]  // 正确
x = [10, 'hello']  // 错误

枚举

enum类型,又称枚举类型,是预先定义的一组命名值的值类型,其中命名值又称为枚举常量。 使用枚举常量时必须以枚举类型名称为前缀。

enum Msg {
  HI = 'hi',
  HELLO = 'hello'
}

// 如果不赋值,默认的值就是0、1、2累加
enum Color { Red, Green, Blue }
let c: Color = Color.Red

unknown

有时候,我们会想要为那些在编译阶段还不清楚类型的变量指定一个类型。那么可以用unknown类型来标记这些变量。

let notSure: unknown = 4
notSure = 'hello'
notSure = false

void

和Java一样,当一个函数没有返回值,通常会见到其返回类型是void。

null和undefined

TypeScript里,undefined和null两者各自有自己的类型,分别叫做undefined和null。

let u: undefined = undefined
let n: null = null

联合类型

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

class Cat {
  // ...
}
class Dog {
  // ...
}
class Frog {
  // ...
}
type Animal = Cat | Dog | Frog | number
// Cat、Dog、Frog是一些类型(类或接口)

let animal: Animal = new Cat()
animal = new Frog()
animal = 42
// 可以将类型为联合类型的变量赋值为任何组成类型的有效值

别名

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

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

其他运算符、表达式、三元运算符都和Java基本一样。

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

  }
}
函数

可选参数的格式可为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

// 匿名函数
let sun = function(n: number): number {
    return x + 1;
}

支持函数重载,和Java一样。

箭头函数或Lambda函数

函数可以定义为箭头函数,箭头函数是定义匿名函数的简写语法,类似lambda表达式,它省略的function关键字。

([param1, param2...]) => {
    ....
}

例如:

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
函数的可选参数

可以在参数名旁使用?实现可选参数的功能。 例如下面的函数lastName是可选的。

function buildName(firstName: string, lastName?: string) {
    .....
}

let result1 = buildName('bob')
let result2 = buildName('bob', 'adams')
可变参数

可变参数(剩余参数会)被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 可以使用省略号( ...)进行定义:

function getEmployeeName(firstName: string, ...restOfName: string[]) {
  return firstName + ' ' + restOfName.join(' ');
}

let employeeName = getEmployeeName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');
闭包

箭头函数通常在另一个函数中定义。作为内部函数,它可以访问外部函数中定义的所有变量和函数。

为了捕获上下文,内部函数将其环境组合成闭包,以允许内部函数在自身环境之外的访问。

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

let z = f()
console.log(z()) // 输出:1
console.log(z()) // 输出:2

在以上示例中,箭头函数闭包捕获count变量。

TypeScript具备面向对象编程的基本语法,例如interface、class、enum等。
也具备封装、继承、多态等面向对象基本特征。

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创建实例:

// 方式1
let p = new Person('John', 'Smith')
console.log(p.fullName())
// 方式2
let p = {name: 'Jack', surname:'Rose'}
console.log(p['name'])

或者,可以使用对象字面量创建实例:

class Point {
  x: number = 0
  y: number = 0
}
let p: Point = {x: 42, y: 42}
静态字段

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

字段初始化

为了减少运行时的错误和获得更好的执行性能, ArkTS要求所有字段在声明时或者构造函数中显式初始化。

接下来的代码展示了如果name的值可以是undefined,那么应该如何写代码。

class Person {
  name ?: string // 可能为`undefined`

  setName(n:string): void {
    this.name = n
  }

  // 编译时错误:name可以是"undefined",所以将这个API的返回值类型标记为string
  getNameWrong(): string {
    return this.name
  }

  getName(): string | undefined { // 返回类型匹配name的类型
    return this.name
  }
}

let jack = new Person()
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"

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

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

继承和实现

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
  }
}


interface DateInterface {
  now(): string;
}
class MyDate implements DateInterface {
  now(): string {
    // 在此实现
    return 'now is now'
  }
}
可见性修饰符

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

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

泛型类型和函数

泛型类型和函数允许创建的代码在各种类型上运行,而不仅支持单一类型。

类和接口可以定义为泛型,将参数添加到类型定义中,如以下示例中的类型参数Element:

class Stack<Element> {
  public pop(): Element {
    // ...
  }
  public push(e: Element):void {
    // ...
  }
}
typescript
class Stack<Element> {
  public pop(): Element {
    // ...
  }
  public push(e: Element):void {
    // ...
  }
}

要使用类型Stack,必须为每个类型参数指定类型实参:

let s = new Stack<string>
s.push('hello')
typescript
let s = new Stack<string>
s.push('hello')

编译器在使用泛型类型和函数时会确保类型安全。参见以下示例:

let s = new Stack<string>
s.push(55) // 将会产生编译时错误
typescript
let s = new Stack<string>
s.push(55) // 将会产生编译时错误
泛型约束

泛型类型的类型参数可以绑定。例如,HashMap<Key, Value>容器中的Key类型参数必须具有哈希方法,即它应该是可哈希的。

interface Hashable {
  hash(): number
}
class HasMap<Key extends Hashable, Value> {
  public set(k: Key, v: Value) {
    let h = k.hash()
    // ...其他代码...
  }
}

在上面的例子中,Key类型扩展了Hashable,Hashable接口的所有方法都可以为key调用。

泛型函数

使用泛型函数可编写更通用的代码。比如返回数组最后一个元素的函数:

function last(x: number[]): number {
  return x[x.length - 1]
}
console.log(last([1, 2, 3])) // 输出:3
typescript
function last(x: number[]): number {
  return x[x.length - 1]
}
console.log(last([1, 2, 3])) // 输出:3

如果需要为任何数组定义相同的函数,使用类型参数将该函数定义为泛型:

function last<T>(x: T[]): T {
  return x[x.length - 1]
}
typescript
function last<T>(x: T[]): T {
  return x[x.length - 1]
}

现在,该函数可以与任何数组一起使用。

在函数调用中,类型实参可以显式或隐式设置:

// 显式设置的类型实参
console.log(last<string>(['aa', 'bb']))
console.log(last<number>([1, 2, 3]))

// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
console.log(last([1, 2, 3]))
typescript
// 显式设置的类型实参
console.log(last<string>(['aa', 'bb']))
console.log(last<number>([1, 2, 3]))

// 隐式设置的类型实参
// 编译器根据调用参数的类型来确定类型实参
console.log(last([1, 2, 3]))

空安全

默认情况下,ArkTS中的所有类型都是不可为空的,因此类型的值不能为空。这类似于TypeScript的严格空值检查模式(strictNullChecks),但规则更严格。

在下面的示例中,所有行都会导致编译时错误:

let x: number = null    // 编译时错误
let y: string = null    // 编译时错误
let z: number[] = null  // 编译时错误
typescript
let x: number = null    // 编译时错误
let y: string = null    // 编译时错误
let z: number[] = null  // 编译时错误

可以为空值的变量定义为联合类型T | null。

let x: number | null = null
x = 1    // ok
x = null // ok
if (x != null) { /* do something */ }

空值合并运算符

空值合并二元运算符?? 用于检查左侧表达式的求值是否等于null。如果是,则表达式的结果为右侧表达式;否则,结果为左侧表达式。

换句话说,a ?? b等价于三元运算符a != null ? a : b。

在以下示例中,getNick方法如果设置了昵称,则返回昵称;否则,返回空字符串:

class Person {
  // ...
  nick: string | null = null
  getNick(): string {
    return this.nick ?? ''
  }
}
typescript
class Person {
  // ...
  nick: string | null = null
  getNick(): string {
    return this.nick ?? ''
  }
}

模块

程序可划分为多组编译单元或模块。

当应用复杂时,我们可以把通用功能都抽取到单独的ts文件中,每个文件都是一个模块(module),模块可以相互加载,提高代码复用性。

模块可以相互加载,并可以使用特殊的指令export和import来交换功能,从另一个模块调用一个模块的函数。 每个模块都有其自己的作用域,即,在模块中创建的任何声明(变量、函数、类等)在该模块之外都不可见,除非它们被显式导出。

与此相对,从另一个模块导出的变量、函数、类、接口等必须首先导入到模块中。

导出

可以使用关键字export导出顶层的声明。

未导出的声明名称被视为私有名称,只能在声明该名称的模块中使用。

注意:通过export方式导出,在导入时要加{}。

export class Point {
  x: number = 0
  y: number = 0
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }
}
export let Origin = new Point(0, 0)
export function Distance(p1: Point, p2: Point): number {
  return Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y))
}
导入

导入声明用于导入从其他模块导出的实体,并在当前模块中提供其绑定。导入声明由两部分组成:

导入路径,用于指定导入的模块; 导入绑定,用于定义导入的模块中的可用实体集和使用形式(限定或不限定使用)。 导入绑定可以有几种形式。

假设模块具有路径“./utils”和导出实体“X”和“Y”。

导入绑定* as A表示绑定名称“A”,通过A.name可访问从导入路径指定的模块导出的所有实体:

import * as Utils from './utils'
Utils.X // 表示来自Utils的X
Utils.Y // 表示来自Utils的Y

导入绑定{ ident1, ..., identN }表示将导出的实体与指定名称绑定,该名称可以用作简单名称:

import { X, Y } from './utils'
X // 表示来自utils的X
Y // 表示来自utils的Y

如果标识符列表定义了ident as alias,则实体ident将绑定在名称alias下:

import { X as Z, Y } from './utils'
Z // 表示来自Utils的X
Y // 表示来自Utils的Y
X // 编译时错误:'X'不可见

语句

和Java一样。

if ... else if ... else 
switch ... case ... default ...

字段初始化

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

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

getter和setter

setter和getter可用于提供对对象属性的受控访问。

class Person {
  name: string = ''
  private _age: number = 0
  get age(): number { return this._age }
  set age(x: number) {
    if (x < 0) {
      throw Error('Invalid age argument')
    }
    this._age = x
  }
}

let p = new Person()
console.log (p.age) // 将打印输出0
p.age = -42 // 设置无效age值会抛出错误

静态方法

使用关键字static将方法声明为静态。静态方法属于类本身,只能访问静态字段。

静态方法定义了类作为一个整体的公共行为。

所有实例都可以访问静态方法。

空安全

默认情况下,ArkTS中的所有类型都是不可为空的,因此类型的值不能为空。这类似于TypeScript的严格空值检查模式(strictNullChecks),但规则更严格。

在下面的示例中,所有行都会导致编译时错误:

let x: number = null    // 编译时错误
let y: string = null    // 编译时错误
let z: number[] = null  // 编译时错误

可以为空值的变量定义为联合类型T | null。

let x: number | null = null
x = 1    // ok
x = null // ok
if (x != null) { /* do something */ }

非空断言运算符

后缀运算符! 可用于断言其操作数为非空。

应用于空值时,运算符将抛出错误。否则,值的类型将从T | null更改为T:

let x: number | null = 1
let y: number
y = x + 1  // 编译时错误:无法对可空值作加法
y = x! + 1 // ok