一文详解JS 闭包!

VS2022 中 std::mutex 静态初始化陷阱:一次诡异崩溃的排查记录 VS2022中使用std::mutex时遇到的静态初始化陷阱问题。该问题源于VS2022 17.4版本为std::mutex添加constexpr支持后,全局静态初始化可能导致系统资源未就绪时锁失效或崩溃。文章分析了问题现象、影响范围,并提供了解决方案。最后给出了排查清单和核心建议 阅读详情

JavaScript 中的闭包是相当重要的概念,并且与作用域相关知识的指向密切相关,在大厂的前端面试过程中经常会被提及。

作用域基本介绍

JavaScript 的作用域通俗来讲,就是指变量能够被访问到的范围,在 JavaScript 中作用域也分为好几种,ES5 之前只有全局作用域和函数作用域两种。ES6 出现之后,又新增了块级作用域,下面我们就来看下这三种作用域的概念,为闭包的学习打好基础。

全局作用域

在编程语言中,不论 Java 也好,JavaScript 也罢,变量一般都会分为全局变量和局部变量两种。那么变量定义在函数外部,代码最前面的一般情况下都是全局变量。

在 JavaScript 中,全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量。下面通过看一段代码来说明一下什么是全局的作用域。

var globalName = 'global';

function getName() { 
  console.log(globalName) // global
  var name = 'inner'
  console.log(name) // inner
} 

getName();
console.log(name); // 
console.log(globalName); //global
function setName(){ 
  vName = 'setName';
}

setName();
console.log(vName); // setName
console.log(window.vName) // setName

从这段代码中我们可以看到,globalName 这个变量无论在什么地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的。

如果在 JavaScript 中所有没有经过定义,而直接被赋值的变量默认就是一个全局变量,比如上面代码中 setName 函数里面的 vName 变量一样。

我们可以发现全局变量也是拥有全局的作用域,无论你在何处都可以使用它,在浏览器控制台输入 window.vName 的时候,就可以访问到 window 上所有全局变量。

当然全局作用域有相应的缺点,我们定义很多全局变量的时候,会容易引起变量命名的冲突,所以在定义变量的时候应该注意作用域的问题。

函数作用域

在 JavaScript 中,函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域,下面我们来看一段代码。

function getName () {
  var name = 'inner';
  console.log(name); //inner
}

getName();
console.log(name);

上面代码中,name 这个变量是在 getName 函数中进行定义的,所以 name 是一个局部的变量,它的作用域就是在 getName 这个函数里边,也称作函数作用域。

除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的。

下面再来看最后一个块级作用域。

块级作用域

ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。

听起来好像还不是很能理解块级作用域的意思,那么我们来举个更形象例子,看看到底哪些才是块级作用域呢?其实就是在 JS 编码过程中 if 语句及 for 语句后面 {…} 这里面所包括的,就是块级作用域。

下面结合一段代码来说明。

console.log(a) //a is not defined

if(true){
  let a = '123';
  console.log(a); // 123
}

console.log(a) //a is not defined

从这段代码可以看出,变量 a 是在 if 语句{…} 中由 let 关键词进行定义的变量,所以它的作用域是 if 语句括号中的那部分,而在外面进行访问 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果,控制台会显示 a 并没有定义。

那么有了上面这几种作用域的概念做铺垫之后,下面我们就可以来学习闭包的概念。

什么是闭包?

先来看下红宝书上和 MDN 上给出的闭包的概念。

红宝书闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。
MDN:一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

乍一看上面的两个比较官方的定义,很难让人理解清晰,尤其是 MDN 的关于闭包的定义,真的比较让人“头晕”,通俗地讲解一下:闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以。

因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。下面我们通过代码先来看一个简单的例子。

function fun1() {
	var a = 1;
	return function(){
		console.log(a);
	};
}

fun1();
var result = fun1();
result();  // 1

结合闭包的概念,我们把这段代码放到控制台执行一下,就可以发现最后输出的结果是 1(即 a 变量的值)。那么可以很清楚地发现,a 变量作为一个 fun1 函数的内部变量,正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,我们最后还是可以拿到 a 变量的值。

闭包产生的原因

我们在前面介绍了作用域的概念,那么你还需要明白作用域链的基本概念。其实很简单,当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

需要注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。那么我们还是通过下面的代码来详细说明一下作用域链。

var a = 1;
function fun1() {
  var a = 2
  function fun2() {
    var a = 3;
    console.log(a);//3
  }

}

从中可以看出,fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。

那么这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。

由此可见,闭包产生的本质就是:当前环境中存在指向父级作用域的引用。那么还是拿上的代码举例。

function fun1() {
  var a = 2
  function fun2() {
    console.log(a);  //2
  }
  return fun2;
}

var result = fun1();
result();

