JavaScript系列——闭包

文章目录

    • 闭包定义
    • 词法作用域
    • 闭包示例
    • 使用场景
      • 创建私有变量
      • ES5 中,解决循环变量的作用域问题
    • 小结

闭包定义

闭包,是函数及其关联的周边环境的引用的组合,在闭包里面,内部函数可以访问外部函数的作用域,而外部函数不能范围内部函数的作用域,从而在内部函数形成一个相对封闭的环境。在JavaScript中,闭包随着函数创建而被创建

词法作用域

词法作用域指的是在源码声明的变量,能够起作用的环境范围,一般是从变量所定义的位置来决定。

看以下代码:

function init() {
  var name = "Mozilla"; // name 是一个被 init 创建的局部变量
  function displayName() {
    // displayName() 是内部函数,一个闭包
    alert(name); // 使用了父函数中声明的变量
  }
  displayName();
}
init();

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。
displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。
请注意,displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。

在ES6出现以前,声明变量使用 var 关键字,这样声明的变量,其实词法作用域是全局的。在此之前,会遇到很多奇怪的问题,闭包的出现,就是为了解决这些问题。

闭包示例

我们将刚刚的init 方法改造一下

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

运行这段代码的效果和之前 init() 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName() 在执行前,从外部函数返回

第一眼看去,执行了makeFunc,name 变量应该会无法再被访问了,然而,执执myFunc 函数是,能够正常弹出Mozilla。

原因在于,在 makeFunc 中的函数中,形成了闭包。闭包是由函数以及声明该函数的词法环境组成。改环境包含了闭包创建时,作用域内的任何布局变量。

在上面的代码中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用
displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。
所以,当myFunc 被调用时,变量name 仍然可用。

闭包可以使得引用的变量可以不被垃圾回收机制回收,下面的例子可以加强理解:

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

makeAdder 执行时,创建了一个闭包,makeAdder(5) 传入x =5,x 被 makeAdder 内部函数引用,因此执行了makeAdder后,x 依然保持在内存中。调用add5(2),返回 x+2 = 5+2= 7。

使用场景

创建私有变量

在 ES6之前,JavaScript不支持声明一个私有变量,使得变量不能在某个作用域外变得不可访问。
虽然JavaScript没有原生支持,但我们可以通过闭包来模拟私有方法和变量。

通过模拟闭包,不仅可以实现变量和方法私有,还可以实现避免同名变量和方法污染全局

看以下代码:

var Counter = (function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

在上面代码中,定义一个Counter 变量,他是一个立即执行函数,返回了三个函数。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

如果我们试图访问privateCounter ,都是无法访问的。
如果想要访问privateCounter 的值,必须通过value 方法。要想改变值,只能通过increment或decrement方法,这样就实现了privateCounter 的私有化。

privateCounter 因为被内部函数changeBy引用,而返回的方法中保持了对changeBy引用,因此关于changeBy所引用的变量,将会保持在内存中,因此每一次通过方法改变privateCounter 的值,下一次访问它,就是他最新的值,而不是0。

每次创建的闭包,他们内部都是互相独立的,互不影响的
看下面的代码,分别维护各个作用域:

var makeCounter = function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

两个计数器 Counter1 和 Counter2 维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

ES5 中,解决循环变量的作用域问题

在 ES5时候,如果我们想要实现这个一个效果:
当我们把鼠标聚焦在某一个表单中,会有标题提示我们要填写的内容,我们很容易想到下面使用var 循环来实现:

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email" /></p>
<p>Name: <input type="text" id="name" name="name" /></p>
<p>Age: <input type="text" id="age" name="age" /></p>
function showHelp(help) {
  document.getElementById("help").innerHTML = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help);
    };
  }
}

执行上面的代码后效果如下图,无论把鼠标聚焦在哪,标题都是提示最后一个age 的提示,无法达到预期的效果。
原因是 在setupHelp 函数内部,for 循环共享的是同一个 item变量,当onfocus 被触发执行时,i已经变成了2,因此,无论聚焦哪个,最后都是触发第三个item。
在这里插入图片描述
解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:

