【HarmonyOS NEXT】ArkTs函数、类、接口、泛型、装饰器解析与使用
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
用于定义抽象类和其中的抽象方法。
什么是抽象类?
- 抽象类是不允许被实例化的
- 抽象类中的抽象方法必须被子类实现
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
例子中,我们想访问arg
的length
属性,但是编译器并不能证明每种类型都有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. 装饰器
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等框架中用于配置和逻辑处理 |
- 3回答
- 1粉丝
- 1关注
- 【HarmonyOS NEXT】ArkTs数据类型解析与使用
- 接口解析
- 接口解析
- 【HarmonyOS NEXT】url地址key value解析器
- 元服务——接口解析
- 元服务——5、接口解析
- 第五章接口解析示例
- @ComponentV2装饰器:自定义组件
- 【HarmonyOS Next开发】Navigation使用
- 【HarmonyOS Next开发】:ListItemGroup使用
- 【HarmonyOS NEXT】鸿蒙 ArkTS 进行RSA数据加密
- 轻松上手-MVVM_关系型数据库_云函数T云数据库
- ArkTS第三方库的语法与使用详解
- 【HarmonyOS Next开发】Tabs使用封装
- HarmonyOS NEXT 地图服务中‘我的位置’功能全解析