散修带你入门鸿蒙应用开发基础第十一节:面向对象思想入门与类的定义

第十一节:面向对象思想入门与类的定义

炼气十一重天

【学习目标】

  1. 理解“类(模板)-对象(实例)”的核心关系,区分面向过程与面向对象编程思维在鸿蒙开发中的适配场景;
  2. 掌握ArkTS类的标准定义语法(属性声明、构造函数、实例方法),熟练使用new关键字实例化对象;
  3. 吃透this关键字的含义与使用场景,规避鸿蒙开发中this的常见使用错误;
  4. 理解封装的核心意义,掌握public/private访问修饰符(protected将在下一节继承章节讲解),实现敏感数据(如商品底价)的安全封装;
  5. 掌握getter/setter方法的设计思路,理解其与普通方法的本质区别,实现鸿蒙应用中属性的安全读写。

【学习重点】

  • ArkTS类“先声明、后赋值”的核心规则,区分与TypeScript的核心差异;
  • this关键字在构造函数/实例方法中的正确使用,规避“属性未绑定”错误;
  • private修饰符在鸿蒙敏感数据场景(商品底价)的核心应用;
  • getter/setter方法与普通方法的本质区别,掌握set/get关键字的设计初衷与选型原则;
  • 类内部方法与全局函数的语法差异(function关键字的使用规则);
  • 面向对象封装思想在鸿蒙业务模型类(商品)中的落地。

一、工程结构

本节示例代码基于鸿蒙Stage模型工程(API 12+),工程名称为ClassObjectDemo,核心目录结构遵循鸿蒙开发规范:

ClassObjectDemo/
├── entry/                          # 应用主模块
│   ├── src/
│   │   ├── main/
│   │   │   ├── ets/                # ArkTS代码根目录
│   │   │   │   ├── pages/          # 页面代码目录(补充完整路径)
│   │   │   │   │   └── Index.ets   # 测试页面
│   │   │   │   └── model/          # 业务模型类目录
│   │   │   │       └── Goods.ets   # 商品类
│   │   │   ├── resources/          # 资源目录(非本节重点)
│   │   │   └── module.json5        # 模块配置文件
│   │   └── oh-package.json5        # 工程依赖配置
└── hvigorfile.ts                   # 构建脚本(默认生成)

二、类与对象基础解析

2.1 面向过程 vs 面向对象

编程的核心是“处理数据+实现逻辑”,两种思维在鸿蒙开发中的适配性差异显著:

编程思维 核心特点 鸿蒙适配场景 缺点
面向过程 数据与方法分离,按步骤执行 简单逻辑(如单个商品信息打印) 多实例场景代码冗余、维护性差
面向对象 数据与方法封装为类,实例化调用 多实例管理(商品) 入门稍复杂,适配工程化开发

2.1.1 面向过程实现

// 定义数据
let product: string = "鸿蒙基础开发手册";
let price: number = 59.9;

// 封装打印方法(需手动传参)
function printGoodsInfo(name: string, price: number): void {
  console.log(`商品名称:${name},价格:${price}元`);
}

// 执行逻辑
printGoodsInfo(product, price);

2.1.2 面向对象实现

步骤1:定义商品类(数据+行为封装)

// model/Goods.ets 
export class Goods {
  // 商品属性(数据封装)
  name: string;
  price: number;
  category?: string;

  // 构造函数:初始化属性(必须先声明后赋值)
  constructor(name: string, price: number, category?: string) {
    this.name = name;
    this.price = price;
    if (category) {
      this.category = category;
    }
  }

  // 实例方法:行为封装
  printInfo(): void {
    console.log(`商品名称:${this.name},价格:${this.price}元`);
  }
}

步骤2:调用商品类

// pages/Index.ets 导入Goods
import { Goods } from '../model/Goods';

// 实例化多商品,自带属性和方法,无需重复传参
aboutToAppear(){
  const phone = new Goods("鸿蒙Mate70手机", 5999.00, "数码产品");
  const watch = new Goods("鸿蒙智能手表", 1299.00, "穿戴设备");
  phone.printInfo(); // 直接调用,逻辑内聚
  watch.printInfo();
}

2.2 核心概念解析

  • 类(Class):描述一类事物的通用模板,定义了该类事物的共同属性和行为(如Goods类定义了商品的名称、价格属性和打印信息方法);
  • 对象(Object):类的具体实例,是类模板的具象化(如phoneGoods类的实例,对应具体的商品);
  • 封装:将数据(属性)和操作数据的逻辑(方法)打包到类中,外部仅通过暴露的接口操作数据,无需关心内部实现。

