散修带你入门鸿蒙应用开发基础第七节:闭包函数基础

ArkTS基础第七节:闭包函数基础

炼气七重天

【学习目标】

  1. 理解闭包的核心概念,明确闭包与作用域链、箭头函数的关联。
  2. 掌握ArkTS中闭包的核心实现方式,理解闭包延长局部变量生命周期的底层逻辑。
  3. 了解闭包的进阶简化写法,熟悉闭包的常见应用场景,能规避闭包引发的内存泄漏风险。

【学习重点】

  1. 闭包核心定义:内层箭头函数引用外层函数的局部变量,形成的“变量绑定”结构。
  2. 核心原理:基于作用域链规则,内层函数保留对外部作用域变量的引用,使变量不随外层函数执行完毕销毁。
  3. 实现形式:掌握闭包的常规实现方式,了解匿名外层函数+立即调用的简化写法(核心逻辑一致);
  4. 实战场景:计数器、数据缓存、私有变量封装;
  5. 风险规避:及时释放闭包引用,避免内存冗余。

【温馨提示】

闭包是作用域链的进阶应用,核心依赖上一节的“箭头函数+作用域生命周期”知识点,是ArkTS实现状态保留、私有逻辑封装的核心工具。

一、闭包:什么是闭包?

1.1 闭包的定义

在ArkTS中,闭包是指内层箭头函数引用了外层函数的局部变量,且内层函数被外层函数返回/导出后,形成的特殊“变量绑定”结构。

核心特征:

  • 外层函数执行完毕后,其局部变量不会被销毁;
  • 内层函数可持续访问并修改该局部变量;
  • 变量被封装在闭包内部,外部无法直接访问,保障数据私有性。

1.2 闭包的核心原理(衔接上一节知识点)

闭包的本质是作用域链的持久化

  1. 外层函数执行时,创建局部变量并形成作用域;
  2. 内层箭头函数引用该局部变量,会“保留”对外层作用域的引用;
  3. 外层函数执行完毕后,因内层函数仍持有引用,其作用域不会被销毁,变量持续存活。
// 闭包核心实现示例(常规写法,优先掌握)
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. 简化版本质是“匿名外层函数+立即调用”,省去了常规写法中外层函数的命名;
  2. 内层箭头函数和私有变量的逻辑与常规写法无差异;
  3. 基础阶段优先掌握常规写法,进阶阶段可根据场景选择简化写法,二者核心闭包逻辑不变。

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 风险规避方案

  1. 用可选类型管理引用:需要释放的闭包,用可选类型(?:)声明变量,兼容“引用”和“释放”两种状态;
  2. 及时释放引用:不再使用时,直接赋值undefined解除引用;
  3. 避免全局闭包实例:优先声明为局部变量,随作用域销毁自动减少内存占用。

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即可解除闭包对外部变量的引用,让垃圾回收机制回收内存;
  • 安全调用:释放后若误调用,用可选链(?.)可避免运行时错误,是闭包释放后的最佳实践。

五、【课堂小结】

  1. 核心概念:闭包是内层箭头函数引用外层局部变量形成的“变量绑定”结构,核心是作用域链的持久化。
  2. 实现形式:掌握闭包的常规实现方式,匿名外层函数+立即调用是等价的进阶简化写法,基础阶段优先掌握常规写法。
  3. 核心原理:内层函数保留对外部变量的引用,使变量不随外层函数执行完毕销毁。
  4. 应用场景:计数器、数据缓存、私有变量封装,是ArkTS实现状态保留的核心方式。
  5. 风险规避:用可选类型声明需释放的闭包变量,不再使用时赋值undefined,避免全局闭包实例,按需使用闭包。
  6. 基础避坑:禁止function嵌套function,内层必须用箭头函数实现闭包;Set转数组优先使用Array.from保证ArkTS兼容性。

六、【课后练习】

  1. 基础实现题:设计闭包实现“修仙经验值管理器”,包含addExp/getExp/resetExp方法,要求用常规写法实现,可尝试写出匿名外层函数+立即调用的简化写法。
  2. 性能优化题:基于闭包实现“数组去重缓存工具”,缓存已去重结果,避免重复计算(注意使用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

【下节预告】

下一节将开启炼气八重天的修炼——高阶函数,作为闭包与箭头函数的“组合技能”,我们将掌握:

  1. 高阶函数的核心定义(函数作为参数传入、作为返回值导出);
  2. 基于闭包封装自定义高阶工具;
  3. 熟练运用ArkTS内置高阶函数(map/filter/reduce)简化集合操作;
  4. 用高阶函数实现逻辑复用,进阶代码模块化能力。

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

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

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

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

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

  2. 认证等级及适配人群

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

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