[toc]
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
下面就是我的学习笔记,对于Javascript初学者应该是很有用的。
一.引入闭包
需求: 点击某个按钮, 提示"点击的是第n个按钮"
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//遍历加监听
for (var i = 0, length = btns.length; i < length; i++) {
var btn = btns[i]
btn.onclick = function () {
alert('第' + (i + 1) + '个')
}
}
</script>
为什么会出现点击的不是依次对应的button,而是最后一个?
1.1 解决办法一:将btn所对应的下标保存在btn上
for (var i = 0, length = btns.length; i < length; i++) {
var btn = btns[i]
//将btn所对应的下标保存在btn上
btn.index = i
btn.onclick = function () {
alert('第' + (this.index + 1) + '个')
}
}
1.2 解决办法二:==利用闭包==
for (var i = 0, length = btns.length; i < length; i++) {
//立即执行函数
(function (j) {
var btn = btns[j]
btn.onclick = function () {
alert('第' + (j + 1) + '个')
}
})(i)
}
1.3 解决办法三:ES6新语法:let关键字加上代码块
for (let i = 0, length = btns.length; i < length; i++) {
let btn = btns[i];
btn.onclick = () => {
alert(`第${i+1}个`);
}
}
二.闭包的概念
2.1 MDN的官方解释
说人话就是只要一个函数内部引用了外部的数据,就形成了闭包。如:
function f1(){
const a = 10
function f2(){
console.log(a) // f2引用了f1的数据a, 形成了闭包
}
}
闭包的一个好处就是可以把数据安全地缓存起来,方便在一定的时候使用它。
2.2 闭包理解的总结
1.如何产生闭包?
==当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
==
2. 闭包到底是什么?
使用chrome调试查看
理解一: 闭包是嵌套的内部函数(绝大部分人)
理解二: 包含被引用变量(函数)的对象(极少数人)
注意: 闭包存在于嵌套的内部函数中
3. 产生闭包的条件?
函数嵌套
内部函数引用了外部函数的数据(变量/函数)
function fn1() {
var a = 2
var b = 'abc'
function fn2() { //执行函数定义就会产生闭包(不用调用内部函数)
console.log(a)
}
// fn2()
}
fn1()
function fun1() {
var a = 3
var fun2 = function () {
console.log(a)
}
}
fun1()
三.闭包的作用
闭包作用:
- ==使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)==
- ==让函数外部可以操作(读写)到函数内部的数据(变量/函数)==
闭包问题:
函数执行完后, 函数内部声明的局部变量是否还存在? 一般是不存在, 存在于闭中的变量才可能存在
在函数外部能直接访问函数内部的局部变量吗? 不能, 但我们可以通过闭包让外部操作它
四.闭包的生命周期
产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
死亡: 在嵌套的内部函数成为垃圾对象时
function fn1() {
//此时闭包就已经产生了(函数提升, 内部函数对象已经创建了)
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
五.闭包的原理
5.1 作用域和作用域链
为什么闭包能实现这样的效果呢,其原理在于js中存在作用域和作用域链。
1.作用域:
变量或者函数的有效访问范围,一个变量或者函数只能在一定的范围内使用,我们称这个范围为作用域。
2.作用域链:
多个作用域之间互相嵌套,就会形成一定的访问规则,这个规则我们称为作用域链
- 当访问一个数据(变量或者函数)时,如果当前作用域存在这个数据,优先使用这个当前作用域内的数据
- 如果当前作用域不存在这个数据,就会往上级作用域依次查找
- 如果直到全局使用域都没找到,则会报错
闭包由两级至多级作用域组成。在一个局部作用域中声明了一个数据,于是只有这个作用域内部可以访问。所以我们在这个使用域内部再声明函数,此时内部的函数是能访问外层函数的数据的。如果我们想操作这个局部数据,就把一个函数返回出去,在外部接收到这个函数,于是就可以在外部通过内部函数操作内部数据了。
六.闭包的应用
- 循环遍历加监听: 给多个li加点击监听, 读取当前下标
- 模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为
- JS框架(jQuery)大量使用了闭包
6.1 闭包的应用一:定义JS模块(写法一)
闭包的应用2 : 定义JS模块
* 具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 只向外暴露一个包信n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
function myModule() {
//私有数据
var msg = 'My atguigu'
//操作数据的函数
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//向外暴露对象(给外部使用的方法)
return {
doSomething: doSomething,
doOtherthing: doOtherthing
}
}
var module = myModule()
module.doSomething()
module.doOtherthing()
6.2 闭包的应用二:定义JS模块(写法二)
(function () {
//私有数据
var msg = 'My atguigu'
//操作数据的函数
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//向外暴露对象(给外部使用的方法)
window.myModule2 = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})()
myModule2.doSomething()
myModule2.doOtherthing()
6.3 模拟银行存取钱案例
比如我们要做一个银行里面的存取钱的效果,如果直接把数据暴露出来,会很危险
// 你存在在银行的钱
let money = 0
// 存钱
function add(n){
money += n
}
// 取钱
function reduce(n){
money -= n
}
此时是我们在全局使用域里面实现了一个数据,我们可以随意在控制台里面操作它
我们就要想办法把这个数据"私有化",使用一个函数把它包起来
function f(){
let money = 0
function add(n){
money += n
}
function reduce(n){
money -= n
}
}
此时money是一个局部变量,只有内部能操作了,而这时add和reduce函数已经形成了闭包。
但是通常写成这们是没有意义的,因为我们没法操作money了,所以我们希望在函数f之外操作money时,我们可以把add和reduce作为f函数的返回值,就可以在外面对money进行操作
又为了我们能看到money的当前值,我们给返回的数据加上一个用户用于查看余额的函数
function f(){
let money = 0
function add(n){
money += n
}
function reduce(n){
money -= n
}
function query(){
console.log(money)
}
return {add,reduce,query}
}
此时只要我们调用一下,就能得到一个对象是,这个对象的方法可以操作money,别的方式操作不了了
let bank = f()
bank.add(100) // money:100
bank.reduce(100) // money: 0
沿着这个思路,我们把银行使用闭包进行模拟:
function f(){
let vault = {} // 一个金库,存在所以人的钱,使用名字为键,金额为值
// name 是用户的名字,value是要存的钱
function add(name,money){
if(typeof money !== 'number' && Object.is(money,NaN) && money <=0){
return console.error("请输入正确的金额")
}
if (!vault[name]) vault[name] = 0
vault[name] += money
}
function reduce(name,money){
if(!(name in vault)) return console.error('没有该用户')
if(typeof money !== 'number' && Object.is(money,NaN) && money <=0)
return cosole.error('请输入正确的金额')
if(money > vault[name]) return console.error('你没有这么多钱')
vault[name] -= money
}
function query(name){
console.log(vault[name])
}
return {add,reduce,query}
}
七.闭包的缺点及解决
7.1 内存溢出与内存泄露
7.1.1 内存溢出
一种程序运行出现的错误
当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
7.1.2 内存泄露
占用的内存没有及时释放
内存泄露积累多了就容易导致内存溢出
常见的内存泄露:
意外的全局变量
没有及时清理的计时器或回调函数
闭包
7.2 闭包的缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 闭包容易造成内存泄露
7.3 闭包问题解决
- 能不用闭包就不用
- 解决: 及时释放 : f = null; //让内部函数对象成为垃圾对象
function fn1() {
var arr = new Array[100000]
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null //让内部函数成为垃圾对象-->回收闭包
7.4 内存泄漏实例分析
闭包虽然可以把安全的把数据缓存下来,但是同样带来了一些问题。因为闭包引用和外部函数的数据,导致外部函数在运行结束时没法对里面的数据回收,会一直占用程序的内存,导致程序运行变慢。———— 我们管无法被回收的数据称为内存泄漏
至于为什么会内存泄漏,我们需要大概了解一下 垃圾回收机制
简单来说,每个数据会有一个被使用的标记,每当有一个地方使用这个数据,这个标记的数量就会+1,当这个标记为0时,就会进入等待回收的状态。
function fn(){
var a = 10
console.log(a)
}
fn()
上面这段代码,我们在调用fn时,会在第3行使用到了a,引用数量+1,使用过后,引用数量-1,这个时候就是0了,于是当fn调用完成,a这个数据就会进入回收流程。
但是如果我们这样写代码
function fn(){
let a = 10
function f2(){
console.log(f2)
}
}
let f = fn()
当fn执行完成后,发现在f2里面有使用到a,此时引用+1,本来在fn结束后,f2也要被回收的,但是发现在外面有一个f保存了f2的引用,f2的引用+1,所以导致f2不能被回收,也就导致了a不能被回收,于是造成了内存泄漏。
如果当我们确实不需要使用这个闭包了,我们可以手动的把引用释放
f = null
此时f就不再对f2进行引用,f2的引用-1,f2的引用为0,当f2被回收后,就没有别的地方用到a了,a的引用-1,于是a的引用也变为0,最终a也被回收。
所以大家在使用闭包的时候,一定要注意:==因为不是所有时候我们都能注意到什么时候这个闭包不用了,手动释放的。因此能不用闭包,就尽量不要使用了。==
八.闭包面试题
8.1 面试题一:
//代码片段一
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
}
};
alert(object.getNameFunc()()); //? the window
//代码片段二
var name2 = "The Window";
var object2 = {
name2: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name2;
};
}
};
alert(object2.getNameFunc()()); //? my object
8.2 面试题二:
function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3) //undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3) //undefined,0,1,2
var c = fun(0).fun(1)
c.fun(2)
c.fun(3) //undefined,0,1,1