马上加入IBC程序猿 各种源码随意下,各种教程随便看! 注册 每日签到 加入编程讨论群

C#教程 ASP.NET教程 C#视频教程程序源码享受不尽 C#技术求助 ASP.NET技术求助

【源码下载】 社群合作 申请版主 程序开发 【远程协助】 每天乐一乐 每日签到 【承接外包项目】 面试-葵花宝典下载

官方一群:

官方二群:

一文读懂JavaScript闭包:不再迷茫!

[复制链接]
查看1972 | 回复2 | 2023-11-7 15:42:19 | 显示全部楼层 |阅读模式
一、JavaScript闭包的定义
概念上,闭包是一个函数与其相关的引用环境的组合。一个闭包允许你访问定义自身外部作用域的变量,即使这个函数执行的环境并非它被声明的环境。每当创建一个函数并且检索一个作用域外的变量时,就会创建一个闭包。

二、JavaScript闭包的工作原理
要理解闭包的工作原理,我们首先需要理解JavaScript中的作用域。我们可以将作用域理解为一系列的"框架",保存了在函数执行过程中定义的变量和函数。
当函数访问一个变量时,它首先会在自己的作用域查找。如果没有找到,它会继续在外层作用域查找,直到查找到全局作用域。如果在所有作用域都没有找到这个变量,那么它就是未定义的。
在JavaScript中,只有函数可以创建新的作用域。闭包发生在一个函数访问一个在其自身作用域之外定义的变量。
下面是一个简单的闭包示例:
[JavaScript] 纯文本查看 复制代码
function outerFunction() {
  const outerVariable = "我是外部变量";

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

const myInnerFunction = outerFunction();
myInnerFunction();  // "我是外部变量"

以上代码中,innerFunction是一个闭包,因为它可以访问outerFunction的作用域,包括其变量outerVariable。

三、JavaScript 闭包的特点
  • 访问外部作用域变量:闭包允许函数访问其定义时所在的外部作用域中的变量。这意味着即使外部作用域已经执行完毕,闭包仍然可以引用和操作外部作用域中的变量。
  • 保持变量状态:闭包可以维持其所在作用域的状态。这意味着当一个闭包被创建后,它可以一直保留对外部变量的引用,即使外部函数已经执行完毕,这些变量也不会被销毁。这种特性使得闭包可以创建私有变量和方法。
  • 延长变量生命周期:由于闭包保持对外部变量的引用,这些变量的生命周期可能会被延长。如果一个闭包被保存在一个全局变量或其他长期存在的地方,那么与该闭包相关的所有变量都不会被垃圾回收,可能会产生内存泄漏。
  • 创建多个副本:每次调用一个函数时,都会创建一个新的闭包,包含独立的变量实例。这意味着可以通过多次调用同一个函数来创建多个副本,每个副本都有自己独立的变量状态。
  • 提供私有性和封装性:通过使用闭包,可以模拟私有变量和方法。将变量和方法封装在闭包中,外部代码无法直接访问和修改这些变量和方法,从而实现了一定程度的数据隐藏和保护。
  • 增加内存消耗:由于闭包保留了外部作用域中的变量,这意味着这些变量不会被垃圾回收,可能会增加内存消耗。因此,在使用闭包时需要注意内存管理,避免出现内存泄漏。

四、JavaScript 闭包的类型

1.JavaScript闭包有以下几种类型:
函数作为返回值的闭包:这是最常见的闭包类型。当一个函数内部返回另一个函数时,被返回的函数可以访问并操作其所在的外部作用域中的变量。
[JavaScript] 纯文本查看 复制代码
function outerFunction() {
  var outerVariable = 'I am outside!';
  
  function innerFunction() {
    console.log(outerVariable);
  }
  
  return innerFunction;
}

var inner = outerFunction();
inner(); // 输出:I am outside!


在这个例子中,innerFunction 是一个闭包,它可以访问并输出 outerFunction 中定义的 outerVariable。

2.函数作为参数的闭包:
当一个函数接受另一个函数作为参数,并在内部使用该参数函数时,参数函数可以访问并操作外部作用域中的变量。
[JavaScript] 纯文本查看 复制代码
function outerFunction(callback) {
  var outerVariable = 'I am outside!';
  
  callback();
}

function innerFunction() {
  console.log(outerVariable);
}

outerFunction(innerFunction); // 输出:I am outside!


在这个例子中,innerFunction 作为参数传递给了 outerFunction,在 outerFunction 内部调用了 callback 函数,使得 innerFunction 成为了闭包,可以访问并输出 outerVariable。

3.自执行函数的闭包:
当一个函数在定义后立即执行,并且在内部定义了其他函数时,这些内部函数可以访问并操作外部作用域中的变量。
[JavaScript] 纯文本查看 复制代码
function() {
  var outerVariable = 'I am outside!';
  
  function innerFunction() {
    console.log(outerVariable);
  }
  
  innerFunction(); // 输出:I am outside!
}();


在这个例子中,立即执行函数内部的 innerFunction 是一个闭包,它可以访问并输出外部作用域中的 outerVariable。


五、JavaScript闭包的用途
闭包有许多用途。在服务端和客户端JavaScript开发中,它们都被广泛使用。

1.创建私有变量和方法
通过使用闭包可以创建私有变量和方法,这些变量和方法只能在闭包内部访问,而外部代码无法直接访问。这样可以实现数据的隐藏和保护,防止外部代码意外修改或访问。
下面是一个简单的示例:
[JavaScript] 纯文本查看 复制代码
function createCounter() {
  let count = 0;
  
  return function() {
    count++;
    console.log(count);
  };
}

const counter = createCounter();

counter(); // 输出1
counter(); // 输出2
counter(); // 输出3

在这个示例中,createCounter() 函数返回了一个匿名函数,该匿名函数引用了 count 变量。由于 count 只能在 createCounter() 函数内部访问,因此它是一个私有变量。每次调用 counter() 函数,都会增加 count 的值并输出。

2.封装模块
使用闭包可以封装一组相关的函数和数据,并将其作为一个模块暴露给外部环境。这种模块化的编程方式可以提高代码的可维护性和复用性,同时减少全局命名空间的污染。下面是一个示例:
[JavaScript] 纯文本查看 复制代码
const myModule = (function() {
  let privateVar = 'I am private';
  
  function privateMethod() {
    console.log('This is a private method');
  }
  
  return {
    publicMethod: function() {
      console.log('This is a public method');
    },
    publicVar: 'I am public'
  };
})();

myModule.publicMethod(); // 输出 "This is a public method"
console.log(myModule.publicVar); // 输出 "I am public"
console.log(myModule.privateVar); // undefined
myModule.privateMethod(); // 抛出 TypeError

在这个示例中,我们使用一个立即调用的函数表达式(IIFE)创建了一个模块。模块包含了一个私有变量 privateVar 和一个私有方法 privateMethod,同时还暴露了两个公共成员 publicVar 和 publicMethod。由于只有公共成员会被暴露给外部环境,因此私有成员和函数对外部代码是不可见的。

3.记忆上下文
闭包可以记忆其所在的外部作用域,使得函数在后续调用时可以访问和操作之前保存的上下文信息。这对于处理需要持久状态的任务非常有用,比如定时器、事件处理等。下面是一个示例:
[JavaScript] 纯文本查看 复制代码
function createTimer(delay) {
  let elapsed = 0;
  
  const timer = setInterval(function() {
    elapsed += delay;
    console.log(`Elapsed time: ${elapsed}`);
  }, delay);
    
  return function() {
    clearInterval(timer);
  };
}

const stopTimer = createTimer(1000);

setTimeout(stopTimer, 5000);


在这个示例中,createTimer() 函数返回了一个匿名函数,该函数可以停止由 setInterval() 创建的定时器。每次调用 createTimer() 函数,都会创建一个新的闭包实例,保存了 elapsed 变量和 timer 定时器。在后续调用时,闭包可以访问和修改之前保存的状态。

4.实现回调函数
闭包可以用作回调函数,将函数作为参数传递给其他函数,在适当的时机被调用。通过闭包,回调函数可以访问并操作外部作用域中的变量,实现更灵活的编程逻辑。下面是一个示例:
[JavaScript] 纯文本查看 复制代码
function add(x, y, callback) {
  const result = x + y;
  callback(result);
}

function logResult(result) {
  console.log(`The result is ${result}`);
}

add(2, 3, logResult);

// 输出 "The result is 5"


在这个示例中,add() 函数接收两个数字和一个回调函数作为参数,计算它们的和并将结果作为参数传递给回调函数。我们可以定义一个回调函数 logResult(),输出结果到控制台。由于回调函数是一个闭包,因此可以访问 add() 函数内部的变量 result。

5.解决循环中的问题
在循环中使用闭包可以解决一些常见的问题,如在定时器或事件处理中正确捕获循环变量的值。由于闭包会创建一个独立的作用域,每次迭代都会生成一个新的闭包实例,从而避免了变量共享和混淆的问题。下面是一个示例:
[JavaScript] 纯文本查看 复制代码
const buttons = document.querySelectorAll('button');

for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function() {
    console.log(`Button ${i} clicked`);
  });
}

