闭包
只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
如何产生闭包
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量或函数时, 就产生了闭包。
闭包到底是什么?
- 理解一: 闭包是嵌套的内部函数(绝大部分人)
- 理解二: 包含被引用变量 or 函数的对象(极少数人)
注意: 闭包存在于嵌套的内部函数中。
产生闭包的条件
- 1.函数嵌套
- 2.内部函数引用了外部函数的数据(变量/函数)。
来看看条件2:
function fn1() {
function fn2() {
}
return fn2;
}
fn1();
上面的代码不会产生闭包,因为内部函数fn2并没有引用外部函数fn1的变量。
PS:还有一个条件是外部函数被调用,内部函数被声明。比如:
function fn1() {
var a = 2
var b = 'abc'
function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数)
console.log(a)
}
}
fn1();
function fn3() {
var a = 3
var fun4 = function () { //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前
console.log(a)
}
}
fn3();
常见的闭包
- 将一个函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用。
闭包1:将一个函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。
上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。
也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。
闭包2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
alert(msg)
}, time)
}
showDelay('atguigu', 2000)
上面的代码中,闭包是里面的funciton,因为它是嵌套的子函数,而且引用了外部函数的变量msg。
闭包的作用
- 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
我们让然拿这段代码来分析:
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2;
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
作用1分析:
上方代码中,外部函数fn1执行完毕后,变量a并没有立即消失,而是保存在内存当中。
作用2分析:
函数fn1中的变量a,是在fn1这个函数作用域内,因此外部无法访问。但是通过闭包,外部就可以操作到变量a。
达到的效果是:外界看不到变量a,但可以操作a。
比如上面达到的效果是:我看不到变量a,但是每次执行函数后,让a加1。当然,如果我真想看到a,我可以在fn2中将a返回即可。
回答几个问题:
- 问题1. 函数执行完后, 函数内部声明的局部变量是否还存在?
答案:一般是不存在, 存在于闭包中的变量才可能存在。
闭包能够一直存在的根本原因是f
,因为f
接收了fn1()
,这个是闭包,闭包里有a。注意,此时,fn2并不存在了,但是里面的对象(即闭包)依然存在,因为用f
接收了。
- 问题2. 在函数外部能直接访问函数内部的局部变量吗?
不能,但我们可以通过闭包让外部操作它。
闭包的生命周期
- 产生: 嵌套内部函数fn2被声明时就产生了(不是在调用)
- 死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了)
闭包的应用:定义具有特定功能的js模块
- 将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能。
方式一
(1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用)
function myModule() {
//私有数据
var msg = 'Smyhvae Haha'
//操作私有数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase()); //字符串大写
}
function doOtherthing() {
console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写
}
//通过【对象字面量】的形式进行包裹,向外暴露多个函数
return {
doSomething1: doSomething,
doOtherthing2: doOtherthing
}
}
上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。
(2)index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>05_闭包的应用_自定义JS模块</title>
</head>
<body>
<!--
闭包的应用 : 定义JS模块
* 具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 【重要】只向外暴露一个包含n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule();
module.doSomething1();
module.doOtherthing2();
</script>
</body>
</html>
方式二
同样是实现方式一种的功能,这里我们采取另外一种方式。
(1)myModule2.js:(是一个立即执行的匿名函数)
(function () {
//私有数据
var msg = 'Smyhvae Haha'
//操作私有数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase())
}
function doOtherthing() {
console.log('doOtherthing() ' + msg.toLowerCase())
}
//外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象
window.myModule = {
doSomething1: doSomething,
doOtherthing2: doOtherthing
}
})()
(2)index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>05_闭包的应用_自定义JS模块2</title>
</head>
<body>
<!--
闭包的应用2 : 定义JS模块
* 具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 只向外暴露一个包信n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->
<!--引入myModule文件-->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
myModule.doSomething1()
myModule.doOtherthing2()
</script>
</body>
</html>
上方两个文件中,我们在myModule2.js
里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。
总结:
当然,方式一和方式二对比后,我们更建议采用方式二,因为很方便。