从上面这段代码可以看出,这里 result 会拿到父级作用域中的变量,输出 2。因为在当前环境中,含有对 fun2 函数的引用,fun2 函数恰恰引用了 window、fun1 和 fun2 的作用域。因此 fun2 函数是可以访问到 fun1 函数的作用域的变量。

那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此还可以这么改代码,如下所示。

var fun3;

function fun1() {
  var a = 2
  fun3 = function() {
    console.log(a);
  }
}
fun1();
fun3();

可以看出,其中实现的结果和前一段代码的效果其实是一样的,就是在给 fun3 函数赋值后,fun3 函数就拥有了 window、fun1 和 fun3 本身这几个作用域的访问权限;然后还是从下往上查找,直到找到 fun1 的作用域中存在 a 这个变量;因此输出的结果还是 2,最后产生了闭包,形式变了,本质没有改变。

因此最后返回的不管是不是函数,也都不能说明没有产生闭包。讲到这里你这里可以再深入体会一下闭包的内涵。

闭包的表现形式

那么明白了闭包的本质之后,我们来看看闭包的表现形式及应用场景到底有哪些呢?我总结了大概有四种场景。

  1. 返回一个函数,上面讲原因的时候已经说过,这里就不赘述了。

  2. 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。请看下面这段代码,这些都是平常开发中用到的形式。

// 定时器

setTimeout(function handler(){
  console.log('1');
},1000);

// 事件监听
$('#app').click(function(){
  console.log('Event Listener');
});

  1. 作为函数参数传递的形式,比如下面的例子。
var a = 1;

function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}

function bar(fn){
  // 这就是闭包
  fn();
}

foo();  // 输出2,而不是1

  1. IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量,如下所示。
var a = 2;

(function IIFE(){
  console.log(a);  // 输出2
})();

IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域,我们经常能在高级的 JavaScript 编程中看见此类函数。

以上关于闭包的基本概念、产生的原因及表现形式这三个方面,你已经有了一定的了解。那么最后一部分我们来看一个比较常见的开发应用场景。

如何解决循环输出问题?

在互联网大厂的面试中,解决循环输出问题是比较高频的面试题,一般都会给一段这样的代码让你来解释,那么结合本课时所讲的内容,我们在这里一起看看这个题目,代码如下。

for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}

上面这段代码执行之后,从控制台执行的结果可以看出来,结果输出的是 5 个 6,那么一般面试官都会先问为什么都是 6?我想让你实现输出 1、2、3、4、5 的话怎么办呢?

因此结合本讲所学的知识我们来思考一下,应该怎么给面试官一个满意的解释。你可以围绕这两点来回答。

  1. setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
  2. 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

那么我们再来看看如何按顺序依次输出 1、2、3、4、5 呢?

利用 IIFE

可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。

for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}

可以看到,通过这样改造使用 IIFE(立即执行函数),可以实现序号的依次输出。

使用 ES6 中的 let

ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。通过改造后的代码,可以实现上面想要的结果。

for(let i = 1; i <= 5; i++){
  setTimeout(function() {
    console.log(i);
  },0)
}

从上面的代码可以看出,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。

定时器传入第三个参数

setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。那么结合第三个参数,调整完之后的代码如下。

for(var i=1;i<=5;i++){
  setTimeout(function(j) {
    console.log(j)
  }, 0, i)
}

从中可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现我们想要的结果,这也是一种解决循环输出问题的途径。

GEPA_AIME教程:使用GEPA优化数学问题求解的完整指南 本教程展示了如何使用DSPy框架中的GEPA(Generative Evolutionary Prompt Adaptation)优化器来提升大语言模型在数学竞赛问题(AIME)上的表现。# 创建基础的链式思考程序"""带有详细反馈的评估指标,为GEPA提供学习信号"""try:# 处理解析错误feedback_text = f"最终答案必须是一个有效的整数。你回答了' 阅读详情

相关推荐

别再死记硬背了!用Python可视化带你直观理解柯西收敛定理(附NumPy代码)

本文通过Python可视化技术,结合NumPy和Matplotlib,直观展示柯西收敛定理的动态过程。文章提供了完整的代码实现,帮助读者从抽象数学定义过渡到具象理解,特别适合数学学习者和Python开发者。通过热力图和交互式参数探索,读者可以清晰观察数列收敛特性,提升数学分析的学习效率。

weixin_28465009的博客 185

【JavaScript】一文了解JS的闭包

详解JavaScript的闭包使用及其日常开发遇到的常见问题

ztK63LrD的博客 4051

STM32H7B0VBT6使用Free RTOS配置SD卡+Fatfs文件管理系统

STM32H7B0VBT6使用Free RTOS配置SD卡+Fatfs文件管理系统,记录配置过程

Jack的小窝 5942

js 【详解】闭包

