JavaScript 中闭包是什么?有哪些应用场景?

简介: JavaScript 中闭包是什么?有哪些应用场景?

给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            推荐:★★★★★

地址:web前端面试题库

闭包是什么?

  • 闭包是指一个函数可以访问并操作其词法作用域外的变量的能力。
  • 闭包就是能够读取其他函数内部变量的函数。
  • 例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。
  • 在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

特点:函数嵌套,并返回子函数,子函数访问了外变量。

//外部函数
function outerFunction() {
    //内部函数外的变量
    var outerVariable = 'I am from outer function'; 
    //返回内部函数
    function innerFunction() { 
        console.log(outerVariable); 
    } 
    return innerFunction; 
} 
// outerFunction执行完之后被销毁,但是outerVariable被引用着所以仍然活着
var closure = outerFunction(); 
closure(); // 输出:I am from outer function

闭包的作用?

  1. 封装私有变量:闭包可以用于创建私有变量和方法。通过在函数内部定义变量,并返回一个内部函数,外部无法直接访问这些变量,从而实现了封装的效果。这样可以避免全局变量的污染,提高代码的可维护性和安全性。
  2. 延迟执行:闭包可以用于实现延迟执行的效果。通过在函数内部定义一个定时器或事件监听器,并返回一个内部函数,可以在需要的时候触发执行。
  3. 记忆化/保持状态:闭包可以用于实现记忆化的效果,即将函数的计算结果缓存起来,以便在后续调用时直接返回缓存的结果,提高函数的执行效率。
  4. 回调函数:闭包可以用于实现回调函数。通过将一个函数作为参数传递给另一个函数,并在内部函数中调用该函数,可以实现异步操作的回调机制。
  5. 模块化开发:闭包可以用于实现模块化开发。通过将一组相关的变量和方法封装在一个闭包中,可以避免全局命名空间的污染,实现模块的独立性和复用性。

闭包的缺陷?

  1. 内存占用:闭包会导致外部函数的变量无法被垃圾回收,从而增加内存占用。如果闭包会长时间存在,那么外部变量将无法被释放,可能导致内存泄漏。
  2. 性能损耗:闭包涉及到作用域链的查找过程,会带来一定的性能损耗。在性能要求高的场景下,需要注意闭包的使用。

闭包的应用场景?

参考链接: js闭包的6种应用场景!!!这下会用了

1. 自执行函数(可以实现单例模式)
let say = (function(){
  let val = 'hello world';
  function say(){
    console.log(val);
  }
  return say;
})()
var Singleton = (function () {
        var instance;
        function createInstance() {
          var object = new Object("I am the instance");
          return object;
        }
        return {
          getInstance: function () {
            if (!instance) {
              instance = createInstance();
            }
            return instance;
          },
        };
      })();
2. 防抖节流
// 节流函数封装
function throttle(func, delay) {
  let timer = null;
  return function () {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, arguments);
        timer = null;
      }, delay);
    }
  };
}
// 防抖函数封装
function debounce(func, delay) {
  let timer = null;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, arguments);
    }, delay);
  };
}
3. 函数柯里化
//柯里化前
function add(a, b, c) {
  return a + b + c;
}
console.log(add(1, 2, 3)); //6
//柯里化后
function addCurried1(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    };
  };
}
//箭头函数简写
const addCurried2 = (a) => (b) => (c) => a + b + c;
console.log(addCurried1(1)(2)(3)); //6
console.log(addCurried2(1)(2)(3)); //6
4. 发布订阅
function createPubSub() {
  // 存储事件及其对应的订阅者
  const subscribers = {};
  // 订阅事件
  function subscribe(event, callback) {
    // 如果事件不存在,则创建一个新的空数组
    if (!subscribers[event]) {
      subscribers[event] = [];
    }
    // 将回调函数添加到订阅者数组中
    subscribers[event].push(callback);
  }
  // 发布事件
  function publish(event, data) {
    // 如果事件不存在,则直接返回
    if (!subscribers[event]) {
      return;
    }
    // 遍历订阅者数组,调用每个订阅者的回调函数
    subscribers[event].forEach((callback) => {
      callback(data);
    });
  }
  // 返回订阅和发布函数
  return {
    subscribe,
    publish,
  };
}
// 使用示例
const pubSub = createPubSub();
// 订阅事件
pubSub.subscribe("event1", (data) => {
  console.log("订阅者1收到事件1的数据:", data);
});
pubSub.subscribe("event2", (data) => {
  console.log("订阅者2收到事件2的数据:", data);
});
// 发布事件
pubSub.publish("event1", "Hello");
// 输出: 订阅者1收到事件1的数据: Hello
pubSub.publish("event2", "World");
// 输出: 订阅者2收到事件2的数据: World
5. 迭代器
function createIterator(arr) {
  let index = 0;
  return {
    next: function() {
      if (index < arr.length) {
        return {
          value: arr[index++],
          done: false
        };
      } else {
        return {
          done: true
        };
      }
    }
  };
}
const myIterator = createIterator([1, 2, 3]);
console.log(myIterator.next()); // { value: 1, done: false }
console.log(myIterator.next()); // { value: 2, done: false }
console.log(myIterator.next()); // { value: 3, done: false }
console.log(myIterator.next()); // { done: true }

