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开发者的关键。通过实践这些概念,你将能够编写更高效、更模块化的代码。

相关推荐
qq_2786672861 小时前
ros中相机话题在web页面上的显示,尝试js解析sensor_msgs/Image数据
前端·javascript·ros
烛阴1 小时前
JavaScript并发控制:从Promise到队列系统
前端·javascript
zhangxingchao1 小时前
关于《黑马鸿蒙5.0零基础入门》课程的总结
前端
zhangxingchao1 小时前
Flutter的Widget世界
前端
&活在当下&2 小时前
element plus 的树形控件,如何根据后台返回的节点key数组,获取节点key对应的node节点
javascript·vue.js·element plus
$程2 小时前
Vue3 项目国际化实践
前端·vue.js
nbsaas-boot2 小时前
Vue 项目中的组件职责划分评审与组件设计规范制定
前端·vue.js·设计规范
fanged2 小时前
Angular--Hello(TODO)
前端·javascript·angular.js
易鹤鹤.2 小时前
openLayers切换基于高德、天地图切换矢量、影像、地形图层
前端
可观测性用观测云3 小时前
从“烟囱式监控”到观测云平台:2025 亚马逊云科技峰会专访
前端