前阵子重新复习了一下js基础知识,第一篇博客就以分享闭包心得为开始吧。

  首先,要理解闭包,就必须要了解一个概念:作用域链。

  作用域链

  作用域代表着可访问变量的集合,变量分为全局变量和局部变量两种,在函数内部所声明的变量被称作局部变量,在全局作用域声明的被称作全局变量。

  JavaScript中,在函数内部可以读取到全局变量以及函数外部所声明的局部变量,但是在函数内部声明的变量在函数外部是无法获取到的。

  来看下面这幅图:

         

  上图中,val1为全局变量,函数func1和func2均能访问到val1。

  val2为在func1下声明的局部变量,它不能被val1所在的全局作用域所访问到,但是位于其内部的函数func2则可以访问到val2的值。

  val3是在func2下声明的局部变量,不论是val1所在的全局作用域还是它的父函数func1都无法访问到val3的值,只有func2函数自身能使用它。(如果在func2内部有新声明的函数,那么该函数同样可以访问到func2中的局部变量。)

  这种由函数为分界线的作用域被称为函数作用域,由上图可知,函数作用域代表着该函数所有可用的变量的集合。

  当一个函数被运行时,他就会创建自己的函数作用域。作用域按调用优先级可分为以下四部分:

    1. 该函数的参数;
    2. 在该函数内部所声明的局部变量;
    3. 在其父函数所在作用域中所声明的局部变量;
    4. 全局变量。

  当函数要访问一个变量的时候,他会首先在自身查找变量,如果查找失败,则会查找其父函数所在作用域,由内向外,直到达到全局作用域。

  这种作用域层层嵌套的形式就被称为作用域链。

  从作用域链的特点中我们能发现这样一条规则:正常途径下,在函数外部是无法访问到函数内部的变量的。

  然而,要想访问到函数内部的变量也并非是不可能的,我们也可以作用域链的这一特点,来解决这个问题。

  让我们看下面这个例子:

     function func1() {
var val1 = 666; return function func2() {
console.log(val1);
}
}
var func3 = func1();
func3();      //

  当我们运行func3()时,会发现控制台打印出了666!也就是val1的值!

  这也就说明,我们在全局作用域运行的func3,获取到了func1中声明的局部变量val1的值。

  而这就是通过闭包实现的,这里func2就是一个闭包。

  闭包

  MDN中闭包的定义为:函数和声明该函数的词法环境组合。

  其实通俗的讲,就是该函数本身以及该函数声明时所在位置的代码(该函数所在的作用域链)。

  在上面的例子中,func2在声明的时候,它所处的作用域链也就随之生成了。当我们在别处,比如说全局作用域下调用func2时,由于作用域链的特性,func2所处的作用域链也会被保留,即func2的父函数中的val1依然可以被调用,形成了一个闭包。

    更进一步!

      首先我们在func2中加入一句代码,改为如下形式:

     return function func2() {
debugger;
console.log(val1);
}

      然后将上述代码拷贝到浏览器控制台中,运行。

          

如图所示,在程序断点处,也就是func2函数内,能看到一个Closure(闭包)。其中存放的就是其所在作用域的变量,在本例子中就是func1中声明的val1;这就是闭包如何在后台运行的。

  在了解的闭包是什么以后,就该谈谈闭包在编程中的应用了。

  闭包的应用

  1. 可以获取到函数内部的变量(参考上例)。

  2. 用闭包模拟私有方法和变量。

    在很多编程语言中,如java,c++,c#等,都支持将方法和变量声明为私有的,即他们只支持被所处同一个类下的其他方法所调用。

    在JavaScript中是没有这种原生支持的,但是MDN告诉我们,我们可以使用闭包来模拟私有方法和变量,并通过创建公共函数来访问这些方法和变量。

    举例时间:

     function counter() {
var num = 0; function changeValBy(val) {
num += val;
} return {
increment:function(){
changeValBy(1);
},
decrement:function(){
changeValBy(-1);
},
viewValue:function(){
console.log(num);
}
}
}

  counter中含有一个私有变量num和一个私有函数changeValBy,它们均无法从函数外部直接访问,只能通过myCounter1中的三个公共函数对它们进行访问和更改。并且由于作用域链的特性,这三个函数位于同一个作用域下,他们所访问的是同一个num和函数changeValBy,即是共享同一环境的闭包。(这一点也会引发一个有趣的问题,下面会提到。)

     myCounter1.viewValue();    //
