零基础鸿蒙应用开发第十六节:闭包函数基础入门

零基础鸿蒙应用开发学习计划表

【学习目标】

  1. 理解闭包的核心定义,明确其与作用域链、箭头函数的关联;
  2. 掌握ArkTS中闭包的标准实现方式,理解“局部变量生命周期延长”的底层逻辑;
  3. 熟悉闭包的常见应用场景(计数器、私有变量、防抖);
  4. 掌握闭包内存泄漏的规避方法,写出规范、安全的闭包代码。

【学习重点】

  1. 核心定义:外层普通函数的局部变量被内层箭头函数引用,且内层函数被外部访问(返回/导出),形成的“变量绑定+作用域持久化”结构;
  2. 核心原理:内层函数保留对外层作用域的引用,使局部变量不随外层函数执行完毕销毁;
  3. 实战场景:基础计数器(状态保留)、私有变量封装(数据安全)、防抖(按钮防连点+避免重复提交);
  4. 风险规避:及时释放闭包引用,避免内存冗余;鸿蒙组件中通过生命周期钩子管理闭包/定时器资源。

【前置知识】

学习前需掌握:箭头函数语法、函数作用域(局部/全局)、函数返回值。

一、工程结构

本节我们将创建名为ClosureBasicDemo的工程,基于 鸿蒙5.0(API12)开发,使用DevEco Studio 6.0+ 工具,项目结构目录如下:

ClosureBasicDemo/
├── entry/
│   ├── src/
│   │   ├── main/
│   │   │   ├── ets/
│   │   │   │   ├── entryability/  # 应用入口(默认生成,无需修改)
│   │   │   │   ├── pages/         # 可视化测试页面
│   │   │   │   │   ├── Index.ets   # 闭包功能测试页
│   │   │   │   │   └── TimerPage.ets # 内存泄漏演示页
│   │   │   │   └── utils/         # 闭包核心逻辑封装
│   │   │   │       └── ClosureTest.ets
│   │   │   ├── resources/         # 资源文件(默认生成)
│   │   │   └── module.json5       # 工程配置(默认生成)

二、闭包:核心概念与底层原理

2.1 闭包的定义

闭包是外层普通函数执行后,其局部变量被内层箭头函数引用,且内层函数被外部持有(返回/导出) 形成的特殊结构,核心特征:

  • 外层函数执行完毕后,局部变量不会被垃圾回收(因内层函数仍引用);
  • 内层函数可访问/修改该变量,且变量仅能通过内层函数操作(私有性);
  • 通俗理解:闭包像给外层变量做了“保护罩”,既不会消失,也不会被外部随意修改。

2.2 核心原理:作用域链持久化

闭包的本质是作用域链未断裂,简化为3步理解:

  1. 外层函数执行时,创建局部变量并生成专属作用域;
  2. 内层箭头函数引用该变量,绑定外层作用域的引用;
  3. 外层函数执行完毕后,因内层函数被外部持有(如返回、赋值给全局变量),作用域不被销毁,变量持续存活。

2.3 核心示例(计数器)

// utils/ClosureTest.ets
/**
 * 闭包核心示例:计数器
 * @returns 内层箭头函数,调用一次计数+1
 */
export function createCounter(): () => number {
  // 外层局部变量(本应随函数执行销毁,闭包使其持久化)
  let count: number = 0; 
  // 内层箭头函数:引用外层变量,且被返回(外部可访问)
  return () => {
    count++;
    return count;
  };
}

// 简化写法(匿名外层函数+立即调用,逻辑与标准写法一致)
export const counterSimple = (() => {
  let count: number = 0;
  return () => count++;
})();

// 测试代码(建议在Index.ets中调用,而非直接写在工具类)
// 👇 以下为测试示例,实际开发中可删除或注释
// const getIncrementId = createCounter();
// console.log(`标准写法第1次调用:${getIncrementId()}`); // 输出:1
// console.log(`标准写法第2次调用:${getIncrementId()}`); // 输出:2
// console.log(`简化写法第1次调用:${counterSimple()}`); // 输出:1
// console.log(`简化写法第2次调用:${counterSimple()}`); // 输出:2

