【前端 教程】详解 闭包

【前端 教程】闭包 详解

1、闭包概念

闭包就是指有权访问另一个函数作用域中的变量的函数。或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用。

js的作用域分两种,全局局部,基于我们所熟悉的作用域链相关知识,我们知道在js作用域环境中访问变量的权利是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量(也叫“JS的链式作用域”),反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数

【JS链式作用域】子对象会一级一级向上寻找所有父对象的变量,反之不行。

【JS变量的两种作用域】全局变量、局部变量(函数内):js中函数内部可以读取全局变量,函数外部不能读取函数内部的局部变量。

需要先了解 JavaScript 语言特有的一种结构–链式作用域,即为,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象是可见的,反之则不成立。

了解这点后,可做以下推论,外部无法访问父函数的局部变量,而子函数可以访问父函数的局部变量,只要将子函数作为返回值,就可以在外部间接访问到父函数的局部变量了。

可能看这么些定义会感到一头雾水

表急
【前端 教程】详解 闭包_第1张图片

function A() {
	var naem = "奋斗中的编程菜鸟";
	return function() {
		return name;
	}
}

var B = A();
console.log(B()) //奋斗中的编程菜鸟

在这段代码中,A()中的返回值是一个匿名函数,这个函数在A()作用域内部,所以它可以获取A()作用域下变量name的值,将这个值作为返回值赋给全局作用域下的变量B,实现了在全局变量下获取到局部变量中的变量的值。

因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

2、闭包的几个经典例子

【前端 教程】详解 闭包_第2张图片

No.1

function Fn() {
	var num = 99;
	return function() {
		var n = 0;
		console.log(++n);
		console.log(++num);
	}
}

var Fn1 = Fn();
Fn1() //1 100
Fn1() //1 101

一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为Fn的返回值被赋值给了Fn1,这时候相当于Fn1=function(){var n = 0 … },并且匿名函数内部引用着Fn里的变量num,所以变量num无法被销毁,而变量n是每次被调用时新创建的,所以每次Fn1执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后就剩下孤零零的num,于是这里就产生了内存消耗的问题

No.2

再来看一个经典例子-定时器与闭包:写一个for循环,让它按顺序打印出当前循环次数。

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

输出结果如下:
【前端 教程】详解 闭包_第3张图片
按照预期它应该依次输出1 2 3 4 5,而结果它输出了五次5,这是为什么呢?原来由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经到5了,所以打印出来五个5,那么我们为了实现预期结果应该怎么改这段代码呢?

答案:使用闭包!!!

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

这样就可以实现按顺序打印出当前循环次数了。

结果如下:
【前端 教程】详解 闭包_第4张图片

PS:理解闭包,需要先看看有关立即执行函数的相关知识,如果还不了解立即执行函数,可以学习这篇博客:

【前端 教程】相关技术文章:
【前端 教程】详解 立即执行函数

在立即执行函数中,还有另外一种写法

这里换一种写法验证一下

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

仍然得到一样的正确的结果输出,如下:
【前端 教程】详解 闭包_第5张图片
这样能够解决开始提到的问题

原理就是使用了闭包

引入闭包来保存变量i,将setTimeout放入立即执行函数中

将for循环中的循环值i作为参数传递,100毫秒后同时打印出1 2 3 4 5

【注意】如果把for循环里面的var变成let,也能实现预期结果。

那要想每隔100ms依次打印出来怎么解决呢?

很简单

那就设置多个定时器

每隔100ms一个

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

得到的结果如下:

这里运行时可以看到是每隔100ms打印一个数的动态结果
【前端 教程】详解 闭包_第6张图片
以上的两个例子都是【闭包作为返回值】

下面看看【闭包作为函数参数传递】

注:这两种也是闭包的主要应用形式。
1、闭包作为返回值
2、闭包作为函数参数传递

var num = 10;
var Fn1 = function(x) {
	if(x > num) {
		console.log(x);
	}
}
void function(Fn2) {
	var num = 100;
	Fn2(50)
}(Fn1)

输出结果,如图:
【前端 教程】详解 闭包_第7张图片
【解释】函数Fn1作为参数传入立即执行函数中,在执行到Fn2(50)的时候,50作为参数传入Fn1中,这时候if(x>num)中的num取的并不是立即执行函数中的num,而是取创建函数的作用域中的num这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值10,即50>10,打印50。

再看一个经典的闭包的例子

