【HarmonyOS NEXT】ArkTs函数、类、接口、泛型、装饰器解析与使用

2024-12-17 19:32:24
122次阅读
0个评论

1. 前置学习文档

  1. 【HarmonyOS NEXT】ArkTs数据类型解析与使用(https://juejin.cn/spost/7448894500348608522)

2. 前言

  在原生JavaScript中只有函数和类的实现,为了更好的面向对象编程TypeScript 引入了接口、泛型、装饰器等特性。ArkTS也继承了这些特性。

3.函数

3.1 函数声明

函数声明引入一个函数,包含其名称、参数列表、返回类型和函数体。
以下示例是一个简单的函数,包含两个string类型的参数,返回类型为string:

function add(x: string, y: string): string {
  let z: string = `${x} ${y}`;
  return z;
}
//另外一种写法,如果能推断出返回类型,可以省略返回类型【但是不建议这么做,转眼间你就看不出返回类型】
function add(x: string, y: string) {
  let z: string = `${x} ${y}`;
  return z;
}

3.2 可选参数

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

3.3 Rest参数[剩余参数]

函数的最后一个参数可以是rest参数。使用rest参数时,允许函数或方法接受任意数量的实参。类似于Kotlin中的可变参数 vararg

rest参数ES6新增的特性,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

3.4 函数类型

函数类型指的是,可以使用Aliases类型 关键字 type 来声明指定的函数类型,Kotlin 也有类似的特性,typealias 简单来说,就是可以把一个函数当做参数传递。

type trigFunc = (x: number) => number // 这是一个函数类型


function do_action(f: trigFunc,args:number):number {
  return f(args);//调用函数
}

function add(x: number): number {
  return x + 10086
}

function sub(x: number): number {
  return x - 10086
}

console.log(do_action(add,100).toString())//输出10186
console.log(do_action(sub,100).toString())//-9986

3.5 箭头函数或Lambda函数

函数可以定义为箭头函数,例如:

//转箭头函数之间
let sum = function (x: number, y: number): number {
  return x + y;
}
//转箭头函数之后
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

3.6 闭包

箭头函数通常在另一个函数中定义。作为内部函数,它可以访问外部函数中定义的所有变量和函数。
为了捕获上下文,内部函数将其环境组合成闭包,以允许内部函数在自身环境之外的访问。
闭包属于JS中比较特殊的内容,在ArkTS中和TS/JS也有一定出入,这里后期专门出一篇文档来说明优缺点和使用场景。

//这个示例中,箭头函数闭包捕获count变量。
function f(): () => number {
  let count = 0;
  return (): number => { count++; return count; }
}

let z = f();
z(); // 返回:1
z(); // 返回:2


//闭包实例代码
function fn1() {
  let a = 1;
  function fn2() {
    a++;
    console.log(a);
  }
  return fn2;
}
const fn2 = fn1();
//闭包函数执行完后外部作用域变量仍然存在,并保持状态
fn2() //2
fn2() //3

3.7 函数重载

通过Union类型声明,即可实现函数中一个属性,支持多种类型

function foo(x: number): void;            /* 第一个函数定义 */
function foo(x: string): void;            /* 第二个函数定义 */
function foo(x: number | string): void {  /* 函数实现 */
}

foo(123);     //  OK,使用第一个定义
foo('aa'); // OK,使用第二个定义

4. 类

ES5之前不存在类的概念,为了使JavaScript更像面向对象,ES6版本引入class概念,但其本质是基于函数去实现的,感兴趣的可以看下面的几篇文章:

4.1 声明和使用

在以下示例中,定义了Person类,该类具有字段name和surname、构造函数和方法fullName:

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

4.2 构造函数

constructor方法是一个特殊的方法,这种方法用于创建和初始化一个由class创建的对象。一个类只能拥有一个名为“constructor”的特殊方法【JS中的Class 本质是基于函数去实现的】。因为只能拥有一个class 构造函数,所以在ArkTS中,没有向Java类的重载。
如果不指定一个构造函数 (constructor) 方法,则使用一个默认的构造函数 (constructor)

//构造函数定义如下:
constructor ([parameters]) {
  // ...
}

//如果未定义构造函数,则会自动创建具有空参数列表的默认构造函数,例如:
class Point {
  x: number = 0
  y: number = 0
}
let p = new Point();

4.3 通过联合类型实现重载

// 声明
function test(param: User): number;
function test(param: number, flag: boolean): number;
// 实现
function test(param: User | number, flag?: boolean) {
  if (typeof param === 'number') {
    return param + (flag ? 1 : 0)
  } else {
    return param.age
  }
}

4.4 静态字段

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

class Person {
  static numberOfPersons = 0
  constructor() {
     // ...
     Person.numberOfPersons++;
     // ...
  }
}
Person.numberOfPersons;

4.5 字段初始化[必看]

为了减少运行时的错误和获得更好的执行性能,
ArkTS要求所有字段在声明时或者构造函数中显式初始化。这和标准TS中的strictPropertyInitialization模式一样。
以下代码是在ArkTS中不合法的代码。

class Person {
  name: string // undefined
  
  setName(n:string): void {
    this.name = n;
  }
  
  getName(): string {
    // 开发者使用"string"作为返回类型,这隐藏了name可能为"undefined"的事实。
    // 更合适的做法是将返回类型标注为"string | undefined",以告诉开发者这个API所有可能的返回值。
    return this.name;
  }
}

let jack = new Person();
// 假设代码中没有对name赋值,例如调用"jack.setName('Jack')"
jack.getName().length; // 运行时异常:name is undefined

在ArkTS中,应该这样写代码。

class Person {
  name: string = ''
  
  setName(n:string): void {
    this.name = n;
  }
  
  // 类型为'string',不可能为"null"或者"undefined"
  getName(): string {
    return this.name;
  }
}

如果你非要声明一个没有初始值的字段,那么可以这样写

class Person {
  name?: string = ''

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

  getName(): string {
    return this.name ?? ""; //注意这里,因为name 未赋值,则是undefined类型,为了保证控安全,使用了空值合并运算符 ?? 来保证一定有值
  }
}

4.6 getter和setter【存取器(Accessors)】

setter和getter可用于提供对对象属性的受控访问。这里的get set方法和kotlin中的类似

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值会抛出错误

4.7 可见性修饰符

类的方法和属性都可以使用可见性修饰符。
可见性修饰符包括:private、protected和public。默认可见性为public。

  • Public(公有)
    public修饰的类成员(字段、方法、构造函数)在程序的任何可访问该类的地方都是可见的。
  • Private(私有)
    private修饰的成员不能在声明该成员的类之外访问,例如:
class C {
  public x: string = ''
  private y: string = ''
  set_y (new_y: string) {
    this.y = new_y; // OK,因为y在类本身中可以访问
  }
}
let c = new C();
c.x = 'a'; // OK,该字段是公有的
c.y = 'b'; // 编译时错误:'y'不可见
  • Protected(受保护)
    protected修饰符的作用与private修饰符非常相似,不同点是protected修饰的成员允许在【子类/派生类】中访问,例如:
class Base {
  protected x: string = ''
  private y: string = ''
}
class Derived extends Base {
  foo() {
    this.x = 'a'; // OK,访问受保护成员
    this.y = 'b'; // 编译时错误,'y'不可见,因为它是私有的
  }
}
  • readonly修饰符
    你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。 熟悉kotlin 的会发现这个特性特别像 kotlin class 中的 val 修饰符
    ArkTS
class Dog {
  public readonly name: String;
  public constructor(name: string) {
    this.name = name;
  }
}

let dog = new Dog("旺财")
dog.name = "狗蛋" //不允许修改

Kotlin

class Dog(val name: String)

val dog = Dog("旺财")
dog.name="狗蛋" //不允许修改

4.8 对象字面量

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

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

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

ArkTS是静态类型语言,如上述示例所示,对象字面量只能在可以推导出该字面量类型的上下文中使用。其他正确的例子:

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

function foo(c: C) {}

let c: C

c = {n: 42, s: 'foo'};  // 使用变量的类型
foo({n: 42, s: 'foo'}); // 使用参数的类型

function bar(): C {
  return {n: 42, s: 'foo'}; // 可以推导出来,所以可以直接使用返回类型 
}

也可以在数组元素类型或类字段类型中使用:

class C {
  n: number = 0
  s: string = ''
}
let cc: C[] = [{n: 1, s: 'a'}, {n: 2, s: 'b'}];

4.9 继承

一个类可以继承另一个类(称为基类)
继承类继承基类的字段和方法,但不继承构造函数。继承类可以新增定义字段和方法,也可以覆盖其基类定义的方法。
基类也称为“父类”或“超类”。继承类也称为“派生类”或“子类”。

class [extends BaseClassName]  {
  // ...
}
class Animal {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    return `我的名字是 ${this.name}`;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name); // 调用父类的 constructor(name)
    console.log(this.name);
  }
  sayHi() {
    return '你好, ' + super.sayHi(); // 调用父类的 sayHi()
  }
}

let a = new Animal('张三');
console.log(a.sayHi()); // 我的名字是张三

let c = new Cat('李四');
console.log(c.sayHi()); // 你好, 我的名字是李四

4.10 抽象类

abstract用于定义抽象类和其中的抽象方法。
什么是抽象类?

  1. 抽象类是不允许被实例化的
  2. 抽象类中的抽象方法必须被子类实现
abstract class Animal {
    public name;
    public constructor(name) {
        this.name = name;
    }
    public abstract sayHi();
}

class Cat extends Animal {
    public sayHi() {
        console.log(`Meow, My name is ${this.name}`);
    }
}
let a = new Animal('张三');//这种不可以,因为抽象类不能被实例化

let cat = new Cat('Tom');

5. 接口

接口声明引入新类型。接口是定义代码协定的常见方式。
任何一个类的实例只要实现了特定接口,就可以通过该接口实现多态。
接口通常包含属性和方法的声明

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

// 实现:
class RectangleSize implements AreaSize {
  color: string;
  width: number
  height: number

  constructor(color: string, width: number, height: number) {
    this.color = color
    this.width = width
    this.height = height
  }

  someMethod(): void {
    console.log('计算面积之前得方法');
  }

  calculateAreaSize(): number {
    this.someMethod();
    return this.width * this.height;
  }
}

