JavaScript 作用域与闭包详解

JavaScript 作用域与闭包详解

作用域和闭包是 JavaScript 中最重要的概念之一,理解它们对于编写高质量代码至关重要。

一、作用域 (Scope)

1. 作用域基本概念

作用域决定了变量和函数的可访问范围。JavaScript 有以下几种作用域:

  • 全局作用域:在函数外部声明的变量
  • 函数作用域:在函数内部声明的变量
  • 块级作用域 (ES6+): 由 letconst{} 内声明的变量
javascript 复制代码
// 全局作用域
var globalVar = "I'm global";

function example() {
  // 函数作用域
  var functionVar = "I'm in function";
  
  if (true) {
    // 块级作用域
    let blockVar = "I'm in block";
    console.log(blockVar); // 可访问
  }
  
  console.log(blockVar); // 报错: blockVar is not defined
}

2. 作用域链

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

  1. 当前作用域
  2. 外层作用域
  3. 直到全局作用域
ini 复制代码
let a = 1;

function outer() {
  let b = 2;
  
  function inner() {
    let c = 3;
    console.log(a + b + c); // 6 (可以访问所有变量)
  }
  
  inner();
}

outer();

3. 变量提升 (Hoisting)

  • var 声明的变量会提升到函数/全局作用域顶部
  • letconst 也有提升,但存在"暂时性死区" (TDZ)
ini 复制代码
console.log(x); // undefined (变量提升)
var x = 5;

console.log(y); // 报错: Cannot access 'y' before initialization
let y = 10;

二、闭包 (Closure)

1. 闭包基本概念

闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。

简单来说:当一个函数可以访问并记住它被声明时所处的环境(包括变量等),即使这个函数在其他地方被调用,就形成了闭包。

闭包的形成条件

  1. 函数嵌套:一个函数内部定义了另一个函数
  2. 内部函数引用外部函数的变量
  3. 内部函数被外部使用(返回、传递给其他函数等)
javascript 复制代码
function outer() {
  let count = 0; // 局部变量
  
  // 内部函数inner就是一个闭包
  return function inner() {
    count++; // 访问外部函数的变量
    return count;
  };
}

const counter = outer(); // outer执行完毕,按理说count应该被销毁

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

在这个例子中:

  1. inner 函数可以访问 outer 函数的 count 变量
  2. 即使 outer 已经执行完毕,count 仍然被保留
  3. 每次调用 counter() 都会修改并记住 count 的值

2.闭包的优缺点

优点:

  1. 实现数据私有化,创建私有变量
  2. 保持变量在内存中,实现状态持久化
  3. 模块化开发,避免全局污染

缺点:

  1. 过度使用可能导致内存占用过高
  2. 不合理的闭包使用可能导致内存泄漏
  3. 可能增加代码复杂度,降低可读性

3. 闭包的实际应用

1) 数据私有化
javascript 复制代码
function createPerson(name) {
  let age = 0;
  
  return {
    getName: () => name,
    getAge: () => age,
    celebrateBirthday: () => {
      age++;
      return `Happy birthday, ${name}! You're now ${age}.`;
    }
  };
}

const john = createPerson("John");
console.log(john.getName()); // "John"
console.log(john.getAge()); // 0
john.celebrateBirthday();
console.log(john.getAge()); // 1
2) 模块模式
ini 复制代码
const calculator = (function() {
  let memory = 0;
  
  return {
    add: function(a, b) {
      memory = a + b;
      return memory;
    },
    subtract: function(a, b) {
      memory = a - b;
      return memory;
    },
    getMemory: function() {
      return memory;
    },
    clearMemory: function() {
      memory = 0;
    }
  };
})();

calculator.add(5, 3); // 8
console.log(calculator.getMemory()); // 8
3)函数工厂
scss 复制代码
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

4. 闭包的注意事项

  1. 内存泄漏风险:闭包会保留对外部变量的引用,可能导致内存无法释放
javascript 复制代码
// 不好的实践
function createHeavyObject() {
  const bigArray = new Array(1000000).fill("data");
  
  return function() {
    console.log("I'm holding a reference to " +bigArray);
  };
}

const leak = createHeavyObject();
leak();
// bigArray 无法被垃圾回收,因为闭包还在引用它
  1. 循环中的闭包问题
javascript 复制代码
// 常见问题
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 全部输出5
  }, 100);
}

// 解决方案1: 使用let (块级作用域)
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i); // 0,1,2,3,4
  }, 100);
}

// 解决方案2: IIFE
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j); // 0,1,2,3,4
    }, 100);
  })(i);
}

三、作用域与闭包的关系

特性 作用域 闭包
定义 变量可访问的范围 函数记住并访问其词法作用域的能力
创建时机 代码编写时确定 函数被定义时创建
生命周期 执行上下文结束时销毁 只要闭包存在,相关作用域就保持
主要用途 控制变量可见性 数据封装、私有变量、函数工厂等

四、最佳实践

  1. 优先使用 letconst 替代 var,避免变量提升和全局污染
  2. 合理使用闭包,避免不必要的内存占用
  3. 模块化代码,利用闭包实现封装
  4. 注意循环中的闭包,使用块级作用域或IIFE解决

五、高级应用

1. 函数柯里化 (Currying)

javascript 复制代码
function multiply(a) {
  return function(b) {
    return function(c) {
      return a * b * c;
    };
  };
}

const result = multiply(2)(3)(4); // 24

2. 记忆化 (Memoization)

ini 复制代码
function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const factorial = memoize(function(n) {
  return n <= 1 ? 1 : n * factorial(n - 1);
});

console.log(factorial(5)); // 120 (计算并缓存)
console.log(factorial(5)); // 120 (直接从缓存读取)

理解作用域和闭包是成为高级JavaScript开发者的关键。通过实践这些概念,你将能够编写更高效、更模块化的代码。

相关推荐
karshey8 分钟前
【前端】sort:js按照固定顺序排序
开发语言·前端·javascript
MyBFuture9 分钟前
索引器实战:对象数组访问技巧及命名空间以及项目文件规范
开发语言·前端·c#·visual studio
IT_陈寒19 分钟前
Redis性能提升50%的7个实战技巧,连官方文档都没讲全!
前端·人工智能·后端
打小就很皮...21 分钟前
React 富文本图片上传 OSS 并防止 Base64 图片粘贴
前端·react.js·base64·oss
咬人喵喵29 分钟前
告别无脑 <div>:HTML 语义化标签入门
前端·css·编辑器·html·svg
404NotFound3051 小时前
基于 Vue 3 和 Guacamole 搭建远程桌面(利用RDP去实现,去除vnc繁琐配置)
前端
咚咚咚ddd1 小时前
AI 应用开发:Agent @在线文档功能 - 前端交互与设计
前端·aigc·agent
旧梦吟1 小时前
脚本工具 批量md转html
前端·python·html5
ohyeah1 小时前
React 中兄弟组件通信的最佳实践:以 Todo 应用为例
前端