2.3 类的标准定义语法

ArkTS类遵循“先声明、后赋值”原则,不支持 TypeScript中“构造函数参数加public简化属性定义”的写法,标准模板:

export class 类名 {
  // 1. 属性声明:指定类型,可选默认值
  公共属性名: 数据类型;
  带默认值属性名: 数据类型 = 默认值;

  // 2. 构造函数(可选):实例化时初始化属性
  constructor(参数1: 数据类型, 参数2?: 数据类型) { // 参数2为可选
    this.公共属性名 = 参数1;
    if (参数2) {
      this.带默认值属性名 = 参数2;
    }
  }

  // 3. 实例方法:描述对象行为
  无返回值方法(): void {
    console.log(this.公共属性名); // 访问属性必须加this
  }

  有返回值方法(参数: 数据类型): 返回值类型 {
    return this.带默认值属性名 + 参数;
  }
}

2.4 function关键字使用规则

核心规则(强制)

  1. 全局/独立函数:必须显式使用function关键字(或箭头函数)声明,这是ArkTS的基础语法规则;
  2. 类内部方法(实例方法、getter/setter、静态方法):禁止使用function关键字,直接以“方法名(参数): 返回值类型”的形式声明,加function会直接触发语法报错。
// 示例1:全局函数(必须加function)
function globalPrintInfo(name: string): void {
  console.log(`全局函数:${name}`);
}

// 示例2:类内部方法(无需加function)
class Goods {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  // ✅ 正确:类内部方法无function关键字
  printInfo(): void {
    console.log(`类方法:${this.name}`);
  }

  // ❌ 错误:类内部方法加function关键字,语法报错
  // function printError(): void {
  //   console.log(this.name);
  // }
}

2.5 this关键字深度解析

this指向“当前对象实例”,仅在构造函数和实例方法中有效,核心作用是区分“类的属性”和“方法/构造函数的参数”。

2.6 常见错误及规避示例

//  错误示例对比
// ❌ ArkTS编译报错示例:未声明属性直接在构造函数赋值
class GoodsError {
  constructor(name: string) {
    this.name = name; // 报错:Property 'name' does not exist on type 'GoodsError'
  }
}

// ✅ 正确写法:先声明、后赋值
class GoodsCorrect {
  name: string; // 第一步:声明属性
  constructor(name: string) {
    this.name = name; // 第二步:构造函数赋值
  }
}
export class Goods {
  // 错误:ArkTS不允许构造函数参数加public简化属性声明
  // constructor(public name: string, public price: number) {}
   
  // 正确设置构造函数 有属性不可简化
  name: string;
  price: number;
  constructor(name: string, price: number) {
    // 错误:参数自赋值,类属性未初始化
    // name = name; 
    // 正确
    this.name = name;
    this.price = price;
  }
 
  // 正确写法:类内部方法无function关键字
  printInfo(): void {
    // 错误:访问未定义的局部变量
    // console.log(name);
    // 正确
    console.log(`商品名称:${this.name},价格:${this.price}元`);
  }
}

三、封装思想及访问控制应用

3.1 封装的核心意义

本节所有封装案例均围绕鸿蒙电商核心的“商品类”展开,封装的核心目标是:将商品的核心数据(名称、底价、分类)与操作逻辑(价格校验、售价计算)打包为独立的业务模型,外部仅通过暴露的接口(getter/setter、普通方法)操作商品数据,既保证数据安全(如底价不被随意修改),又降低代码耦合度。

鸿蒙开发中需封装的场景:

  • 电商类:商品底价、供应商信息。

3.2 访问修饰符规则

ArkTS提供三种核心访问修饰符,默认所有成员为public,用于控制属性/方法的访问范围:

修饰符 访问范围 鸿蒙应用场景 示例
public 类内、类外均可访问 普通非敏感数据(商品名称、分类) public name: string;
private 仅类内可访问 敏感数据(商品底价) private _basePrice: number;
protected 类内、子类可访问,类外不可 层级类中间数据(下一节《继承基础与多态入门》 详细讲解) protected config: string;

3.3 private修饰符实战

以商品底价为例,用private隐藏敏感数据,仅通过公共方法/getter/setter实现受控访问:

四、getter/setter方法(安全读写属性)

对于需对外暴露但需校验的属性(如商品底价),通过getter/setter实现“校验式读写”,同时深度解析set关键字的设计初衷、与普通方法的本质区别及选型原则。

4.1 实战示例(商品底价管理)

