进击的 JavaScript(四) 之 闭包

本文详细讲解了闭包的概念、本质及实现方式。首先介绍了函数的引用类型特点,然后解释了参数传递机制,接着讨论了作用域和垃圾收集机制。通过具体示例说明了闭包如何在函数外部访问内部变量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一节说了执行上下文,这节咱们就乘胜追击来搞搞闭包!头疼的东西让你不再头疼!

一、函数也是引用类型的。
function f(){ console.log("not change") };

var ff = f;
 
function f(){ console.log("changed") };

ff();

//"changed"
//ff 保存着函数 f 的引用,改变f 的值, ff也变了

//来个对比,估计你就明白了。
var f = "not change";

var ff = f;

f = "changed";

console.log(ff);

//"not change"
//ff 保存着跟 f 一样的值,改变f 的值, ff 不会变

其实,就是引用类型 和 基本类型的 区别。


二、函数创建一个参数,就相当于在其内部声明了该变量
function f(arg){
    console.log(arg)
}

f();

//undefined

function f(arg){
    arg = 5;
    console.log(arg);
}
f();

//5

三、参数传递,就相当于变量复制(值的传递)

基本类型时,变量保存的是数据,引用类型时,变量保存的是内存地址。参数传递,就是把变量保存的值 复制给 参数。

var o = { a: 5 };

function f(arg){
    arg.a = 6;
}

f(o);

console.log(o.a);
//6

四、垃圾收集机制

JavaScript 具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。函数中,正常的局部变量和函数声明只在函数执行的过程中存在,当函数执行结束后,就会释放它们所占的内存(销毁变量和函数)。

而js 中 主要有两种收集方式:

  1. 标记清除(常见) //给变量标记为“进入环境” 和 “离开环境”,回收标记为“离开环境”的变量。
  2. 引用计数 // 一个引用类型值,被赋值给一个变量,引用次数加1,通过变量取得引用类型值,则减1,回收为次数为0 的引用类型值。

知道个大概情况就可以了,《JavaScript高级程序设计 第三版》 4.3节 有详解,有兴趣,可以看下。.


五、作用域

之前说过,JavaScript中的作用域无非就是两种:全局作用域局部作用域
根据作用域链的特性,我们知道,作用域链是单向的。也就是说,在函数内部,可以直接访问函数外部和全局变量,函数。但是,反过来,函数外部和全局,是访问不了函数内的变量,函数的。

function testA(){
    var a = 666;
}
console.log(a);

//报错,a is not defined

var b = 566;
function testB(){
    console.log(b);
}

//566

但是,有时候,我们需要在函数外部 访问函数内部的变量,函数。一般情况下,我们是办不到的,这时,我们就需要闭包来实现了。


六、开始闭包!
function fa(){
    var va = "this is fa";

    function fb(){
        console.log(va);
    }
    
    return fb;
}

var fc = fa();
fc();

//"this is fa"

想要读取fa 函数内的变量 va,我们在内部定义了一个函数 fb,但是不执行它,把它返回给外部,用 变量fc接受。此时,在外部再执行fc,就读取了fa 函数内的变量 va


七、闭包的概念

其实,简单点说,就是在 A 函数内部,存在 B 函数, B函数 在 A 函数 执行完毕后再执行。B执行时,访问了已经执行完毕的 A函数内部的变量和函数。

由此可知:闭包是函数A的执行环境 以及 执行环境中的函数 B组合而构成的。

上篇文章中说过,变量等 都储存在 其所在执行环境的活动对象中,所以说是 函数A 的执行环境。

当 函数A执行完毕后,函数B再执行,B的作用域中就保留着 函数A 的活动对象,因此B中可以访问 A中的 变量,函数,arguments对象。此时产生了闭包。大部分书中,都把 函数B 称为闭包,而在谷歌浏览器中,把 A函数称为闭包。


八、闭包的本质

之前说过,当函数执行完毕后,局部活动对象就会被销毁。其中保存的变量,函数都会被销毁。内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况就不同了。

以上面的例子来说,函数fb 和其所在的环境 函数fa,就组成了闭包。函数fa执行完毕后,按道理说, 函数fa 执行环境中的 活动对象就应该被销毁了。但是,因为 函数fa 执行时,其中的 函数fb 被 返回,被 变量fc 引用着。导致,函数fa 的活动对象没有被销毁。而在其后 fc() 执行,就是 函数fb 执行时,构建的作用域中保存着 函数fa 的活动对象,因此,函数fb 中 可以通过作用域链访问 函数fa 中的变量。

