都知道ArkTS是在保留TypeScript(简称TS)基本语法风格的基础上,进一步通过规范强化了静态检查和分析,使得开发者在程序开发阶段能够检测出更多错误,提升程序的稳定性和运行性能。那么对于没有TS语法学习经验的人来说,刚开始语法不是很习惯,或者说很难理解的,这里就对ArkTS一些常用语法做一些说明,帮助大家快速上手。

声明

变量:
变量是指可以在程序执行期间具有不同的值

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

常量:
常量是指只读类型,只能被赋值一次。

const hello: string = 'hello';
hello = 'hello, world'

这个时候会提示编译时错误:Cannot assign to ‘hello’ because it is a constant.(无法将值分配给“hello”,因为它是一个常量。)

当我们没有显式指定类型,ArkTS会自动推断类型。如:

let hi2 = 'hello, world';

这个时候会推断hi2为string类型。

类型

1、基本类型
1.1、number

let n1 = 3;
let n2 = 3.141592;
let n3 = 0.5;
let n4 = 1e2;

整数和浮点数都可以被赋给此类型的变量,类型范围为(-9007199254740991~9007199254740991),基本满足日常开发需要。如果需要更大的数据类型,那么可以使用下面的类型。
1.2、BigInt

let bigInt1: BigInt = BigInt('999999999999999999999999999999999999999999999999999999999999');
let bigInt2:BigInt = 0n;

以上两种写法都能正确的声明BigInt类型。

1.3、boolean
由true和false两个逻辑值组成。

let isDone: boolean = false;
if (isDone) {
  console.info('Done!');
}