myCounter1.increment();
myCounter1.increment();
myCounter1.viewValue(); //
myCounter1.decrement();
myCounter1.viewValue(); //
num; // Uncaught ReferenceError: num is not defined at <anonymous>:1:1

    当我们运行这三个函数时会发现这三个函数是共享同一环境的。并且直接访问num是访问不到的。

      更进一步!

        我们再新声明一个myCounter2,

     var myCounter2 = counter();

        并且同时调用myCounter1和myCounter2,

     myCounter1.viewValue();      //
myCounter2.viewValue(); //
myCounter1.increment();
myCounter1.increment();
myCounter1.viewValue(); //
myCounter2.viewValue(); //
myCounter2.increment();
myCounter1.viewValue(); //
myCounter2.viewValue(); //

        会发现myCounter1和myCounter2是各自独立的,是两个不同的闭包。当其中一个闭包中的变量发生更改时,是不会影响到另一个闭包中的变量的。

        这就是闭包的第三种应用:

  3.提供了数据隐藏和封装。

  说完了闭包的应用,再来谈谈闭包应用时要注意的问题。

  1.内存管理和垃圾回收

  JavaScript 通过自动垃圾回收来管理内存。这意味着,当数据不再可引用时(即没有可用于可执行代码的对该数据的剩余引用),它将被“垃圾回收”,并在稍后的某个时间点被销毁。这可以释放该数据曾经消耗的资源(即计算机内存),从而使这些资源可供重新使用。

  让我们结合闭包来看一下垃圾回收。我们知道,父函数的变量可以被嵌套的内层函数访问。如果嵌套函数捕获并使用其父函数的变量(或其作用域链上的变量,如其父函数的父函数的变量),那么只要使用这些变量的函数仍可被引用,这些变量就会一直保留在内存中。

为了防止闭包中造成内存泄漏,当我们不在需要这些变量和函数时,要即使将其释放掉。

2.在循环中创建闭包

先来看一个在闭包中很经典的例子:

     for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i);
},100);
}

打眼一看,这段代码的运行结果应该是每间隔100毫秒依次打印出0-9,然而实际运行后发现,十次打印竟然全都是10!这是为什么呢?

让我们把问题分解开来:

1.为什么十次打印值都一样?

答:正如闭包的第二点应用一样,这十个打印函数都处于同一作用域链下,是共享同一环境的闭包,它们访问是同一个i值,所以打印出来的值都一样。

2. 为什么打印出来的值是10?

答:这与setTimeout的运行机制有关系,setTimeout是一个异步函数,当他被调用后,是不会立即执行的,而是添加到当前任务队列后面,直到之前的任务都执行完毕后才会执行。

所以上面的代码的实际运行过程是这样的:

for循环每执行一次,就将一个setTimeout任务添加到队尾,直到for循环执行完毕,才会开始执行十次setTimeout任务,然而此时的i已经变成了10,所以就会打印出十次10啦!

如何解决?

解决的方法有很多,我这里只列出和本文知识点有关的两个解决方式:
1.利用闭包应用的第三点,让每个打印函数成为独立的闭包,并利用闭包保存当前循环的i值:

     for(var i = 0; i<10;i++) {
(function func(j) {
setTimeout(function() {
console.log(j);},100)
})(i);
}

2.因为过多的闭包会占用更多的内存,所以为了避免在不必要的时候使用闭包,我们可以采用let关键字:

     for(let i=0;i<10;i++){
setTimeout(function(){
console.log(i);
},100);
}

为什么let关键字这么神奇,这就要从ES6中var,let,const的区别说起了……呃,跑题了,等以后新开一篇再写吧~当然有兴趣的读者也可以自行百度谷歌学习~

  关于闭包的三三两两就先到这里啦,第一次写博客,经验不足,着实花费了不少时间。如果你觉得有文章有什么纰漏之处,或者觉得有什么疑问和不解,欢迎吐槽和讨论!让我们在前端学习上共同进步!

参考:  MDN上的闭包;              
优达学城前端进阶课程;