我已经尽力地说明白了。就看各位的了。哈哈!其实,简单的说:就是fa函数执行完毕了,其内部的 fb函数没有执行,并返回fb的引用,当fb再次执行时,fb的作用域中保留着 fa函数的活动对象。

再来个有趣经典的例子:

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

//每隔一秒输出一个6,共5个。

是不是跟你想的不一样?其实,这个例子重点就在setTimeout函数上,这个函数的第一个参数接受一个函数作为回调函数,这个回调函数并不会立即执行,它会在当前代码执行完,并在给定的时间后执行。这样就导致了上面情况的发生。

可以下面对这个例子进行变形,可以有助于你的理解把:

var i = 1;
while(i <= 5){
    setTimeout(function(){
        console.log(i);
    },i*1000)
     
    i = i+1;
}

正因为,setTimeout里的第一个函数不会立即执行,当这段代码执行完之后,i 已经 被赋值为6了(等于5时,进入循环,最后又加了1),所以 这时再执行setTimeout 的回调函数,读取 i 的值,回调函数作用域内没有i,向上读取,上面作用域内i的值就是6了。但是 i * 1000,是立即执行的,所以,每次读的 i 值 都是对的。

这时候,就需要利用闭包来保存每个循环时, i 不同的值。

function makeClosures(i){     //这里就和 内部的匿名函数构成闭包了
    var i = i;    //这步是不需要的,为了让看客们看的轻松点
    return function(){
        console.log(i);     //匿名没有执行,它可以访问i 的值,保存着这个i 的值。
    }
}

for (var i=1; i<=5; i++) {
    setTimeout(makeClosures(i),i*1000);  
    
    //这里简单说下,这里makeClosures(i), 是函数执行,并不是传参,不是一个概念
    //每次循环时,都执行了makeClosures函数,都返回了一个没有被执行的匿名函数
    //(这里就是返回了5个匿名函数),每个匿名函数都是一个局部作用域,保存着每次传进来的i值
    //因此,每个匿名函数执行时,读取`i`值,都是自己作用域内保存的值,是不一样的。所以,就得到了想要的结果
}

//1
//2
//3
//4
//5

闭包的关键就在,外部的函数执行完毕后,内部的函数再执行,并访问了外部函数内的变量。

你可能在别处,或者自己想到了下面这种解法:

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

如果你一直把这个当做闭包,那你可能看到的是不同的闭包定义吧(犀牛书和高程对闭包的定义不同)。严格来说,这不是闭包,这是利用了立即执行函数函数作用域 来解决的。

做下变形,你再看看:

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

这样看就很明显了吧,主要是利用了函数作用域,而使用立即执行函数,是为了简化步骤。

总结:判断是不是闭包,我总结了要满足以下三点:

  1. 两个函数。有内函数 和 外函数。
  2. 外函数执行完毕后,内函数 还没有执行。
  3. 当内函数执行时(通过外部引用或者返回内函数),访问了 外函数内部的 变量,函数等(说是访问,其实内函数保存着外函数的活动对象,因此,arguments对象也可以访问到)。


附录:

其实这道题,知道ES6let 关键词,估计也想到了另一个解法:

for (let i=1; i<=5; i++) {   //这里的关键就是使用的let 关键词,来形成块级作用域
    setTimeout(function(){
        console.log(i);
    },i*1000);
}

我不知道,大家有没有疑惑啊,为啥使用了块级作用域就可以了呢。反正我当初就纠结了半天。

11月 2日修正:

这个答案的关键就在于 块级作用域的规则了。它让let声明的变量只在{}内有效,外部是访问不了的。

做下变形,这个是为了方便理解的,事实并非如此:

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

当for 的() 内使用 let时,for 循环就存在两个作用域,() 括号里的父作用域,和 {} 中括号里的 子作用域。

每次循环都会创建一个 子作用域。保存着父作用域传来的值,这样,每个子作用域内的值都是不同的。当setTimeout 的匿名函数执行时,自己的作用域没有i 的值,向上读取到了该 子作用域i 值。因此每次的值才会不一样。

上面用立即执行函数模拟块级作用域,就是这个道理啦!

