写点什么

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

作者:冉冉同学
  • 2024-12-16
    山东
  • 本文字数:8330 字

    阅读完需:约 27 分钟

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

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*2multiply(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() // 返回0sum(1, 2, 3) // 返回6
复制代码

3.4 函数类型

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


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())//输出10186console.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(); // 返回:1z(); // 返回:2

//闭包实例代码function fn1() { let a = 1; function fn2() { a++; console.log(a); } return fn2;}const fn2 = fn1();//闭包函数执行完后外部作用域变量仍然存在,并保持状态fn2() //2fn2() //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 本质是基于函数去实现的】。<u>因为只能拥有一个 class 构造函数,所以在 ArkTS 中,没有向 Java 类的重载。 </u>


如果不指定一个构造函数 (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; // 输出0p.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 注解的区别


发布于: 刚刚阅读数: 3
用户头像

冉冉同学

关注

还未添加个人签名 2018-05-12 加入

还未添加个人简介

评论

发布
暂无评论
【HarmonyOS NEXT】ArkTs函数、类、接口、泛型、装饰器解析与使用_鸿蒙_冉冉同学_InfoQ写作社区