首页 > 编程笔记 > JavaScript笔记 > JS函数
JS闭包精讲
闭包是 JavaScript 的重要特性之一,在函数式编程中有着重要的作用,本节介绍闭包的结构和基本用法。
函数被调用后,在默认情况下上下文活动对象会被立即释放,避免占用系统资源。但是,若函数内的私有变量、参数、私有函数等被外界引用,则这个上下文活动对象暂时会继续存在,直到所有外界引用被注销。
但是,函数作用域是封闭的,外界无法访问。那么在什么情况下,外界可以访问到函数内的私有成员呢?
根据作用域链,内部函数可以访问外部函数的私有成员。如果内部函数引用了外部函数的私有成员,同时内部函数又被传给外界,或者对外界开放,那么闭包体就形成了。这个外部函数就是一个闭包体,它被调用后,活动对象暂时不会被注销,其属性会继续存在,通过内部函数可以持续读写外部函数的私有成员。
下面是一个典型的闭包结构。
如下结构形式也可以形成闭包:通过全局变量引用内部函数,实现内部函数对外开放。
但是在浏览器中预览时,会发现浏览器抛出异常。
通过事件绑定,mouseover 事件处理函数被外界引用(li 元素),这样就形成了一个闭包体。虽然在 for 语句中为每个选项卡 li 分别绑定事件处理函数,但是这个操作是动态的,因此 tab[i] 中 i 的值也是动态的,所以就出现了上述异常。
定义闭包
闭包就是一个能够持续存在的函数上下文活动对象。形成原理
函数被调用时,会产生一个临时上下文活动对象。它是函数作用域的顶级对象,作用域内所有私有方法有变量、参数、私有函数等都将作为上下文活动对象的属性而存在。函数被调用后,在默认情况下上下文活动对象会被立即释放,避免占用系统资源。但是,若函数内的私有变量、参数、私有函数等被外界引用,则这个上下文活动对象暂时会继续存在,直到所有外界引用被注销。
但是,函数作用域是封闭的,外界无法访问。那么在什么情况下,外界可以访问到函数内的私有成员呢?
根据作用域链,内部函数可以访问外部函数的私有成员。如果内部函数引用了外部函数的私有成员,同时内部函数又被传给外界,或者对外界开放,那么闭包体就形成了。这个外部函数就是一个闭包体,它被调用后,活动对象暂时不会被注销,其属性会继续存在,通过内部函数可以持续读写外部函数的私有成员。
闭包结构
典型的闭包体是一个嵌套结构的函数。内部函数引用外部函数的私有成员,同时内部函数又被外界引用,当外部函数被调用后,就形成了闭包。这个函数也称为闭包函数。下面是一个典型的闭包结构。
function f(x) { //外部函数 return function (y) { //内部函数,通过返回内部函数,实现外部引用 return x + y; //访问外部函数的参数 }; } var c = f(5); //调用外部函数,获取引用内部函数 console.log(c(6)); //调用内部函数,原外部函数的参数继续存在解析过程简单描述如下:
- 在 JavaScript 脚本预编译期,声明的函数 f 和变量 c,先被词法预解析。
- 在 JavaScript 执行期,调用函数 f,并传入值 5。
- 在解析函数 f 时,将创建执行环境(函数作用域)和活动对象,并把参数和私有变量、内部函数都映射为活动对象的属性。
- 参数 x 的值为 5,映射到活动对象的 x 属性。
- 内部函数通过作用域链引用了参数 x,但是还没有被执行。
- 外部函数被调用后,返回内部函数,导致内部函数被外界变量 c 引用。
- JavaScript 解析器检测到外部函数的活动对象的属性被外界引用,无法注销该活动对象,于是在内存中继续维持该对象的存在。
- 当调用 c,即调用内部函数时,可以看到外部函数的参数 x 存储的值继续存在。这样就可以实现后续运算操作,返回 x+y=5=6=11。
如下结构形式也可以形成闭包:通过全局变量引用内部函数,实现内部函数对外开放。
var c; //声明全局变量 function f(x) { //外部函数 c = function (y) { //内部函数,通过向全局变量开放实现外部引用 return x + y; //访问外部函数的参数 }; } f(5); //调用外部函数 console.log(c(6)); //使用全局变量c调用内部函数,返回11
闭包变体
除了嵌套函数外,如果外部引用函数内部的私有数组或对象,也容易形成闭包。var add; //全局变量 function f() { //外部函数 var a = [1,2,3]; //私有变量,引用型数组 add = function (x) { //测试函数,对外开放 a[0] = x * x; //修改私有数组的元素值 } return a; //返回私有数组的引用 } var c = f(); console.log(c[0]); //读取闭包内数组,返回1 add(5); //测试修改数组 console.log(c[0]); //读取闭包内数组,返回25 add(10); //测试修改数组 console.log(c[0]); //读取闭包内数组,返回100与函数相同,对象和数组也是引用型数据。调用函数 f,返回私有数组 a 的引用,即传值给局部变量 c,而 a 是函数 f 的私有变量,当被调用后,活动对象继续存在,这样就形成了闭包。
这种特殊形式的闭包没有实际应用价值,因为其功能单一,只能作为一个静态的、单向的闭包。而闭包函数可以设计各种复杂的运算表达式,它是函数式变成的基础。
反之,如果返回的是一个简单的值,就无法形成闭包,值传递是直接复制。外部变量 c 得到的仅是一个值,而不是对函数内部变量的引用。这样当函数调用后,将直接注销对象。function f(x) { //外部函数 var a = 1; //私有变量 return a; } var c = f(5); console.log(c); //仅是一个值,返回1
使用闭包
下面结合示例介绍闭包的简单使用,以加深对闭包的理解。示例1
使用闭包实现优雅的打包,定义存储器。var f = function () { //外部函数 var a = []; //私有数组初始化 return function (x) { //返回内部函数 a.push(x); //添加元素 return a; //返回私有数组 }; } () //直接调用函数,生成执行环境 var a = f(1); //添加值 console.log(a); //返回1 var b = f(2); //添加值 console.log(b); //返回1,2在上面示例中,通过外部函数设计一个闭包,定义一个永久的存储器。当调用外部函数生成执行环境之后,就可以利用返回的匿名函数不断地的向闭包体内的数组 a 传入新值,传入的值会持续存在。
示例2
在网页中事件处理函数很容易形成闭包。<script> function f() { var a = 1; b = function () { console.log("a =" + a); } c = function () { a ++; } d = function () { a --; } } </script> <button onclick="f()">生成闭包</button> <button onclick="b()">查看 a 的值</button> <button onclick="c()">递增</button> <button onclick="d()">递减</button>在浏览器中浏览时,首先点击“生成闭包”按钮,生成一个闭包;点击“查看 a 的值”按钮,可以随时查看闭包内私有变量 a 的值;点击“递增”“递减”按钮时,可以动态修改闭包内变量 a 的值,效果如图所示。
闭包的局限性
闭包的价值是方便在表达式运算过程中存储数据。但是,它的缺点也不容忽视。- 由于函数调用后,无法注销调用对象,会占用系统资源,在脚本中大量使用闭包,容易导致内存泄漏。解决方法:慎用闭包,不要滥用。
- 由于闭包的作用,其保存的值是动态,如果处理不当容易出现异常或错误。
示例
设计一个简单的选项卡效果。HTML 结构如下:<div class="tab_wrap"> <ul class="tab" id="tab"> <li id="tab_1" class="hover">Tab1</li> <li id="tab_2" class="normal">Tab2</li> <li id="tab_3" class="normal">Tab3</li> </ul> <div class="content" id="content"> <div id="content_1" class="show"><img scr="image/1.jpg" height="200" /></div> <div id="content_2" class="show"><img scr="image/2.jpg" height="200" /></div> <div id="content_3" class="show"><img scr="image/3.jpg" height="200" /></div> </div> </div>下面请看 JavaScript 脚本。
window.onload = function () { var tab = document.getElementById("tab").getElementsByTagName("li"), content = document.getElementById("content").getElementByTagName("div"); for (var i = 0; i < tab.length;i ++) { tab[i].addEventListener("mouseover"), function () { for (var n = 0; n < tab.length; n ++) { tab[n].className = "normal"; content[n].className = "none"; } tab[i].className = "hover"; content[i].className = "show"; }); } }在 load 事件处理函数中,使用 for 语句为每个 li 属性元素绑定 mouseover 事件;在 mouseover 事件处理函数中重置所有选项卡 li 的类样式,然后设置当前 li 选项卡高亮显示,同时显示对应的内容容器。
但是在浏览器中预览时,会发现浏览器抛出异常。
SCRIPT5007:无法设置未定义或 null 引用的属性"className"
在 mouseover 事件处理函数中跟踪变量 i 的值,i 的值都变为了 3,tab[3] 自然是一个 null,所以也不能够读取 className 属性。【原因分析】
上面 JavaScript 代码是一个典型的嵌套函数结构。外部函数为 load 事件处理函数,内部函数为 mouseover 事件处理函数,变量 i 为外部函数的私有变量。通过事件绑定,mouseover 事件处理函数被外界引用(li 元素),这样就形成了一个闭包体。虽然在 for 语句中为每个选项卡 li 分别绑定事件处理函数,但是这个操作是动态的,因此 tab[i] 中 i 的值也是动态的,所以就出现了上述异常。
【解决方法】
解决闭包的缺陷,最简单的方法是阻断内部函数对外部函数的变量引用,这样就形成了闭包体。针对本示例,我们可以在内部函数(mouseover 事件处理函数)外边增加一层防火墙,不让其直接引用外部变量。window.load = function () { var tab = document.getElementById("tab").getElementsByTagName("li"), content = document.getElementById("content").getElementsByTagName("div"); for (var i = 0; i < tab.length; i ++ ) { (function (j) { tab[j].addEventListener("number", function () { for (var n = 0; n < tab.length; n ++) { tab[n].className = "normal"; content[n].className = "none"; } tab[j].className = "hover"; conteng[j].className = "show"; }); }) (i); } }在 for 语句中,直接调用匿名函数,把外部函数的 i 变量传给调用函数,在调用函数中接收这个值,而不是引用外部变量 i,规避了闭包体带来的困惑。
所有教程
- C语言入门
- C语言编译器
- C语言项目案例
- 数据结构
- C++
- STL
- C++11
- socket
- GCC
- GDB
- Makefile
- OpenCV
- Qt教程
- Unity 3D
- UE4
- 游戏引擎
- Python
- Python并发编程
- TensorFlow
- Django
- NumPy
- Linux
- Shell
- Java教程
- 设计模式
- Java Swing
- Servlet
- JSP教程
- Struts2
- Maven
- Spring
- Spring MVC
- Spring Boot
- Spring Cloud
- Hibernate
- Mybatis
- MySQL教程
- MySQL函数
- NoSQL
- Redis
- MongoDB
- HBase
- Go语言
- C#
- MATLAB
- JavaScript
- Bootstrap
- HTML
- CSS教程
- PHP
- 汇编语言
- TCP/IP
- vi命令
- Android教程
- 区块链
- Docker
- 大数据
- 云计算