// model/Goods.ets
export class Goods {
  public name: string;
  // 步骤1:声明时设置默认值“商品分类”,未传入参数时生效
  public category: string = "商品分类";
  private _basePrice: number; // 私有底价(加下划线区分setter名,避免重名)

  constructor(name: string, basePrice: number, category?: string) {
    // 可选优化:对名称做非空校验(新手可自行补充,如this.name = name.trim() || "未命名商品")
    this.name = name;
    // 校验:底价不能为负
    this._basePrice = basePrice > 0 ? basePrice : 0;
    // 步骤2:传入合法的category参数时覆盖默认值,否则保留默认值
    if (category?.trim()) {
      this.category = category.trim();
    }
  }

  // setter:受控修改底价(仅负责单个属性的校验与修改)
  set basePrice(newPrice: number) {
    if (newPrice < 0) {
      console.log("商品底价不能为负,修改失败");
      return;
    }
    this._basePrice = newPrice;
    console.log(`${this.name}底价修改为:${newPrice}元`);
  }

  // getter:受控读取底价
  get basePrice() {
    return this._basePrice;
  }
}

// 【辅助测试函数】:仅用于快速验证类的功能,实际开发中直接实例化Goods类即可
export function testGoodsClass() {
 // 实例化商品并测试底价读写
  const phone = new Goods("鸿蒙手机", 5999.00, "数码产品");
  console.log("商品名称:", phone.name); // 合法访问public属性
  console.log("初始底价:", phone.basePrice); // 通过getter读取底价

  phone.basePrice = 4999; // 通过setter修改底价(合法)
  console.log("修改后底价:", phone.basePrice); 

  phone.basePrice = -100; // 校验失败,底价不修改
  // 错误:直接访问private属性,编译报错
  // console.log(phone._basePrice);
}

4.2 调用测试

// pages/Index.ets
import { testGoodsClass, Goods } from '../model/Goods';

aboutToAppear(){
  // 1. 辅助测试函数调用(快速验证功能)
  testGoodsClass();

  // 2. 实际开发场景:直接实例化Goods类
  const watch = new Goods("鸿蒙智能手表", 1299.00, "穿戴设备");
  watch.basePrice = 1199;
  console.log(`智能手表底价:${watch.basePrice}`);

}

4.3 深度解析:setget关键字的核心价值

set(及get)是ArkTS/TypeScript为“属性的受控读写”设计的专用语法,并非普通方法的“语法糖”,其核心价值体现在语法特性、语义设计、工程规范三个维度:
setter的核心配套价值是与getter联动,让私有属性的“读”和“写”都以属性语法暴露,同时隐藏内部实现:
这种模式下,外部调用者无需关心“属性是私有还是公共”,只需按统一的属性语法操作,而开发者可在get/set中封装所有读写逻辑,符合“开闭原则”(扩展开放、修改封闭)。在鸿蒙电商应用中,商品底价的setter可统一接入价格风控逻辑,无需修改外部调用代码,这就是开闭原则的落地。

4.3.1 对比普通方法与setter/getter

// 语法特性:属性式操作
class Goods {
  name: string;
  private _basePrice: number;

  constructor(name: string, basePrice: number) {
    this.name = name;
    this._basePrice = basePrice > 0 ? basePrice : 0;
  }

  // ① 普通方法:通用方法调用语法
  setBasePriceNormal(newPrice: number) {
    if (newPrice < 0) {
      console.log("商品底价不能为负,修改失败");
      return;
    }
    this._basePrice = newPrice;
  }

  // ② setter方法:属性赋值语法
  set basePrice(newPrice: number) {
    if (newPrice < 0) {
      console.log("商品底价不能为负,修改失败");
      return;
    }
    this._basePrice = newPrice;
  }

  // getter:受控读取属性
  get basePrice() {
    return this._basePrice;
  }
}

export function testGoodsClass(){
    // 调用对比
  const phone = new Goods("鸿蒙手机", 5999.00);
  // 普通方法:方法名(参数)修改价格
  phone.setBasePriceNormal(3999);
  console.log(`普通方法设置鸿蒙手机底价:${phone.basePrice}`); // 输出:3999
  // setter:对象.属性 = 值 修改价格
  phone.basePrice = 4999;
  console.log(`setter设置鸿蒙手机底价:${phone.basePrice}`); // 输出:4999
}
  • 普通方法:遵循“方法调用”语法,强调“执行一个动作”;
  • setter方法:遵循“属性赋值”语法,强调“修改一个属性”,贴合面向对象中“属性封装”的设计思想。
    如果_basePrice直接可以访问、数值可随意修改,无数值安全验证输出负数都可以。使用set既满足了数值安全设置,又可以使用属性赋值操作。