1.4、string
代表字符序列,可以使用转义字符来表示字符。字面量由单引号(')或双引号(")之间括起来的零个或多个字符组成。字符串字面量还有一特殊形式,是用反向单引号(`)括起来的模板字面量。

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

1.5、void
void类型用于指定函数没有返回值。此类型只有一个值,同样是void。由于void是引用类型,因此它可以用于泛型类型参数。这个类型在定义接口的时候经常用到。

class Class<T> {
}
let instance: Class<void>;

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

let o1: Object = 'Alice';
let o2: Object = ['a', 'b'];
let o3: Object = 1;
let o4: object = [1, 2, 3];

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

let names: string[] = ['Alice', 'Bob', 'Carol'];

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

enum ColorSet { Red, Green, Blue }
let c: ColorSet = ColorSet.Red;

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

class Cat {
  name: string = 'cat';
}
class Dog {
  name: string = 'dog';
}
class Frog {
  name: string = 'frog';
}
type Animal = Cat | Dog | Frog | number | string | null | undefined;

let animal: Animal = new Cat();
animal = new Frog();
animal = 42;
animal = 'dog';
animal = undefined;

可以使用不同机制获取联合类型中的特定类型值。

class Cat { sleep () {}; meow () {} }
class Dog { sleep () {}; bark () {} }
class Frog { sleep () {}; leap () {} }

type Animal = Cat | Dog | Frog;

foo(animal: Animal) {
  if (animal instanceof Frog) {  // 判断animal是否是Frog类型
    animal.leap();  // animal在这里是Frog类型
  }
  animal.sleep(); // Animal具有sleep方法
}

如果以上代码没有看明白,那么可以看一下,下面的简单使用:

let value: string | number;
value = 'hello';
value = 123;

当使用联合类型的时候,单个变量可以拥有多个类型。那么结合上面的代码,会联想到重载这个概念,那么在Arkts中有没有重载这个概念呢?我们先看一下java的重载实现:

class Example {
    void print(int value) {
        System.out.println("Integer: " + value);
    }
    void print(String value) {
        System.out.println("String: " + value);
    }
}

Arkts的重载实现:

foo(x: number): void;
foo(x: string): void;
foo(x: number | string): void {
}

foo(123);
foo('aa');

根据代码我们不难看出Arkts重载实现只是定义了两个函数,而真正的函数实现只有一个,并且实现函数需兼容所有重载签名,通常需要在函数体内手动判断参数类型。这时候再看最开始的两个方法是不是就理解了。
2.5、Aliases
Aliases类型为匿名类型(如数组、函数、对象字面量或联合类型)提供名称,或为已定义的类型提供替代名称,即类型别名,使用关键字type进行声明。

// 二维数组类型
type Matrix = number[][];
const gameBoard: Matrix = [
  [1, 0],
  [0, 1]
];

// 函数类型
type Handler = (s: string, no: number) => string;
const repeatString: Handler = (str, times) => {
  return str.repeat(times);
};
console.info(repeatString('abc', 3)); // 'abcabcabc'

// 泛型函数类型
type Predicate<T> = (x: T) => boolean;
const isEven: Predicate<number> = (num) => num % 2 === 0;

// 可为空的对象类型
type NullableObject = Object | null;
class Cat {}
let animalData: NullableObject = new Cat();
let emptyData: NullableObject = null;

运算符

1、比较运算符
需要注意的是:=== / ==

// ==只比较目标的值相等
console.info(String(null == undefined)); // true
// ===比较目标的值和类型都相等
console.info(String(null === undefined)); // false

其他运算符与java等语言基本相同,这里不过多赘述。

函数

1、声明一个函数

add(x: string, y: string): string {
  let z: string = `${x} ${y}`;
  return z;
}

2、可选参数

hello(name?: string) {
  if (name == undefined) {
    console.info('Hello!');
  } else {
    console.info(`Hello, ${name}!`);
  }
}

如果一个函数拥有参数有值和没有值两种执行逻辑,这时候就可以用可选参数,参数用name?: Type的格式。除上面这种写法之外,还有一种默认参数值的写法:

multiply(n: number, coeff: number = 2): number {
  return n * coeff;
}
multiply(2);
multiply(2, 3);

如果在函数调用的时候,默认值参数省略,那么将会使用参数的默认值为实参,也就是multiply(2)实际返回结果是2*2.

3、rest参数
rest参数允许函数接收一个不定长数组,用于处理不定数量的参数输入。格式为…restName: Type[]。需要注意,函数的最后一个参数可以是rest参数,这是什么意思呢,请看下面代码:

// 正确示范 1
sum(...numbers: number[]): number{
    let res = 0;
    for (let n of numbers) {
      res += n;
    }
    return res;
  }
// 正确示范 2
sum(name:string, ...numbers: number[]): number{
    let res = 0;
    for (let n of numbers) {
      res += n;
    }
    return res;
  }
// 错误写法,提示错误:A rest parameter must be last in a parameter list. (rest参数必须位于参数列表的最后。)
sum(...numbers: number[], name:string): number{
    let res = 0;
    for (let n of numbers) {
      res += n;
    }
    return res;
  }

4、函数类型
通常用于定义回调函数。

export type trigFunc = (x: number) => number // 这是一个函数类型
export class Test {
   static do_action(f: trigFunc) {
     const result = f(3); // 调用函数
     console.log("回调函数执行结果:", result);
   }

 static test(){
   Test.do_action( (x) =>{
     return x * 2
   })
  }
}

// 在Index中调用
aboutToAppear(): void {
  Test.test();
}

输出结果为:6;那么为什么会输出6呢,我们跟着代码来理一下。
在do_action函数中,trigFunc被作为参数使用,然后对f进行赋值3,当调用do_action方法时,x * 2其实就已经变成3*2,所以结果为6。

5、箭头函数(Lambda函数)

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

class Person {
  name: string = '';
  surname: string = '';
  constructor (n: string, sn: string) {
    this.name = n;
    this.surname = sn;
  }
  fullName(): string {
    return this.name + ' ' + this.surname;
  }
}

基础写法如上,constructor为构造函数,其他相关知识与java等语言相视,这里不做过多解释。需要详细讲解的内容如下。

1、对象字面量
对象字面量是一个表达式,可用于创建类实例并提供一些初始值。它在某些情况下更方便,可以用来代替new表达式。表示方式是:封闭在花括号对({})中的’属性名:值’的列表。

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

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

需要注意的是,对象字面量只能在可以推导出该字面量类型的上下文中使用。

1.2、Record类型的对象字面量
泛型Record<K, V>用于将类型(键类型)的属性映射到另一个类型(值类型)。类型K可以是字符串类型或数值类型(不包括BigInt),而V可以是任何类型。

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

2、抽象类
抽象类的子类可以是抽象类也可以是非抽象类。抽象父类的非抽象子类可以实例化。

abstract class Base {
  field: number;
  constructor(p: number) {
    this.field = p;
  }
}

class Derived extends Base {
  constructor(p: number) {
    super(p);
  }
}

let x = new Derived(666);

2.1、抽象方法
只有抽象类内才能有抽象方法,带有abstract修饰符的方法称为抽象方法,抽象方法可以被声明但不能被实现。

class Y {
  abstract method(p: string)  //编译时错误:抽象方法只能在抽象类内。
}

抽象类和抽象方法存在的意义是:定义统一的 “行为规范”(抽象方法),约束所有子类必须遵循这个规范,同时封装子类的通用逻辑(抽象类的普通方法),实现 “规范统一、逻辑复用、差异化实现”。

接口

我们都知道在Java中接口的作用是为了实现解耦和统一规范,在Arkts中也有接口的存在,其作用与Java相同。
接口示例:

interface Style {
  color: string; // 属性
}
interface AreaSize {
  calculateAreaSize(): number; // 方法的声明
  someMethod(): void;     // 方法的声明
}

实现接口类的示例:

// 接口:
interface AreaSize {
  calculateAreaSize(): number; // 方法的声明
  someMethod(): void;     // 方法的声明
}

// 实现:
class RectangleSize implements AreaSize {
  private width: number = 0;
  private height: number = 0;
  someMethod(): void {
    console.info('someMethod called');
  }
  calculateAreaSize(): number {
    this.someMethod(); // 调用另一个方法并返回结果
    return this.width * this.height;
  }
}

1、接口继承
接口可以继承其他接口,示例如下:

interface Style {
  color: string;
}

interface ExtendedStyle extends Style {
  width: number;
}

继承接口包含被继承接口的所有属性和方法,此外还可以添加自己的属性和方法。

泛型类型和函数

1、泛型类和接口
类和接口可以定义为泛型,将参数添加到类型定义中。

class CustomStack<Element> {
  public push(e: Element):void {
    // ...
  }
}

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

let s = new CustomStack<string>();
s.push('hello');

1.2、泛型约束
泛型类型的类型参数可以被限制只能取某些特定的值。例如,MyHashMap<Key, Value>这个类中的Key类型参数必须具有hash方法,Key类型扩展了Hashable,Hashable接口的所有方法都可以为key调用。

interface Hashable {
  hash(): number;
}
class MyHashMap<Key extends Hashable, Value> {
  public set(k: Key, v: Value) {
    let h = k.hash();
  }
}

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

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

2.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>(): void {
  // ...
}
foo();
// 此函数在语义上等价于下面的调用
foo<number>();

空安全

默认情况下,ArkTS中的所有类型都不允许为空,但是可以为空值的变量定义为联合类型T | null。

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

let x: number | null = null; // 这种联合类型是允许的
x = 1;    // ok
x = null; // ok
if (x != null) 

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

class A {
  value: number = 0;
}

function foo(a: A | null) {
  a.value;   // 编译时错误:无法访问可空值的属性
  a!.value;  // 编译通过,如果运行时a的值非空,可以访问到a的属性;如果运行时a的值为空,则发生运行时异常
}

通过上面代码我们知道,当使用!进行非空断言时,如果变量为空,对其属性进行访问时,会出现运行时异常,所以在开发中不建议使用非空断言(有此业务/编码需求的除外)。

2、空值合并运算符
空值合并二元运算符??用于检查左侧表达式的求值是否等于null或者undefined。如果是,则表达式的结果为右侧表达式;否则,结果为左侧表达式。(a ?? b等价于三元运算符(a != null && a != undefined) ? a : b)

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

3、可选链
访问对象属性时,如果属性是undefined或null,可选链运算符返回undefined。

class Person {
  nick: string | null = null;
  spouse?: Person

  setSpouse(spouse: Person): void {
    this.spouse = spouse;
  }

  getSpouseNick(): string | null | undefined {
    return this.spouse?.nick;
  }

  constructor(nick: string) {
    this.nick = nick;
    this.spouse = undefined;
  }
}
Logo

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

更多推荐