零基础鸿蒙应用开发第十六节:闭包函数基础入门
【学习目标】
- 理解闭包的核心定义,明确其与作用域链、箭头函数的关联;
- 掌握ArkTS中闭包的标准实现方式,理解“局部变量生命周期延长”的底层逻辑;
- 熟悉闭包的常见应用场景(计数器、私有变量、防抖);
- 掌握闭包内存泄漏的规避方法,写出规范、安全的闭包代码。
【学习重点】
- 核心定义:外层普通函数的局部变量被内层箭头函数引用,且内层函数被外部访问(返回/导出),形成的“变量绑定+作用域持久化”结构;
- 核心原理:内层函数保留对外层作用域的引用,使局部变量不随外层函数执行完毕销毁;
- 实战场景:基础计数器(状态保留)、私有变量封装(数据安全)、防抖(按钮防连点+避免重复提交);
- 风险规避:及时释放闭包引用,避免内存冗余;鸿蒙组件中通过生命周期钩子管理闭包/定时器资源。
【前置知识】
学习前需掌握:箭头函数语法、函数作用域(局部/全局)、函数返回值。
一、工程结构
本节我们将创建名为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步理解:
- 外层函数执行时,创建局部变量并生成专属作用域;
- 内层箭头函数引用该变量,绑定外层作用域的引用;
- 外层函数执行完毕后,因内层函数被外部持有(如返回、赋值给全局变量),作用域不被销毁,变量持续存活。
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 规避方案
- 用可选类型声明闭包变量(
(() => void) | undefined),兼容“引用/释放”状态; - 不再使用时,赋值
undefined释放引用; - 鸿蒙组件中,在
aboutToDisappear生命周期清空闭包/定时器引用; - 定时器类资源,跳转/销毁前必须调用
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(修复后) | 仅一个计时器日志,退出后日志停止,内存稳定 |

7.2 内存分析(Profiler工具使用)
注:仅支持鸿蒙真机设备,模拟器暂不支持Profiler。
- 打开DevEco Studio的Profiler工具:顶部菜单栏 → View → Tool Windows → Profiler;
- 连接真机,运行应用,选择“Memory”标签;
- 重复进入/退出TimerPage页面20次:
- 未修复(泄漏):内存曲线持续上升,无回落;

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

八、核心总结
- 闭包核心:外层普通函数+内层箭头函数引用外层变量+内层函数被外部持有,本质是作用域链持久化;
- ArkTS规范:内层必须用箭头函数(禁止普通函数嵌套),避免this指向混乱;
- 核心价值:无全局污染前提下,实现局部变量的状态持久化和私有封装;
- 应用场景:计数器(状态保留)、私有变量管理(数据安全)、防抖(按钮防连点);
- 内存安全:
- 闭包变量用可选类型声明,不再使用时赋值
undefined; - 鸿蒙组件中,
aboutToDisappear生命周期清空闭包/定时器引用; - 定时器需先判断ID非空再清除,避免报错。
- 闭包变量用可选类型声明,不再使用时赋值
九、代码仓库
- 工程名称:
ClosureBasicDemo - 代码仓库:https://gitee.com/juhetianxia321/harmony-os-code-base.git
十、下节预告
前序学习的接口、普通函数、闭包均受限于“固定类型”,适配新类型需重复编写逻辑,导致代码冗余。而泛型是ArkTS解决“类型复用”的核心方案,可让函数、接口、闭包突破类型限制,实现“一份逻辑适配多类型”。
下一节我们将学习泛型的核心原理与应用实战:
- 吃透泛型核心本质:以类型参数替代固定类型,从根源减少重复代码;
- 掌握泛型标准语法,解决类型固定导致的逻辑重复问题;
- 学会泛型类型约束(extends),兼顾复用灵活性与类型安全;
- 落地泛型在数据筛选、计数器闭包、运算函数等场景的应用,实现“一次编写,多类型复用”。
这一节将打通“接口约束→闭包状态→泛型复用”链路,让函数、接口、闭包具备跨类型通用能力,写出更简洁、规范的ArkTS代码!
浙公网安备 33010602011771号