4.3.2 明确的职责边界,提升代码可读性与可维护性

特性 普通方法 setter方法
职责范围 可包含任意逻辑(修改属性、调用接口、日志记录等) 唯一职责:受控修改单个属性
代码可读性 需阅读方法体才能确定核心逻辑 见名知意,set前缀直接表明“修改属性”
工程规范性 无强制约束,易出现职责混乱 符合面向对象“单一职责”原则

4.3.3 选型原则(通用,适用于所有开发场景)

场景 推荐方案 核心依据
仅需受控修改单个属性(含校验/简单逻辑) setter方法 语义清晰、符合属性封装设计
方法包含多逻辑(修改属性+调用接口/日志/多属性修改) 普通方法 避免setter职责过载
属性需对外暴露“读+写”能力 getter+setter 统一语法,完整封装读写逻辑
属性仅需“读”能力,无需修改 仅getter方法 隐藏修改入口,保证数据安全

五、核心案例:鸿蒙商品管理类

整合封装、this、访问控制,实现商品底价的安全管理:

// model/Goods.ets
export class Goods { 
  public name: string;
  // 新增:商品分类属性,设置默认值,未传入category时生效
  public category: string = "商品分类";
  private _basePrice: number; // 私有底价
  // 新增:利润利率设为类属性,支持外部灵活调整(默认10%)
  public profitRate: number = 0.1;
  // 可选属性`?`放在最后(必须遵守)
  constructor(name: string, basePrice: number, category?: string) {
    this.name = name
    // 可选优化:
    this.category = category?.trim() || this.category

// 校验:底价不能为负(基础数值校验)
    this._basePrice = basePrice > 0 ? basePrice : 0;
// 传入合法的category时覆盖默认值,否则保留默认值
    this.category = category || this.category
    
  }

  // 普通方法-修改底价(用于对比setter)
  setBasePriceNormal(newPrice: number) {
    if (newPrice < 0) {
      console.log("商品底价不能为负,修改失败");
      return;
    }
    this._basePrice = newPrice;
  }

  // setter:修改底价(单一职责:仅受控修改底价)
  set basePrice(newPrice: number) {
    if (newPrice < 0) {
      console.log("商品底价不能为负,修改失败");
      return;
    }
    this._basePrice = newPrice;
    console.log(`${this.name}底价修改为:${newPrice}元`);
  }

  // getter:读取底价
  get basePrice() {
    return this._basePrice;
  }

  // 普通方法:多逻辑组合(计算售价+数据格式化)
  calculateSellingPrice(): number {
    // 利润利率从类属性读取,支持外部调整
    const sellingPrice = this._basePrice * (1 + this.profitRate);
    return parseFloat(sellingPrice.toFixed(2));
  }

  // 普通方法:多逻辑组合(打印详情+数据校验)
  printBaseInfo(): void {
    if (!this.name) console.log("商品名称未设置");
    console.log(`
      商品名称:${this.name}
      商品分类:${this.category}
      商品售价:${this.calculateSellingPrice()}元
    `);
  }
}

Index页面直接创建类、调用方法


// pages/Index.ets 测试调用(实际开发场景,推荐)
import { Goods } from '../model/Goods';

aboutToAppear() {
  const phone = new Goods("鸿蒙手机", 5999.00, "数码产品");
  // 普通方法修改底价
  phone.setBasePriceNormal(3999);
  console.log(`普通方法设置鸿蒙手机底价:${phone.basePrice}`); // 输出:3999
  console.log(`商品售价:${phone.calculateSellingPrice()}`); // 4398.9

  // 调整利润利率(数码产品设为15%)
  phone.profitRate = 0.15;
  // setter修改底价
  phone.basePrice = 4999;
  console.log(`setter设置鸿蒙手机底价:${phone.basePrice}`); // 输出:4999
  console.log(`商品售价:${phone.calculateSellingPrice()}`); // 5748.85
  
  // 打印商品详情
  phone.printBaseInfo();

  // 测试category默认值:未传入category时使用“商品分类”
  const book = new Goods("鸿蒙开发手册", 59.9);
  console.log(`图书分类:${book.category}`); // 输出:商品分类
}

运行预览Log日志

类_20251211103932_507_125

六、常见问题解答

6.1 类可以没有构造函数吗?

可以。ArkTS会自动生成空构造函数,但未设置默认值的属性必须在构造函数中赋值,否则编译报错:

// model/Goods.ets
export class EmptyClass {
  msg: string = "默认信息"; // 设默认值,无构造函数也合法
}

// pages/Index.ets 调用测试(补充路径)
import { EmptyClass } from '../model/Goods';

const obj = new EmptyClass();
console.log(obj.msg); // 输出:默认信息

6.2 多个对象实例的属性是否相互独立?

是。每个对象拥有独立的内存空间,修改一个对象的属性不会影响其他对象:

// pages/Index.ets 调用测试(补充导入)
import { Goods } from '../model/Goods';

const phone = new Goods("鸿蒙手机", 2999);
const watch = new Goods("鸿蒙手表", 299);
phone.basePrice = 2899;

console.log(`phone售价:${phone.calculateSellingPrice()}`); // 3187.9
console.log(`watch售价:${watch.calculateSellingPrice()}`); // 328.9

6.3 getter/setter和普通方法的本质区别?

  • 语法层面:getter/setter遵循“属性语法”,普通方法遵循“方法调用语法”;
  • 语义层面:getter/setter专注于“单个属性的受控读写”,普通方法可包含任意逻辑;
  • 设计层面:getter/setter符合“单一职责”“属性封装”等面向对象设计原则,普通方法无强制约束;
  • 能力层面:getter/setter可联动实现完整的属性读写封装,普通方法需手动维护读写逻辑的一致性。

七、课堂小结

  1. 面向对象通过“类+对象”封装数据和方法,解决面向过程多实例代码冗余问题,核心是“数据与行为的绑定”;
  2. ArkTS类需遵循“先声明、后赋值”原则,不支持TypeScript构造函数参数加public的简化写法,this关键字是访问实例属性的唯一合法方式;
  3. function关键字的强制规则:全局/独立函数必须显式声明,类内部方法禁止使用,加function会直接语法报错;
  4. 封装的核心是用private隐藏敏感数据(如商品底价),仅通过受控接口暴露操作能力,public/private明确了属性/方法的访问边界;
  5. set/get关键字并非普通方法的语法糖,而是面向对象“属性封装”的专用语法,核心价值是:
    • 语法层面:属性式操作,贴合属性读写的直觉;
    • 语义层面:单一职责,明确区分“属性修改”与“通用逻辑执行”;
    • 设计层面:与get联动实现完整的属性读写封装,符合开闭原则;
  6. setter与普通方法的选型需遵循“单一职责”原则:仅受控修改单个属性用setter,包含多逻辑组合用普通方法,该原则适用于所有开发场景;
  7. 下一节将通过继承扩展商品类,学习protected修饰符和子类与父类的协同逻辑,实现鸿蒙商品分类的统一管理。

八、代码仓库

本节代码已同步至:https://gitee.com/juhetianxia321/harmony-os-code-base.git

九、下节预告

  1. 下一节:继承基础与多态入门将深入学习面向对象进阶核心:
    • 类的继承规则:extends关键字、super调用父类构造/方法的核心语法;
    • protected修饰符实战:子类复用父类中间数据(如商品底价);
    • 多态入门:父类引用管理子类对象,实现鸿蒙商品分类逻辑的统一管理;
  2. 继承与多态是鸿蒙电商场景“商品分类管理”的核心设计思想,掌握后将为后续抽象类、接口学习打下基础,最终通过“鸿蒙电商商品管理逻辑”整合所有知识点,实现完整业务流程。

十、鸿蒙开发者学习与认证指引

(一)、官方学习班级报名(免费)

  1. 班级链接HarmonyOS赋能资源丰富度建设(第四期)
  2. 学号填写规则:填写个人手机号码即可完成班级信息登记

(二)、HarmonyOS应用开发者认证考试(免费)

  1. 考试链接HarmonyOS开发者能力认证入口

  2. 认证等级及适配人群

    • 基础认证:适配软件工程师、移动应用开发人员,需掌握HarmonyOS基础概念、DevEco Studio基础使用、ArkTS及ArkUI基础开发等能力;
    • 高级认证:适配项目经理、工程架构师,需掌握系统核心技术理念、应用架构设计、关键技术开发及应用上架运维等能力;
    • 专家认证:适配研发经理、解决方案专家,需掌握分布式技术原理、端云一体化开发、跨端迁移及性能优化等高级能力。
  3. 认证权益:通过认证可获得电子版证书以及其他专属权益。

posted @ 2025-12-12 17:17  鸿蒙-散修  阅读(0)  评论(0)    收藏  举报