在这个示例中,我们使用一个循环绑定多个按钮的点击事件。由于回调函数是一个闭包,每次迭代都会创建一个新的闭包实例,因此可以正确捕获循环变量的值。如果不使用闭包,在回调函数中访问 i 变量时,会发现每个按钮都输出了最终的值。

6.缓存数据
闭包可以用作缓存数据的存储结构。通过将计算结果保存在闭包内部的变量中,可以避免重复计算,提高程序的运行效率。下面是一个示例:
[JavaScript] 纯文本查看 复制代码
function createMultiplier(factor) {
  const cache = {};
  
  return function(number) {
    if (number in cache) {
      console.log(`Retrieving from cache: ${number}`);
      return cache[number];
    } else {
      console.log(`Calculating result: ${number}`);
      const result = number * factor;
      cache[number] = result;
      return result;
    }
  };
}

const double = createMultiplier(2);

console.log(double(3)); // 输出 "Calculating result: 3" 和 "6"
console.log(double(4)); // 输出 "Calculating result: 4" 和 "8"
console.log(double(3)); // 输出 "Retrieving from cache: 3" 和 "6"
在这个示例中,createMultiplier() 函数返回一个函数,该函数可以将输入数字乘以指定的因子。同时,该函数通过闭包实现了缓存功能。如果输入的数字已经存在于缓存中,则直接返回缓存中的结果。否则,计算并缓存计算结果。这样可以避免重复计算,提高程序效率。