function showHelp(help) {
  document.getElementById("help").innerHTML = help;
}

function makeHelpCallback(help) {
  return function () {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

这份代码中,makeHelpCallback 内部创建了闭包,使得传入makeHelpCallback的item,可以在其内部保持其独立性

通过for 循环,分别创建了三个闭包,每一个闭包都是互相独立的。而且makeHelpCallback外部的item变化,不影响makeHelpCallback内部的值,因为闭包里面词法环境是独立的,不与函数外部的item词法环境共享值,因此即使外部item 变化,也不影响闭包的词法环境所引用的变量值。

小结

  • 闭包是一个函数及其词法环境的引用的组合
  • 闭包内部的词法环境(变量的作用域)是独立的,每次执行函数,都会创建新的闭包,每次创建的闭包都有其独立的词法环境
  • 闭包内部所引用的变量不会随着函数执行而被销毁,而会一直保持在内存中
  • 闭包可以用于模拟私有变量和方法和解决 var循环共享值问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/311055.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

江山易改本性难移之ZYNQ SDK QSPI固化bug及其解决方法

之前在Vivado2018.3通过QSPI方式固化程序时出现问题&#xff0c;显示flash擦除成功&#xff0c;但最后总是不能写入到flash中。 查资料发现从VIVADO 2017.3版本开始&#xff0c;Xilinx官方为了使Zynq-7000和Zynq UltraScale 实现流程相同&#xff0c;在QSPI FLASH使用上做了变化…

【数模百科】一篇文章讲清楚层次分析法的原理和解法步骤

本文节选自 层次分析法原理 - 数模百科&#xff0c;如果你想了解更多关于层次分析法的信息&#xff0c;请移步数模百科。 层次分析法&#xff08;Analytic Hierarchy Process&#xff0c;简称AHP&#xff09;是一种解决复杂决策问题的方法。这个方法是由美国运筹学家托马斯萨蒂…

Java 常见缓存详解以及解决方案

一. 演示Mybatis 一级缓存 首先我们准备一个接口 两个实现的方法&#xff0c; 当我们调用这个queryAll&#xff08;&#xff09;方法时我们需要调用selectAll&#xff08;&#xff09;方法来查询数据 调用此接口实现效果 这个时候我们就可以发现了问题&#xff0c;我们调用方法…

发起人自选-钉钉审批

场景描述 配置一个审批流程&#xff0c;在某些审批节点&#xff0c;不能确定谁具体来审批&#xff0c;所以需要手工选择一个人或者多个人保证流程能得以顺利通过。有些审批流程的做法是&#xff0c;上一个节点来选择指定的人&#xff0c;而钉钉的做法是发起人来指定。 钉钉设…

软件测试|探索Python中获取最高数值的几种方法

前言 在数据分析、统计和编程领域&#xff0c;经常会遇到需要从一组数值中找出最高数值的情况。Python 作为一门功能丰富的编程语言&#xff0c;提供了多种方法来实现这一目标。在本文中&#xff0c;我们将探索几种获取最高数值的方法&#xff0c;帮助大家在不同情况下选择最适…

EasyMR:为 AI 未来赋能,打造弹性大数据引擎的革命

如果要评一个2023科技圈的热搜榜&#xff0c;那么以人工智能聊天机器人 ChatGPT 为代表的 AI大模型 绝对会霸榜整个2023。 ChatGPT 于2022年11月30日发布。产品发布5日&#xff0c;注册用户数就超过100万。推出仅两个月后&#xff0c;它在2023年1月末的月活用户已经突破了1亿&…

Qt QSpinBox微调框控件

文章目录 1 属性和方法1.1 值1.2 步长1.3 循环1.4 加速1.5 前缀和后缀1.6 信号和槽 2 实例2.1 布局2.2 代码实现 微调框&#xff0c;允许用户按照一定的步长&#xff0c;来增加或减少其中显示的数值 修改微调框数值的方式包括&#xff1a; 单击右侧的向上/向下按钮按键盘的向上…

C语言 - 最简单,最易懂的指针、引用讲解

一、变量、地址、变量值 二、直接上代码&#xff0c;一边看上图&#xff0c;一边讲解 #include <stdio.h>struct Hello {int a;int b; };int main() {struct Hello h;h.a 10;h.b 20;struct Hello *hp;hp &h;printf("1: h的地址是%d&#xff0c;hp地址是%d \…

18.标题统计*

题目 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);String str sc.nextLine();int res 0;for(int i0;i<str.length();i) {char c str.charAt(i);if(c! && c!\n) {res;}}System.o…

LabVIEW在金属铜大气腐蚀预测评价系统中的应用

为了应对电子设备和仪器中金属铜因大气腐蚀带来的挑战&#xff0c;开发一种基于LabVIEW平台的先进预测评价系统。这个系统的设计宗旨是准确预测并评估在不同室内外环境中金属铜的腐蚀状况。我们团队在LabVIEW的强大数据处理和图形化编程支持下&#xff0c;结合实际的大气腐蚀数…

解锁行内元素和块元素的奥秘:网页开发的基础

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Eureka切换Nacos时发现两个注册中心的解决方法

报错信息如下&#xff0c;意思是发现了两个注册中心 Field autoServiceRegistration in org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration required a single bean, but 2 were found: - nacosAutoServiceRegistration: defined…

手把手教你升级GPT-4,内附详细步骤

目录 1、先介绍一下 GPT 升级 2、第一种: 免费升级 支付宝购买礼品卡给美区 apple id 充值 3、第二种&#xff1a;5分钟快速升级 方法 平时我会在朋友圈分享一些利用 GPT-4 画的图片&#xff0c;比如下面这个扑克牌风格的"黑红小狗武士"。 用 GPT-4 做绘画仅仅是…

【C++】手撕 list类(包含迭代器)

目录 1&#xff0c;list的介绍及使用 2&#xff0c;list_node 3&#xff0c;list_node() 3&#xff0c;list 4&#xff0c;list() 5&#xff0c;push_back(const T& x) 6&#xff0c;print() 7&#xff0c;_list_iterator 8&#xff0c;operator*() 9&#xff0c…

什么是 RFID 及其工作原理?

一、自动识别技术 自1999年麻省理工学院研究人员的首创开始&#xff0c;自动识别技术&#xff08;简称auto-ID&#xff09;的领域不断扩大。自动识别技术形成了多种技术路线&#xff0c;使我们能够自动、精确地捕获、识别和存储与物体、物品或个人相关的数据&#xff0c;从而减…

[足式机器人]Part2 Dr. CAN学习笔记-Advanced控制理论 Ch04-3.5连续系统离散化

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-Advanced控制理论 Ch04-3.5连续系统离散化

VS2022 | 显示Unreal Engine日志

VS2022 | 显示Unreal Engine日志 视图 -> 其他窗口 -> Unreal Engine日志 视图 -> 其他窗口 -> Unreal Engine日志

Python将Labelme文件的真实框和预测框绘制到图片上(v2.0)

Python将Labelme文件的真实框和预测框绘制到图片上&#xff08;v2.0&#xff09; 前言前提条件相关介绍实验环境Python将Labelme文件的标注信息绘制到图片上代码实现输出结果 前言 此版代码&#xff0c;相较于Python将Labelme文件的真实框和预测框绘制到图片上&#xff0c;将无…

每日一练:LeeCode-104. 二叉树的最大深度【二叉树】

本文是力扣LeeCode-104. 二叉树的最大深度 学习与理解过程&#xff0c;本文仅做学习之用&#xff0c;对本题感兴趣的小伙伴可以出门左拐LeeCode。 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例…

Neo4j备份

这里主要讲Neo4j在windows环境下如何备份&#xff0c;Linux环境同理 Neo4j恢复看这里:Neo4j恢复-CSDN博客 Step1:停服 关闭neo4j.bat console会话窗口即可 Step2: 备份 找到数据目录&#xff0c;并备份、压缩 copy即可 data - 20240108.7z Step3: 启动服务 进入命令行&am…