前端随笔 - JavaScript中的闭包的更多相关文章

  1. 前端面试之JavaScript中的闭包!

    前端面试之JavaScript中的闭包! 闭包 闭包( closure )指有权访问另一个函数作用域中变量的函数. ----- JavaScript 高级程序设计 闭包其实可以理解为是一个函数 简单理 ...

  2. javascript中的闭包解析

    学习javaScript已经有一段时间了,在这段时间里,已经感受到了JavaScript的种种魅力,这是一门神奇的语言,同时也是一门正在逐步完善的语言,相信在大家的逐步修改中,这门语言会逐步的完善下去 ...

  3. 一篇文章把你带入到JavaScript中的闭包与高级函数

    在JavaScript中,函数是一等公民.JavaScript是一门面向对象的编程语言,但是同时也有很多函数式编程的特性,如Lambda表达式,闭包,高阶函数等,函数式编程时一种编程范式. funct ...

  4. 让你分分钟学会Javascript中的闭包

    Javascript中的闭包 前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它 ...

  5. 难道这就是JavaScript中的"闭包"

    其实对于JavaScript中的"闭包"还没真正理解,这次在实际Coding中似乎遇到了"闭包"的问题,仅此摘录,以待深究. 表现为jQuery的post方法回 ...

  6. 浅谈JavaScript中的闭包

    浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...

  7. JavaScript中的闭包理解

    原创文章,转载请注明:JavaScript中的闭包理解  By Lucio.Yang 1.JavaScript闭包 在小学期开发项目的时候,用node.js开发了服务器,过程中遇到了node.js的第 ...

  8. 【JS】JavaScript中的闭包

    在JavaScript中,闭包指的是有权访问另一个函数作用域中的变量的函数:创建闭包最常见的方式就是在一个函数内创建另一个函数.如下例子: function A(propertyName){ retu ...

  9. Javascript中的闭包(转载)

    前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它.下面是作者从作用域链慢慢讲到 ...

随机推荐

  1. Nodejs经验谈

    前言 这里主要说一下之前使用Nodejs开发踩过的坑,只说坑不填坑,那就是赤裸地耍流氓,文中有大量的说明及填坑方法,了解的看官可以直接跳过. PS:说实话,Nodejs的坑确实蛮多的:但是上手简单,扩 ...

  2. Python爬虫 - 爬取百度html代码前200行

    Python爬虫 - 爬取百度html代码前200行 - 改进版,  增加了对字符串的.strip()处理 源代码如下: # 改进版, 增加了 .strip()方法的使用 # coding=utf-8 ...

  3. 关于非现场审计软件的一些介绍(ACL、IEDA、Teammate)

    http://group.vsharing.com/Article.aspx?aid=661512 IDEA是由caseware开发的数据分析软件.caseware的网址如下:http://www.c ...

  4. QQ connect client request's parameters are invalid, invalid openid 问题的解决

    很多人的这个问题是POST的时候发生,我的也恰好在POST的时候发生.后来我发现可能是因为QQ的这个后端是采用类PHP的语言开发,在动态语言的获取参数时POST参数和GET参数是可以分开读取的,也就是 ...

  5. vue实现淘宝商品详情页属性选择功能

    方法一是自己想出来的,方法二来自忘记哪里看到的了 不知道是不是你要的效果: 方法一:利用input[type="radio"] css代码: input { display: no ...

  6. 人脸检测? 对Python来说太简单, 调用dlib包就可以完成

    "Dlib 是一个现代化的 C ++ 工具包,包含用于创建复杂软件的机器学习算法和工具 " .它使您能够直接在 Python 中运行许多任务,其中一个例子就是人脸检测. 安装 dl ...

  7. 【读英文文档】Whetting Your Appetite(刺激你的食欲)

    如果你有很多工作是通过计算机来完成的,那么你一定希望其中的很多事情能够自动地实现.比方说,你希望在文本文件中实现查找和替换的功能,以某一种机制实现照片的重命名以及重新排序的功能,一个小型的数据库甚至是 ...

  8. 【热身】github的使用

    GitHub 可以托管各种Git版本库,并提供一个web界面,但与其它像 SourceForge或Google Code这样的服务不同,GitHub的独特卖点在于从另外一个项目进行分支的简易性.为一个 ...

  9. java项目获取根路径(web项目和application项目的区分)

    Java项目中经常要读取配置文件,涉及到读取配置文件的地方,就会要读定位文件的路径.因此,在项目如何正确获取文件路径尤为关键. 根据不同的java项目,在获取文件路径时候有一些 小区别 测试环境:E: ...

  10. 深入浅出 TCP/IP 协议

    TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输.TCP/IP 协议采用4层结构,分别是应用层.传输层.网络层和链路 ...