【JavaScript】闭包函数总结


一、什么是闭包?

闭包出现的原因:js允许函数嵌套。由于在Javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成定义在一个函数内部的函数。

正常函数嵌套:

function f(){
	var i = 0;
    function s(){
   		console.log(i++); //0
	}
    s();
}
f();

js中,正常的函数嵌套,其父函数f()调用时,父函数会在内存中产生;执行期间,子函数s()被调用,并在内存产生,执行完毕后,子函数会自动注销回收,随后父函数执行完毕也注销回收。

闭包的产生
父函数f()调用时,体内创建的子函数s()并未被立即调用,而是返回函数指针,这便导致子函数不会被系统回收注销,由于js存在链式机制的存在,子函数在执行时又会访问的父函数数据,产生函数作用域链,进而导致父函数在链式机制的作用下无法进行内存释放。这种在链式机制作用下,导致父函数为了维持函数作用域链而无法注销的子函数,即为“闭包函数”。

function f(){
    let n=0;
    function s(){
   		n++;
	}
    return s();
}
f();

二、闭包函数的用途

  1. 读取函数内部的变量,让这些变量的值始终保持在内存中;

  2. 方便调用上下文的局部变量,利于代码封装。

    注意:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题

三、闭包的几种形式

  1. 函数返回值:返回函数function(y),外界保持对makeAdder()作用域引用。

    function makeAdder(x) {
      return function(y) {
        return x + y;
      };
    }
    
    var add5 = makeAdder(5);
    var add10 = makeAdder(10);
    
    // 调用闭包函数
    console.log(add5(2));  // 7
    console.log(add10(2)); // 12
    

    在这个示例中, makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。add5add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。

  2. 函数赋值:

    1)将F作用域里的函数N赋值给全局作用域的inner,所以F执行之后,inner保持了对F作用域里的引用

    var inner;
    var F = function(){
        var b = 'local';
        var N = function(){
            return b;
        };
        inner = N;
    };
    F();
    console.log(inner()); // local
    

    2)IIFE中给getter()和setter()函数提供将要操作的变量,并使其保存在闭包函数内部,防止其暴露在外部,类似函数模块化:

    var getValue,setValue;
    (function(){
        var secret = 0;
        getValue = function(){
            return secret;
        }
        setValue = function(v){
            if(typeof v === 'number'){
                secret = v;
            }
        }
    })();
    console.log(getValue());//0
    setValue(1);
    console.log(getValue());//1
    
  3. 函数参数:将F里的N作为参数给外界的Inner,F执行之后,外界Inner保持了对F里的引用

    var Inner = function(fn){
        console.log(fn());  //local
    }
    var F = function(){
        var b = 'local';
        var N = function(){
            return b;
        }
        Inner(N);
    }
    F();
    
  4. IIFE:由前面的示例代码可知,函数F()都是在声明后立即被调用,因此可以使用IIFE来替代。但是,要注意的是,这里的Inner()只能使用函数声明语句的形式,而不能使用函数表达式

    function Inner(fn){
        console.log(fn()); //local
    }
    
    (function(){
        var b = 'local';
        var N = function(){
            return b;
        }
        Inner(N);
    })()
    
  5. 循环赋值:通过循环产生多个闭包函数

    function foo(){
        var arr = [];
        for(var i = 0; i < 2; i++){
            arr[i] = (function fn(j){
                return function test(){
                    return j;
                }
            })(i);
        }
        return arr;
    }
    var bar = foo();
    console.log(bar[0]()); //0  
    
  6. 迭代器

    // 累加器
     var add = (function(){
        var counter = 0;
        return function(){
            return ++counter; 
        }
     })();
     console.log(add()); //1
     console.log(add()); //2 
     console.log(add()); //3
        
    // 迭代器
    function setup(x){
        var i = 0;
        return function(){
            return x[i++];
        }
    }
    var next = setup(['a','b','c','d']);
    console.log(next()); // 'a'
    console.log(next()); // 'b'
    console.log(next()); // 'c'
    console.log(next()); // 'd'
    
  7. 缓存机制:

    通过闭包加入缓存机制,使得相同的参数不用重复计算,来提高函数的性能。

    var mult=(function(){
        var cache={}; 
        var calculate=function(){
            var a=1;
            for ( var i = 0, l = arguments.length; i < l; i++ ){
                a = a * arguments[i];
            }
            return a;
        }
        return function(){
            var args=Array.prototype.join.call(arguments,',');
            if(cache[args]){
                return cache[args];
            }else{
                var ss=calculate.apply(null,arguments);
                return cache[args]=ss;
            }
        }
    })();
    
    console.log( mult( 1,2,3 ) ); //结果6
    console.log( mult( 1,3,3 ) );//结果9
    