2.4 错误写法与原因

// utils/ClosureTest.ets
// 错误写法1:function嵌套function
function badCounter1() {
  let count: number = 0;
  // 编译报错:ArkTS禁止普通函数嵌套(避免this指向混乱),内层必须用箭头函数
  // function inner() { 
  //   return count++;
  // }
}

// 错误写法2:内层未引用外层变量(无闭包效果)
function badCounter2() {
  let count: number = 0;
  // 无变量引用,外层执行后count直接销毁,仅普通函数返回
  return () => {
    console.log("未引用外层变量,无闭包效果");
  };
}

// 错误写法3:内层函数未被外部持有(仅内部执行)
function badCounter3() {
  let count: number = 0;
  // 内层函数仅在外部函数内执行,未返回/导出,执行后count销毁
  const inner = () => count++;
  inner();
}

2.5 闭包原理可视化流程图(计数器)

graph LR A["调用外层函数createCounter()"] --> B["创建局部变量count=0<br/>生成外层专属作用域"] B --> C["内层箭头函数引用count<br/>绑定外层作用域引用"] C --> D["外层函数执行完毕<br/>返回内层函数到外部(如getIncrementId)"] D --> E["外部调用getIncrementId()"] E --> F["通过绑定的作用域找到count<br/>修改并返回值(count++)"] F --> G["count持续存活<br/>等待下次调用(作用域未销毁)"]

2.6 闭包 vs 普通函数(核心对比)

特性 普通函数 闭包函数
变量生命周期 外层执行完毕,局部变量立即销毁 外层执行完毕,变量因内层引用保留
外部访问性 无法访问外层局部变量 可通过内层函数访问/修改变量
变量私有性 天然私有,但无状态保留能力 私有性+状态持久化
内存占用 执行后释放,内存占用低 持续占用内存(需主动释放)
适用场景 无状态保留需求的一次性逻辑 需状态保留+私有封装的场景(计数器/防抖)

对比示例

// utils/ClosureTest.ets
/**
 * 普通函数:变量随执行销毁
 */
export function normalFunc(): void {
  let temp: number = 10;
  console.log(`普通函数内:${temp}`); // 输出:10
}

/**
 * 闭包函数:变量持久化
 */
export function closureFunc(): () => number {
  let temp: number = 10;
  return () => {
    temp += 5;
    return temp;
  };
}

// 测试示例(建议在Index.ets中调用)
// normalFunc(); // 执行后temp销毁,外部无法访问
// const getTemp = closureFunc();
// console.log(`闭包第1次调用:${getTemp()}`); // 输出:15
// console.log(`闭包第2次调用:${getTemp()}`); // 输出:20(temp未销毁)

2.7 新手常见误区

误区 正确理解
闭包=全局变量 闭包变量是局部变量(仅内层函数可操作),全局变量可任意修改,二者本质不同
所有场景都用闭包 仅需“状态保留+私有封装”时使用,无需求时用普通函数更节省内存
闭包一定会内存泄漏 合理释放引用(如赋值undefined)则无泄漏风险,泄漏是未释放引用导致
内层用普通函数也能实现闭包 ArkTS禁止普通函数嵌套,必须用箭头函数,否则编译报错

三、闭包的常见应用场景

3.1 场景1:私有变量封装

需求:管理用户信息,外部无法直接修改变量,仅能通过指定方法操作(附带数据校验)。

// utils/ClosureTest.ets
/** User操作接口定义 */
interface UserManager {
  getName: () => string; // 获取名称:无参数,返回string
  setName: (newName: string) => void; // 设置名称:接收字符串,无返回值
}

/**
 * 私有变量封装:用户信息管理
 * @returns 包含getName/setName的方法对象(闭包保留私有name变量)
 */
export function createUserManager(): UserManager {
  let name: string = "默认名称"; // 私有变量,外部不可直接访问/修改
  return {
    // 获取名称(只读)
    getName: () => name,
    // 设置名称(附带空值校验,保证数据合法性)
    setName: (newName: string): void => {
      if (newName.trim()) { // 过滤空值/纯空格
        name = newName;
      }
    }
  };
}