如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包。

朝阳39的博客 3万+

EDA虚拟机分享2020版本(Synopsys2020)

EDA虚拟机分享

亦安的数字小站 9177

rv1126 SDK编译【2020-0912版本SDK包】

rv1126 sdk 编译

学习记录 1万+

JS闭包详解

js中闭包closure,是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来,根据定义,包含变量的函数就是闭包。当两个函数彼此嵌套时,内部的函数就是闭包。闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量。

linfeng_meng的博客 6839

数据分析:美团大赛的具体案例制作

在数据集中,存在三个变量的经纬度数据,分别是商家的经纬度数据('sender_lng', 'sender_lat'),顾客的经纬度数据('recipient_lng', 'recipient_lat')和骑手的经纬度数据('grab_lng', 'grab_lat')。根据结果可知,平台订单创建时间的中位数为2022-10-21 02:59:01,平台订单创建时间的众数为2022-10-19 03:29:41、2022-10-23 03:29:47,与直方图相符。接下来,我们对是否为预定订单变量进行分析。

m0_68269346的博客 2495

JS-闭包详解

闭包是JS中的一块硬骨头-不好啃,需要我们慢慢去咀嚼才能体会出其中的味道,嚼的不好,一不小心还容易崩牙!今天就让我们好好来咀嚼下这块硬骨头把!!! 1.什么是闭包? 所谓闭包:在一个函数作用域中 保留 它上级作用域的局部变量,这些局部变量不会随着上级函数的执行完毕而被销毁(个人理解)。 简单的理解闭包:子函数可以使用父函数的局部变量(包括参数,因为参数也是局部变量);  function

每天进步一点点 4579

C++语言程序设计教程第4版杨进才沈显君课后习题答案解析

为了更好地学习这门课程,我尝试在网上寻找一些学习资料和参考书籍,但效果并不是很理想。因此,我希望能有一些学长学姐或者是有经验的程序员能够分享一些学习经验和方法,让我更好地掌握这门课程。学习过程中,我发现这本书的内容非常丰富,讲解也非常详细,对于像我这样编程基础薄弱的学生来说,是一本非常实用的教材。同时,我也希望有经验的程序员能够推荐一些适合初学者的C++语言学习资料和工具,以便我能更好地学习和掌握这门课程。总之,希望能得到大家的帮助和支持,让我更好地学习和掌握C++语言程序设计。主 编: 杨进才,沈显君。

weixin_58833001的博客 1331

探索数据库领域 SQL 的 JSON 数据处理技巧

在当今数字化时代,JSON(JavaScript Object Notation)作为一种轻量级的数据交换格式,被广泛应用于各种领域,如 Web 应用、移动应用、大数据处理等。数据库作为数据存储和管理的核心,需要具备处理 JSON 数据的能力。本文章的目的在于深入探索 SQL 语言在数据库中处理 JSON 数据的技巧,涵盖从基础的 JSON 数据查询到复杂的 JSON 数据操作,帮助读者全面了解和掌握 SQL 处理 JSON 数据的方法。

2502_91592937的博客 924

JavaScript 闭包详解

JavaScript闭包 文章目录JavaScript闭包一、为什么要闭包二、外部得以访问函数内变量三、某些变量得以常驻内存1.垃圾回收机制对闭包的处理2.结合立即执行函数来保存某些变量总结 # 前言 ## 1.什么是闭包函数 闭包函数是声明在另一个函数内的函数,是被嵌套在父函数内部的子函数,在《JS高级程序设计-第3版》中对闭包解释是:"闭包是指有权访问另外一个函数作用域中的变量的函数." 闭包函数可以访问[包裹其的函数]内的各种参数和变量,即便外部函数已经执行完毕.(至于为什么请看下文). 一、为什么

白瑕 的博客 2470

在Open3D中从网格中采样点云

在本文中,我们将学习如何使用Open3D从网格中采样点云。点云采样是点云处理的重要步骤之一,能够为后续的任务提供必要的输入。通过这些示例代码,我们可以很容易地使用Open3D库从网格模型中采样点云。值得注意的是,根据实际应用需求,我们可以调整采样点的数量和采样方法来满足不同的场景需求。除了使用Poisson Disk算法,Open3D还提供了其他的点云采样方法,如Uniform采样和Random采样。这个函数使用了Poisson Disk采样算法,可以在保持点云均匀性的同时,尽可能避免重叠点的生成。

FxRuby的博客 412

JS中的闭包详解

闭包是指有权访问另一个函数作用域的变量的函数。在JavaScript中,闭包可以通过函数嵌套和变量引用实现。闭包是JavaScript中一个强大的特性,它允许函数访问其创建时的词法作用域。通过闭包,可以实现数据封装、事件处理、延迟执行等多种高级功能。理解闭包的概念和工作原理对于深入掌握JavaScript至关重要。版权声明:本博客内容为原创,转载请保留原文链接及作者信息。参考文章什么是闭包?以及闭包的作用—JavaScript基础篇 - 优快云博客。

