闭包深度理解

一、闭包的概念理解

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

闭包的一些特点:

  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 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

其实,我们上述的改进是将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

二、闭包经典问题

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

原文地址:https://www.cnblogs.com/Bonnie3449/p/9289192.html

时间: 2024-10-16 17:14:49

闭包深度理解的相关文章

对于linux下system()函数的深度理解(整理)

对于linux下system()函数的深度理解(整理) (2013-02-07 08:58:54) 这几天调程序(嵌入式linux),发现程序有时就莫名其妙的死掉,每次都定位在程序中不同的system()函数,直接在shell下输入system()函数中调用的命令也都一切正常.就没理这个bug,以为是其他的代码影响到这个,或是内核驱动文件系统什么的异常导致,昨天有出现了这个问题,就随手百了一下度,问题出现了,很多人都说system()函数要慎用要少用要能不用则不用,system()函数不稳定?

闭包的理解

闭包的理解, wondow.onload=function  aaa(){   //父函数 var a=12;//局部变量 function  bbb(){   //子函数 alert(a);  //子函数可以使用父函数的局部变量,这种现象叫做闭包,是变量作用域的一种形式. } bbb() }

javascript中重要概念-闭包-深入理解

在上次的分享中javascript--函数参数与闭包--详解,对闭包的解释不够深入.本人经过一段时间的学习,对闭包的概念又有了新的理解.于是便把学习的过程整理成文章,一是为了加深自己闭包的理解,二是给读者提供学习的途径,避免走弯路. 在javascript--函数参数与闭包--详解这篇文章中,我详细介绍了闭包的概念.以下的分享对闭包的基本概念只会稍稍带过.如果对闭包的概念不熟悉的同学,请移步至javascript--函数参数与闭包--详解. 以下的分享会分为如下内容: 1.let命令 2.闭包特

RRDtool深度理解

RRDtool深入学习 介绍 RRDtool:Round Robin Database Tool(轮询的数据库工具) 是一种存储数据的方式,使用固定大小的空间来存储数据,并有一个指针指向最新的数据的位置.我们可以把用于存储数据的数据库的空间看成一个圆,上面有很多刻度.这些刻度所在的位置就代表用于存储数据的地方.所谓指针,可以认为是从圆心指向这些刻度的一条直线.指针会随着数据的读写自动移动.要注意的是,这个圆没有起点和终点,所以指针可以一直移动,而不用担心到达终点后就无法前进的问题.在一段时间后,

关于闭包的理解

发表一下关于闭包的理解:首先把每一个函数看成一个一个的"小黑屋"小黑屋里面可以看到外面的东西,但外面却看不到小黑屋里面的东西,就好比是函数可以访问外面全局变量,但是外面却访问不了"小黑屋"里面的变量:变量分为全局变量和局部变量:列:函数可以访问外面全局变量 var a=1; function fun1(){ alert(a); }fun1(); // 1 列:外面却访问不了"小黑屋"里面的变量 function fun2(){ var a=2;

javascript 闭包的理解

看过很多谈如何理解闭包的方法,但大多数文章,都是照抄或者解释<Javascript高级程序设计(第三版)>对于闭包的讲解,甚至例程都不约而同的引用高程三181页‘闭包与变量’一节的那个“返回数组各个项,结果各个项的值都相同”的例程,还有些文章的讲解过程上一步与下一步之间的跨度简直就是一步登天,让人反复看半天都无法理解. 闭包的理解需要很多概念做铺垫,包括变量作用域链.执行环境.变量活动对象.引用式垃圾内存收集机制等,如果对本文涉及的这些概念不理解,可以去找本<Javascript高级程序

作用域+闭包+this理解

函数预解析过程   函数会覆盖同名变量 也就是var  他的优先级高   如果是同名函数则后者覆盖前者   逐行解读代码的时候 表达式 和参数 会改变预解析仓库里面的值..也就是表达式能干掉函数   域和域之间的关系 <script>标签存在上下文关系   走完上下文标签的变量  下面的可以用上面的   自上而下  函数  json等 子级作用域到父级作用域的过程 叫作用域链        由里到外 当子级找不到变量 会到父级找  如果有表达式的话就会更改全局变量 也就是函数里面没有var  

闭包的理解学习

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 下面就是我的学习笔记,对于Javascript初学者应该是很有用的. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量. var n=999; function f1(){ alert(n); } f1(); // 999 另一方面,在函数外

Java深度理解——Java字节代码的操纵

导读:Java作为业界应用最为广泛的语言之一,深得众多软件厂商和开发者的推崇,更是被包括Oracle在内的众多JCP成员积极地推动发展.但是对于 Java语言的深度理解和运用,毕竟是很少会有人涉及的话题.InfoQ中文站特地邀请IBM高级工程师成富为大家撰写这个<Java深度历险>专栏,旨在就Java的一些深度和高级特性分享他的经验.在一般的Java应用开发过程中,开发人员使用Java的方式比较简单.打开惯用的IDE,编写Java源代码,再利用IDE提供的功能直接运行 Java 程序就可以了.