如何释放闭包?

释放闭包通常是通过解除对闭包的引用来实现的。当不再需要使用闭包时,可以将闭包所在的变量设置null 或者将闭包所在的变量赋予其他值,从而断开对闭包的引用。这样,JavaScript 引擎在下一次垃圾回收时会判断闭包不再被引用,从而释放闭包占用的内存空间。

相关概念

函数的词法作用域(也叫静态作用域/闭包作用域)

是指在函数定义时确定的作用域,而不是在函数调用时确定的作用域。它是由函数在定义时所处的上下文环境决定的,与函数的调用位置无关。

规则

  • 函数内部可以访问函数外部的变量
  • 函数内部的变量在函数外部不可访问
  • 函数内部可以访问函数外部的函数
  • 函数内部的函数可以访问外部函数的变量

词法作用域的优势在于它提供了更可靠和可预测的变量访问方式。在函数定义时,就确定了函数内部可以访问的变量,不会受到函数调用位置的影响。这种静态作用域的特性使得代码更易于理解和维护,并且可以实现一些高级的编程技巧,如闭包和模块化开发。

执行上下文

每当 JavaScript 代码执行时,都会创建一个执行上下文(是 JavaScript 引擎内部的一种数据结构,用于管理代码的执行环境、变量的作用域),并按照特定的规则进行管理和销毁。

执行上下文可以分为三种类型:
  1. 全局执行上下文(Global Execution Context):全局执行上下文是在整个脚本文件执行时创建的,它是最外层的执行上下文。在全局执行上下文中,变量和函数声明会被提升,并且会创建全局对象(如浏览器环境中的 window 对象)和全局变量。
  2. 函数执行上下文(Function Execution Context):每当函数被调用时,都会创建一个函数执行上下文。函数执行上下文中包含了函数的参数、局部变量、函数内部的变量和函数声明。每个函数执行上下文都有自己的作用域链和 this 值。
  3. Eval 执行上下文(Eval Execution Context):在使用 eval() 函数执行代码时,会创建一个 eval 执行上下文。它与全局执行上下文类似,但是它有自己的词法作用域。
执行上下文的生命周期包括以下阶段:
  1. 创建阶段(Creation Phase):在这个阶段,JavaScript 引擎会创建执行上下文,并进行变量和函数的声明。变量会被初始化为 undefined,函数会被存储在内存中。
  2. 执行阶段(Execution Phase):在这个阶段,JavaScript 引擎会按照代码的顺序执行语句,给变量赋值,执行函数调用等操作。
观察和理解执行上下文

执行上下文的管理和切换由 JavaScript 引擎自动完成,开发者可以通过了解执行上下文的概念和规则,更好地理解代码的执行过程,以及变量和函数的作用范围。

尽管无法直接访问或输出执行上下文,但我们可以通过一些间接的方式来观察和理解执行上下文的行为和特性:

  1. 使用 console.log():可以在代码中使用 console.log() 方法输出变量的值、函数的执行结果等信息,从而间接观察到执行上下文的影响。
  2. 使用调试工具:现代的浏览器和开发工具提供了强大的调试功能,可以在代码执行过程中查看执行上下文的变化、变量的值以及调用栈等信息。
  3. 使用闭包:通过创建闭包,我们可以间接地观察到执行上下文的作用。闭包可以让内部函数访问外部函数的变量,从而形成一个包含外部执行上下文的闭包执行上下文。

给大家推荐一个实用面试题库

1、前端面试题库 (面试必备)            推荐:★★★★★

