五. type、typeof、类、剩余和展开、简单和复杂类型
今日核心:
ArkTS 语法进阶:type、typeof、类、剩余和展开、简单和复杂类型
1. ArkTS-回顾
在继续深入学习ArkTS之前先回顾一下核心的概念
- ArkTS和 TS 以及 JS 的关系
a. JS: JavaScript,常用于网页开发,页面的效果
b. TS: TypeScript,微软开发的,比 JS 多了类型系统
c. ArkTS: JS、TS能用的,他 基本 都能用,写 UI
d. 注意:看文档的时候,可能需要去 JS 的文档,TS 的文档找一部分的内容
2. 声明式 UI 开发范式
- 组件语法
容器组件(参数) {
子组件....
}
.属性1()
.属性2()
.属性N()
普通组件(参数)
.属性1()
.属性2()
.属性N()
2. 类型别名
类型别名,顾名思义就是给某个类型起别名,之后就可以通过这个别名来使用类型啦。咱们开发中用到的一些内置类型就是通过 type 来定义的哦
type 别名 = 类型
// 后续在使用类型时 直接使用【别名】即可
试一试:
- 使用 类型别名 保存联合类型
- 直接通过类型别名来使用这个联合类型
- 看看 ResourceColor 和 ResourceStr 是如何定义的!
// 定义类型别名
type IDType = string | number
// 使用类型别名
function printID(id:IDType ) {
console.log(id+'')
}
// 调用函数
printID(10)
printID('20')
@Entry
@Component
struct Page01_type {
@State message: string = 'Type类型别名';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
3. typeof运算符
可以通过 typeof 运算符来获取类型,他返回的是一个字符串
// 后面跟上需要获取类型的 数据或变量 即可
typeof 表达式
试一试:
- 依次获取 number、string、boolean、undefined、function 的类型
- 获取 对象、数组、null的类型
// 前面 5 个可以正常获取到类型
console.log(typeof 123) // number
console.log(typeof '123') // string
console.log(typeof false) // boolean
console.log(typeof undefined) // undefined
function func() {
}
console.log(typeof func) // function
interface Person{
name:string
}
// 对象 数组 null 获取到的都是 object
const p: Person = {name:'jack'}
console.log(typeof null) // object
console.log(typeof [1, 2, 3]) // object
console.log(typeof p) // object
@Entry
@Component
struct Page02_typeof {
@State message: string = 'typeof';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
4. 类
类是用于创建对象的模板。他们用代码封装数据以处理该数据。同时类声明也会引入一个新类型,并定义其字段、方法和构造函数。
// 类名 首字母大写(规范)
class 类名{
// 字段
// 方法
// 构造函数
}
// 使用类 实例化对象 基于类 创建对象
const x:类名 = new 类名()
4.1. 实例属性
通过实例属性(字段)来保存各种类型的数据
// 类
class 类名{
// 字段名+类型+初始值
字段名:类型='xxx'
// 可选字段可以不设置初始值
字段名?:类型
}
// 可选字段在使用时需要配合 可选链操作符 避免出错
试一试:
- 定义类 Person
- 字段:
a. name,字符串,默认为 ‘’
b. food,字符串,可选- 实例化 Person 对象,并赋值name 属性
- 依次打印 name,food 的 length 属性
class Person {
name: string = 'jack'
food?: string
}
const p = new Person()
p.name = 'jack'
console.log(p.name)
console.log('', p.food?.length)
4.2. 构造函数
上一节的代码实在 实例化 之后,挨个对属性进行赋值,如果自定义了构造函数,可以在构造函数中完成该操作
class 类{
字段A:类型
字段B:类型
constructor(参数...) {
// 通过 new 实例化的时候 会调用 constructor
// 通过关键字 this 可以获取到实例对象
}
}
const 实例 = new 类()
试一试:
- 定义类
a. 添加多个字段,不设置默认值- 构造函数接收参数,内部完成字段的 初始化 操作
- 使用接口类型简化参数传递
- 描述一下
interface IFood {
name: string
price: number
}
class Food {
name: string
price: number
// constructor(name:string,price:number) {
// this.name=name
// this.price = price
// }
constructor(value: IFood) {
this.name = value.name
this.price = value.price
}
}
// const f = new Food('西兰花炒蛋', 15)
const f = new Food({ name: '花菜炒蛋', price: 10 })
现在再来看看当初创建控制器的写法,并且跳进去看看定义:const sc = new Scroller()
4.3. 实例方法
类中可以定义 方法,并且在内部编写逻辑.这种方法需要通过实例化的对象调用-称之为实例方法
class 类名{
方法名(参数...):返回值类型{
// 逻辑...
// 可以通过 this 获取实例对象
}
}
试一试:
- 定义类
- 添加字段,在构造函数中 进行初始化
- 添加方法,内部通过 this 获取实例对象并使用
- 描述如下代码的意义
a. const ls = new Scroller()
b. ls.scrollToIndex(1)
class Person{
name:string
constructor(name:string) {
this.name = name
}
sayHi(name:string){
console.log(`你好${name},我的名字是:${this.name}`)
}
}
const p:Person = new Person('jack')
p.sayHi('rose')
4.4. 静态属性、方法
类还可以添加静态属性、方法,后续访问需要通过 类 来完成
// 定义
class 类{
static 字段:类型
static 方法(){}
}
// 使用
类.字段
类.方法()
试一试
- 定义类
- 添加静态属性、静态方法
- 使用定义的 静态属性、方法
- 跳转到 TransitionEffect.move() 的定义查看是什么方法?
class Person{
static staticField:string ='静态字段'
static staticMethod(){
console.log('静态方法')
}
}
Person.staticField
Person.staticMethod()
4.5. 继承
类可以通过 继承 快速获取另外一个类的 字段 和 方法
class 父类 {
// 字段
// 方法
// 构造函数
}
class 子类 extends 父类{
// 自己的字段(属性)
// 自己的方法
// 可以重写父类方法
}
父类:也可以叫做 基类 、超类 等
子类:也可以叫做 派生类、继承类 等
试一试:
- 定义父类,添加属性(字段)、方法、构造函数
- 定义子类,继承父类
- 实例化子类,看看能跟欧访问父类的 属性、方法、构造函数
- 子类重写父类同名方法,测试调用
- 看看组件的继承关系,比如Row,
a. 看看 Row 方法的返回值的继承关系?
b. 看看通用属性是定义在哪里的?
class Person {
name: string
age: number
sayHi() {
console.log(`你好,我叫:${this.name}`)
}
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
// 通过 extends 继承父类
class Student extends Person {
// 子类 可以添加自己的属性 和 字段
height: number = 170
sayHello() {
console.log('你好吗~')
}
// 子类可以重写同名字段 、方法
sayHi(): void {
console.log('子类的 sayHi')
}
}
const s: Student = new Student('jack', 18)
// 直接可以使用 继承而来的 属性 方法 构造函数
s.name
s.sayHi()
4.6. super 关键字
子类通过 super 可以访问父类的实例字段、实例方法和构造函数。可以在适当的时候使用
class 父类 {
func(){
}
}
class 子类 extends 父类 {
constructor() {
super() // 调用父类构造函数
}
方法(){
super.方法() // 调用父类方法
}
}
试一试:
- 定义父类,添加属性、方法,构造函数
- 定义子类、
a. 添加属性、
b. 构造函数、内部通过 super 调用父类构造函数完成父类属性的初始化
c. 添加方法、内部通过 super 调用父类方法,复用逻辑
class Person {
name: string
age: number
sayHi() {
console.log(`你好,我叫:${this.name}`)
}
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
class Student extends Person {
height: number
constructor(name: string, age: number, height: number) {
// name 和 age 的初始化通过 super 来调用父类的构造函数
super(name, age)
this.height = height
}
sayHi(): void {
// 通过 super 访问父类的 字段 方法
super.sayHi()
super.name
super.age
console.log('子类的 sayHi')
}
}
const s: Student = new Student('jack', 18, 170)
4.7. instanceof
instanceof 运算符可以用来检测某个对象是否是某个类的实例
// 返回判断结果 布尔值
实例对象 instanceof Class
试一试:
- 定义父类
- 定义子类,继承父类
- 实例化子类 并通过 instanceof 进行判断
- 判断 数组 是否为 Array 的实例
class Person {
name: string = ''
}
class Student extends Person {
age: number
constructor(age: number) {
super()
this.age = age
}
}
const s = new Student(10)
console.log('isStudent', s instanceof Student)
console.log('isPerson', s instanceof Person)
console.log('isArray', [1, 2, 3,] instanceof Array)
const arr = [1, 2, 3]
// 等同于
const arr2 = new Array(1, 2, 3)
4.8. 修饰符
类的方法和属性可以通过修饰符来 限制访问
修饰符包括:readonly、private、protected和public。省略不写默认为 public
4.8.1. readonly(只读)
readonly 的意思是只读,可以用来修饰属性(字段),修饰之后外部只可以取值,无法修改
class 类{
readonly 属性:类型
}
试一试
- 定义类
a. 添加属性(字段)
b. 挑选一个设置 readonly- 实例化对象,尝试 读写 readonly 修饰的属性
- 看看 Math.pi 是如何定义的
4.8.2. private(私有)
private修饰的成员不能在声明该成员的类之外访问,包括子类
class 类{
private 属性:类型
private 方法(){}
}
试一试:
- 定义类
a. 添加属性(字段)、方法
b. 通过 private 修饰- 类内部访问 private 修饰的内容
- 类外部访问 private 修饰的内容
class Person {
// private 设置的时候不嫩省略
private name: string = ''
private age: number = 0
public sayHi() {
// 内部可以访问
console.log(`你好,我叫:${this.name}`)
}
}
class Student extends Person{
public sayHello() {
// 内部可以访问
console.log(`你好,我叫:${super.name}`) // 无法访问 报错
}
}
const p = new Person()
// p.name // 无法访问 报错
p.sayHi()
4.8.3. protected(受保护)
protected修饰符的作用与private修饰符非常相似,不同点是protected修饰的成员允许在派生类(子类)中访问
class 父类{
protect 属性:类型
protect 方法(){}
}
class 子类 extends 父类{
方法(){
// 可以访问父类 protect 修饰的 属性、方法
}
}
试一试:
- 定义父类
a. 添加属性、方法
b. 通过 protected、private 修饰 - 定义子类
a. 内部访问父类中通过protected、private 修饰的内容
// protect 受保护
class Person {
protected name: string = ''
private age:number=18
sayHi():void{
console.log(this.name)
console.log(this.age+'')
}
}
class Student extends Person{
sayHello(){
console.log(this.name) // 可以访问
console.log(this.age+'') // 无法访问 报错
}
}
4.8.4. public(公共)
public修饰的类成员(字段、方法、构造函数)在程序的任何可访问该类的地方都是可见的。
class 类{
public 属性
public 方法(){}
}
默认的修饰符就是 public,外部可以访问
class Person {
public name: string = ''
// 省略 等同于使用 public
age: number = 0
public sayHi(){
console.log(`你好,我叫:${this.name}`)
}
}
// public 公有
const p = new Person()
// 使用 public 修饰的字段和方法,通过对象直接可以访问
p.name
p.age
p.sayHi()
修饰符名 | 作用 | 适用范围 |
---|---|---|
readonly | 只读 | 属性 |
private | 私有 | 属性、方法 |
protect | 保护 | 属性、方法 |
public | 公共 | 属性、方法 |
4.9. 案例-根据描述实现类
接下来咱们来一起根据描述来实现几个具体的类,来巩固一下刚刚学习的语法
编写一个名为 Vehicle 的基类,表示交通工具:
- 属性:
○ id: 交通工具唯一标识符(字符串类型),要求在创建对象时必填。
○ color: 交通工具颜色(字符串类型),默认值为 ‘未知’。 - 方法:
○ setColor(color: string): 设置交通工具颜色,返回一个字符串描述新的颜色设置情况: ‘颜色设置为 xxx’。 - 静态方法:
● generateID(prefix: string): 生成一个标识符(字符串)并返回。该方法接受一个前缀字符串参数,并返回一个格式为 标示符+4位随机数 的唯一标识符,比如传入 car,返回car9854
基于 Vehicle 类创建两个子类:
● Car 类:继承自 Vehicle 类,表示汽车。汽车除了继承自 Vehicle 类的属性和方法外,还具有以下属性和方法:
○ 属性:
■ make: 汽车品牌(字符串类型),要求在创建对象时必填。
■ model: 汽车型号(字符串类型),要求在创建对象时必填。
○ 方法:
■ startEngine(): 启动汽车引擎。返回一个字符串描述汽车信息+引擎启动情况,例如 “比亚迪秦 启动~”。
● Bike 类:继承自 Vehicle 类,表示自行车。自行车除了继承自 Vehicle 类的属性和方法外,还具有以下属性:
○ 属性:
■ frameMaterial: 自行车车架材质(字符串类型),要求在创建对象时必填。
// 基类 Vehicle
class Vehicle {
id:string //标示符
color:string='未知' // 颜色
constructor( id: string, color?: string ) {
this.id=id
if(color!=undefined){
this.color=color
}
}
setColor(color: string): string {
this.color = color;
return `颜色设置为 ${color}`;
}
static generateID(prefix: string): string {
const randomSuffix = Math.floor(Math.random() * 10000)
return `${prefix}${randomSuffix}`;
}
}
// 子类 Car
class Car extends Vehicle {
make:string// 品牌
model:string // 型号
constructor(
id: string,
color: string,
make: string,
model: string
) {
super(id, color);
this.make=make
this.model=model
}
startEngine(): string {
return `${this.make} ${this.model} 启动~`;
}
}
// 子类 Bike
class Bike extends Vehicle {
frameMaterial:string // 材质
constructor(id: string, color: string, frameMaterial: string) {
super(id, color);
this.frameMaterial=frameMaterial
}
}
// 示例用法
const carId = Vehicle.generateID('car');
const myCar = new Car(carId, '蓝色', '比亚迪', '秦');
console.log(myCar.startEngine()); // 输出类似 "比亚迪秦 启动~"
const bikeId = Vehicle.generateID('bike');
const myBike = new Bike(bikeId, '红色', '铝合金');
@Entry
@Component
struct Page05_ClassDemo {
@State message: string = 'class的练习题';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
5. 剩余和展开
接下来学习一个运算符 …
…在不同的情况下有不同的效果,分别是 剩余参数 和 展开
5.1. 剩余参数
通过剩余参数的语法,我们可以将 函数 或 方法 中一个不定数量的参数表示为一个数组
// 剩余参数 【只能写在最后一位】
function 函数名(参数1,参数2,...剩余参数数组){
// 逻辑代码
// 剩余参数之前的参数 挨个获取即可
// 剩余参数:以数组的形式获取
}
试一试:
- 定义累加函数
a. 默认有 2个参数
b. 可以在 2 个参数以外再传递任意个参数
c. 类型均为 number - 返回累加的结果
function sum(numA:number,numB:number,...theArgs:number[]) {
let total = numA+numbB;
for (const arg of theArgs) {
total += arg;
}
return total;
}
console.log(sum(1, 2, 3).toString()) // 6
console.log(sum(1, 2, 3, 4).toString()) // 10
5.2. 展开
出于程序稳定性,以及运行性能考虑,在 ArkTS 中 …(展开运算符) 只能用在数组上
注:TS中 … 数组和对象均可以使用
试一试:
- 数组合并
- 数组的 push 方法结合…运算符,顺便确认 push 方法的参数
示例代码:
日常开发中,用来展开数组,常用于用数组合并 及 传递参数时
const numArr1: number[] = [1, 2, 3, 4]
const numArr2: number[] = [5, 6, 7]
// 合并到一起
const totalArr: number[] = [...numArr1, ...numArr2]
// 添加
const numArr3:number[] = [8,9,10]
const numArr4:number[] = [11,12,13]
// 将 numArr4 展开,传递给push
numArr3.push(...numArr4)
6. 简单类型和复杂类型
ArkTS中的数据类型整体可以分为 两大类:
- 基本数据类型(简单数据类型)
number 数字型、string 字符串型、boolean布尔型、undefined未定义、null空类型 - 引用数据类型(复杂数据类型)
Object、Function、Array
这两类数据在内存中保存的方式略有不同,导致在日常开发中对这两类数据进行赋值操作时会有不同的结果
比如如下代码: - num1和 num2 的值分别是?
- p1.name和p2.name 分别是?
// 基本数据类型
let numA: number = 10
let numB: number = numA
numB++
console.log('numA:', numA) // ?
console.log('numB:', numB) // ?
// 引用数据类型
class Person {
name: string = ''
constructor(name: string) {
this.name = name
}
}
const p1: Person = new Person('jack')
const p2: Person = p1
// 修改 P2 是否会报错
p2.name = 'rose'
console.log('p1.name:', p1.name) // ?
console.log('p2.name:', p2.name) // ?
6.1. 内存中堆栈空间
咱们可以把内存理解为有 2 类空间:
- 栈:访问速度快,基本数据类型存放到栈里面
- 堆:存储容量大,引用数据类型存放到堆里面
基本数据类型和复杂数据类型是如何保存的呢?
6.1.1. 基本数据类型存储
变量的数据直接存放在栈空间中,访问速度快
6.1.2. 引用数据类型存储
- 栈空间:存放 变量的内存地址(堆中的地址)
- 堆空间:存放 变量的 值
思考:
- 通过索引,修改上图中 numArr 某一项的值,改的是哪里的数据?
6.2. 站在内存角度看变量赋值
6.2.1. 基本数据类型
num1 num2 都保存在堆内存中,虽然值相同,但是各自是独立的空间,后续操作 互不影响
let num1: number = 10
let num2: number = num1
6.2.2. 引用数据类型
p1 和 p2 栈内存中有各自独立的空间,但是保存的是堆内存的地址,指向的是同一个数据:
修改 p2.name ,p1.name 也会受到影响,因为是同一份数据
class Person {
name: string = ''
constructor(name: string) {
this.name = name
}
}
const p1: Person = new Person('jack')
const p2: Person = p1
p2.name = 'rose'