JavaScript闭包(内存泄漏、溢出以及内存回收),超直白解析

1 引言

变量作用域

首先我们先铺垫一个知识点——变量作用域:

变量根据作用域的不同分为两种:全局变量和局部变量。

  1. 函数内部可以使用全局变量。
  2. 函数外部不可以使用局部变量。
  3. 当函数执行完毕,本作用域内的局部变量会销毁。

如果我想在函数外部引用这个函数的局部变量呢?

2 闭包

闭包是什么?

闭包(closure)指有权访问另一个函数作用域中变量的函数。 ----- JavaScript 高级程序设计

闭包有什么用?

1延伸变量作用域范围,读取函数内部的变量
2让这些变量的值始终保持在内存中

简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量。

闭包案例一

我们来看一个简单的闭包案例。

        function fn1() {
            var num = 10;
            function fn2() {
                console.log(num);
            }
            fn2();
        }
        fn1(); //输出结果10

这样的一个函数写法我们已经见过或者用过很多次了,但其实这就是一个闭包的运用。
我们可以用Chrome的调试工具验证一下。

如图(看不清图片的伙伴们可以把图片放大)
在 Scope 选项(Scope 作用域的意思)中,有两个参数(global 全局作用域、local 局部作用域)。我在fn1函数调用的前面(21行)中设置了一个断点,进行单步调试,当执行到 fn2() 时,Scope 里面会多一个 Closure (闭包)参数 ,这就表明产生了闭包。被访问的变量是num,包含num的函数为fn1。

fn2的作用域当中访问到了fn1函数中的num这个局部变量 ,所以此时fn1 就是一个闭包函数(被访问的变量所在的函数就是一个闭包函数)

也有人说,闭包是一种现象,一个作用域访问了另外一个函数中的局部变量,如果有这种现象的产生,就有了闭包的发生。 我觉的这样理解也是没有什么问题的。

闭包案例二

接下来我们来看一个稍微复杂一点点的闭包

        function fn() {
            var num = 10;
            return function() {
                console.log(num);
            }
        }
        var f = fn();
        // 上面这步类似于
        // var f = function() {
        //         console.log(num);
        //     }
        f();//输出结果10

在f=fn()这步操作中,执行了num =10 的赋值,并且给f赋值了一个匿名函数,这个函数是fn中return 返回的那个匿名函数,注意此时只是赋值了,并没有调用。
然后在f()中调用了那个匿名函数,此时我们便做到了在 fn() 函数外面访问 fn() 中的局部变量 num 。

$闭包延伸了变量作用域范围,读取了函数内部的变量$

闭包案例三

首先我们看一下这个闭包案例

        var fn  =function(){
            var sum = 0
            return function(){
                sum++
                console.log(sum);
            }
        }
        fn()() //1
        fn()() //1
        //fn()进行sum变量申明并且返回一个匿名函数,第二个()意思是执行这个匿名函数

这里出现了一个小问题,sum为什么没有自增?如果想要实现自增怎么操作?
回答这个问题需要先了解一下js中内存回收机制。(详细内容可以看文章后面的3 Js内存回收机制

我这里直接简单解释一下,执行fn()() 后,fn()()已经执行完毕,没有其他资源在引用fn,此时内存回收机制会认为fn不需要了,就会在内存中释放它。

那如何不被回收呢?

        var fn  =function(){
            var sum = 0
            return function(){
                sum++
                console.log(sum);
            }
        }
        fn1=fn() 
        fn1()   //1
        fn1()   //2
        fn1()   //3

这种情况下,fn1一直在引用fn(),此时内存就不会被释放,就能实现值的累加。那么问题又来了,这样的函数如果太多,就会造成内存泄漏(内存泄漏、内存溢出的知识点在文章后面4 内存溢出、内存泄漏

内存泄漏了怎么办呢?我们可以手动释放一下

        var fn  =function(){
            var sum = 0
            return function(){
                sum++
                console.log(sum);
            }
        }
        fn1=fn() 
        fn1()   //1
        fn1()   //2
        fn1()   //3
        fn1 = null // fn1的引用fn被手动释放了
        fn1=fn()  //num再次归零
        fn1() //1

3 Js内存回收机制

由于字符串、对象和数组没有固定大小,当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。

1标记清除

这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。

2引用计数

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

4 内存溢出、内存泄漏

内存溢出

内存溢出一般是指执行程序时,程序会向系统申请一定大小的内存,当系统现在的实际内存少于需要的内存时,就会造成内存溢出

内存溢出造成的结果是先前保存的数据会被覆盖或者后来的数据会没地方存

内存泄漏

内存泄漏是指程序执行时,一些变量没有及时释放,一直占用着内存
而这种占用内存的行为就叫做内存泄漏。

作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积。

内存泄漏如果一直堆积,最终会导致内存溢出问题

5 总结

  1. 闭包是什么?

闭包是一个函数 (一个作用域可以访问另外一个函数的局部变量)

  1. 闭包的作用是什么?

1延伸变量作用域范围,读取函数内部的变量
2让这些变量的值始终保持在内存中

原文地址:https://www.cnblogs.com/mingo233/p/13582147.html