ning的博客 1817

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

什么是闭包,JavaScript闭包详解

JavaScript闭包详解:闭包是函数与可访问的外部变量的组合,具有记忆词法作用域的特性。核心特征在于函数能访问定义时的作用域变量,即使在其他位置执行。常见用途包括封装私有变量、创建函数工厂和处理回调。但需注意内存泄漏风险及循环中的变量共享问题。React Hooks也依赖闭包机制保存状态。理解闭包有助于编写高效灵活的代码,但需正确处理变量引用和生命周期。

qq_44136882的博客 1331

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

【JavaScript】闭包详解

在 JavaScript 中,闭包是指函数能够记住并访问其所在词法作用域(Lexical Scope)中的变量,即使在该函数执行完毕并在其定义的作用域之外调用时,依然能够访问这些变量。闭包最直接的表现形式是内部函数可以访问外部函数的变量。闭包是 JavaScript 中强大且常用的特性,它能够帮助开发者实现数据封装、状态保持等高级功能。在理解闭包的基础上,合理地应用闭包能够提高代码的安全性和可维护性。但在实际开发中,开发者也需要注意闭包带来的潜在问题,如内存泄漏和调试困难等。

lph159的博客 3430

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

一文读懂js闭包

一文读懂js闭包什么是闭包闭包的两种主要形式函数作为返回值闭包作为参数传递闭包的利弊好的地方不好的地方推荐阅读 你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特 《人生果实》经典语录 什么是闭包 js的作用域分两种,全局和局部,基于作用域链相关知识 在js作用域环境中访问变量的权利是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量, 反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的

陈坚泓的博客 571

Javascript-js闭包详解

本片博客旨在从js的底层逻辑上解释什么是js闭包、js闭包的应用、以及如何解决js闭包的带来的问题

“ccc111”的博客 647

JavaScript闭包详解

函数是一等公民,而闭包是它们的灵魂伴侣。

chaosweet的博客 1378

详解JavaScript的闭包

什么是闭包: 有权访问另一个函数作用域的变量的函数 简单的说,Javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。 闭包是嵌套的内部函数 闭包存在于嵌套的内部函数中 闭包的主要作用: ...

wfly blog 975

一文详解js闭包,初学者也能学会的闭包

变量的作用域分为全局作用域和局部作用域,也就是全局变量和局部变量。JavaScript有一个特殊的地方是函数内部可以直接读取全局变量,但是在函数外部,无法读取到函数内部的变量。对函数内部变量进行一系列操作之后,需要在函数外部访问到,怎么才能实现呢?这就需要使用闭包了。当一个嵌套的内部子函数引用了嵌套的外部父函数的变量(这个变量也可以是函数)时,闭包就产生了。

czjl6886的博客 838

一文整明白js中闭包,闭包的原理

介绍了闭包的概念,闭包的原理,闭包的作用,以及一些个人理解

weixin_64104823的博客 899

javascript中的闭包closure详解

闭包closure是javascript中一个非常强大的功能。所谓闭包就是函数中的函数,内部函数可以访问外部函数的作用域范围,从而可以使用闭包来做一些比较强大的工作。 今天将会给大家详细介绍一下闭包。

程序那些事 6206

JS闭包 详解

JS闭包 文章目录JS闭包概念闭包如何工作循环和闭包模块基于函数的模块机制基于独立文件的模块机制/ES6模块 闭包是JavaScript中的一个重要概念,彻底理解闭包对于学习各种框架实现原理,工具方法优化原理,TS类等有着非常重要的影响。 概念 在《JavaScript高级程序设计》(第三版)第7.2节指出 闭包是指有权访问另一个 函数作用域中的变量的函数。 创建闭包的常见方式,就是在一个函数内部创建另一个函数 /* * @Description: * @Author: anqing.liang

安陵的博客 1174

详解javascript中的闭包

闭包:利用函数的嵌套,实现将外层函数中的局部变量,在外层函数外部修改的过程。本质上,闭包是将函数内部和函数外部连接起来的桥梁。 闭包的形成环境: 函数的嵌套 (例:有一个函数A , 在函数A内部返回一个函数B) 内部函数使用外部函数的变量 (在函数B中访问函数A的私有作用域变量) 将内部函数返回,在外部函数的外部声明变量接收返回值,并执行 (在函数A外部,有变量引用函数B) 以上三个条件,缺一...

anr_safely的博客 995
上一篇: JS 常见的 6 种继承方式
下一篇: apply&call&bind原理介绍
全栈大师兄
博客等级 码龄11年 448粉丝 175原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值