// 测试示例(建议在Index.ets中调用)
// const user = createUserManager();
// console.log(`初始名称:${user.getName()}`); // 默认名称
// user.setName(""); // 空值:修改无效
// user.setName("张三");
// console.log(`修改后:${user.getName()}`); // 张三

3.2 场景2:防抖(按钮防连点)

鸿蒙开发中,Button(如订单提交、表单确认)快速多次点击会导致重复提交,闭包可通过私有状态实现“延时内忽略重复点击”,避免全局变量污染。

闭包实现防抖

// utils/ClosureTest.ets
/**
 * 闭包实现通用按钮防连点(无参数,立即执行+延时内禁止重复点击)
 * @param fn 需防连点的业务函数(无参数,无返回值)
 * @param delay 延时时间(默认1000ms)
 * @returns 防连点后的箭头函数(无参数)
 */
export function createDebounce(fn: () => void, delay: number = 1000): () => void {
  // 闭包保留的私有状态:是否处于延时锁定中(外部无法直接修改)
  let isDelaying: boolean = false;

  return () => {
    if (isDelaying) {
      console.log(`【防连点】延时期内忽略点击(剩余约${delay}ms)`);
      return;
    }

    fn(); // 立即执行业务逻辑
    isDelaying = true; // 标记锁定

    // 延时结束后重置状态(闭包保留isDelaying,直到手动释放)
    setTimeout(() => {
      isDelaying = false;
      console.log(`【防连点】延时期结束,可再次点击`);
    }, delay);
  };
}

/**
 * 模拟按钮点击的业务逻辑(如表单提交/订单确认)
 */
export function mockBtnSubmit(): void {
  console.log(`【按钮提交】执行业务逻辑,时间:${new Date().toLocaleTimeString()}`);
}

// 测试示例(建议在Index.ets中调用)
// const debouncedBtnClick = createDebounce(mockBtnSubmit, 1000);
// console.log("开始模拟快速点击:");
// debouncedBtnClick(); // ✅ 第1次:执行
// debouncedBtnClick(); // ❌ 第2次:忽略
// debouncedBtnClick(); // ❌ 第3次:忽略
// // 1秒后再次点击
// setTimeout(() => {
//   debouncedBtnClick(); // ✅ 执行
// }, 1100);

四、闭包的注意事项:规避内存泄漏

4.1 泄漏原因

闭包会延长变量生命周期,若长期保留闭包引用(如全局存储防抖函数、组件销毁后未清除定时器/闭包),私有状态变量会持续占用内存,导致泄漏。

4.2 规避方案

  1. 可选类型声明闭包变量((() => void) | undefined),兼容“引用/释放”状态;
  2. 不再使用时,赋值undefined释放引用;
  3. 鸿蒙组件中,在aboutToDisappear生命周期清空闭包/定时器引用;
  4. 定时器类资源,跳转/销毁前必须调用clearInterval/clearTimeout

五、内存泄漏演示(TimerPage.ets)

5.1 演示代码(故意保留泄漏问题)

// pages/TimerPage.ets
import router from '@ohos.router';

@Entry
@Component
struct TimerPage {
  // 定时器计时秒数
  @State count: number = 0;
  // 存储定时器ID,用于后续清除定时器
  private timerId: number | null = null;

  // 组件即将显示时触发
  aboutToAppear(): void {
    this.startTimer();
  }

  // 组件即将消失时触发(故意注释清除逻辑,演示泄漏)
  aboutToDisappear(): void {
    // this.clearTimer(); // 注释后:组件销毁,定时器仍运行
  }

