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
复制代码
函数的最后一个参数可以是 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<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())//输出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 本质是基于函数去实现的】。<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; // 输出0
p.age = -42; // 设置无效age值会抛出错误
复制代码
4.7 可见性修饰符
类的方法和属性都可以使用可见性修饰符。
可见性修饰符包括:private、protected 和 public。默认可见性为 public。
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'不可见
复制代码
class Base {
protected x: string = ''
private y: string = ''
}
class Derived extends Base {
foo() {
this.x = 'a'; // OK,访问受保护成员
this.y = 'b'; // 编译时错误,'y'不可见,因为它是私有的
}
}
复制代码
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. 装饰器
随着 TypeScript 和 ES6 里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
在 ArkTS 中 常见的状态管理其实就是一种装饰器
7.1 装饰器的分类
7.2 类装饰器
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
复制代码
类装饰器顾名思义,就是用来装饰类的。它接收一个参数:
7.3 属性装饰器
属性装饰器声明:
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
复制代码
属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:
7.4 方法装饰器
方法装饰器声明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
复制代码
方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:
7.5 参数装饰器
参数装饰器声明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
复制代码
参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:
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 注解的区别
评论