JavaScript的闭包总结

闭包

developer.mozilla.org/zh-CN/docs/...

一、基本概念

1. 定义

闭包是指能够访问其他函数内部变量的函数 ,或者说函数与其相关的引用化解组合而成的实体

2. 核心特征

l 函数嵌套函数

l 内部函数可以访问外部函数的变量和参数

l 外部函数的变量会持久保存在内存中

二、创建闭包的典型场景

1. 函数返回函数

2. 函数作为参数传递

3. IIFE(立即执行函数表达式)

三、闭包的核心原理

1. 作用域链机制

作用域链基本概念

作用域链是一个由多个变量对象(VO)或活动对象(AO)组成的链表,它决定了函数在查找变量时的搜索顺序。

作用域链的组成
  1. 当前函数的变量对象(AO - Activation Object)
  2. 闭包所在的父级函数变量对象
  3. 更外层的变量对象(直到全局对象)
闭包作用域链的形成过程
  1. 函数定义时

    • 函数会保存其被创建时的作用域链(称为[[Scope]]属性)
    • 这个[[Scope]]包含了函数定义时可访问的所有变量对象
  2. 函数调用时

    • 创建新的执行上下文
    • 创建新的活动对象(AO)用于存储局部变量
    • 将AO添加到[[Scope]]前端,形成完整的作用域链
示例分析
javascript 复制代码
function outer() {
  let outerVar = 'outer';
  
  return function inner() {
    let innerVar = 'inner';
    console.log(outerVar + innerVar);
  };
}

const closure = outer();
closure();
作用域链形成过程
  1. outer函数定义时:

    • outer.[[Scope]] = [全局变量对象]
  2. outer函数执行时:

    • outer的执行上下文作用域链 = [outer的AO, 全局变量对象]
  3. inner函数定义时(在outer内部):

    • inner.[[Scope]] = [outer的AO, 全局变量对象]
  4. closure()执行时:

    • inner的执行上下文作用域链 = [inner的AO, outer的AO, 全局变量对象]
变量查找机制

当闭包访问一个变量时,JavaScript引擎会按照以下顺序查找:

  1. 首先检查当前函数的活动对象(AO)
  2. 如果没找到,沿着作用域链向外查找
  3. 直到找到该变量或到达全局对象
闭包作用域链的特点
  1. 静态作用域(词法作用域):

    • 作用域链在函数定义时就已确定,而不是调用时
    • 这是闭包能够"记住"外部变量的关键
  2. 持久性

    • 即使外部函数已执行完毕,其变量对象仍被闭包引用
    • 只有当所有引用都消失时,这些变量才会被回收
  3. 层级性

    • 多层嵌套函数会形成多层作用域链
    • 每个闭包都有自己的作用域链副本

2. 内存模型

当闭包形成时,内存中会发生以下变化:

text 复制代码
[栈内存]
  |
  |-- 执行上下文 (创建闭包时的上下文)
  |   |-- 变量对象 (VO)
  |   |   |-- 局部变量1
  |   |   |-- 局部变量2
  |   |   └-- ...
  |   └-- 作用域链
  |
  └-- [闭包对象] (存储在堆内存中)
      |-- [[Scope]] (指向创建时的作用域链)
      |-- 函数代码
      └-- 可能的其他属性 (如name, length等)
关键特点
  1. 堆内存存储:闭包本身存储在堆内存中,而不是栈内存
  2. 引用保持:闭包会保持对其外部变量的引用,阻止垃圾回收
  3. 独立环境:每次函数调用都会创建一个新的闭包环境
示例分析
javascript 复制代码
function outer() {
  let count = 0;
  
  return function inner() {
    count++;
    return count;
  };
}

const counter = outer();

内存模型:

  1. 调用outer()时,创建执行上下文,count变量存储在变量对象中
  2. 返回inner函数时,形成闭包,inner[[Scope]]属性指向outer的作用域链
  3. 即使outer执行完毕,其变量对象仍被inner的闭包引用,不会被回收

四、闭包的实际应用

1. 数据封装和私有变量