内容概要:本文详细介绍了扫描单分子定位显微镜(scanSMLM)技术及其在三维超分辨体积成像中的应用。scanSMLM通过电调透镜(ETL)实现快速轴向扫描,结合4f检测系统将不同焦平面的荧光信号聚焦到固定成像面,从而实现快速、大视场的三维超分辨成像。文章不仅涵盖了系统硬件的设计与实现,还提供了详细的软件代码实现,包括ETL控制、3D样本模拟、体积扫描、单分子定位、3D重建和分子聚分析等功能。此外,文章还比较了循环扫描与常规扫描模式,展示了前者在光漂白效应上的优势,并通过荧光珠校准、肌动蛋白丝、线粒体网络和流感A病毒血凝素(HA)蛋白聚的三维成像实验,验证了系统的性能和应用潜力。最后,文章深入探讨了HA蛋白聚与病毒感染的关系,模拟了24小时内HA聚的动态变化,提供了从分子到细胞尺度的多尺度分析能力。 适合人群:具备生物学、物理学或工程学背景,对超分辨显微成像技术感兴趣的科研人员,尤其是从事细胞生物学、病毒学或光学成像研究的科学家和技术人员。 使用场景及目标:①理解和掌握scanSMLM技术的工作原理及其在三维超分辨成像中的应用;②学习如何通过Python代码实现完整的scanSMLM系统,包括硬件控制、图像采集、3D重建和数据分析;③应用于单分子水平研究细胞内结构和动态过程,如病毒入侵机制、蛋白质聚等。 其他说明:本文提供的代码不仅实现了scanSMLM系统的完整工作流程,还涵盖了多种超分辨成像技术的模拟和比较,如STED、GSDIM等。此外,文章还强调了系统在硬件改动小、成像速度快等方面的优势,为研究人员提供了从理论到实践的全面指导。
内容概要:本文详细介绍了基于Seggiani提出的渣层计算模型,针对Prenflo气流床气化炉中炉渣的积累和流动进行了模拟。模型不仅集成了三维代码以提供气化炉内部的温度和浓度分布,还探讨了操作条件变化对炉渣行为的影响。文章通过Python代码实现了模型的核心功能,包括炉渣粘度模型、流动速率计算、厚度更新、与三维模型的集成以及可视化展示。此外,还扩展了模型以考虑炉渣组成对特性的影响,并引入了Bingham流体模型,更精确地描述了含未溶解颗粒的熔渣流动。最后,通过实例展示了氧气-蒸汽流量增加2%时的动态响应,分析了温度、流动特性和渣层分布的变化。 适合人群:从事煤气化技术研究的专业人士、化工过程模拟工程师、以及对工业气化炉操作优化感兴趣的科研人员。 使用场景及目标:①评估不同操作条件下气化炉内炉渣的行为变化;②预测并优化气化炉的操作参数(如温度、氧煤比等),以防止炉渣堵塞;③为工业气化炉的设计和操作提供理论支持和技术指导。 其他说明:该模型的实现基于理论公式和经验数据,为确保模型准确性,实际应用中需要根据具体气化炉的数据进行参数校准。模型还考虑了多个物理场的耦合,包括质量、动量和能量守恒方程,能够模拟不同操作条件下的渣层演变。此外,提供了稳态求解器和动态模拟工具,可用于扰动测试和工业应用案例分析。
闭包JavaScript一个重要且强大的概念。它指的是函数能够访问自己定义范围之外的变量,并且这些变量在函数执行完毕后仍然可以被访问到。闭包的应用场景很广泛,它可以用来创建私有变量、实现模块化、延迟执行等等。了解和掌握闭包对于理解JavaScript语言的本质,以及提高代码的可维护性和灵活性都非常有帮助。 从技术的角度来看,所有的JavaScript函数都可以被看作是闭包。因为在函数内部,它们可以访问在函数定义范围之外的变量。这也是为什么在JavaScript中,函数可以在定义之后仍然可以访问外部的变量。 总结起来,闭包JavaScript一个非常重要的概念,它能够让函数访问自己定义范围之外的变量,并且这些变量在函数执行完毕后仍然可以被访问到。了解闭包可以帮助我们更好地理解JavaScript语言的本质,并且提高代码的可维护性和灵活性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [深入理解JavaScript闭包](https://blog.youkuaiyun.com/qq_54384868/article/details/130182961)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [JavaScript深入之闭包](https://blog.youkuaiyun.com/TZOF_/article/details/117048674)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值