传统的Web开发使用的语言:
- HTML: 控制页面元素
- CSS: 控制布局和样式
- JavaScript: 控制页面逻辑和数据状态
需要三种语言,而且语法不一样。
但是对华为来说Java又用不了,Web开发又太复杂,所以有了ArkTS。
为了确保应用开发的最佳体验,ArkTS提供对方舟开发框架ArkUI的声明式语法和其他特性的支持。由于此部分特性不在既有TypeScript的范围内。
但是对于Android和iOS的开发者,都会想到,如果有前端开发这一套,效率能行吗?
别忘了,鸿蒙还提供了方舟编译器(ArkCompiler),ArkCompiler AOT优化 : 隐藏继承优化、循环高级优化、过程空间优化、性能提升20%。 随着移动设备在人们的日常生活中变得越来越普遍,许多编程语言在设计之初没有考虑到移动设备,导致应用的运行缓慢、低效、功耗大,针对移动环境的编程语言优化需求也越来越大。ArkTS是专为解决这些问题而设计的,聚焦于提高运行效率。 ArkTS的一大特性是它专注于低运行时开销。ArkTS对TypeScript的动态类型特性施加了更严格的限制,以减少运行时开销,提高执行效率。通过取消动态类型特性,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'
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)
}
let isDone: boolean = false
字符串字面量由单引号(‘)或双引号(’)之间括起来的零个或多个字符组成。字符串字面量还有一特殊形式,是用反向单引号(`)括起来的模板字面量。
let s1 = 'Hello, world!\n'
let s2 = 'this is a string'
let a = 'Success'
let s3 = `The result is ${a}`
void类型用于指定函数没有返回值。 此类型只有一个值,同样是void。由于void是引用类型,因此它可以用于泛型类型参数。
Object类型是所有引用类型的基类型。任何值,包括基本类型的值(它们会被自动装箱),都可以直接被赋给Object类型的变量。
// 方式1
let names: string[] = ['Alice', 'Bob', 'Carol']
let firstName = names[0]
// 方式2
let names: Array<string> = {'Jack', 'Rose'}
console.log(firstName)
两种方式都可以
当一个对象实现了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类型来标记这些变量。
let notSure: unknown = 4
notSure = 'hello'
notSure = false
和Java一样,当一个函数没有返回值,通常会见到其返回类型是void。
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表达式,它省略的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模式一样。
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
- 邮箱 :charon.chui@gmail.com
- Good Luck!