四、闭包应用

  1. 闭包-函数回调

JavaScript代码都是基于事件的定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title></title>
</head>
<style>
    body{
        font-size: 10px;
    }
    h1{
        font-size: 1.8rem;
    }
    h2{
        font-size: 1.5rem;
    }
</style>
<body>
    <p>body字体</p>
    <h1>一级标题</h1>
    <h2>二级标题</h2>

    <a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a>

    <script>
        function changeSize(size){
            return function(){
                document.body.style.fontSize = size + 'px';
            };
        }

        var size12 = changeSize(12);
        var size14 = changeSize(14);
        var size16 = changeSize(16);

        document.getElementById('size-12').onclick = size12;
        document.getElementById('size-14').onclick = size14;
        document.getElementById('size-16').onclick = size16;

    </script>
</body>
</html>

文本尺寸调整按钮可以修改 body 元素的 font-size 属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。

  1. 闭包-模块化/封装

JavaScript 不支持私有方法声明,但可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
};

// 实例化counter1 和 counter2
var Counter1 = makeCounter();
var Counter2 = makeCounter();

console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */

// counter1的操作对counter2的运行不产生影响
console.log(Counter2.value()); /* logs 0 */

这次我们只创建的函数,为三个函数:Counter.incrementCounter.decrementCounter.value提供相同的作用域。函数体内包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

把这个函数储存在另外一个变量makeCounter中,并用变量makeCounter创建Counter1Counter2,访问 privateCounter 变量和 changeBy 函数。请注意两个计数器 Counter1Counter2 是如何维护它们各自的独立性的,每个闭包都是引用自己词法作用域内的变量 privateCounter

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量,即实现了JavaScript函数的模块化处理,适合于进行数据隐藏和封装。

五、闭包踩坑

循环生成闭包函数时容易出现的错误,请看下一段代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title></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 (var i = 0; i < helpText.length; i++) {
                var item = helpText[i];
                document.getElementById(item.id).onfocus = function() {
                    showHelp(item.help);
                }
            }
        }
        setupHelp();
    </script>
</body>
</html>

我们想要实现的效果时点击相应对话框时,出现对应的提醒,数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的input 的 ID。通过循环这三项定义,依次为相应input添加了一个 onfocus 事件处理函数,以便显示帮助信息,如图:
在这里插入图片描述
但真正实现的均无论焦点在哪个input上,显示的都是关于年龄的信息:
在这里插入图片描述

分析

原因是赋值给 onfocus 的是闭包函数。

通过for循环创建的三个闭包,共享一个作用域。在这个作用域中存在一个变量item声明使用的是var item,由于变量提升,为setuphelp()内变量,所以具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项,而这是由Js机制决定的:Js是单线程的,一个时间点只能做一件事,优先处理同步任务,之后再执行异步任务;按照代码从上往下执行,遇到异步(事件、定时器等)时,就挂起并放到异步任务里,继续执行同步任务,只有同步任务执行完了,才去看看有没有异步任务,然后再按照顺序执行!

这里for循环是同步任务,onfocus是异步任务,所以等for循环执行完了,此时i=3,item={ 'id': 'age', 'help': 'Your age (you must be over 16)'},所以每次点击输入框时,均提示年龄。

解决办法

  • 使用更多闭包:

    这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,help 指向 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 (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp(); 
  • 使用匿名闭包,使得item作用域限制在for循环内部的匿名闭包函数域内,item仅被本次for循环产生的匿名闭包享有。
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 (var i = 0; i < helpText.length; i++) {
    	// 使用匿名闭包函数(函数每次循环时立即执行)
        // 当前循环项的item与事件回调相关联起来,而不是循环结束的值
        (function() {
           var item = helpText[i];
           document.getElementById(item.id).onfocus = function() {
             showHelp(item.help);
           }
        })();
  }
}

setupHelp();
  • let变量声明

使用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 (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

六、闭包与性能

由于闭包会使一些数据无法被及时销毁,要尽量减少闭包的使用,在程序开发中,选择主动把一些变量封闭在闭包中,主要考虑的是在不污染全局环境的前提下,保存后期需要使用的变量。从提高性能的角度考虑,一旦全局和闭包中的数据不再有用,最好通过将其值设置为 null 来释放其引用,以便垃圾收集器下次运行时将其回收。

function fnTest(_i) {
    var i = _i;
    function fnAdd() {
        console.log(i++);
    }
    return fnAdd;
}
var fun = fnTest(100);
fun(); //100,i常驻内存
fun(); //101,i常驻内存
fun(); //102,i常驻内存
// 闭包回收
fun=null; 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值