5.1 接口属性

接口属性可以是字段、getter、setter或getter和setter组合的形式。
属性字段只是getter/setter对的便捷写法。以下表达方式是等价的:

interface Style {
  color: string
}

interface Style {
  get color(): string
  set color(x: string)
}

实现接口的类也可以使用以下两种方式:

interface Style {
  color: string
}

class StyledRectangle implements Style {
  color: string = ''
}

interface Style {
  color: string
}

class StyledRectangle implements Style {
  private _color: string = ''
  get color(): string { 
    //获取颜色之前可以做一些处理
    return this._color;
  }
  set color(x: string) {
    //设置颜色之前可以做一些校验
    this._color = x;
  }
}

5.2 接口继承

接口可以继承其他接口,继承接口包含被继承接口的所有属性和方法,还可以添加自己的属性和方法,如下面的示例所示:

interface Style {
  color: string
}

interface ExtendedStyle extends Style {
  width: number
}

class Cat implements ExtendedStyle {
  width: number;
  color: string;

  constructor(width: number, color: string) {
    this.width = width
    this.color = color
  }
}

6. 泛型

如果需要创建可重用的组件,一个组件可以支持多种类型的数据。此时就可以用到泛型来实现。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

6.1 使用

//下面是一个创建指定长度和指定类型数组的方法
function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

createArray<string>(3, 'x'); // 输出长度为3,内容都是X的数组,['x', 'x', 'x']
createArray<number>(3, 1); // 输出长度为3,内容都是1的数组,[1, 1, 1]

6.2 泛型约束

我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在下面 printLength例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

这个特性类似Kotlin 泛型中的型变特性 ,即只可以消费而不可以生产

function printLength<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

