ArkTS
作为鸿蒙开发的编程语言,我们先来一图看看这个语言,我们可以看到ArkTS是在TS(TypeScript)的基础上改造的,而TS又是在JS(JavaSript)上改造的,一句话总结就是ArkTS是TS的超集,TS是JS的超集。但ArkTS虽然基于TS,但在某些方面可能施加更严格的类型约束,以适应跨端开发的需求
废话不多说,我们紧接着来说语法,在语法上,其与其他编程语言在语法上并没有很本质的不同,毕竟都是高级的开发编程语言,大家都是有各自的特点,但目的都是为了开发软件,我们就仅在语言层面进行总结
语法
变量类型及变量声明方式
基础的变量声明方式
基础数据类型
- number
- string
- boolean
复合数据类型
- Array(容器类型——还有很多其它的容器如)
let arr = new Array<string>();
- Object(类型)
Object 类型是一个所有非原始类型的基类。原始类型包括 string、number、boolean等内置类型,所以Object 类型可以接收所有非原始类型的值,如对象字面量、数组、函数等let obj:Object = new Stack<number>();
但要注意在默认情况下,ArkTS中所有类型都不允许赋值为null或undefined
其它特殊类型
-
联合类型
联合类型即给声明能够接受多种类型的变量,通过竖线(“|”)将不同的类型联合起来,自定义类型也可以联合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; // 可以将类型为联合类型的变量赋值为任何组成类型的有效值
-
void类型
一般void类型都是用于作为函数返回值 -
null和undefined
- null类型主要表示空值,用于明确表示一个变量没有指向任何对象或暂时不知道给什么具体值
- undefined主要表示一个变量声明了但还未赋值,如在函数使用了可选参数未传递时则该参数的值就为undefined
总结来说,null是一个明确的“空”值,而undefined表示一个变量或值尚未被定义或赋值。
-
enum类型
枚举类型即包含一组常量的集合,如enum Color { Red, Green, Blue }
定义了该类型后可以在声明类型时使用该类型进行声明,如
let favoriteColor: Color = Color.Green; console.log(favoriteColor); // 输出:1
-
元组类型(tuple)
本质也是一个数组,不过是知道元素类型的数组,如:let tuple: [string, number, boolean] = ["hello", 42, true];
访问元素时也是通过数组下标进行访问,不过需要注意对应的类型匹配,元组的元素数量和类型必须与定义时一致,否则会导致编译错误。
补充说明
- 匿名类型(起别名):type
Aliases类型为匿名类型(数组、函数、对象字面量或联合类型)提供名称,或为已有类型提供替代名称type int = number; let i: int = 1; type Matrix = number[][]; type Handler = (s: string, no: number) => string; type Predicate <T> = (x: T) => Boolean; type NullableObject = Object | null;
- tips:注意在ArkTS中,类型都必须指明,但在TS中可以使用any关键字接收任意类型的变量,需要注意区分
- 空值安全
上面说过了,默认情况下,ArkTS中的所有类型都是不可为空的,因此类型的值不能为空,当使用的变量为空或者有可能为空时,编译器都会报错,但一些情况下我们也要赋空值或undefined,下面情况说明了如何在ArkTS中能够使用空值和undefined初始化变量- 使用非空断言运算符(!):紧接在变量后使用"!",判断变量为空时则不使用该值,不为空时则使用该值
class C { value: number | null = 1; } let c = new C(); let y: number; y = c.value + 1; // 编译时错误:无法对可空值作做加法 y = c.value! + 1; // ok,值为2 // 若c.value为空时则值为1
- 空值合并运算符(??):a ?? b等价于三元运算符(a != null && a != undefined) ? a : b,即如果a为空或undefined时则值等于b,否则值等于a
- 可选链(后缀运算符?):在访问某个变量时,在其后加个"?“,访问时则会判断其是否为null或undefined,如果是则返回undefined,不是则访问该变量。在一些类中声明变量时在其后加个”?"也表示该变量可为undefined
// 如果一个Person的实例有不为空的spouse属性, // 且spouse有不为空的nick属性,则输出spouse.nick。否则,输出undefined class Person { nick: string | null = null spouse?: Person constructor(nick: string) { this.nick = nick; this.spouse = undefined; } } let p: Person = new Person('Alice'); console.log(p.spouse?.nick); // undefined) p.spouse = p; console.log(p.spouse?.nick);
其它部分常用语句
此处仅列举与C++有些区别的语句
for
-
一般的循环:for(let i = 0; i < 10; i+= 2){…}
-
范围for: for (forVar of expression) {statements}
forVar: expression的一个元素;expression:一个“容器” 如:
for (let ch of 'a string object') {/* process ch */}
break
一般正常使用
如果break语句后带有标识符,则将控制流转移到该标识符所包含的语句块之外。如:
let x = 1
label: while (true) {
switch (x) {
case 1: // statements
break label // 中断while语句
}
}
函数
函数的声明
函数声明中,函数的返回值类型可以省略
tips:返回值的类型也可以是一个联合类型
函数参数
- 可选参数:name?:type
此处的name如果未传入则是undefinedfunction 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); // 返回22 multiply(2, 3); // 返回23
- 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 // 使用Object类型时的数组,可传入多种不同类型的参数,都会保存到给的数组里 function exampleFunction(...args: Object[]) { args.forEach(arg => { console.log(arg.toString()); }); } exampleFunction(1, 'string', true);
函数类型——箭头函数(lambda表达式)
lambda表达式
语法:(参数列表):返回值=>{函数体}
我们可以将函数类型保存:type trigFunc = (x: number) => number // 这是一个函数类型
lambda的返回值可省略,返回值省略时根据函数体推导返回值,函数体仅一行时可以去掉大括号,所以指向类型为number时说明返回值的类型是number,代码含义即为一个函数类型为参数是一个number类型,返回值为number类型的函数
函数闭包
即在函数体内部又包含一个函数,那么这个函数从声明到内部这包含的另一个函数之间的作用域的变量会被保存下来(类似变成了局部全局)
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); // 使用第一个定义
foo('aa'); // 使用第二个定义
类
类的基本操作都与C++相似,通过上面的变量、函数的内容即可封装一个类,下面主要介绍一些不同的地方
关于封装
ArkTS中,要求所有成员必须在声明时就进行定义初始化(或在构造函数中进行初始化),即所有字段在声明时或者构造函数中显式初始化
但也可以通过可选链的方式表示为undefined,但此时使用值时则需要对应注意
getter和setter
在类内实现getter和setter时,首先可以使用deveco一键生成
我们要关注一点的是此处生成的函数形式是get/set 属性(): 返回类型{return 返回值} 而不是一般的声明函数的方式,如:
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值会抛出错误
构造函数
构造函数在实现上与其他语言也大体相似,未提供构造时默认会自动创建具有空参数列表的默认构造函数
但需要注意的是,构造函数也可以重载多个形式,但与函数重载相同,重载时实现在类中只能有一个,重载定义可以有多个,如:
class C {
constructor(x: number) /* 第一个签名 */
constructor(x: string) /* 第二个签名 */
constructor(x: number | string) { /* 实现签名 */
}
}
let c1 = new C(123); // OK,使用第一个签名
let c2 = new C('abc'); // OK,使用第二个签名
// 以下方式则是错误的
class A{
constructor(x: number) {
}
constructor(x: string) {
}
}
关于类的实例创建
-
类都是使用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};
-
静态字段:使用static声明变量即可(而不是用let或const)
-
继承
继承时,父类中受访问限定符成员也会相同继承到子类- 使用extends进行继承,只能继承一个类
但可以通过继承接口来继承多个接口,使用implements- 一个类,可以同时继承某一个类之后再继承多个接口
- 继承接口时,必须实现接口内定义的所有方法(除非接口内的方法已有默认实现)
- 在子类中,父类通过super进行访问,super即代表了父类的实例
- 在继承类了父类的子类中,构造函数第一行必须通过super(…)显式的调用父类的构造函数,当然如果不提供构造函数,会使用默认生成的构造函数,其会自动去调用父类的构造函数
- 关于继承后的方法重写
- 重写本质是为了形成多态
- 重写时需要与被重写的方法具有相同的方法名、参数类型与返回类型
- 类中的重载——本质也和arkts的函数重载相同,只能重载定义(建立签名),实现只能有一个。重载的函数的名称和参数列表不能均相同
- 使用extends进行继承,只能继承一个类
-
多态在继承后直接重写同名函数即可
-
类的创建
- 直接使用new创建对象
- 关于对象字面量:封闭在花括号对({})中的’属性名:值’的列表。
-
使用{…}(对象字面量)创建,内部的参数为类内的成员(按照顺序进行初始化)
但是要确保对象字面量的结构符合类的结构,但并没有创建类的实例。 -
对象字面量只能在可以推导出该字面量类型的上下文中使用。其他正确的例子:
class C { n: number = 0 s: string = '' } let c: C = {n: 42, s: 'foo'}; // 若类C为下述方式,则无法保证对应的对象字面量是C类的实例 class C { a: number = 0; b: string = ''; constructor(a: number, b : string) { } }
-
-
Record类型(就是哈希表的一种,但该语言中也有Map类型)
let hash = new Map<Number, string>();//哈希表类型
泛型Record<K, V>用于将类型(键类型)的属性映射到另一个类型(值类型)
- Key只能是字符串或数值类型,但Value可以是任意类型
tips:使用时需要先进行初始化
// 创建一个哈希表 let hashTable: NumberStringHashTable = {}; // 访问和修改哈希表中的值 console.log(hashTable[1]); // 输出: onehashTable[1] = 'ONE'; console.log(hashTable[1]); // 输出: ONE // 添加新的键值对 hashTable[4] = 'four'; // 遍历哈希表 Object.keys(hashTable).forEach(key => {\ console.log(`${key} = ${hashTable[key]}`);
接口
- 接口的声明
- 使用interface关键字
- 接口可以继承其他接口,其他类继承可以继承多个接口(implements)
- 接口中的函数声明可以不使用(省略掉)function关键字
泛型编程(模板)
用于定义模板类、模板接口以及模板函数等,直接用尖括号表示即可
-
模板类和接口
class CustomStack<Element> { public push(e: Element):void { // ... } } // 要使用类型CustomStack,必须为每个类型参数指定类型实参 let s = new CustomStack<string>(); s.push('hello'); // 在使用泛型类型和函数时会确保类型安全 s.push(55); // 将会产生编译时错误
-
泛型类型的类型参数可以绑定.例如,HashMap<Key, Value>容器中的Key类型参数必须具有哈希方法,即它应该是可哈希的。
即通过继承接口的方式,使得对应的泛型参数(类型)在传递过来时必须实现对应的接口,
如Key
类型参数被约束为Hashable
接口的实现。这意味着只有那些实现了hash
方法的类型才能用作这个哈希映射的键。interface Hashable { hash(): number } class HasMap<Key extends Hashable, Value> { public set(k: Key, v: Value) { let h = k.hash(); // ...其他代码... } }
-
泛型函数
语法:
function last<T>(x: T): T
在函数名后加上尖括号,之后即可在参数列表、返回类型以及函数体中使用对应的模板参数
返回数组最后一个元素的函数
function last(x: number[]): number { return x[x.length - 1]; } last([1, 2, 3]); // 3 // 模板函数: function last<T>(x: T[]): T { return x[x.length - 1]; }
-
泛型的默认值
在泛型的类型参数可以设置默认值,此时使用时则可以不指定泛型的形参而直接使用泛型类型的名称,如下
class SomeType {} interface Interface <T1 = SomeType> { } class Base <T2 = SomeType> { } class Derived1 extends Base implements Interface { } // Derived1在语义上等价于Derived2 class Derived2 extends Base<SomeType> implements Interface<SomeType> { } function foo<T = number>(): T { // ... } foo(); // 此函数在语义上等价于下面的调用 foo<number>();
模块
常见的为ets文件中的模块的导入与导出
模块即可以理解是一个作用域,小到函数体、类,大到文件等,每个模块都有自己的作用域,该模块之外都不可见,除非被显式导出
与此相对,从另一个模块导出的变量、函数、类、接口等必须首先导入到模块中
-
导出(类、变量、函数都可导出)
- 通过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)); }
-
导入
-
静态导入
即直接使用导入声明对想要的模块直接先进行导入,导入声明用于导入从其他模块导出的实体,并在当前模块中提供其绑定。导入声明由两部分组成:
- 导入路径,用于指定导入的模块;
- 导入绑定,用于定义导入的模块中的可用实体集和使用形式(限定或不限定使用)
- 导入形式
-
直接导入,并且绑定名称
import * as Utils from './utils' Utils.X // 表示来自Utils的X Utils.Y // 表示来自Utils的Y
-
绑定对应的导入实体名称
import { X, Y } from './utils' X // 表示来自utils的X Y // 表示来自utils的Y
-
给绑定的导入实体起别名
import { X as Z, Y } from './utils' Z // 表示来自Utils的X Y // 表示来自Utils的Y X // 编译时错误:'X'不可见
-
-
动态导入(后续再进行详解)
动态导入即是根据条件导入模块(或按需导入),使用import()语法实现
import()语法通常称为动态导入dynamic import,是一种类似函数的表达式,用来动态导入模块。以这种方式调用,将返回一个promise。
-