零基础鸿蒙应用开发第二十一节:面向对象思想入门与类的定义
【学习目标】
- 理解“类(模板)-对象(实例)”的核心关系,区分面向过程与面向对象编程思维在鸿蒙开发中的适配场景;
- 掌握ArkTS类的标准定义语法(属性声明、构造函数、实例方法),熟练使用
new关键字实例化对象; - 吃透
this关键字的含义与使用场景,规避鸿蒙开发中this的常见使用错误; - 理解封装的核心意义,掌握
public/private访问修饰符(protected将在下一节继承章节讲解),实现数据的安全封装; - 掌握getter/setter方法的设计思路,理解其与普通方法的本质区别,实现鸿蒙应用中商品售价、库存、折扣属性的安全读写。
【学习重点】
- ArkTS类“先声明、后赋值”的核心规则,区分与TypeScript的核心差异;
this关键字在构造函数/实例方法中的正确使用,规避“属性未绑定”错误;private修饰符在鸿蒙电商场景(商品售价、折扣)的核心应用;- getter/setter方法与普通方法的本质区别,掌握
set/get关键字的设计初衷与选型原则; - 类内部方法与全局函数的语法差异(
function关键字的使用规则); - 面向对象封装思想在鸿蒙业务模型类(商品)中的落地。
一、工程结构
本节我们将创建名为ClassObjectDemo的工程,基于 鸿蒙5.0(API12) 开发,使用 DevEco Studio 6.0+ 工具,项目结构目录如下:
ClassObjectDemo/
├── entry/ # 应用主模块
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/ # ArkTS代码根目录
│ │ │ │ ├── pages/ # 页面代码目录
│ │ │ │ │ └── Index.ets # 测试页面
│ │ │ │ └── model/ # 业务模型类目录
│ │ │ │ └── Goods.ets # 商品类(核心)
│ │ │ ├── resources/ # 资源目录(非本节重点)
│ │ │ └── module.json5 # 模块配置文件
│ │ └── oh-package.json5 # 工程依赖配置
└── hvigorfile.ts # 构建脚本(默认生成)
二、类与对象基础解析
2.1 面向过程 vs 面向对象
编程的核心是“处理数据+实现逻辑”,两种思维在鸿蒙电商开发中的适配性差异显著:
| 编程思维 | 核心特点 | 鸿蒙适配场景 | 缺点 |
|---|---|---|---|
| 面向过程 | 数据与方法分离,按步骤执行 | 简单逻辑(如单个商品信息打印) | 多实例场景代码冗余、维护性差 |
| 面向对象 | 数据与方法封装为类,实例化调用 | 多实例管理(电商商品列表) | 入门稍复杂,适配工程化开发 |
2.2 面向过程实现
// 定义电商商品数据(仅保留核心字段:名称、售价、折扣、库存)
let productName: string = "鸿蒙Mate70手机";
let productPrice: number = 5999;
let productDiscount: number = 0.8; // 8折(注:0.8表示8折,非0.8折)
let productStock: number = 100;
// 封装打印方法(需手动传参)
function printGoodsInfo(name: string, price: number, discount: number, stock: number): void {
const finalPrice = price * discount; // 折扣后价格
console.log(`商品名称:${name},原价:${price}元,折扣:${discount * 10}折,折后价:${finalPrice}元,库存:${stock}件`);
}
// 执行逻辑
printGoodsInfo(productName, productPrice, productDiscount, productStock);
打印输出:
商品名称:鸿蒙Mate70手机,原价:5999元,折扣:8折,折后价:4799.2元,库存:100件
2.3 核心概念解析
在看面向对象的具体实现前,先明确三个核心概念,这是理解面向对象的基础:
- 类(Class):描述一类事物的通用模板,定义了该类事物的共同属性和行为(比如电商场景中的
Goods类,会定义商品的名称、售价、库存、折扣这些共同属性,以及打印信息、计算折后价这些共同行为); - 对象(Object):类的具体实例,是类模板的具象化(比如
phone是Goods类的实例,对应“鸿蒙Mate70手机”这个具体商品,watch是另一个实例,对应“鸿蒙智能手表”); - 封装:将数据(属性)和操作数据的逻辑(方法)打包到类中,外部仅通过暴露的接口操作数据,无需关心内部实现(比如商品的售价是敏感数据,我们可以封装起来,外部只能通过指定方式修改,不能直接赋值)。
2.4 类的标准定义语法
ArkTS类遵循“先声明、后赋值”原则,不支持 TypeScript中“构造函数参数加public简化属性定义”的写法,标准模板:
export class 类名 {
// 1. 公共属性声明:指定类型,可选默认值(非敏感数据,如商品名称、分类)
公共属性名: 数据类型;
带默认值属性名: 数据类型 = 默认值;
// 2. 私有属性声明:敏感数据,仅类内访问(如商品售价、库存、折扣)
private _私有属性名: 数据类型 = 默认值;
// 3. 构造函数:实例化时初始化属性,需先声明后赋值,可选参数放置最后
constructor(参数1: 数据类型, 参数2: 数据类型, 可选参数?: 数据类型) {
this.公共属性名 = 参数1; // 给公共属性赋值
this._私有属性名 = 参数2; // 给私有属性赋值
}
// 4. 实例方法:描述对象行为,禁止使用function关键字
无返回值方法(): void {
console.log(this.公共属性名); // 访问属性必须加this
}
有返回值方法(): 返回值类型 {
return this._私有属性名 * this._私有属性名2; // 如售价×折扣计算折后价
}
}
2.5 面向对象实现
有了核心概念和语法基础,我们就可以用面向对象的方式重构电商商品的逻辑,解决面向过程的冗余问题:
步骤1:定义商品类(数据+行为封装)
// model/Goods.ets
export class Goods {
// 公共属性:类内外均可访问,默认可省略public
public name: string;
// 默认分类名称,赋予默认值"商品分类"
category: string = "商品分类";
// 私有属性:仅类内可访问
private _price: number; // 商品售价(核心案例:加下划线区分getter/setter)
private stock: number; // 商品库存
private discount: number = 1; // 商品折扣(默认1=原价/10折)
// 构造函数:初始化属性(必须先声明后赋值)
constructor(name: string, price: number, stock: number, discount?: number, category?: string) {
this.name = name;
// 基础校验:售价、库存不能为负,负数则设为0(使用Math.max简化逻辑)
this._price = Math.max(price, 0);
this.stock = Math.max(stock, 0);
// 折扣校验:传入合法值则赋值,否则用默认值(1=原价/10折)
if (discount && discount > 0 && discount <= 1) {
this.discount = discount;
}
// 分类:传入合法值则覆盖默认值(空值/空格不覆盖)
this.category = category?.trim() || this.category;
}
// 实例方法:打印商品信息(行为封装)
printInfo(): void {
const finalPrice = parseFloat((this._price * this.discount).toFixed(2)); // 保留两位小数
console.log(`
商品名称:${this.name}
商品分类:${this.category}
商品原价:${this._price}元
商品折扣:${this.discount * 10}折
折后售价:${finalPrice}元
商品库存:${this.stock}件
`);
}
}
步骤2:调用商品类
// pages/Index.ets 导入Goods
import { Goods } from '../model/Goods';
aboutToAppear() {
// 实例化商品对象(类→对象的过程)
const phone = new Goods("鸿蒙Mate70手机", 5999, 100, 0.8, "数码产品");
const watch = new Goods("鸿蒙智能手表", 1299, 50, 0.9, "穿戴设备");
// 调用对象的方法
phone.printInfo();
watch.printInfo();
// 访问公共属性(可直接读写)
console.log(`分类名字:${phone.category}`); // 输出:数码产品
phone.category = "新数码产品";
console.log(`分类名字:${phone.category}`); // 输出:新数码产品
// 私有属性无法直接访问(编译报错)
// console.log("价格:",phone._price);
// console.log("库存",phone.stock);
}
打印输出:
// phone.printInfo() 输出:
商品名称:鸿蒙Mate70手机
商品分类:数码产品
商品原价:5999元
商品折扣:8折
折后售价:4799.2元
商品库存:100件
// watch.printInfo() 输出:
商品名称:鸿蒙智能手表
商品分类:穿戴设备
商品原价:1299元
商品折扣:9折
折后售价:1169.1元
商品库存:50件
// 访问公共属性输出:
分类名字:数码产品
分类名字:新数码产品
2.6 function关键字使用规则
核心规则(强制):
- 全局/独立函数:必须显式使用
function关键字(或箭头函数)声明,这是ArkTS的基础语法规则; - 类内部方法(实例方法、getter/setter、静态方法):禁止使用
function关键字,直接以“方法名(参数): 返回值类型”的形式声明,加function会直接触发语法报错。
// 示例1:全局函数(必须加function)
function globalPrintName(name: string): void {
console.log(`全局函数:${name}`);
}
// 调用全局函数
globalPrintName("鸿蒙Mate70手机");
// 示例2:类内部方法(无需加function)
class ErrorExample {
name: string;
constructor(name: string) {
this.name = name;
}
// ✅ 正确:类内部方法无function关键字
printName(): void {
console.log(`类方法:${this.name}`);
}
// ❌ 错误:类内部方法加function关键字,语法报错
// function printError(): void {
// console.log(this.name);
// }
}
// 调用类方法
const errorObj = new ErrorExample("鸿蒙智能手表");
errorObj.printName();
打印输出:
全局函数:鸿蒙Mate70手机
类方法:鸿蒙智能手表
2.7 this关键字深度解析
this指向“当前对象实例”,仅在构造函数和实例方法中有效,核心作用是区分“类的属性”和“方法/构造函数的参数”。例如:
- 构造函数中的
this._price:指类的私有属性_price; - 构造函数的参数
price:仅作用于构造函数内部,与类属性无关; - 若省略
this(如console.log(price)),会被识别为局部变量(参数),而非类属性,导致逻辑错误。
2.8 常见错误及规避示例
// 错误示例对比
// ❌ ArkTS编译报错示例:未声明属性直接在构造函数赋值
class ErrorExample1 {
constructor(name: string, price: number) {
this.name = name; // 报错:Property 'name' does not exist on type 'ErrorExample1'
this._price = price; // 报错:Property '_price' does not exist on type 'ErrorExample1'
}
}
// ✅ 正确写法:先声明、后赋值
class ErrorExample2 {
// 第一步:声明属性(公共+私有)
name: string;
private _price: number;
constructor(name: string, price: number) {
// 第二步:构造函数赋值(必须加this)
this.name = name;
this._price = Math.max(price, 0);
// ❌ 错误:参数自赋值,类属性未初始化
// name = name;
// _price = price;
// ❌ 错误:访问未定义的局部变量(混淆参数与类属性)
// console.log(price); // 这里是构造函数参数,不是类属性
// console.log(_price); // 未声明的局部变量(类属性需用this._price访问)
}
// 正确访问类属性
getPrice(): number {
return this._price; // 必须加this访问类属性
}
}
// 调用正确示例
const correctObj = new ErrorExample2("鸿蒙平板", 2999);
console.log(`商品名称:${correctObj.name}`);
console.log(`商品售价:${correctObj.getPrice()}元`);
打印输出:
商品名称:鸿蒙平板
商品售价:2999元
三、封装思想及访问控制应用
3.1 封装的核心意义
本节封装案例围绕鸿蒙电商核心的“商品类”展开,封装的核心目标是:将商品的核心数据(售价、库存、折扣)与操作逻辑(价格计算、信息打印)打包为独立的业务模型,外部仅通过暴露的接口(如getter/setter、普通方法)操作数据,既保证数据安全(如售价不能为负),又降低代码耦合度。
鸿蒙电商开发中需封装的场景:
- 敏感数据(如售价、库存、折扣):需校验合法性,禁止外部随意修改;
- 普通数据(如商品名称、分类):可直接暴露,无需额外限制。
3.2 访问修饰符规则
ArkTS提供三种核心访问修饰符,默认所有成员为public,用于控制属性/方法的访问范围:
| 修饰符 | 访问范围 | 鸿蒙电商应用场景 | 示例 |
|---|---|---|---|
| public | 类内、类外均可访问 | 普通非敏感数据(商品名称、分类) | public name: string; |
| private | 仅类内可访问 | 敏感数据(商品售价、库存、折扣) | private _price: number; |
| protected | 类内、子类可访问,类外不可 | 层级类中间数据(下一节继承章节讲解) | protected config: string; |
3.3 读写私有数据的两种方式
私有数据(如_price、stock、discount)无法被外部直接访问,需通过“公共方法”或“getter/setter”实现受控读写。本节以_price(售价)为核心案例,演示两种方式的实现与区别。
1. 通过公共方法访问/修改私有字段(传统方式)
// model/Goods.ets 新增公共方法(可选,用于对比getter/setter)
export class Goods {
// 其他代码不变...
/**
* 公共方法:设置售价(带校验)
* @param newPrice 新售价
*/
setPrice(newPrice: number) {
if (newPrice < 0) {
console.warn(`【${this.name}】售价不能为负,修改失败`);
return;
}
this._price = newPrice;
console.log(`【${this.name}】售价修改为:${newPrice}元`);
}
/**
* 公共方法:获取售价
* @returns 当前售价
*/
getPrice() {
return this._price;
}
}
// pages/Index.ets 测试公共方法
aboutToAppear() {
const phone = new Goods("鸿蒙Mate70手机", 5999, 100, 0.8, "数码产品");
phone.setPrice(-100); // 非法值:修改失败
console.log(`第一次读取售价:${phone.getPrice()}`); // 输出:5999
phone.setPrice(6999); // 合法值:修改成功
console.log(`第二次读取售价:${phone.getPrice()}`); // 输出:6999
}
打印输出:
W 鸿蒙Mate70手机】售价不能为负,修改失败
第一次读取售价:5999
【鸿蒙Mate70手机】售价修改为:6999元
第二次读取售价:6999
上述普通方法可实现私有属性的受控读写,但ArkTS提供了更贴合“属性操作”语义的语法——getter/setter,下面我们用它重构商品类的私有属性访问逻辑。
2. 通过getter/setter访问/修改私有字段(推荐方式)
getter/setter是ArkTS专门为“属性受控读写”设计的语法,比普通方法更贴合“属性操作”的语义,本节重点讲解这种方式:
// model/Goods.ets 替换公共方法为getter/setter
export class Goods {
// 公共属性(不变)
public name: string;
category: string = "商品分类";
// 私有属性:仅类内可访问(核心案例)
private _price: number;
private _discount: number = 1;
private _stock: number;
// 构造函数(不变)
constructor(name: string, price: number, stock: number, discount?: number, category?: string) {
this.name = name;
this._price = Math.max(price, 0);
this._stock = Math.max(stock, 0);
if (discount && discount > 0 && discount <= 1) {
this._discount = discount;
}
this.category = category?.trim() || this.category;
}
// 实例方法(不变)
printInfo(): void {
const finalPrice = parseFloat((this._price * this._discount).toFixed(2));
console.log(`
商品名称:${this.name}
商品分类:${this.category}
商品原价:${this._price}元
商品折扣:${this._discount * 10}折
折后售价:${finalPrice}元
商品库存:${this._stock}件
`);
}
/**
* setter:修改售价(带安全校验)
* 语法:set + 属性名(无下划线),参数为新值
*/
set price(newPrice: number) {
if (newPrice < 0) {
console.warn(`【${this.name}】售价不能为负,修改失败`);
return;
}
this._price = newPrice;
console.log(`【${this.name}】售价修改为:${newPrice}元`);
}
/**
* getter:读取售价
* 语法:get + 属性名(无下划线),无参数,返回属性值
* 注意:getter和setter需手动成对定义,不会自动生成
*/
get price() {
return this._price;
}
// 折扣的getter/setter
set discount(newDiscount:number) {
if (newDiscount > 0 && newDiscount <= 1) {
this._discount = newDiscount;
console.log(`【${this.name}】折扣修改为:${newDiscount * 10}折`);
} else {
console.warn(`【${this.name}】折扣无效(需0~1之间),修改失败`);
}
}
get discount(){
return this._discount;
}
// 库存的getter/setter
set stock(newStock:number) {
if (newStock < 0) {
console.warn(`【${this.name}】库存不能为负,修改失败`);
return;
}
this._stock = newStock;
console.log(`【${this.name}】库存修改为:${newStock}件`);
}
get stock(){
return this._stock;
}
}
四、getter/setter方法的实战与深度解析
4.1 调用测试(getter/setter)
// pages/Index.ets
import { Goods } from '../model/Goods';
@Entry
@Component
struct Index {
aboutToAppear(): void {
const phone = new Goods("鸿蒙Mate70手机", 5999, 100, 0.8, "数码产品");
const watch = new Goods("鸿蒙智能手表", 1299, 50, 0.9, "穿戴设备");
// 测试售价的getter/setter
phone.price = -100; // 非法值:修改失败(触发setter校验)
console.log(`第一次读取售价:${phone.price}`); // 读取:5999(触发getter)
phone.price = 7999; // 合法值:修改成功
console.log(`第二次读取售价:${phone.price}`); // 读取:7999
// 测试折扣的getter/setter
watch.discount = 1.2; // 非法值:修改失败
console.log(`第一次读取折扣:${watch.discount * 10}折`); // 读取:9折
watch.discount = 0.85; // 合法值:修改成功
console.log(`第二次读取折扣:${watch.discount * 10}折`); // 读取:8.5折
// 测试库存的getter/setter
watch.stock = -20; // 非法值:修改失败
console.log(`第一次读取库存:${watch.stock}件`); // 读取:50件
watch.stock = 80; // 合法值:修改成功
console.log(`第二次读取库存:${watch.stock}件`); // 读取:80件
// 打印商品信息(验证售价/折扣/库存修改生效)
phone.printInfo();
watch.printInfo();
/**
* getter/setter核心特点:
* 1. 需手动成对定义(仅get表示只读,仅set表示只写,通常成对使用)
* 2. 访问语法与普通属性一致(对象.属性),更直观
* 3. setter仅负责单个属性的校验与修改,符合"单一职责"
* 4. 隐藏私有属性的真实名称(_price),降低外部依赖
* 5. 不写get 方法直接 phone.price 读取值:undefined
*/
}
build() {
Text("鸿蒙商品管理项目")
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin(20);
}
}
打印输出:
【鸿蒙Mate70手机】售价不能为负,修改失败
第一次读取售价:5999
【鸿蒙Mate70手机】售价修改为:7999元
第二次读取售价:7999
【鸿蒙智能手表】折扣无效(需0~1之间),修改失败
第一次读取折扣:9折
【鸿蒙智能手表】折扣修改为:8.5折
第二次读取折扣:8.5折
【鸿蒙智能手表】库存不能为负,修改失败
第一次读取库存:50件
【鸿蒙智能手表】库存修改为:80件
第二次读取库存:80件
// phone.printInfo() 输出:
商品名称:鸿蒙Mate70手机
商品分类:数码产品
商品原价:7999元
商品折扣:8折
折后售价:6399.2元
商品库存:100件
// watch.printInfo() 输出:
商品名称:鸿蒙智能手表
商品分类:穿戴设备
商品原价:1299元
商品折扣:8.5折
折后售价:1104.15元
商品库存:80件
4.2 getter/setter与普通方法的本质区别
| 特性 | 普通方法(如setPrice/getPrice) | getter/setter方法 | 鸿蒙开发推荐场景 |
|---|---|---|---|
| 语法形式 | 方法调用(对象.方法名(参数)) |
属性操作(对象.属性 = 值/对象.属性) |
- |
| 职责范围 | 可包含多逻辑(修改属性+日志+接口调用) | 仅负责单个属性的受控读写,单一职责 | - |
| 语义表达 | 强调“执行动作”(如“设置售价”) | 强调“操作属性”(如“修改售价属性”) | - |
| 工程规范性 | 无强制约束,易职责混乱 | 符合面向对象封装设计,语义更清晰 | - |
| 推荐场景 | 多逻辑组合操作(改价+同步服务器) | 单个属性的简单校验(售价/库存/折扣) | ✅ 优先选型 |
4.3 选型原则(鸿蒙开发场景)
- 场景1:仅需受控读写单个属性(如售价、库存)→ 优先用getter/setter;
- 场景2:需包含多逻辑(如修改属性+同步服务器+打印日志)→ 用普通方法;
- 场景3:属性仅需“读”(如折后价)→ 仅定义getter(或普通方法);
- 场景4:属性无需校验,直接读写→ 用public属性(无需封装)。
五、常见问题解答
5.1 类可以没有构造函数吗?
可以。ArkTS会自动生成空构造函数,但未设置默认值的属性必须在构造函数中赋值,否则编译报错:
// model/EmptyClass.ets
export class EmptyClass {
msg: string = "默认信息"; // 设默认值,无构造函数也合法
// 未设默认值的属性,无构造函数会报错:Property 'price' has no initializer and is not definitely assigned in the constructor.
// price: number;
}
// pages/Index.ets 调用测试
import { EmptyClass } from '../model/Goods';
const obj = new EmptyClass();
console.log(obj.msg); // 输出:默认信息
打印输出:
默认信息
5.2 多个对象实例的属性是否相互独立?
是。每个对象拥有独立的内存空间,修改一个对象的属性不会影响其他对象:
// pages/Index.ets 调用测试
import { Goods } from '../model/Goods';
const phone = new Goods("鸿蒙Mate70手机", 5999, 100, 0.8);
const watch = new Goods("鸿蒙智能手表", 1299, 50, 0.9);
phone.price = 4999; // 仅修改phone的售价
const phoneFinalPrice = parseFloat((phone.price * phone.discount).toFixed(2));
const watchFinalPrice = parseFloat((watch.price * watch.discount).toFixed(2));
console.log(`手机折后价:${phoneFinalPrice}元`); // 输出:3999.2
console.log(`手表折后价:${watchFinalPrice}元`); // 输出:1169.1(不受phone影响)
打印输出:
【鸿蒙Mate70手机】售价修改为:4999元
手机折后价:3999.2元
手表折后价:1169.1元
5.3 getter/setter必须成对定义吗?
不一定:
- 成对定义(get+set):属性可读写(如售价);
- 仅定义get:属性只读(如商品ID,一旦创建不可修改);
- 仅定义set:属性只写(极少用,如临时接收数据);
- 注意:仅定义set时,外部读取属性值,会得到undefined未定义。
// 仅定义getter(只读属性)
export class ReadOnlyClass {
private _id: string = "GOODS_001";
get id() {
return this._id;
}
}
// 仅定义setter(只写属性)
export class WriteOnlyClass {
private _tempData: string = "";
set tempData(data: string) {
this._tempData = data;
console.log(`临时数据已接收:${data}`);
}
}
// 调用测试
import { ReadOnlyClass } from '../model/ReadOnlyClass';
import { WriteOnlyClass } from '../model/WriteOnlyClass';
aboutToAppear(){
const readOnlyObj = new ReadOnlyClass();
console.log(`商品ID:${readOnlyObj.id}`); // 可读
// readOnlyObj.id = "GOODS_002"; // 报错:Cannot assign to 'id' because it is a read-only property.
const writeOnlyObj = new WriteOnlyClass();
writeOnlyObj.tempData = "测试数据"; // 可写
console.log(`临时数据读取:${writeOnlyObj.tempData}`); // 输出:undefined
}
打印输出:
商品ID:GOODS_001
临时数据已接收:测试数据
临时数据读取:undefined
六、内容总结
- 面向对象的核心是“类(模板)+对象(实例)”,通过封装解决面向过程多实例代码冗余的问题,适配鸿蒙电商商品管理场景;
- ArkTS类必须遵循“先声明、后赋值”,不支持TypeScript的构造函数参数简化写法,
this是访问实例属性的唯一合法方式; function关键字规则:全局函数必须加,类内部方法禁止加,否则语法报错;- 封装的核心是用
private隐藏敏感数据,通过getter/setter或普通方法暴露受控接口,既保证数据安全,又简化外部调用; - getter/setter是属性受控读写的专用语法,比普通方法更贴合属性操作语义,优先用于单个属性的校验与读写(如鸿蒙商品的售价、库存)。
七、代码仓库
- 工程名称:ClassObjectDemo
- 仓库地址:https://gitee.com/juhetianxia321/harmony-os-code-base.git
八、下节预告
- 下一节:类的继承与多态入门将深入学习面向对象进阶核心:
- 类的继承规则:
extends关键字、super调用父类构造/方法的核心语法; protected修饰符实战:子类复用父类中间数据(如商品售价);- 多态入门:父类引用管理子类对象,实现鸿蒙电商商品分类逻辑的统一管理;
- 类的继承规则:
- 继承与多态是鸿蒙电商场景“商品分类管理”的核心设计思想,掌握后将为后续抽象类、接口学习打下基础。
浙公网安备 33010602011771号