<ul id=”test”>
 2     <li>这是第一条</li>
 3     <li>这是第二条</li>
 4     <li>这是第三条</li>
 5 </ul>
 6 
 7 <script>
 8     var liList=document.getElementsByTagName('li');
 9     for(var i=0;i<liList.length;i++)
10     {
11         liList[i].onclick=function(){
12             console.log(i);
13         }
14     };
15 </script>

通常,看到这样的代码,会觉得这样的执行效果是点击第一个li,则会输出1,点击第二个li,则会输出二,以此类推。

但是真正的执行效果是,不管点击第几个li,都会输出3。

因为 i 是贯穿整个作用域的,而不是给每个 li 分配了一个 i,用户触发的onclick事件之前,for循环已经执行结束了,而for循环执行完的时候i=3。

那么,怎么解决这个问题呢?

不错,还是运用闭包!

使用闭包给i创建单独作用域,保存i值。

<script>
 2     var liList=document.getElementsByTagName('li');
 3     for(var i=0;i<liList.length;i++)
 4     {
 5         (function(li) {
 6            liList[li].onclick=function(){
 7                console.log(li);
 8            }
 9        })(i)
10     };
11 </script>

在立即执行函数执行的时候,i 的值被赋值给 li,此后 li 的值一直不变,如图6所示。i 的值从 0 变化到 3,对应3 个立即执行函数,这 3个立即执行函数里面的 li 「分别」是 0、1、2。

【注意】就像上面说到的,其实用ES6语法中的let也可以实现上述的功能。

代码如下:

<script>
2      var liList=document.getElementsByTagName('li');
3      for(let i=0;i<liList.length;i++)
4      {
5             liList[i].onclick=function(){
6                 console.log(i);
7              }
8      }
9 </script>

也可以实现同样的效果

关于【let 和 var 定义变量的区别】参见下面的文章

【JS 教程】相关技术文章:
【JS let 和 var】JavaScript中,let 和 var 定义变量的区别

为了深入理解

再看一个闭包例子
【前端 教程】详解 闭包_第8张图片
看懂下面的例子