地址:web前端面试题库

相关文章
|
1月前
|
前端开发 JavaScript Java
JavaScript闭包深入剖析:性能剖析与优化技巧
JavaScript 闭包是强大而灵活的特性,广泛应用于数据封装、函数柯里化和事件处理等场景。闭包通过保存外部作用域的变量,实现了私有变量和方法的创建,提升了代码的安全性和可维护性。然而,闭包也可能带来性能问题,如内存泄漏和执行效率下降。为优化闭包性能,建议采取以下策略:及时解除对不再使用的闭包变量的引用,减少闭包的创建次数,使用 WeakMap 管理弱引用,以及优化闭包结构以减少作用域链查找的开销。在实际开发中,无论是 Web 前端还是 Node.js 后端,这些优化措施都能显著提升程序的性能和稳定性。
128 70
|
23天前
|
自然语言处理 JavaScript 前端开发
当面试官再问我JS闭包时,我能答出来的都在这里了。
闭包(Closure)是前端面试中的高频考点,广泛应用于函数式编程中。它不仅指函数内部定义的函数,还涉及内存管理、作用域链和垃圾回收机制。闭包可以让函数访问其外部作用域的变量,但也可能引发内存泄漏等问题。通过合理使用闭包,可以实现模块化、高阶函数和回调函数等应用场景。然而,滥用闭包可能导致代码复杂度增加、调试困难以及潜在的性能问题。为了避免这些问题,开发时应谨慎处理闭包,避免不必要的嵌套,并及时清理不再使用的变量和监听器。
100 16
当面试官再问我JS闭包时,我能答出来的都在这里了。
|
4月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
232 77
|
2月前
|
缓存 NoSQL JavaScript
Vue.js应用结合Redis数据库:实践与优化
将Vue.js应用与Redis结合,可以实现高效的数据管理和快速响应的用户体验。通过合理的实践步骤和优化策略,可以充分发挥两者的优势,提高应用的性能和可靠性。希望本文能为您在实际开发中提供有价值的参考。
65 11
|
2月前
|
敏捷开发 人工智能 JavaScript
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
Figma-Low-Code 是一个开源项目,能够直接将 Figma 设计转换为 Vue.js 应用程序,减少设计师与开发者之间的交接时间,支持低代码渲染和数据绑定。
137 3
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
|
2月前
|
JavaScript 前端开发
【Vue.js】监听器功能(EventListener)的实际应用【合集】
而此次问题的核心就在于,Vue实例化的时机过早,在其所依赖的DOM结构尚未完整构建完成时就已启动挂载流程,从而导致无法找到对应的DOM元素,最终致使计算器功能出现异常,输出框错误地显示“{{current}}”,并且按钮的交互功能也完全丧失响应。为了让代码结构更为清晰,便于后续的维护与管理工作,我打算把HTML文件中标签内的JavaScript代码迁移到外部的JS文件里,随后在HTML文件中对其进行引用。
57 8
|
2月前
|
监控 安全 中间件
Next.js 实战 (十):中间件的魅力,打造更快更安全的应用
这篇文章介绍了什么是Next.js中的中间件以及其应用场景。中间件可以用于处理每个传入请求,比如实现日志记录、身份验证、重定向、CORS配置等功能。文章还提供了一个身份验证中间件的示例代码,以及如何使用限流中间件来限制同一IP地址的请求次数。中间件相当于一个构建模块,能够简化HTTP请求的预处理和后处理,提高代码的可维护性,有助于创建快速、安全和用户友好的Web体验。
|
4月前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
99 31
|
3月前
纸屑飘落生日蛋糕场景js+css3动画特效
纸屑飘落生日蛋糕CSS3动画特效是一款js+css3制作的全屏纸屑飘落,生日蛋糕点亮庆祝动画特效。
63 3
|
4月前
|
JavaScript 前端开发 API
深入理解Node.js事件循环及其在后端开发中的应用
本文旨在揭示Node.js的核心特性之一——事件循环,并探讨其对后端开发实践的深远影响。通过剖析事件循环的工作原理和关键组件,我们不仅能够更好地理解Node.js的非阻塞I/O模型,还能学会如何优化我们的后端应用以提高性能和响应能力。文章将结合实例分析事件循环在处理大量并发请求时的优势,以及如何避免常见的编程陷阱,从而为读者提供从理论到实践的全面指导。

热门文章

最新文章