六、闭包的注意事项
虽然闭包有许多有用的应用,但不恰当的使用闭包也可以带来问题,尤其是在处理内存管理的时候。因为闭包可能会导致原本应该被垃圾回收的对象得不到清理,进而产生内存泄漏。

1.内存泄漏
闭包会引用外部作用域中的变量,如果闭包长时间存在而不释放相关资源,可能导致内存泄漏。解决方法是在不需要使用闭包时,及时释放它们所占用的资源,比如清除定时器、解绑事件监听器等。
[JavaScript] 纯文本查看 复制代码
function createClosure() {
  const data = 'Some data';
  
  // 创建闭包并在适当时机释放资源
  const closure = function() {
    console.log(data);
    // 其他操作...
  };
  
  // 定时器
  const timer = setInterval(closure, 1000);
  
  // 清除定时器并释放资源
  function releaseResources() {
    clearInterval(timer);
    // 其他资源释放操作...
  }
  
  return releaseResources;
}

const release = createClosure();

// 当不再需要闭包时,调用 release() 进行资源释放
release();


2.变量共享和混淆
由于闭包会保留对外部作用域中变量的引用,可能出现多个闭包共享同一个变量的情况,进而导致意外的结果。解决方法是在创建闭包时使用立即执行函数,将每个闭包实例与其所需的变量隔离开来。
[JavaScript] 纯文本查看 复制代码
for (let i = 0; i < 5; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index);
    }, 1000);
  })(i);
}

3.作用域链增加:闭包会在内存中保留对其所在的外部作用域的引用,如果闭包存在的时间过长或嵌套层级过深,会导致作用域链变长,影响性能。解决方法是在不需要使用闭包时,尽早释放它们,避免过度嵌套闭包。
[JavaScript] 纯文本查看 复制代码
function outer() {
  const data = 'Some data';
  
  // 创建嵌套闭包
  function inner() {
    console.log(data);
    // 其他操作...
  }
  
  // 调用 inner() 并及时释放闭包
  inner();
}

outer();


在这个示例中,inner() 函数是一个闭包,它保留了对外部作用域的引用。由于 inner() 在 outer() 中调用,因此可以尽早释放 inner() 所占用的资源,避免过度嵌套闭包。


总结
总的来说,闭包是一种将函数内部和外部连接起来的桥梁。掌握了闭包,就等于掌握了JavaScript的一个关键概念,理解了JavaScript的灵魂。它在许多现代的JavaScript设计模式中都有所应用,你会在闭包中发现JavaScript的深奥之趣。但同时,我们也需要注意闭包的过度使用可能会导致内存泄漏等问题,所以在编程中应合理使用。


*滑块验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则