资讯专栏INFORMATION COLUMN

作用域闭包

Anleb / 2028人阅读

摘要:由于声明在函数内部,所以它拥有涵盖内部作用域的闭包,使得该作用域能够一直存活,以便在以后的任何时间进行引用。尽管本身并不是观察闭包的恰当例子,但他的确创建了一个封闭的作用域,并且也是最常用来创建被封闭起来的闭包的工具。

在讲解作用域闭包的内容之前,需要对以下概念有所掌握:

JavaScript具有两种作用域:全局作用域和函数作用域,至于块作用域也不能说没有,比如说: try ...catch...语句中,catch分句就是块作用域,还有with语句等。

ES6中的let关键字,可以用来在任意代码块中声明变量。

什么事立即执行函数表达式以及它的作用。

老生常谈什么是闭包

闭包的概念:函数可以记住并访问所在的词法作用域时,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

    function foo(){
        var a = 2;
        function bar(){
            console.log(a);
        }
        return bar;
    }
    var baz = foo();
    baz(); //这就是闭包的效果

函数bar()的词法作用域能够访问foo()的内部作用域,然后我们将bar()函数本身当作一个值进行传递。在foo()执行后,其返回值赋值给变量baz并调用baz()。
在foo()执行后,通常会期待foo()的整个内部作用于都被销毁,因为我们知道引擎有垃圾回收机制来释放不在使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
但是,闭包的神奇之处在于可以阻止这件事情发生。事实上内部作用域依然存在,因此,没有被回收。那么是谁在使用这个内部作用域呢?当然是bar()在使用。
由于bar()声明在foo()函数内部,所以它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以便bar()在以后的任何时间进行引用。
bar()函数在foo()调用完成后,依旧持有对其作用域的引用,而这个引用就叫做闭包

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处调用时都可以观察到闭包

    function foo(){
        var a = 2;
        function baz(){
            console.log(a)//2
        }
        bar(baz);
    }
    function bar(fn){
        fn(); //这就是闭包
    }

相比于上面代码的枯燥,这有一个更加常见的例子

    function wait(message){
        setTimeout(function time(){
            console.log(message);
        }, 1000);
    }
    wait("hello clousre");

简单分析一下这段代码:我们将一个名为time的内部函数传递给setTimeout(),time具有涵盖wait()作用域的闭包,因此,还保有对变量message的引用。
wait(..)执行1000ms后,它的内部作用域并不会消失,time()函数依旧保有对wait()作用域的闭包,在引擎内部,内置的工具函数setTimeout()会持有一个对参数的引用,这个参数也许叫作fn或者func之类的。引擎会调用这个函数,而词法作用域在这个过程中保持完整。
这就是闭包

那么闭包有哪些应用呢?其实包括定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers或者任何其他的异步(或者同步)任务中,只要使用回掉函数,实际上就是在使用闭包!

这里我们再看一个特别典型闭包的例子,但严格来说它并不是闭包

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

IIFE即立即执行函数表达式,第一个()让函数变为函数表达式,第二个()函数执行。为什么说他严格上来讲并不是闭包呢?因为在示例代码中函数并不是在它本身的词法作用域之外执行的它在其定义时所在的作用域执行,a是通过词法作用域查找到的,并不是闭包发现的。
尽管IIFE本身并不是观察闭包的恰当例子,但他的确创建了一个封闭的作用域,并且也是最常用来创建被封闭起来的闭包的工具。

循环和闭包

说到闭包我们接触最早的也许就是for循环的例子:

    for(var i = 1; i<6; i++){
        setTImeout(function time(){
            console.log(i)
        }, i*1000)
    }

记得第一次看见这段代码的时候,那是被深深的虐到,作为C语言起手的同学,当时真的是一脸的懵逼,为什么会输出5个6, 为什么会输出5个6,为什么?当时其他人的讲解也是模模糊糊的,虽然提出了解决方法,当还是无法理解这其中的机制原理,所以,我痛下决心把它弄懂!也许只有我不懂吧!

问:为什么会输出66666呢?
答:能输出66666说明for循环内部的代码的确执行了5次。
问:那6是从哪来的呢?
答:6是我们循环的终止条件,所以输出6。
问:那为什么不是循环一次,输出一个值, 1,2,3,4,5这样呢?
答:setTimeout()函数是在循环结束时执行的,就算是你设置setTimeout(fn, 0),它也是在for循环完成后立即执行,总之就是在for循环执行完成后才执行。

好了,这就不难理解了为什么会输出66666了。但这也就引出了一个更深入的话题,代码中到底什么缺陷导致它的行为同语义暗示的不一致呢?

缺陷是:我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。所以,实际的样子是这样。

而我们想象中的样子确是这样。

下面回到正题。既然明白了缺陷是什么,那么要怎样做才能达到我们想象中的样子呢?答案是我们需要在每一次迭代的过程中都创建一个闭包作用域。在上文中我们已经有所铺垫,IIFE会通过声明立即执行一个函数来创建作用域。so我们可以将代码改成下面的样子:

    for(var i=1; i<6; i++){
        (function(){
            setTImeout(function time(){
                console.log(i)
            }, i*1000)
        })();
    }

这样每一次迭代我们都创建了一个封闭的作用域(你可以想象为上图中黄色的矩形部分)。但是这样做仍旧不行,为什么呢?因为虽然每个延迟函数都会将IIFE在每次迭代中创建的作用域封闭起来,但我们封闭的作用域是空的,所以必须传点东西过去才能实现我们想要的结果。

    for(var i=1; i<6; i++){
        (function(){
            var j = i
            setTImeout(function time(){
                console.log(j)
            }, j*1000)
        })();
    }

ok!试试现在他能正常工作吗?对这段代码再进行一点改进

    for(var i=1; i<6; i++){
        (function(j){
            setTImeout(function time(){
                console.log(j)
            }, j*1000)
        })(i);
    }

总的来说,就是在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数可以将新的作用域封闭在每个迭代内部,我们同时在迭代的过程中将每次迭代的i值作为参数传入进新的作用域,这样在迭代中创建的封闭作用域就都会含有一个具有正确值的变量供我们访问。ok,it"s work!

块作用域

仔细思考我们前面的解决方案。我们使用IIFE在每次迭代时都创建一个新的作用域。也就是说,每次迭代我们都需要一个块作用域。前面我们提到,你需要对ES6中的let关键字进行了解,它可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
本质上来讲它是将一个块转换成可以被关闭的作用域。

    for(var i=1; i<6; i++){
            let j = i; //闭包的块作用域
            setTImeout(function time(){
                console.log(j)
            }, j*1000)
    }

如果将let声明在for循环的头部那么将会有一些特殊的行为,有多特殊呢?它会指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。不管这句话有多拗口,看看代码吧!

        for(let i=1; i<6; i++){
            setTImeout(function time(){
                console.log(i)
            }, i*1000)
    }

有没有似曾相识的感觉,有没有感动到,我已经老泪纵横了。。。

下一节讲闭包运用--模块机制

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79010.html

相关文章

  • 还担心面试官问闭包

    摘要:一言以蔽之,闭包,你就得掌握。当函数记住并访问所在的词法作用域,闭包就产生了。所以闭包才会得以实现。从技术上讲,这就是闭包。执行后,他的内部作用域并不会消失,函数依然保持有作用域的闭包。 网上总结闭包的文章已经烂大街了,不敢说笔者这篇文章多么多么xxx,只是个人理解总结。各位看官瞅瞅就好,大神还希望多多指正。此篇文章总结与《JavaScript忍者秘籍》 《你不知道的JavaScri...

    tinyq 评论0 收藏0
  • js闭包的本质

    摘要:也正因为这个闭包的特性,闭包函数可以让父函数的数据一直驻留在内存中保存,从而这也是后来模块化的基础。只有闭包函数,可以让它的父函数作用域永恒,像全局作用域,一直在内存中存在。的本质就是如此,每个模块文件就是一个大闭包。 为什么会有闭包 js之所以会有闭包,是因为js不同于其他规范的语言,js允许一个函数中再嵌套子函数,正是因为这种允许函数嵌套,导致js出现了所谓闭包。 function...

    qianfeng 评论0 收藏0
  • 作用闭包,你真的懂了吗?

    摘要:曾几何时,闭包好像就是一个十分难以捉摸透的东西,看了很多文章,对闭包都各有说法,以致让我十分晕,什么内部变量外部变量的,而且大多数都只描述一个过程,没有给闭包的定义,最后,举几个例子,告诉你这就是闭包。 曾几何时,闭包好像就是一个十分难以捉摸透的东西,看了很多文章,对闭包都各有说法,以致让我十分晕,什么内部变量、外部变量的,而且大多数都只描述一个过程,没有给闭包的定义,最后,举几个例子...

    yangrd 评论0 收藏0
  • 【JS脚丫系列】重温闭包

    摘要:内部的称为内部函数或闭包函数。过度使用闭包会导致性能下降。,闭包函数分为定义时,和运行时。循环会先运行完毕,此时,闭包函数并没有运行。闭包只能取得外部函数中的最后一个值。事件绑定种的匿名函数也是闭包函数。而对象中的闭包函数,指向。 闭包概念解释: 闭包(也叫词法闭包或者函数闭包)。 在一个函数parent内声明另一个函数child,形成了嵌套。函数child使用了函数parent的参数...

    MartinDai 评论0 收藏0
  • JavaScript基础系列---闭包及其应用

    摘要:所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。所以本文中将以维基百科中的定义为准即在计算机科学中,闭包,又称词法闭包或函数闭包,是引用了自由变量的函数。 闭包(closure)是JavaScript中一个神秘的概念,许多人都对它难以理解,我也一直处于似懂非懂的状态,前几天深入了解了一下执行环境以及作用域链,可戳查看详情,而闭包与作用域及作用域链的关系密不可分,所...

    leoperfect 评论0 收藏0

发表评论

0条评论

Anleb

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<