散修带你入门鸿蒙应用开发基础第七节:闭包函数基础
ArkTS基础第七节:闭包函数基础
炼气七重天
【学习目标】
- 理解闭包的核心概念,明确闭包与作用域链、箭头函数的关联。
- 掌握ArkTS中闭包的核心实现方式,理解闭包延长局部变量生命周期的底层逻辑。
- 了解闭包的进阶简化写法,熟悉闭包的常见应用场景,能规避闭包引发的内存泄漏风险。
【学习重点】
- 闭包核心定义:内层箭头函数引用外层函数的局部变量,形成的“变量绑定”结构。
- 核心原理:基于作用域链规则,内层函数保留对外部作用域变量的引用,使变量不随外层函数执行完毕销毁。
- 实现形式:掌握闭包的常规实现方式,了解匿名外层函数+立即调用的简化写法(核心逻辑一致);
- 实战场景:计数器、数据缓存、私有变量封装;
- 风险规避:及时释放闭包引用,避免内存冗余。
【温馨提示】
闭包是作用域链的进阶应用,核心依赖上一节的“箭头函数+作用域生命周期”知识点,是ArkTS实现状态保留、私有逻辑封装的核心工具。
一、闭包:什么是闭包?
1.1 闭包的定义
在ArkTS中,闭包是指内层箭头函数引用了外层函数的局部变量,且内层函数被外层函数返回/导出后,形成的特殊“变量绑定”结构。
核心特征:
- 外层函数执行完毕后,其局部变量不会被销毁;
- 内层函数可持续访问并修改该局部变量;
- 变量被封装在闭包内部,外部无法直接访问,保障数据私有性。
1.2 闭包的核心原理(衔接上一节知识点)
闭包的本质是作用域链的持久化:
- 外层函数执行时,创建局部变量并形成作用域;
- 内层箭头函数引用该局部变量,会“保留”对外层作用域的引用;
- 外层函数执行完毕后,因内层函数仍持有引用,其作用域不会被销毁,变量持续存活。
// 闭包核心实现示例(常规写法,优先掌握)
function createCounter() {
// 外层函数作用域:创建count变量,作用域范围→createCounter函数内部
let count: number = 0;
// 内层箭头函数作用域:引用外层作用域的count,保留对外层作用域的引用
return () => {
count++;
return count;
};
}
// 全局作用域:仅能通过getIncrementId访问count,无法直接操作
const getIncrementId = createCounter();
// 外层函数已执行完毕,但count未销毁(闭包保留引用)
console.log(`计数器第1次调用结果:${getIncrementId()}`); // 输出:计数器第1次调用结果:1
console.log(`计数器第2次调用结果:${getIncrementId()}`); // 输出:计数器第2次调用结果:2
console.log(`计数器第3次调用结果:${getIncrementId()}`); // 输出:计数器第3次调用结果:3
// 错误写法(编译报错)普通函数禁止嵌套普通函数
function badCounter() {
let count = 0;
function inner() { // 禁止function嵌套function
return count++;
}
}
1.3 闭包与普通函数的对比
- 无闭包的普通函数:外层函数执行后,局部变量立即销毁;
- 有闭包的函数:外层函数执行后,变量因内层函数引用保留。
// 普通函数变量随函数执行销毁
function normalFunc() {
// 外层函数作用域:temp仅在normalFunc执行时存在
let temp: number = 10;
console.log(`普通函数内temp的值:${temp}`);
}
normalFunc(); // 输出:普通函数内temp的值:10
// 无法再访问temp,已销毁
// 闭包函数(常规写法)
function closureFunc() {
// 外层函数作用域:temp被内层箭头函数引用,作用域不会销毁
let temp: number = 10;
return () => {
temp += 5;
return temp;
};
}
// 全局作用域:通过getTemp间接操作外层作用域的temp
const getTemp = closureFunc();
console.log(`闭包函数第1次调用temp的值:${getTemp()}`); // 输出:闭包函数第1次调用temp的值:15
console.log(`闭包函数第2次调用temp的值:${getTemp()}`); // 输出:闭包函数第2次调用temp的值:20
1.4 闭包的进阶简化写法:匿名外层函数+立即调用(了解即可)
当外层函数仅被调用一次时,无需单独命名,可通过“匿名外层函数+立即调用”实现简化,其核心逻辑与常规写法完全等价,仅写法形式不同。
基础阶段优先掌握常规写法,简化写法仅作理解,无需深入纠结。
核心等价推导
// 常规写法(基础阶段重点掌握)
function createCounter() {
let privateCount = 0;
return () => {
privateCount++;
return privateCount;
};
}
const addClosure = createCounter();
// 简化写法:合并“定义+调用”(仅作了解)
const addClosureSimple = (
() => { // 匿名外层函数(等价于createCounter)
let privateCount = 0;
return () => {
privateCount++;
return privateCount;
};
}
)(); // 关键:()表示定义后立刻调用外层函数
// 两种写法效果完全一致
console.log(`常规写法闭包调用结果:${addClosure()}`); // 常规写法闭包调用结果:1
console.log(`简化写法闭包调用结果:${addClosureSimple()}`); // 简化写法闭包调用结果:1
拆解说明
- 简化版本质是“匿名外层函数+立即调用”,省去了常规写法中外层函数的命名;
- 内层箭头函数和私有变量的逻辑与常规写法无差异;
- 基础阶段优先掌握常规写法,进阶阶段可根据场景选择简化写法,二者核心闭包逻辑不变。
1.5 闭包常见误区
误区1:闭包等同于全局变量
- 错误理解:闭包变量可全局访问,和全局变量功能一致;
- 正确理解:闭包变量是“私有且持久化”的,仅能通过内层函数访问修改,而全局变量可被任意代码篡改,易引发数据污染。
// 反例:全局变量(数据易污染)
let globalCount = 0;
function addGlobal() {
globalCount++;
}
addGlobal();
globalCount = 100; // 可直接修改,无数据保护
console.log(`全局变量修改后的值:${globalCount}`); // 输出:全局变量修改后的值:100
// 正例:闭包变量(私有安全,常规写法)
function createClosureCounter() {
let privateCount = 0;
return () => {
privateCount++;
return privateCount;
};
}
const addClosure = createClosureCounter();
addClosure();
console.log(`闭包变量调用后的值:${addClosure()}`); // 输出:闭包变量调用后的值:2
// privateCount = 100; // 无法访问,保障数据安全
误区2:所有场景都适合用闭包
- 错误做法:简单数值计算等基础逻辑也封装为闭包;
- 正确做法:仅在需要“状态保留+私有封装”时使用闭包,避免无意义的内存占用。
误区3:混淆“匿名外层函数+立即调用”和“闭包”的关系
- 错误理解:匿名外层函数+立即调用就是闭包;
- 正确理解:匿名外层函数+立即调用是闭包的写法优化,闭包的核心是“内层函数引用外层变量”,和是否“立即执行”无关。
外层function嵌套内层function 不支持
// 错误写法(基础易踩坑):外层function嵌套内层function,编译报错 ts支持
function badClosure() {
let num = 10;
function inner() { // ArkTS禁止function嵌套function
return num++;
}
return inner;
}
// 正确写法:内层改为箭头函数,符合ArkTS语法+闭包逻辑
function goodClosure() {
let num = 10;
return () => { // 内层改为箭头函数
return num++;
};
}
// 测试正确写法
const getNum = goodClosure();
console.log(`闭包函数第1次调用num的值:${getNum()}`); // 输出:闭包函数第1次调用num的值:11
console.log(`闭包函数第2次调用num的值:${getNum()}`); // 输出:闭包函数第2次调用num的值:12
二、ArkTS中闭包的典型实现
2.1 单变量闭包
核心:外层函数返回内层箭头函数,内层函数仅引用一个局部变量。
// 实现:累加器(每次调用增加指定数值)
function createAdder(step: number) {
// step为外层函数参数(局部变量),作用域范围→createAdder内部
return (num: number) => {
return num + step;
};
}
// 创建步长为5的累加器
const add5 = createAdder(5);
console.log(`累加器add5(10)的结果:${add5(10)}`); // 输出:累加器add5(10)的结果:15
console.log(`累加器add5(20)的结果:${add5(20)}`); // 输出:累加器add5(20)的结果:25
// 提示:也可使用匿名外层函数+立即调用实现(进阶写法,核心逻辑一致)
// const add10 = (() => { const s=10; return (n)=>n+s; })();
// console.log(`累加器add10(10)的结果:${add10(10)}`);
2.2 多变量闭包
内层函数引用多个外层局部变量,实现复杂逻辑封装
// 提前声明接口(后续系统学习,先用于约束返回结构)
interface UserManager {
getName: () => string;
getAge: () => number;
setAge: (newAge: number) => void;
}
// 补充:等级计数器接口(约束场景1的返回结构)
interface LevelCounter {
getLevel: () => string;
upgrade: () => string;
}
// 第二步:实现用户状态管理器(严格遵循接口约束)
function createUserManager(initName: string, initAge: number): UserManager {
// 外层函数作用域:name和age为私有变量,仅内层函数可访问
let name: string = initName;
let age: number = initAge;
// 返回包含多个箭头函数的对象,严格匹配UserManager接口
return {
getName: (): string => name,
getAge: (): number => age,
setAge: (newAge: number): void => {
if (newAge > 0) {
age = newAge;
}
}
};
}
// 创建用户管理器实例
const userManager = createUserManager("张三", 18);
console.log(`用户姓名:${userManager.getName()}`); // 输出:用户姓名:张三
console.log(`用户初始年龄:${userManager.getAge()}`); // 输出:用户初始年龄:18
userManager.setAge(20);
console.log(`用户修改后的年龄:${userManager.getAge()}`); // 输出:用户修改后的年龄:20
三、闭包的常见应用场景
3.1 场景1:计数器(高频实战)
实现独立的计数逻辑,计数变量私有,外部无法直接修改。
// 实现:等级计数器(每次调用提升1级)
function createLevelCounter(): LevelCounter { // 补充接口约束返回值
// 外层函数作用域:level为私有计数变量,外部不可访问
let level: number = 1;
return {
getLevel: () => `当前等级:炼气${level}重`,
upgrade: () => {
level++;
return `升级成功!当前等级:炼气${level}重`;
}
};
}
// 创建计数器实例
const levelCounter = createLevelCounter();
console.log(`等级计数器初始值:${levelCounter.getLevel()}`); // 输出:等级计数器初始值:当前等级:炼气1重
console.log(`等级计数器升级结果:${levelCounter.upgrade()}`); // 输出:等级计数器升级结果:升级成功!当前等级:炼气2重
3.2 场景2:数据缓存(优化性能)
// 实现:阶乘计算缓存(已计算的结果不再重复计算)
function createFactorialCache() {
// 外层函数作用域:cache为私有缓存对象,仅内层函数可操作
const cache: Record<number, number> = {};
const calcFactorial = (n: number): number => {
if (n === 0 || n === 1) {
return 1;
}
// 优先读取缓存:如果缓存中已有n的阶乘,直接返回,避免重复计算
if (cache[n]) {
console.log(`从缓存获取${n}的阶乘:${cache[n]}`);
return cache[n];
}
const result = n * calcFactorial(n - 1);
cache[n] = result;
console.log(`计算并缓存${n}的阶乘:${result}`);
return result;
};
return calcFactorial;
}
const factorial = createFactorialCache();
console.log(`5的阶乘最终结果:${factorial(5)}`); // 输出:5的阶乘最终结果:120
console.log(`5的阶乘二次调用结果:${factorial(5)}`); // 输出:5的阶乘二次调用结果:120
3.3 场景3:私有变量封装
将变量封装在闭包内,外部仅能通过指定方法访问/修改,保障数据安全。
// 提前声明:资源管理器接口(约束返回值结构,规范数据类型)
interface ResourceManager {
getSpirit: () => number; // 获取灵力:返回数值类型
costSpirit: (num: number) => string; // 消耗灵力:入参数值,返回描述字符串
recoverSpirit: (num: number) => string; // 恢复灵力:入参数值,返回描述字符串
}
// 实现:资源管理器(仅允许通过方法修改资源)
function createResourceManager():ResourceManager {
// 外层函数作用域:spirit为私有变量,外部无法直接修改
let spirit: number = 100; // 灵力值
return {
// 获取灵力
getSpirit: () => spirit,
// 消耗灵力
costSpirit: (num: number) => {
if (spirit >= num) {
spirit -= num;
return `消耗${num}灵力,剩余${spirit}`;
}
return `消耗${num}灵力,灵力不足!当前灵力:${spirit}`;
},
// 恢复灵力
recoverSpirit: (num: number) => {
spirit += num;
return `恢复${num}灵力,当前${spirit}`;
}
};
}
const resourceManager = createResourceManager();
console.log(`初始灵力值:${resourceManager.getSpirit()}`); // 输出:初始灵力值:100
console.log(`消耗30灵力结果:${resourceManager.costSpirit(30)}`); // 输出:消耗30灵力结果:消耗30灵力,剩余70
console.log(`恢复20灵力结果:${resourceManager.recoverSpirit(20)}`); // 输出:恢复20灵力结果:恢复20灵力,当前90
// 无法直接修改spirit,保障数据安全
四、闭包的注意事项(规避内存风险)
4.1 内存泄漏风险
闭包会延长变量生命周期,若闭包实例长期未释放,会导致变量持续占用内存,引发内存泄漏。
4.2 风险规避方案
- 用可选类型管理引用:需要释放的闭包,用可选类型(
?:)声明变量,兼容“引用”和“释放”两种状态; - 及时释放引用:不再使用时,直接赋值
undefined解除引用; - 避免全局闭包实例:优先声明为局部变量,随作用域销毁自动减少内存占用。
4.3 释放引用示例(核心演示)
// 1. 关键:定义联合类型“变量要么是闭包函数,要么是undefined”
let tempCounter: (() => number)|undefined;
// 2. 定义闭包:临时计数器(仅短期使用)
function createTempCounter() {
let count: number = 0;
return () => {
count++;
return count;
};
}
// 3. 引用闭包(业务逻辑执行时)
tempCounter = createTempCounter();
console.log(`临时计数器第1次调用:${tempCounter()}`); // 输出:临时计数器第1次调用:1
console.log(`临时计数器第2次调用:${tempCounter()}`); // 输出:临时计数器第2次调用:2
// 4. 业务结束后释放:直接赋值undefined
tempCounter = undefined;
4.4 为什么这样写?
- 可选类型(
?:):等价于() => number | undefined,既允许变量引用闭包(() => number类型),也允许赋值undefined(释放状态),避免类型不匹配报错; - 释放逻辑简单:无需复杂操作,仅
变量 = undefined即可解除闭包对外部变量的引用,让垃圾回收机制回收内存; - 安全调用:释放后若误调用,用可选链(
?.)可避免运行时错误,是闭包释放后的最佳实践。
五、【课堂小结】
- 核心概念:闭包是内层箭头函数引用外层局部变量形成的“变量绑定”结构,核心是作用域链的持久化。
- 实现形式:掌握闭包的常规实现方式,匿名外层函数+立即调用是等价的进阶简化写法,基础阶段优先掌握常规写法。
- 核心原理:内层函数保留对外部变量的引用,使变量不随外层函数执行完毕销毁。
- 应用场景:计数器、数据缓存、私有变量封装,是ArkTS实现状态保留的核心方式。
- 风险规避:用可选类型声明需释放的闭包变量,不再使用时赋值
undefined,避免全局闭包实例,按需使用闭包。 - 基础避坑:禁止function嵌套function,内层必须用箭头函数实现闭包;Set转数组优先使用Array.from保证ArkTS兼容性。
六、【课后练习】
- 基础实现题:设计闭包实现“修仙经验值管理器”,包含
addExp/getExp/resetExp方法,要求用常规写法实现,可尝试写出匿名外层函数+立即调用的简化写法。 - 性能优化题:基于闭包实现“数组去重缓存工具”,缓存已去重结果,避免重复计算(注意使用Array.from实现Set转数组)。
七、【课后练习参考答案与解析】
1. 修仙经验值管理器
// 第一步:声明经验管理器接口(强化类型约束)
interface ExpManager {
addExp: (num: number) => string;
getExp: () => string;
resetExp: () => string;
}
// 第二步:常规写法(核心实现)
function createExpManager(): ExpManager {
let exp: number = 0;
return {
addExp: (num: number): string => {
exp += num;
return `增加${num}经验,当前${exp}`;
},
getExp: (): string => `当前经验:${exp}`,
resetExp: (): string => {
exp = 0;
return "经验已重置为0";
}
};
}
// 测试
const expManager = createExpManager();
console.log(`添加50经验结果:${expManager.addExp(50)}`); // 输出:添加50经验结果:增加50经验,当前50
console.log(`获取当前经验:${expManager.getExp()}`); // 输出:获取当前经验:当前经验:50
console.log(`重置经验结果:${expManager.resetExp()}`); // 输出:重置经验结果:经验已重置为0
- 解析:通过闭包封装
exp变量实现数据私有;简化写法可将外层函数改为匿名并立即调用,逻辑不变。
2. 数组去重缓存工具
// 实现:数组去重缓存工具
function createUniqueArrayCache() {
// 私有缓存对象,仅内层函数可访问
const cache: Record<string, number[]> = {};
return (arr: number[]): number[] => {
const key = arr.join(",");
if (cache[key]) {
console.log(`从缓存获取${key}的去重结果:${cache[key]}`);
return cache[key];
}
const uniqueArr = Array.from(new Set(arr));
cache[key] = uniqueArr;
console.log(`计算并缓存${key}的去重结果:${uniqueArr}`);
return uniqueArr;
};
}
// 测试
const uniqueArray = createUniqueArrayCache();
console.log(`[1,2,2,3]去重结果:${uniqueArray([1,2,2,3])}`); // 输出:[1,2,2,3]去重结果:1,2,3
console.log(`[1,2,2,3]二次去重结果:${uniqueArray([1,2,2,3])}`); // 输出:[1,2,2,3]二次去重结果:1,2,3
【代码仓库】
工程名称:ClosureDemo本节代码已同步至:https://gitee.com/juhetianxia321/harmony-os-code-base.git
【下节预告】
下一节将开启炼气八重天的修炼——高阶函数,作为闭包与箭头函数的“组合技能”,我们将掌握:
- 高阶函数的核心定义(函数作为参数传入、作为返回值导出);
- 基于闭包封装自定义高阶工具;
- 熟练运用ArkTS内置高阶函数(
map/filter/reduce)简化集合操作; - 用高阶函数实现逻辑复用,进阶代码模块化能力。
七、鸿蒙开发者学习与认证指引
(一)、官方学习班级报名(免费)
- 班级链接:HarmonyOS赋能资源丰富度建设(第四期)
- 学号填写规则:填写个人手机号码即可完成班级信息登记
(二)、HarmonyOS应用开发者认证考试(免费)
-
考试链接:HarmonyOS开发者能力认证入口
-
认证等级及适配人群
- 基础认证:适配软件工程师、移动应用开发人员,需掌握HarmonyOS基础概念、DevEco Studio基础使用、ArkTS及ArkUI基础开发等能力;
- 高级认证:适配项目经理、工程架构师,需掌握系统核心技术理念、应用架构设计、关键技术开发及应用上架运维等能力;
- 专家认证:适配研发经理、解决方案专家,需掌握分布式技术原理、端云一体化开发、跨端迁移及性能优化等高级能力。
-
认证权益:通过认证可获得电子版证书以及其他专属权益。
浙公网安备 33010602011771号