• 闭包深度理解


    一、闭包的概念理解

    定义 某函数的 词法作用域 以外调用该函数时,该函数依然保留有对其 定义时的词法作用域 的引用。那么这个 引用 就叫做闭包。

    闭包的一些特点:

    1. 当函数在定义时的词法作用域以外调用时,闭包使得函数可以继续访问其定义时的词法作用域

    2. 闭包可以阻止内存空间的回收

    3. 只要使用了回调函数,实际上就在使用闭包

    Tip: 词法作用域是定义在词法阶段的作用域,即是由 编写代码时 函数、变量声明的位置来决定的。也就是说,词法作用域是在 编写代码时 绑定的。(对比this,其是在 代码运行时绑定)

    对于这里的在词法作用域以外调用的 以外 可以总结为两种:即时间或空间上的以外。

    1. 空间上的以外。

    Eg1:

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

    baz()可以被正常执行。它是在自己定义时的词法作用域以外的地方被执行的。故体现了闭包的特点1。

    foo()执行后,通常情况下foo()的整个内部作用域都会被销毁,因为引擎有垃圾回收器来释放不再使用的内存空间。由于foo()的内容看上去不会再被使用,所以很自然地考虑对其进行回收。然而闭包可以阻止该回收的发生。 bar所声明的位置,决定了其拥有对foo内部作用域的引用,这使得该作用域能够一直存活,以供bar()在以后人任何时间进行引用。故体现了闭包的特点2。

    当函数在定义时的词法作用域以外调用时,都能观察到闭包

    Eg2:

    var fn;
    
    function foo() {
    	var a = 2;
    
    	function baz() {
    		console.log(a);
    	}
    	fn = baz;//将baz分配给全局变量
    }
    
    function bar(fn) {
    	fn();//这里是在baz定义的作用域之外调用了baz,baz仍然保留有对定义时的作用域foo()的引用,故产生了闭包。
    }
    
    bar();//2
    

    无论通过何种手段将函数内部的函数传递到所在词法作用域以外,它都会持有对原始定义时的词法作用域的引用,无论在何处执行这个函数都会产生闭包。

    这里闭包的一个经典应用就是 模块

    2. 时间上的以外。

    Eg1:

    function wait(message) {
    	setTimeout( function timer() {
    		console.log(message);
    	}, 1000);
    }
    
    wait("Hello!");
    

    wait()执行1s以后,timer依然保留有对wait()内部作用域的引用,故产生了闭包。

    定时器、事件监听器、Ajax请求、跨窗口通信、web workers 或者其它任何异步任务中,只要使用了 回调函数,实际上就是在使用闭包

    Eg2:

      for (var i = 1; i <= 5; i++) {
    		setTimeout(function timer() {
    			console.log(i);
    		}, i * 1000);
      }
    	//输出6 6 6 6 6
    

    该代码段会输出 6 6 6 6 6。

    这是因为延迟函数的回调会在循环结束后才执行。虽然我们试图期望每次循环在运行时都会给自己捕获一个i的副本,但是根据作用域的工作原理,尽管循环中的五个函数都是在每次循环中分别定义的,但是 它们都被封闭在一个共享的全局作用域中,所以实际上只有一个i。 循环结构让我们误以为背后还有更复杂的机制在起作用,但实际上并没有。

    解决方案1:使用闭包

    想要得到想要的1 2 3 4 5,必须 针对每个循环增加一个闭包作用域。而IIFE可以办到这一点。所以,可以将上述代码改写成这样:

      for (var i = 1; i <= 5; i++) {
          (function(j) {
            setTimeout(function timer() {
              console.log(j);
            }, j * 1000);
          })(i);
      }
    	//输出1 2 3 4 5
    

    或这样:

      for (var i = 1; i <= 5; i++) {
    		(function() {
    			var j = i; //只要针对每次的循环增加一个作用域,并把i保存在这个作用域即可,无论是通过变量的方式还是通过参数的方式。
    			setTimeout(function timer() {
    				console.log(j);
    			}, j * 1000);
    		})();
      }
    	//输出1 2 3 4 5
    
    解决方案2:使用es6的let

    其实,我们上述的改进是将for循环的每个循环体都封闭为一个独立的作用域,你们ES6的let可以轻易地办到这一点:

    	for (let i = 1; i <= 5; i++) {
    		let j = i; //这样就不必再在setTimeout外包装一层作用域了,因为let本身就是声明一个作用域被限制在块级中的本地变量,此处每个循环体就是一个块,j的作用域仅仅在这个块中。
    	  setTimeout(function timer() {
    			console.log(j);
    		}, j*1000);
    	}
    	//输出1 2 3 4 5
    

    更进一步,因为for循环头部的let上面有一个特殊行为,即变量在循环过程中会被不止声明一次,每次循环都会声明。之后下一次循环会使用上一次循环结束时的值来初始化这个变量。所以其实对于每一次循环,都有一个独立的i存在于这个循环块作用域中:

    for (let i = 1; i <= 5; i++) {
    	setTimeout(function timer() {
    		console.log(i);
    	}, i*1000);
    }
    //输出1 2 3 4 5
    
    解决方案3:使用setTimeout的第三个参数

    setTimeout第三个及以后的参数param1, ..., paramN: 可选
    附加参数,一旦定时器到期,它们会作为参数传递给function 或 执行字符串(setTimeout参数中的code)。

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

    二、闭包经典问题

    1. 问题:如何实现JavaScript代码的模块模式 与 单例模式?

    解答:

    模块模式:

    
    	function CoolModule() {
    		var something = 'cool';
    		var another = [1, 2, 3];
    
    		function doSomething() {
    			conosle.log(somthing);
    		}
    	  
    		function doAnother() {
    			console.log(another.join('!'));
    		}
    
    		return {
    			doSomething: doSomething,
    			doAnother: doAnother
    		}
    	}
    
    	var foo = CoolModule();
    	foo.doSomething();//'cool'
    	foo.doAnother();//'1! 2! 3!'
    
    

    该模式被称为 模块模式。 CoolModule是一个函数,必须通过调用它来创建一个模块实例,然后就可以暴露出doSomething和doAnother方法。

    doSomething和doAnother函数具有 涵盖模块实例内部作用域的闭包

    这里CoolModule函数可以被叫做是模块创建器,可以 被调用任意多次,每次调用都会创建一个新的模块实例当只需要一个实例时,可以使用一种单例模式

    单例模式:

    	var foo = (function() {
    		var something = 'cool';
    		var another = [1,2,3];
    
    		function doSomething() {
    			console.log( something );
    		}
    
    		function doAnother() {
    			console.log( another.join('!'));
    		}
    
    		return {
    			doSomething: doSomething,
    			doAnother: doAnother
    		}
    	})();
    
    

    将模块创建器函数转换成IIFE,即实现了单例模式。

    2. 问题: 思考下面的代码段:

    	for(var i=0;i<5;i++){
    	    var btn=document.createElement('button');
    	    btn.appendChild(document.createTextNode('Button'+i));//document.createTextNode(<text>)方法:创建一个带有指定内容的新Text对象(即上述button元素上写的文字)
    	    btn.addEventListener(//为元素添加事件监听器
    	      'click',
    	       function(){
    	          console.log(i);
    	        }
    	    );
    	    document.body.appendChild(btn);
    	}
    

    a. 点击“Button4”后输出什么?如何使得输出和预期相同

    b. 给出一个可以和预期相同的写法。

    答案:

    a. 输出5,因为形成了闭包,循环结束后,i为5,所有按钮点击都是5

    b. 有两种思路可以解决该问题:

    (1) 循环比较法(不推荐)

    for(var i=0;i<5;i++){
    		var btn=document.createElement('button');
    		btn.appendChild(document.createTextNode('Button'+i));
    		btn.addEventListener(
    			'click',
    				function(e){
    					for(var i=0;i<5;i++){
    						if (e.target.innerHTML=='Button'+i) {
    								console.log(i);
    						}
    					}
    				}
    		);
    		document.body.appendChild(btn);//document.A.appendChild(B)方法:将B元素添加为A的子元素
    }
    

    (2)DOM污染法

    就是利用button本身自己的属性。这里button本身的text是Buttoni,所以如果直接使用Buttoni就也不存在额外的DOM污染。

    如果button上的text没有包含i的信息,则可以给button添加属性,如将button的index设为i:

    for(let i=0;i<5;i++) {
    	const btn=document.createElement('button');
    	btn.index=i;
    	btn.addEventListener('click', ()=> {
    		console.log(btn.index);
    	});
    	document.body.appendChild(btn);
    }
    

    (2) 闭包法

    这是错误的:

    for(var i=0;i<5;i++){
    		var btn=document.createElement('button');
    		btn.appendChild(document.createTextNode('Button'+i));
    		btn.addEventListener(
    			'click',
    				(function(i){
    					(function(){
    						console.log(i);
    					})();
    				})(i)
    		);
    		document.body.appendChild(btn);
    }
    

    这也是错误的:

    for(var i=0;i<5;i++){
    		var btn=document.createElement('button');
    		btn.appendChild(document.createTextNode('Button'+i));
    		btn.addEventListener(
    			'click',
    				(function(i){
    						console.log(i);
    				})(i)
    		);
    		document.body.appendChild(btn);
    }
    

    这才是正确的:

    for(var i=0;i<5;i++){
    		var btn=document.createElement('button');
    		btn.appendChild(document.createTextNode('Button'+i));
    		(function(a){
    				btn.addEventListener(
    						'click',
    						function () {
    								console.log(a);
    						}
    				)
    		})(i);
    		document.body.appendChild(btn);
    }
    

    其实,闭包法就是在要引用外部变量i的函数外面再包裹一个 用作块级作用域的匿名函数

    (function(i){
    	//某某内部使用了i的函数
    })(i);
    

    3. 问题:实现一段脚本,使得点击对应链接alert出相应的编号

    解答:

    (1) DOM污染法

    通过给document元素对象添加了属性值,故污染了DOM

    var lis = document.links;// 属于DOM Document对象,非Dom Element对象,返回文档里具备href属性的a和area元素的对象。
    for(var i = 0, length = lis.length; i < length; i++) {
    	lis[i].index = i;//此index为自己设置的任意变量值,可任意替换为myindex等等,也可使用固有的元素对象属性,如id等
    	lis[i].onclick = function( ) {
    		alert(this.index);//也可用function(e),后面this换为e.target
    	};
    
    }
    

    (2) 使用闭包

    var lis=document.links;
    
    for(var i=0,len=lis.length;i<len;i++){
    		(function(a){
    				lis[a].onclick=function(){
    						alert(a);
    				};
    		})(i);
    }
    

    (3)循环比较法(不推荐)

    var lis=document.links;
    
    for(var i=0,len=lis.length;i<len;i++){
        lis[i].onclick=function(){
            for(var j=0;j<lis.length;j++){
                if (this==lis[j]) {
                    alert(j);
                }
            }
        };
    }
    

    其实,上述j也可就写作i,因为内部循环参数是在局部函数中的,故循环完成后自动销毁,对外部i没有影响。

    更多关于闭包其实闭包并不高深莫测

    4. 问题:有如下一段html:

    <div class="article-list">
    	<div class="article">文章</div>
    	<div class="article">文章</div>
    	<div class="article">文章</div>
    </div>
    

    使用闭包法实现点击第n块article,输出 Article:n。

    解答:

    使用闭包法有以下几种不同的写法,都可以实现想要的效果:

    写法1

    const articleLists = Array.from(document.querySelectorAll('.article-list .article'));
      articleLists.forEach((elem, index) => {
        (function() {
           elem.addEventListener('click', function(){
            console.log(index);
            const labelForListArticle = `Article: ${index+1}`;
            console.log('click ', labelForListArticle);
           })
        })(index);
      });
    

    写法2

    const articleLists = Array.from(document.querySelectorAll('.article-list .article'));
      articleLists.forEach((elem, index) => {
        (function(i) {
           elem.addEventListener('click', function(){
            console.log(i);
            const labelForListArticle = `Article: ${i+1}`;
            console.log('click ', labelForListArticle);
           })
        })(index);
      });
    

    写法3

    const articleLists = Array.from(document.querySelectorAll('.article-list .article'));
      articleLists.forEach((elem, index) => {
    	 const labelForListArticle = `Article: ${index+1}`;
        (function(label) {
           elem.addEventListener('click', function(){
            console.log(index);
            console.log('click ', label);
           })
        })(labelForListArticle);
      });
    

    参考资料

    http://www.cnblogs.com/zichi/p/4359786.htmlT8

    《你不知道的JavaScript》上卷 Part1 Chapter5

  • 相关阅读:
    SQLServerframework启动报异常:Module的类型初始值设定项引发异常
    在coding或者github建立个人站点域名绑定
    Github速度慢的解决方法
    jsoup爬取网站图片
    activeMQ类别和流程
    Session session = connection.createSession(paramA,paramB);参数解析
    Ehcache入门经典:第二篇ehcache.xml的参数
    Ehcache入门经典:第一篇
    处理高并发
    扩充次数和创建个数问题
  • 原文地址:https://www.cnblogs.com/Bonnie3449/p/9289192.html
Copyright © 2020-2023  润新知