  build() {
    RelativeContainer() {
      // 1. 返回上一页按钮(独立ID,避免重复)
      Text("返回上一页")
        .id('backBtnText')
        .fontSize(20)
        .fontWeight(FontWeight.Medium)
        .fontColor('#0066CC')
        .alignRules({
          top: { anchor: '__container__', align: VerticalAlign.Top }, // 定位到顶部
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .margin({ top: 50 })
        .onClick(() => {
          // this.clearTimer(); // 注释后:跳转前不清除定时器
          router.back(); // 返回上一页
        })

      Text(`计时器:${this.count}秒`)
        .id('countTitleText')
        // 兜底:未定义资源时使用默认值28
        .fontSize($r('app.float.page_text_font_size') || 28)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center }, // 居中
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.resetTimer(); // 点击标题重置倒计时
        })
        .margin({ top: 50 })
    }
    .height('100%')
    .width('100%')
  }

  // 启动定时器(故意注释防重复逻辑,加速泄漏)
  private startTimer(): void {
    // if (this.timerId !== null) return; // 注释后:多次调用创建多个定时器

    // 创建定时器:每秒执行一次逻辑
    this.timerId = setInterval(() => {
      this.count++;
      console.log(`【泄漏演示】计时器:${this.count}秒`)
    }, 1000); // 1000ms = 1秒
  }

  // 重置定时器
  private resetTimer(): void {
    this.count = 0; // 恢复初始计时秒数
    this.startTimer(); // 重启定时器(注释防重复后,新增多个定时器)
  }

  // 清除定时器(补充空值判断,避免报错)
  private clearTimer(): void {
    if (this.timerId !== null) { // 新增:空值判断,避免clearInterval(null)报错
      clearInterval(this.timerId);
      this.timerId = null; // 清空ID,标记定时器已停止
    }
  }
}

5.2 泄漏现象说明

  • 重复进入/退出TimerPage页面2次,控制台会打印2个独立的计时器日志(如“计时器:1秒”、“计时器:1秒”),说明上一次的定时器未销毁;
  • 多次操作后,内存占用持续上升,直至应用卡顿。

5.3 修复方案(核心修改点)

// 1. 恢复aboutToDisappear中的清除逻辑
aboutToDisappear(): void {
  this.clearTimer(); // 组件销毁时清除定时器
}

// 2. 恢复返回按钮的清除逻辑
.onClick(() => {
  this.clearTimer(); // 跳转前清除定时器
  router.back();
})

// 3. 恢复startTimer的防重复逻辑
private startTimer(): void {
  if (this.timerId !== null) return; // 避免重复创建定时器
  this.timerId = setInterval(() => {
    this.count++;
    console.log(`【修复后】计时器:${this.count}秒`)
  }, 1000);
}

六、可视化测试页面(Index.ets)

// pages/Index.ets
import router from '@ohos.router';
import {
  createCounter,
  counterSimple,
  createUserManager,
  createDebounce,
  mockBtnSubmit,
} from '../utils/ClosureTest';

@Entry
@Component
struct Index {
  // 优化:简化闭包变量类型声明(移除冗余的undefined)
  private debouncedFunc?: (() => void);

  aboutToAppear(): void {
    // 创建防抖函数(闭包保留isDelaying状态)
    this.debouncedFunc = createDebounce(mockBtnSubmit, 10000);
  }

