一、闭包定义

函数执行后返回的结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,则形成了闭包。

闭包举例:

function add(){
    var a=0;
    return function add1(){
        a++;
        console.log(a);
    }
}
const res = add();
//调用3次函数
res();
res();
res();
//1 2 3

二、闭包的特点

  1. 函数嵌套函数
  2. 内部函数可以访问外部函数的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

三、闭包原理

函数执行会分成两个阶段(预编译阶段和执行阶段)。

  • 预编译阶段:如果发现内部函数使用了外部函数的变量,就会在内存中创建一个“闭包”对象并保存对应的变量值,如果已经创建了闭包则只需要增加对应的属性值即可。
  • 执行阶段:执行完成后,函数的执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但是其内部函数还持有该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量。

闭包引用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕后,其执行作用域链被销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被销毁后活动对象才会被销毁。

四、闭包的应用场景。

  1. 模块封装,在各模块规范出现之前,都是用这样的方式防止变量污染全局。
    举例:
var A = (function () {
    // 这样声明为模块私有变量,外界无法直接访问到,避免了变量污染全局
    var x = 0;
    function A() { };
    A.prototype.a = function a(){
        return x;
    }
    return A;
}())
console.log(A)//[Function: A]
  1. 在循环中创建闭包,防止取到意外的值。
    举例:数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的input 的 ID。通过循环这三项定义,依次为相应input添加了一个 onfocus 事件处理函数,以便显示帮助信息。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <p id="help">Helpful notes will appear here</p>
    <p>E-mail: <input type="text" id="email" name="email"></p>
    <p>Name: <input type="text" id="name" name="name"></p>
    <p>Age: <input type="text" id="age" name="age"></p>
    <script>
     function showHelp(help) {
            document.getElementById('help').innerHTML = help;
            
        }
	 function setUpHelp() {
            var helpText = [
                { 'id': 'email', 'help': 'Your e-mail address' },
                { 'id': 'name', 'help': 'Your full name' },
                { 'id': 'age', 'help': 'Your age (you must be over 16)' }
            ]

            for (let i = 0; i < helpText.length; i++) {
                let item = helpText[i];
                // onfocus事件,对象聚焦时发生
                document.getElementById(item.id).onfocus = function(){
                    showHelp(item.help)
                }
            }
        }
        setUpHelp();     
    </script>
</body>

</html>

运行这段代码后,会发现并没有达到想要的效果。无论焦点在那个input上,显示的都是关于年龄的提示信息。在这里插入图片描述
造成这样的结果的原因是:赋值给onfocus的是闭包。这些闭包是由他们的函数定义和在setUpHelp作用域中捕获的环境所组成的。这三个闭包在循环过程中被创建,但是他们共享了同一个词法作用域,这个作用域存在一个变量item。因为变量item是var声明的,由于变量提升,所以具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发前就早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。

解决以上问题的方法之一就是使用更多的闭包:

// 第一种解决方案 使用更多的闭包
        function showHelp(help) {
            document.getElementById('help').innerHTML = help;
        }
        // 使用多一个闭包
        function makeHelpCallback(help){
            return function(){
                showHelp(help)
            }
        }

        function setUpHelp() {
            var helpText = [
                { 'id': 'email', 'help': 'Your e-mail address' },
                { 'id': 'name', 'help': 'Your full name' },
                { 'id': 'age', 'help': 'Your age (you must be over 16)' }
            ]

            for (let i = 0; i < helpText.length; i++) {
                var item = helpText[i];
                // onfocus事件,对象聚焦时发生
                document.getElementById(item.id).onfocus = makeHelpCallback(item.help)
            }
        }
        setUpHelp();

这一段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境,makeHelpCallback函数为每一个回调创建了一个新的词法环境。在这些环境中,help指向helpText数组中对应的字符串。

第二种解决办法:使用匿名闭包

 //第二种解决方法:匿名闭包
        function showHelp(help) {
            document.getElementById('help').innerHTML = help;
        }

        function setUpHelp() {
            var helpText = [
                { 'id': 'email', 'help': 'Your e-mail address' },
                { 'id': 'name', 'help': 'Your full name' },
                { 'id': 'age', 'help': 'Your age (you must be over 16)' }
            ]

            for (let i = 0; i < helpText.length; i++) {
               (function(){
                var item = helpText[i];
                // onfocus事件,对象聚焦时发生
                document.getElementById(item.id).onfocus = function(){
                    showHelp(item.help)
                }
               })();// 马上把当前循环项的item与事件的回调相关联
            }
        }
        setUpHelp();

第三种解决办法:不用闭包,直接使用let关键词声明helpText,不会出现变量提升,且为每个闭包都绑定了相应的块级作用域。


         //第三种解决方法:var改成let,没有了变量提升
         function showHelp(help) {
            document.getElementById('help').innerHTML = help;
        }

        function setUpHelp() {
            var helpText = [
                { 'id': 'email', 'help': 'Your e-mail address' },
                { 'id': 'name', 'help': 'Your full name' },
                { 'id': 'age', 'help': 'Your age (you must be over 16)' }
            ]

            for (let i = 0; i < helpText.length; i++) {
                let item = helpText[i];
                // onfocus事件,对象聚焦时发生
                document.getElementById(item.id).onfocus = function(){
                    showHelp(item.help)
                }
            }
        }
        setUpHelp();

五、闭包的优点和缺点

优点:

  • 内部函数可以访问外部函数作用域中的变量,且访问到的变量长期驻扎在内存中,可供以后使用。
  • 创建变量的私有空间,防止变量污染全局

缺点:

  • 对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收机制回收,增大了内存的使用量,所以使用不当会导致内存泄漏。
  • 对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度。
  • 可能获取到意外的值。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