JavaScript中的闭包究竟是什么

1,038 阅读8分钟

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

前言

在上一篇文章 《 深入谈谈JavaScript的作用域及作用域链》中,我们详细的了解了JavaScript中的作用域和作用域链。有了作用域的知识作基础,再来看闭包相关的知识就很简单了,简直是易如反掌,手到擒来,那么我们今天就来谈谈JavaScript中的闭包。如果看完学会了请点个赞,最好关注一下(嘿嘿嘿...),然后评论“学会了”,如果学到了请评论“学到了”,千万不要出现“学会了,但没有完全学会”、“学到了,但没有完全学到”的情况~

好了,让我们一起开始愉快的学习吧!


闭包是个什么东东

闭包是 函数 和声明该函数的 词法环境 的组合。新手看了这句话可能会很懵,下面我来解释一下这句话:

函数:内部函数 声明该函数:外部函数 词法环境: 作用域(作用域链,详情见上篇文章) 组合:以上形成的综合体 理解了上面这句话的含义之后,我们就能知道 闭包的形成条件

两个为嵌套关系的函数,内部函数访问了外部函数的声明的变量。

由此我们可以看出,闭包的基本模型如下:

    function outer(){
        var num = 100

        function inner(){
            console.log(num)
        }

        return inner
    }
    var ret = outer()
    ret()

闭包的作用

一,私有变量,保护数据的安全

很多时候,我们并不希望数据会被外部更改,我们希望数据的更改被限制在某个范围内,让数据的更改变的安全可维护。

下面我们来看一个利用闭包制作的计数器函数,在这个计数器函数中,我们要将count变量给保护起来,除了内部返回的fn函数可以修改count的值,其他地方都不可以可以修改count的值。如下代码:

    // 功能:统计fn函数调用的次数
    function outer(){
        var count = 0

        function fn(){
            count++
            console.log("函数fn 被调用了" + count + "次")
        }

        return fn
    }

    var ret = outer()
    ret()

二,持久化数据

在上面的计数器函数中,我们会想到一个问题:为什么调用ret的时候,count的值是在++,而不是每次都从0开始?

这是因为函数在调用的时候,会在内存里面去开辟一块空间来执行函数内部的代码,当函数调用结束的时候,函数开辟的空间会被销毁掉。而在上面计数器函数中,count的值存在于outer函数中,outer函数作为一个整体不会被销毁,count的值自然也不会被销毁掉。

我们可以利用闭包这一特性做些什么呢?比如计算斐波那契数列的时候。

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

现在我们要生成一个斐波那契数列,会有大量重复的计算,如果我们要获取斐波那契第1000个数值,用时多久呢?如下代码:

    const fb = function() {
        function fn(n) {
            if (n === 1 || n === 2) {
                return 1
            }
            var num = fn(n - 1) + fn(n - 2)
            return num
        }
        return fn
    }()
    console.log(fb(100))

你会发现你的浏览器卡死了,因为大量重复的计算占了太多的内存空间。但如果使用闭包持久化数据(缓存)呢?如下代码:

   const fb = function() {
        var cache = [0,1] // 建立缓存区
        function fn(n) {
            var num = cache[n]
            if(typeof num !== 'number'){ // 如果缓存区没有当前位数对应的斐波那契值
                cache[n] = fn(n-1) + fn(n-2) // 当前位数对应的斐波那契值加入缓存
                num = cache[n]
            }
            return num // 如果缓存区有当前位数对应的斐波那契值,直接返回
        }
        return fn
    }()
    console.log(fb(1000))

我们可以看出,即使计算的数值是1000位的数字,也是秒出结果。在斐波那契数列计算越复杂的情况,使用闭包的持久化数据越快。

闭包内存泄露

什么是内存泄露呢?通俗的讲,内存泄露是指声明的变量,没有被使用,在js执行结束之后,没有被销毁。也就是说,一块内存空间被占用了,一直得不到回收,别的对象不能再使用这块内存空间。

很多教程资料都说使用闭包一定会内存泄露,其实这种说法是不严谨的。

我们要明确一点:JavaScript中内存释放的机制不需要人为操控,是js引擎自动释放的。

说起来内存泄露,其实在 IE9 之前才会有闭包变量引起内存泄漏的问题。这是因为 IE9 之前采用的垃圾回收算法不是现在使用的标记-清除算法,而是引用计数算法。引用计数算法在处理 COM 对象(组件对象模型)会有循环引用的问题,而循环引用才是导致内存泄漏的元凶。在早期的 V8 中,由于闭包引用的变量被挂载了全局的大对象 windows 中,所以这一变量由老生代区采用标记-清除算法进行回收。频繁的垃圾回收会生成大量的内存碎片,所以也会导致内存泄漏问题。为了解决这一问题,以及频繁的垃圾回收导致的全停顿问题(垃圾回收在主线程执行),后来 v8 又采用了标记-清除整理算法,以及增量回收、并行回收、并发回收等垃圾回收技术。

一,引用计数算法

js会自动去分配内存,存储对象,定期检查对象被引用次数,以引用次数来判断是否回收。如下代码:

    // 把创建对象的地址赋值给变量obj,这个对象被引用了,这个对象引用计数为1(此时这个对象不会被垃圾回收掉)
    var obj = {
        name: "大冰块"
    }

    // 把obj的地址赋值给变量one,one也指向了对象,这个对象被引用了2次(不会被回收)
    var one = obj

    // 此时修改obj = 1,obj不指向对象了,但是这个对象被引用了1次不会被回收)
    obj = 1

    // 此时修改one = null,让one也不指向对象了,对象被引用了0次,0次引用的对象,被标记为垃圾内存,被垃圾回收机制给回收掉。
    one = null

引用计数存在的缺陷:循环引用的错误。如下代码:

    function fn(){
        var obj1 = {} // {}这个对象被引用了1次
        var obj2 = {} // {}这个对象被引用了2次

        obj1.a = obj2 // {}这个对象被引用了3次
        obj2.b = obj1 // {}这个对象被引用了4次
    }
    // 调用fn结束,理应要销毁obj1、obj2这两个对象,但是由于这两个对象都不是0次引用对象,不能够被回收。
    fn()
    fn()
    fn()
    fn()
    fn()
    // 如果此时多次调用这个fn函数,就会造成内存泄漏的问题。

二,标记-清除算法

直接从window上开始找,从window这个根对象往下查找,如果找不到引用了这个对象的变量,该对象就作为垃圾内存回收掉。所以标记-清除算法解决了循环引用的问题。

    var obj = {
        name: "大冰块"
    }

    obj = null // 没有再引用 `{name: "大冰块"}` 这个对象,该对象被回收掉。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法,所以在新一代浏览器中,使用闭包几乎不会出现内存泄漏问题。

当然,关于遇到网页占用内存过大的问题,如果是因为闭包内存泄露,我们还有终极大招,那就是手动释放闭包占用的内存:

    function outer(){
        ...
    }
    var ret = outer()
    ret = null

后记

相信通过本文的学习,你一定对闭包有了更深的理解与认识,再也不怕在面试中遇到了它了。最后我再举个例子,就好像这篇文章你看完了,点了个赞,加入了收藏夹,关注了作者,这些都是在你的闭包领域做的,别人并不能去把你的赞取消,收藏取消,关注也取消。懂了么?

PS: 今天是我参加掘金更文挑战的第10天,没有存稿的我,日日努力淦文章,已经坚持10天啦!有志者,事竟成。大家一起加油哦~

更文挑战的文章目录如下: