1. 概述
HarmonyOS 应用的主要开发语言是 ArkTS,它由 TypeScript(简称TS)扩展而来,在继承TypeScript语法的基础上进行了一系列优化,使开发者能够以更简洁、更自然的方式开发应用。
注意:TypeScript 本身也是由另一门语言 JavaScript 扩展而来,它主要是在JavaScript的基础上添加了静态类型定义。因此三者的关系如下图所示
2. TypeScript 快速入门
TypeScript提供了一个线上的 Playground 供练习使用,地址为TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript。
2.1. 声明
变量声明
常量声明
let
用于声明变量,而const
用于声明常量,两者的区别是:变量在赋值后可以修改,而常量在赋值后便不能再修改。
let a:number = 100
const b:number = 200;
类型推断
如果一个变量或常量的声明包含了初始值,TS 便可以根据初始值进行类型推断,此时我们就可以不用显式的指定其类型,例如:
let c = 60;
console.log(typeof c); //number
2.2. 常用数据类型
number类型
number
表示数字,包括整数和浮点数,例如: 340
、-23
、29.5
、-13.4
let a :number = 340
let b :number = -23
let c :number = 29.5
let d :number = -13.4
string类型
string
表示字符串,例如: 你好
、hello
let a:string = '你好'
let b:string = 'hello'
boolean类型
boolean
表示布尔值,可选值为:true
、false
let isOpen:boolean = true
let isDone:boolean = false
数组类型
数组类型定义由两部分组成,元素类型[]
,例如number[]
表示数字数组,string[]
表示字符串数组,数组类型的变量可由数组字面量——[item1,item2,item3]
进行初始化。
let a: number[] = []
let b: string[] = ['你好', 'hello']
对象类型
对象(object)类型的声明很简单,只需声明属性名称和属性类型即可,例如{id:number,name:string}
,对象类型的变量可以通过对象字面量——{id:1,name:'zhangsan'}
进行初始化。
let person: { id: number, name: string } = { id: 1, name: 'zhangsan' };
2.3. 函数
可选参数:可选参数通过参数名后的?
进行标识,例如下面的gender?
参数。
function getPersonInfo(name: string, age: number, gender?: string) {
if (!gender) {
gender = '未知'
}
return `name:${name},age:${age},gender:${gender}`;
}
let p1 = getPersonInfo('zhagnsan', 10, '男')
let p2 = getPersonInfo('lisi', 15);
console.log(p1);
console.log(p2);
默认参数:可在函数的参数列表为参数指定默认值,如以下案例中的gender: string='未知'
参数。
function getPersonInfo(name: string, age: number, gender: string='未知') {
return `name:${name},age:${age},gender:${gender}`;
}
let p1 = getPersonInfo('zhagnsan', 10, '男')
let p2 = getPersonInfo('lisi', 15);
console.log(p1);
console.log(p2);
联合类型:一个函数可能用于处理不同类型的值,这种情况可以使用联合类型,例如以下案例中的message: number | string
function printNumberOrString(message: number | string) {
console.log(message)
}
printNumberOrString('a')
printNumberOrString(1)
任意类型:若函数需要处理任意类型的值,则可以使用any
类型,例如以下案例中的message: any
function print(message:any) {
console.log(message)
}
print('a')
print(1)
print(true)
特殊类型:若函数没有返回值,则可以使用void
作为返回值类型,其含义为空。
function test(): void {
console.log('hello');
}
类型推断:函数的返回值类型可根据函数内容推断出来,因此可以省略不写。
function test() {
console.log('hello');
}
function sum(a: number, b: number) {
console.log(a + b);
}
匿名函数:匿名函数的语法结构简洁,特别适用于简单且仅需一次性使用的场景。
let numbers: number[] = [1, 2, 3, 4, 5]
numbers.forEach(function (number) {
console.log(number);
})
注意:匿名函数能够根据上下文推断出参数类型,因此参数类型可以省略。
箭头函数:匿名函数的语法还可以进一步的简化,只保留参数列表和函数体两个核心部分,两者用=>
符号连接。
let numbers: number[] = [1, 2, 3, 4, 5]
numbers.forEach((num) => { console.log(num) })
2.4. 类(class)
概述:
类(class)是对象的蓝图或模板,它定义了对象的属性(数据)和行为(方法)。通过类可以创建多个具有相似结构和行为的对象。例如定义一个 Person
类,其对象可以有张三
、李四
等等。
通过一个简单案例,学习一下类的定义语法
示例代码:
class Person {
id: number;
name: string;
age: number = 18;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
introduce(): string {
return `hello,I am ${this.name},and I am ${this.age} years old.`
}
}
对象创建:创建对象的关键字为new
,具体语法如下
let person = new Person(1,'zhangsan,10);
对象属性的访问
console.log(person.name); //读
person.name = 'lisi'; //写
console.log(person.name);
对象方法的调用:对象创建后,便可通过对象调用类中声明的方法,如下
let intro = person.introduce();
console.log(intro);
静态成员
Typescript 中的类中可以包含静态成员(静态属性和静态方法),静态成员隶属于类本身,而不属于某个对象实例。静态成员通用用于定义一些常量,或者工具方法。
- 声明静态成员:定义静态成员需要使用
static
关键字。
class Constants{
static count:number=1;
}
class Utils{
static toLowerCase(str:string){
return str.toLowerCase();
}
}
console.log(Constants.count);
console.log(Utils.toLowerCase('Hello World'));
- 使用静态成员:静态成员无需通过对象实例访问,直接通过类本身访问即可。
console.log(Constants.count);
console.log(Utils.toLowerCase('Hello World'));
继承:
继承是面向对象编程中的重要机制,允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。子类可以直接使用父类的特性,并根据需要添加新的特性或覆盖现有的特性。这种机制赋予面向对象程序良好的扩展性。
下面通过一个例子演示继承的特性
class Student extends Person {
classNumber: string;
constructor(id: number, name: string, age: number, classNumber: string) {
super(id, name, age);
this.classNumber = classNumber;
}
introduce(): string {
return `hello,I am ${this.name},and I am ${this.age} years old, and I am a student`
}
}
let student = new Student(1,'xiaoming',10,'三年二班');
console.log(student.introduce());
- 关键字:
extends
- 新增属性:
classNumber:string;
- 覆盖父类方法:
introduce
- 构造器:子类构造器中需调用
super
(父类构造器)对继承的父类属性进行初始化。
权限修饰符
权限修饰符用于控制类成员(属性、方法等)的访问权限。它们有助于维护代码的封装性和安全性。TypeScript提供了三种访问修饰符,分别是 public、private 和 protected。
public
:公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的。private
:私有的,只能在声明它的类中的被访问。protected
:受保护的,只能在声明它的类和其子类中被访问。
2.5. 接口(interface)
概述
接口(interface)是面向对象编程中的另一个重要概念。接口通常会作为类(class)的一种契约或规范,确保类实现了特定的行为或功能。通常情况下,接口中只会包含属性和方法的声明,而不包含具体的实现细节,具体的细节由其实现类完成。
接口定义:接口使用interface
关键字定义,具体如下
interface Person {
id: number;
name: string;
age: number;
job: string;
introduce(): void;
}
接口实现
接口的实现需要用到implements
关键字,实现类中,需要包含接口属性的赋值逻辑,以及接口方法的实现逻辑。
class Teacher implements Person {
id: number;
name: string;
age: number;
job: string = 'Teacher';
constructor(id: number, name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
introduce(): void {
console.log(`Hello,I am ${this.name}`);
}
}
作用
在传统的面向对象编程的场景中,接口主要用于设计和组织代码,使代码更加容易扩展和维护。下面举例说明。假如现在需要实现一个订单支付系统,按照面向对象编程的习惯,首先需要定义一个订单类(Order),如下
class Order {
total_amount: number;
constructor(total_amount: number) {
this.total_amount = total_amount;
}
pay() {
console.log(`Pay:${this.total_amount}`);
}
}
很容易预想到,订单需要将来可能需要支持多种支付方式,为了方便后期让代码支持新的支付方式,我们可以对代码进行如下改造。首先定义一个支付策略的接口,接口中声明了一个pay
方法,用来规范实现类必须实现支付逻辑。
interface PaymentStrategy {
pay(amount: number): void;
}
然后在订单类中增加一个PaymentStrategy
的属性,并且在订单类中的pay
方法中调用PaymentStrategy
的pay
方法,如下
class Order {
total_amount: number;
paymentStrategy: PaymentStrategy;
constructor(total_amount: number, paymentStrategy: PaymentStrategy) {
this.total_amount = total_amount;
this.paymentStrategy = paymentStrategy;
}
pay() {
this.paymentStrategy.pay(this.total_amount);
}
}
这样改造完之后,就可以很容易的在不改变现有代码的情况下,支持新的支付方式了。
比如现在需要支持AliPay
,那我们就可以创建AliPay
这个类(class)并实现(implement)PaymentStrategy
这个接口,如下
class AliPay implements PaymentStrategy {
pay(amount: number): void {
console.log(`AliPay:${amount}`);
}
}
这样一来,之后创建的订单就可以使用AliPay
这个支付方式了。
let order = new Order(1000,new AliPay());
order.pay();
TS 中的接口的特殊性
TypeScript 中的接口是一个非常灵活的概念,除了用作类的规范,让类去实现之外,也常用于直接描述对象的类型,例如,现有一个方法的定义如下
function getPersonInfo(person: { id: number, name: string, age: number }) {
console.log(`[id:${person.id},name:${person.name},age:${person.age}]`);
}
可以看到该函数的参数类型为一个一般对象:{ id: number, name: string, age: number }
,此时就可以声明一个接口来描述参数类型,如下,这样一来,函数定义就会更加简洁明了。
interface Person {
id: number;
name: string;
age: number;
}
function getPersonInfo(person: Person) {
console.log(`[id:${person.id},name:${person.name},age:${person.age}]`);
}
为使接口使用起来更加灵活,TypeScript提出了可选属性的概念,语法如下
interface Person {
id: number;
name: string;
age?: number;
}
上述接口中的age
字段就是一个可选属性,在声明该接口类型的对象时,便可根据实际情况选择性的包含或者不包含 age
字段,如下
getPersonInfo({ id: 1, name: 'zhangsan', age: 10 })
getPersonInfo({ id: 2, name: 'lisi' })
2.6. 枚举
概述
枚举(Enumeration)是一种编程语言中常见的数据类型,用于定义一组有限的命名常量,常用于表示特定的状态、类型或选项,例如方向(上、下、左、右)、季节(春、夏、秋、冬)等。
enum Direction {
UP,
BOTTOM,
LEFT,
RIGHT
}
function move(direction: Direction) {
switch (direction) {
case Direction.UP:
console.log('向上移动');
break;
case Direction.BOTTOM:
console.log('向下移动');
break;
case Direction.LEFT:
console.log('向左移动');
break;
case Direction.RIGHT:
console.log('向右移动');
break;
default:
console.log('原地不动')
break;
}
}
move(Direction.UP);
枚举的使用记住两个原则
- 枚举值的访问
像访问对象属性一样访问枚举值,例如Direction.UP
- 枚举值的类型
枚举值的类型为enum
的名称,例如Direction.UP
和Direction. BOTTOM
等值的类型都是Direction
枚举原理
默认情况下,每个属性的值都是数字,并且从 0
开始递增,例如上述案例中的Direction
枚举中,Direction.UP
的值为0
,Direction.BOTTOM
的值为1
,依次类推,具体如下
console.log(Direction.UP) //0
console.log(Direction.BOTTOM) //1
console.log(Direction.LEFT) //2
console.log(Direction.RIGHT) //3
除了使用默认的数字作为属性的值,我们还能手动为每个属性赋值,例如
enum Direction {
UP = 1,
BOTTOM = 2,
LEFT = 3,
RIGHT = 4
}
console.log(Direction.UP) //1
console.log(Direction.BOTTOM) //2
console.log(Direction.LEFT) //3
console.log(Direction.RIGHT) //4
再例如
enum Direction {
UP = 'up',
BOTTOM = 'bottom',
LEFT = 'left',
RIGHT = 'right'
}
console.log(Direction.UP) //up
console.log(Direction.BOTTOM) //bottom
console.log(Direction.LEFT) //left
console.log(Direction.RIGHT) //right
注意:
多数情况下,我们只是用枚举来表示几种不同的状态,以便在不同的状态下采取不同的行动,这时一般无需关注每个属性具体的值。
2.7. 模块化
概述
模块化是将复杂的程序拆解为多个独立的文件单元,每个文件被称为一个模块。在 TypeScript 中,默认情况下,每个模块都拥有自己的作用域,这意味着在一个模块中声明的任何内容(如变量、函数、类等)在该模块外部是不可见的,除非明确导出。同时,为了在一个模块中使用其他模块的内容,必须先将这些内容显式导入到当前模块中。
导出:导出须使用export
关键字,语法如下
export function hello() {
console.log('hello module A');
}
export const str = 'hello world';
const num = 1;
导入:导入须使用import
关键字,语法如下
import { hello, str } from './moduleA';
hello();
console.log(str);
避免命名冲突
若多个模块中具有命名相同的变量、函数等内容,将这些内容导入到同一模块下就会出现命名冲突。例如,在上述案例的基础上,又增加了一个 moduleC
,内容如下
export function hello() {
console.log('hello module C');
}
export const str = 'module C';
moduleB
同时引入 moduleA
和 moduleC
的内容,如下,显然就会出命名冲突
import { hello, str } from "./moduleA";
import { hello, str } from "./moduleC";
hello() //?
console.log(str); //?
有多种方式可以用来解决命名冲突,下面逐一介绍
- 导入同时进行重命名
import { hello as helloFromA, str as strFromA } from "./moduleA";
import { hello as helloFromC, str as strFromC } from "./moduleC";
helloFromA();
console.log(strFromA);
helloFromC();
console.log(strFromC);
- 创建模块对象
上述导入重命名的方式能够很好的解决命名冲突的问题,但是当冲突内容较多时,这种写法会比较冗长。除了导入重命名外,还可以将某个模块的内容统一导入到一个模块对象上,这样就能简洁有效的解决命名冲突的问题了,具体语法如下
import * as A from "./moduleA";
import * as C from "./moduleC";
A.hello();
console.log(A.str);
C.hello();
console.log(C.str);
除了上述导入导出的语法之外,还有一种语法,叫做默认导入导出,这种语法相对简洁一些。
- 默认导出:默认导出允许一个模块指定一个(最多一个)默认的导出项,语法如下
export default function hello(){
console.log('moduleA');
}
默认导出支持匿名导出项,语法如下
export default function () {
console.log('moduleB');
}
- 默认导入
由于每个模块最多有一个默认导出,因此默认导入无需指定导入项的原名称,并且无需使用{}
。
import helloFromA from "./moduleA";
import helloFromB from "./moduleB";
上述语法相当于以下语法的简写
import { default as helloFromA } from "./moduleA";
import { default as helloFromB } from "./moduleB";
2.8. 装饰器
概述
在TypeScript中,装饰器是一种特殊类型的声明,可以被附加到类,属性,方法上。装饰器的核心思想是在尽量不改变原始类的定义的情况下,为类添加新的特性。
装饰器使用@Expression
的形式。其中,Expression
为一个函数,这个函数负责定义为类添加的新特性,其会在运行时被调用。
ArkTS 提供了多种装饰器,同时在鸿蒙应用的开发中我们也会大量使用这些装饰器,因此学习装饰器语法,对于理解鸿蒙应用的执行原理有很大帮助。
装饰器的分类
按照声明位置的不同,装饰器可以分为类装饰器、方法装饰器和属性装饰器等,不同装饰器可用于实现不同的功能。
- 类装饰器:
类装饰器可以拦截并修改类的构造函数,这使得我们可以在实例化对象时增加一些额外的逻辑。
@ClassDecorator
class A {
}
/**
* 装饰器函数
* 在第一次引用A类时执行
* @param target 被装饰的类
*/
function ClassDecorator (target) {
// 给目标类添加静态属性
target.xxx = 'abc'
}
console.log((A as any).xxx) // abc
- 方法装饰器
使用方法装饰器可以拦截并修改所装饰的方法,这使得我们可以在调用该方法之前或之后执行一些额外的逻辑。
class A {
@MethodDecorator
hello(){
console.log('hello()');
}
}
/**
* 方法装饰器
* 在第一次调用方法前执行
* @param target 方法所属类的原型对象
* @param name 方法名
* @param descriptor 方法属性对应的描述符对象
*/
function MethodDecorator (target: object, name: string, descriptor: PropertyDescriptor) {
console.log(`${name}方法将要第一次调用了`, target, name, descriptor);
}
const a = new A()
a.hello()
- 属性装饰器
属性装饰器可以拦截对属性的读写操作,并在这些操作之前或之后执行一些额外的逻辑。
class A {
@PropertyDecorator
name: string;
}
/**
* 属性装饰器
* 在第一次创建对象内部初始化属性时执行
* @param target 属性所属类的原型对象
* @param name 属性名
*/
function PropertyDecorator (target: object, name: string) {
console.log(`将要第一次操作${name}属性`, target, name);
}
const a = new A() // 内部会将name属性添加给a对象,自动调用PropertyDecorator
简单案例
下面通过一个简单的案例,演示装饰器的作用,例如现在需要监听某一个属性的变化,当其发生变化时,自动执行一些额外的逻辑。此时就可以通过一个属性装饰器来监视读写操作。
class Person {
@log
name: string;
constructor(name: string) {
this.name = name;
}
}
function log(target: object, name: string) {
console.log('----------log')
let value: any;
Object.defineProperty(target, name, {
set (newValue: any) {
console.log(`监视到${name}属性修改为${newValue}`);
value = newValue;
},
get () {
console.log(`监视到读取${name}属性`)
return value;
}
})
}
let person = new Person('张三');
person.name='李四'
console.log(person.name)
标签:Direction,TypeScript,console,log,鸿蒙,number,语法,string,name
From: https://blog.csdn.net/2401_83335728/article/details/141960494