  build() {
    Column({ space: 20 }) {
      // 标题
      Text("闭包基础入门测试")
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .textAlign(TextAlign.Center);

      // 测试计数器
      Button("测试计数器(标准写法)")
        .width(220)
        .height(60)
        .onClick(() => {
          console.log("\n===== 计数器(标准写法)测试 =====");
          const counter = createCounter();
          console.log(`第1次计数:${counter()}`); // 1
          console.log(`第2次计数:${counter()}`); // 2
        });

      Button("测试计数器(简化写法)")
        .width(220)
        .height(60)
        .onClick(() => {
          console.log("\n===== 计数器(简化写法)测试 =====");
          console.log(`第1次计数:${counterSimple()}`); // 1
          console.log(`第2次计数:${counterSimple()}`); // 2
        });

      // 测试私有变量封装
      Button("测试私有变量封装")
        .width(220)
        .height(60)
        .onClick(() => {
          console.log("\n===== 私有变量封装测试 =====");
          const user = createUserManager();
          console.log(`初始名称:${user.getName()}`); // 默认名称
          user.setName("李四");
          console.log(`修改后名称:${user.getName()}`); // 李四
          user.setName(""); // 空值修改无效
          console.log(`空值修改后:${user.getName()}`); // 李四
        });

      // 测试按钮防连点防抖
      Text("防抖测试:点击后10秒内重复点击无效")
        .fontSize(16)
        .fontColor(Color.Grey);
      Button("提交订单(防连点)")
        .width(220)
        .height(60)
        .backgroundColor(Color.Blue)
        .fontColor(Color.White)
        .onClick(() => {
          if (this.debouncedFunc) {
            console.log("\n===== 按钮防连点测试 =====");
            this.debouncedFunc();
          } else {
            console.log("\n===== 防抖闭包函数已释放,点击无效 =====");
          }
        });

      // 测试防抖内存释放
      Button("测试防抖内存释放")
        .width(220)
        .height(60)
        .onClick(() => {
          console.log("\n===== 防抖闭包释放测试 =====");
          this.debouncedFunc = undefined; // 释放闭包引用,触发垃圾回收
          console.log("防抖闭包已释放,再次点击提交订单无效");
        });

      // 演示内存泄漏
      Button("演示内存资源占用不释放")
        .width(220)
        .height(60)
        .onClick(() => {
          router.push({
            url: "pages/TimerPage" // 需确保module.json5中注册该路径
          });
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(20);
  }

  // 鸿蒙组件销毁时,释放防抖闭包引用(避免内存泄漏)
  aboutToDisappear() {
    this.debouncedFunc = undefined;
  }
}

七、运行结果与分析

7.1 功能测试结果

操作 预期结果
点击“测试计数器(标准写法)” 控制台输出1、2(状态保留)
点击“提交订单(防连点)”快速多次 仅第一次执行,后续10秒内忽略
点击“测试防抖内存释放”后再点提交 控制台提示“防抖闭包已释放”,无业务执行
重复进入/退出TimerPage(未修复) 控制台打印多个计时器日志,内存持续上升
重复进入/退出TimerPage(修复后) 仅一个计时器日志,退出后日志停止,内存稳定

计时器_20260111161342_621_125

7.2 内存分析(Profiler工具使用)

注:仅支持鸿蒙真机设备,模拟器暂不支持Profiler。

  1. 打开DevEco Studio的Profiler工具:顶部菜单栏 → View → Tool Windows → Profiler;
  2. 连接真机,运行应用,选择“Memory”标签;
  3. 重复进入/退出TimerPage页面20次:
  • 未修复(泄漏):内存曲线持续上升,无回落;

内存泄漏_20260111163322_622_125

  • 修复后(无泄漏):内存曲线有小幅波动,但整体稳定,退出页面后内存回落。

内存资源释放_20260111164104_623_125

八、核心总结

  1. 闭包核心:外层普通函数+内层箭头函数引用外层变量+内层函数被外部持有,本质是作用域链持久化;
  2. ArkTS规范:内层必须用箭头函数(禁止普通函数嵌套),避免this指向混乱;
  3. 核心价值:无全局污染前提下,实现局部变量的状态持久化和私有封装;
  4. 应用场景:计数器(状态保留)、私有变量管理(数据安全)、防抖(按钮防连点);
  5. 内存安全
    • 闭包变量用可选类型声明,不再使用时赋值undefined
    • 鸿蒙组件中,aboutToDisappear生命周期清空闭包/定时器引用;
    • 定时器需先判断ID非空再清除,避免报错。

九、代码仓库

十、下节预告

前序学习的接口、普通函数、闭包均受限于“固定类型”,适配新类型需重复编写逻辑,导致代码冗余。而泛型是ArkTS解决“类型复用”的核心方案,可让函数、接口、闭包突破类型限制,实现“一份逻辑适配多类型”。

下一节我们将学习泛型的核心原理与应用实战

  1. 吃透泛型核心本质:以类型参数替代固定类型,从根源减少重复代码;
  2. 掌握泛型标准语法,解决类型固定导致的逻辑重复问题;
  3. 学会泛型类型约束(extends),兼顾复用灵活性与类型安全;
  4. 落地泛型在数据筛选、计数器闭包、运算函数等场景的应用,实现“一次编写,多类型复用”。

这一节将打通“接口约束→闭包状态→泛型复用”链路,让函数、接口、闭包具备跨类型通用能力,写出更简洁、规范的ArkTS代码!

posted @ 2026-01-20 22:19  鸿蒙-散修  阅读(0)  评论(0)    收藏  举报