就能弄懂闭包可以让变量保存在内存中的含义了

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      console.log(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

运行结果如下:
【前端 教程】详解 闭包_第9张图片
【解释】在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个

匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

3、闭包的特性:

①函数嵌套函数

②函数内部可以引用函数外部的参数和变量

③参数和变量不会被垃圾回收机制回收

理解上面还需要补充一下
【Javascript的垃圾回收机制】
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这样,就不会被GC所回收,这就是为什么函数a执行后不会被回收的原因。

4、闭包的好处

  • 读取函数内部的变量
  • 让这些变量的值始终保持在内存中
  • 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
  • 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
  • 匿名自执行函数可以减少内存消耗
  • 方便调用上下文的局部变量。利于代码封装

【解释】以上面的例子来说明,Fn1是Fn2的父函数,Fn2被赋给了一个全局变量,Fn2始终存在内存中,Fn2的存在依赖f1,因此Fn1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收。

父函数将子函数作为返回值,再将子函数赋值给一个变量,所以子函数会存在于内存中,而子函数依赖于父函数存在,所以父函数也会存在于内存中,也就不会被垃圾回收机制回收。

5、闭包的坏处

  • 被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
  • 由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响

6、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
【前端 教程】详解 闭包_第10张图片
------------------------可爱的分割线-----------------------------------

以上就是关于【闭包】的讲解了,个人认为还是比较详细的了

理解并掌握闭包是很重要的

这篇文章应该可以帮助大家很好地掌握闭包的概念和闭包的运用

希望可以帮助到大家
【前端 教程】详解 闭包_第11张图片
本菜鸟不定期更新技术博客

喜欢的可以关注、评论、转发三连
【前端 教程】详解 闭包_第12张图片
其他技术博客见我的博客列表

大家需要的自取
【前端 教程】详解 闭包_第13张图片

【Java Web】相关技术文章:
【Java Web总结】Java Web项目中 的.classpath、.mymetadata、.project文件作用
【Java Web问题解决】Tomcat报错javax.servlet.ServletException: Error instantiating servlet class.报错404
【比较】什么是“服务器端跳转”“客户端跳转”,二者有什么区别?
【总结】表单提交的get和post有什么不同?
【Java Web问题解决】Tomcat报错:java.lang.ClassCastException: cannot be cast to javax.servlet.Filter解决办法
【Java Web问题解决】Filter过滤器初始化方法init()执行了两次原因及解决方法
【总结】Java Web 中的4种属性范围(page、request、session、application)
【Java Web问题解决】Tomcat报错:java.sql.SQLException: No suitable driver found for jdbc:mysql://
【Java Web问题解决】Tomcat启动时控制台出现中文乱码的问题解决方法
【示例项目】java实现通过身份证号码判断籍贯所在地区
【总结】HTTP协议中的状态码(200、403、404、500等)
【Java Web问题解决】提交表单后显示乱码原因及解决办法
【Java Web总结】JSP页面的生命周期详解
【Java Web总结】JSP页面实现类详解
【Java Web 问题解决】Tomcat启动失败 报错:Server Tomcat v9.0 Server at localhost failed to start.
【Java Web问题解决】连接数据库出错:java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/
【Java Web问题解决】使用过滤器Filter解决提交表单后显示乱码问题
【Java Web问题解决】过滤器Filter进行编码过滤后页面空白、显示不了原因及解决办法

【Linux 操作系统】相关技术文章:
【Linux问题解决】Ubuntu Linux 安装gcc4.9 g++4.9报错“没有可供安装的候选者”解决办法
【Linux教程】Ubuntu Linux 更换源教程
【Linux教程】如何实现在Ubuntu Linux和windows之间复制粘贴、拖拽复制文件?
【Linux问题解决】操作系统用C语言多线程编程 对‘pthread_create’未定义的引用 报错解决办法
【Linux教程】Linux中用C语言多线程编程之pthread_join()函数
【Linux操作系统、C语言】在Linux中用C语言进行OpenMP并行程序设计之常见指令、库函数和指令总结
【VMware 虚拟机问题解决】VMware Workstation pr无法在Windows上运行的解决方案
【Linux 问题解决】Ubuntu执行apt-get命令报错:无法获得锁 /var/lib/dpkg/lock…解决方案

【Python】相关技术文章:
【总结】Python与C语言、Java等语言基本语法的不同点
【总结】分析Python中的循环技巧
【总结】Python语言是编译型语言还是解释型语言?(Python程序执行过程)
【总结】Python2 和 Python3 的区别
利用Python一层循环打印 * 型三角形
【总结】Python与C语言、Java等语言基本语法的不同点
【总结】你知道吗?——元组其实是可变的序列!
【Python爬虫教程】Python爬虫基本流程及相关技术支持
【Python问题解决】PyCharm中debug报错:using cython not found. pydev debugger: process 13108 is connecting原因及解决
【Python总结】闭包及其应用

【IntelliJ IDEA教程】相关技术文章:
【Intellij IDEA教程】怎么自动清除无效的import导入包、清除无效的import导入包的快捷键
【IntelliJ IDEA教程】在IntelliJ IDEA启动项目 Warning:java: 源值1.5已过时, 将在未来所有发行版中删除 解决办法
【IntelliJ IDEA教程】提示信息Unmapped Spring configuration files found.Please configure Spring facet. 解决办法
【IntelliJ IDEA教程】怎么取消IntelliJ IDEA对单词拼写的检查

【Jupyter Notebook教程】相关技术文章:
【Python教程】Jupyter Notebook把一段很长的代码分成多行的解决办法】

【操作系统 教程】相关技术文章:
【Linux操作系统 教程】进程间的五种通信方式详解之——管道】

【Android 教程】相关技术文章:
【Android教程】Android Studio找不到连接的手机完全解决办法】
【Android问题解决】java.io.IOException: Cleartext HTTP traffic to … not permitted完美解决

【React 教程】相关技术文章:
【yarn问题解决】An unexpected error occured:“https://npm-registry.toolsfdg.net/”connnect ECONNREFUSED10.
【React + Antd 教程】Failed to compile. Module not found: Can’t resolve 'antd/dist’完美解决

【JS 教程】相关技术文章:
【JS语法糖】常见的几种JS语法糖
【JS 教程】相关技术文章:
【JavaScript 教程】ES6 中的 Promise对象 详解

【Windows 教程】相关技术文章:
【Windows技巧】完美解决“如何在任意文件夹中右键打开cmd终端?”右键快捷方式打开指定文件夹目录下的cmd终端

关于JSP页面的生命周期详解,可参考如下的技术文章:
【Java Web总结】JSP页面的生命周期详解

【前端 教程】相关技术文章:
【前端 教程】详解 立即执行函数

你可能感兴趣的:(Web前端开发)