此时泛型约束就派上了用场,我们定义一个接口来描述约束条件。 创建一个包含.length属性的接口,使用这个接口和extends关键字来实现约束:

interface LengthInterface {
  length: number;
}

function loggingIdentity<T extends LengthInterface>(arg: T): T {
  console.log(arg.length+"");
  return arg;
}

loggingIdentity<string>("小猪佩奇身上纹")//日志打印 7【因为string 】

6.3 泛型默认值

泛型类型的类型参数可以设置默认值。这样可以不指定实际的类型实参,而只使用泛型类型名称。
当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

interface A<T=string> {
  name: T;
}

const strA: A = { name: 123 };//报错,因为没有指定泛型类型,泛型默认是string类型,但是创建的泛型是数字类型
const numB: A<number> = { name: 101 };//正确,因为指定了类型,所以可用

7. 装饰器

  • 随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
  • 装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
  • 在ArkTS中 常见的状态管理其实就是一种装饰器

7.1 装饰器的分类

  • 类装饰器(Class decorators)
  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

7.2 类装饰器

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

类装饰器顾名思义,就是用来装饰类的。它接收一个参数:

  • target: TFunction - 被装饰的类

7.3 属性装饰器

属性装饰器声明:

declare type PropertyDecorator = (target:Object, 
  propertyKey: string | symbol ) => void;

属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 被装饰类的属性名

7.4 方法装饰器

方法装饰器声明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,                  
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 属性描述符

7.5 参数装饰器

参数装饰器声明:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中参数的索引值

7.6 综合例子

7.6.1 新建装饰器

新建一个GlobalDecorators.ets 的文件用于存放自定义装饰器的方法

/**
 * @param target: TFunction - 被装饰的类
 */
export function 类装饰器(target: Object) {
  console.log("我是类装饰器:target:" + target)
}

/**
 * @param target - 被装饰的类
 * @param key 被装饰类的属性名
 */
export function 属性装饰器(target: Object, key: string) {
  console.log("我是属性装饰器:target:" + target + "__key:" + key)
}

/**
 * @param target: Object - 被装饰的类
 * @param propertyKey: string - 方法名
 * @param descriptor: PropertyDescriptor - 属性描述符
 */
export function 方法装饰器(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("我是方法装饰器:target:" + target + "__propertyKey:" + propertyKey + "__descriptor:" + descriptor)
}

/**
 * @param target: Object - 被装饰的类
 * @param propertyKey: string - 方法名
 * @param parameterIndex: number - 方法中参数的索引值
 */
export function 参数装饰器(target: Object, propertyKey: string, parameterIndex: number) {
  console.log("我是参数装饰器:target:" + target + "__propertyKey:" + propertyKey + "__parameterIndex:" + parameterIndex)
}

7.6.2 使用装饰器

import { Person, 类装饰器, 属性装饰器, 方法装饰器 } from '../GlobalDecorators';

@类装饰器
export class Person {
  @属性装饰器
  public name: string = ""

  @方法装饰器
  dog(@参数装饰器 str: string) {
    console.log(str)
  }
}

7.7 ArkTs装饰器和Java注解的区别

特性 ArkTs 装饰器 Java 注解
设计目的 用于修改类的结构和行为 提供元数据信息,不直接影响行为
运行时行为 可以直接影响对象的行为,因为它们是函数 不直接影响行为,需要通过反射等机制来处理
实现机制 作为运行时执行的函数 作为元数据标记,可选地通过注解处理器在编译时处理
使用场景 类、方法、属性、参数、访问器的装饰 类、方法、字段、参数等的元数据描述
参数 可以接受参数 可以接受参数,通常作为配置选项
反射 TypeScript不直接支持反射,但可以通过JavaScript特性来实现 Java支持反射,可在运行时查询注解信息
编译时代码分析 不适用 可以通过注解处理器进行代码生成等编译时分析
框架应用 Angular等框架中用于定义组件、服务等,ArkUI中各种状态管理 Spring、Arouter等框架中用于配置和逻辑处理
收藏00

登录 后评论。没有帐号? 注册 一个。