javascript 复制代码
function createCounter() {
  let count = 0; // 外部变量
  return function() { // 闭包
      return ++count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

2. 模块模式

javascript 复制代码
const module = (function() {
  let privateVar = "I'm private";
  return {
      getVar: function() {
          return privateVar;
      },
      setVar: function(newVal) {
          privateVar = newVal;
      }
  };
})();

console.log(module.getVar()); // "I'm private"
module.setVar("Updated");
console.log(module.getVar()); // "Updated"

3. 函数工厂

javascript 复制代码
function makeMultiplier(x) {
  return function(y) {
      return x * y;
  };
}

const double = makeMultiplier(2);
console.log(double(5)); // 10
const triple = makeMultiplier(3);
console.log(triple(5)); // 15

4. 事件处理和回调

javascript 复制代码
function delayLog(message, delay) {
  setTimeout(function() {
      console.log(message); // 闭包记住message
  }, delay);
}

delayLog("Hello after 1s", 1000);

五、闭包的优缺点

优点

  1. 数据封装(私有变量)

闭包可以创建私有变量,避免全局污染,实现模块化编程。

javascript 复制代码
function createCounter() {
  let count = 0; // 外部无法直接访问
  return {
      increment: function() { return ++count; },
      getCount: function() { return count; }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
// console.log(count); // Error: count未定义(私有变量)
  1. 保持状态

闭包可以记住外部函数的变量,即使外部函数已经执行完毕。

javascript 复制代码
function memoize(fn) {
  const cache = {}; // 缓存数据
  return function(arg) {
      if (cache[arg]) return cache[arg];
      cache[arg] = fn(arg);
      return cache[arg];
  };
}
  1. 函数工厂

可以动态生成不同行为的函数,提高代码复用性。

javascript 复制代码
function makeGreeting(greeting) {
  return function(name) {
      return `${greeting}, ${name}!`;
  };
}

const sayHello = makeGreeting("Hello");
console.log(sayHello("Alice")); // "Hello, Alice!"
  1. 回调函数 & 事件处理

闭包可以保留上下文,适用于异步操作(如 setTimeout、Promise、事件监听)。

javascript 复制代码
function delayLog(message, delay) {
  setTimeout(function() {
      console.log(message); // 闭包记住message
  }, delay);
}

delayLog("Hello after 1s", 1000);

缺点

  1. 内存泄漏风险

闭包会长期持有外部变量的引用,导致垃圾回收(GC)无法释放内存。

示例(意外内存泄漏):

场景1:DOM 元素与闭包

javascript 复制代码
function setupButton() {
  const button = document.getElementById('myButton');
  button.addEventListener('click', function() {
    // 这个闭包隐式引用了button
    console.log('Button clicked');
  });
}

场景2:大对象意外保留

javascript 复制代码
function outer() {
  const bigData = new Array(1000000).fill("data"); // 大数据
  return function() {
      console.log("Closure keeps bigData in memory!");
  };
}

const closure = outer(); // bigData 不会被释放

解决方案:

当 JavaScript 引擎创建闭包时,它会保留整个外部函数的词法环境,而不仅仅是闭包实际使用的变量。

如果你确实不需要保留 bigData,可以手动释放:

javascript 复制代码
function outer() {
  const bigData = new Array(1000000).fill("data");
  const result = function() {
      console.log("Closure");
  };

  // 手动释放不再需要的大数据
  bigData.length = 0; // 或者 bigData = null;
  return result;
}
  1. 性能影响

l 闭包访问外部变量比访问局部变量稍慢(需查找作用域链)。

l 过度使用闭包可能影响执行效率。

  1. 变量共享问题

在循环中使用闭包可能导致变量共享问题 (如 for 循环中的 var)。

错误示例:

javascript 复制代码
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
      console.log(i); // 全部输出3(i是共享的)
  }, 1000);
}

解决方法( 使用 letIIFE):

javascript 复制代码
for (let i = 0; i < 3; i++) { // let 创建块级作用域
  setTimeout(function() {
      console.log(i); // 0, 1, 2
  }, 1000);
}

相关推荐
路光.1 分钟前
触发事件,按钮loading状态,封装hooks
前端·typescript·vue3hooks
我爱996!3 分钟前
SpringMVC——响应
java·服务器·前端
咔咔一顿操作1 小时前
Vue 3 入门教程7 - 状态管理工具 Pinia
前端·javascript·vue.js·vue3
kk爱闹1 小时前
用el-table实现的可编辑的动态表格组件
前端·vue.js
漂流瓶jz2 小时前
JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
前端·javascript·编译原理
换日线°2 小时前
css 不错的按钮动画
前端·css·微信小程序
风象南2 小时前
前端渲染三国杀:SSR、SPA、SSG
前端
90后的晨仔2 小时前
表单输入绑定详解:Vue 中的 v-model 实践指南
前端·vue.js
陈佬昔没带相机3 小时前
围观前后端对接的 TypeScript 最佳实践,我们缺什么?
前端·后端·api
90后的晨仔3 小时前
Vue 事件处理深入指南:从基础到进阶
前端·vue.js