(三) js闭包
1. 案例引入
先看一个简单的需求:
- 我们想要实现一个简单的累加器, 每调用一次累加函数, 变量就加 1
这真的是一个简单的需求, 我们可以立马写出来
var counter = 0
function addCounter(){
counter++
console.log(counter);
}
addCounter() // 1
addCounter() // 2
addCounter() // 3
但是你有没有想过: 为什么 counter
可以一直被累加 ?
这里就牵扯到了js中的 垃圾回收机制
所谓的垃圾回收, 就是确定哪个变量不再使用, 然后释放它占用的内存, 这是一个周期性的检测
但是对于全局变量而言, 垃圾回收机制并不知道它什么时候不再使用, 因此也就无法释放其占用的内存, 全局变量也就会一直被保存在内存当中, 这也就是我们可以一直使用它的原因
其实从执行上下文的角度来看, 全局变量的声明是在全局上下文中进行的, 被一直保存在GO中, 而全局上下文是在我们关闭浏览器时才会被销毁, 因此其他函数就可以通过其保存的上下文环境 (也就是作用域链), 到GO中引用counter
这样, 第一个问题我们就解决了
好, 我们再来思考另外一个问题, 如果另外一个函数也用到了 counter
, 或者另外一个开发者声明了一个同样的全局变量 counter
,那么这势必会影响上面累加器的运作, 因为 counter
的值被其他函数 / 变量声明 改变了, 这显然是我们所不希望的
那既然不想counter被其他因素所改变, 那我们把counter作为累加器的内部变量不就行了么, 这样只有函数自己能用, 行吗? 显然不行
因为函数在执行完毕后, 会销毁其所在的上下文环境, 保存在AO中的变量会被垃圾回收机制释放, 对于无法一直保存在内存中的变量, 我们显然无法一直对他进行操作
那有没有办法可以解决这个需求: 我们既想把 counter 声明为局部变量, 又让他一直保留在内存中呢 ?
答案是可以的, 这就是我们需要闭包的原因
2. 什么是闭包
个人理解:
-
我们知道: 执行上下文是一个运行时环境, 当函数执行完毕, 函数上下文会被释放, 局部变量自然也就无法一直保留
-
而闭包的存在, 就是帮助我们把这些会被释放的上下文保存到内存中,使其一直存在
MDN中给出闭包的如下定义:
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
示例
function fn() {
var count = 0
function addCount() {
count++
}
addCount()
}
fn()
上面有没有形成闭包呢? 我们根据MDN的定义来分析一下:
addCount
是不是一个函数? 是addCount
对其周围状态有没有引用 ? 有, 引用了所处上下文环境中的count
既然形成了闭包, 那能不能完成我们上面的需求呢: 我们既想把 counter 声明为局部变量, 又让他一直保留在内存中
我们来接着调用addCount
函数, 看三次调用以后, count
的值是不是3
注意看 watch中count的变化
通过演示我们发现, count
并没有一直被保留在内存中, 而是每次调用都被初始化为了0
这显然无法完成我们的需求
ECMAScript中, 给出如下定义:
-
从理论角度:所有的函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
-
从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
根据ECMAScript给出的定义, 我们再来对比MDN给出的定义, 我们发现, MDN中对闭包的定义其实就是ECMAScript中的闭包的理论角度,
但这对我们的需求并没有实际的作用, 那我们再来看 从实际角度出发给出的定义
示例
function fn() {
var count = 0
function addCount() {
count++
}
return addCount
}
var add = fn()
add()
add()
add()
注意: 与上面代码不同的是, 我们这次将addCount函数作为返回值传递到外部
注意此时watch中count的变化
我们可以清晰的看到, 内部变量count
, 在函数外部被一直引用, 而且实现了值的累加 !
也就是说, count
被一直保留在了内存之中, 这也真正实现了我们上面所一直所提的需求:
- 我们既想把 counter 声明为局部变量, 又让他一直保留在内存中
其实说了这么多, 可能还是比较抽象, 我们先回顾以下闭包的理解:
-
我们知道: 执行上下文是一个运行时环境, 当函数执行完毕, 函数上下文会被释放, 局部变量自然也就无法一直保留
-
而闭包的存在, 就是帮助我们把这些会被释放的上下文保存到内存中,使其一直存在
我们举个例子:
假设我家院子 (内存)里有一个大水缸 , 水缸里面有满满的水 (全局变量), 这个水过路口渴的人 (其他函数等)都可以取来喝,但是不同的人都来喝, 不就把我家的水给 污染 了么, 况且这是我家, 你们怎么能想喝就喝呢, 于是我想出了这样一个办法:
我先把水(此时变成局部变量)都移到纸箱(外层函数)里, 但是纸箱存不住水 (函数被释放)呀, 于是我又马上把这些水都灌进水瓶 (内层函数)里, 然后把这些水瓶放在纸箱里 (函数嵌套), 这样我们的水得以保存, 但是别人也想喝怎么办, 那就按照我的说明去纸箱里拿水瓶 (暴露给外部的引用, 到此形成闭包), 并且你只能通过暴露出的水瓶来喝水
后来家家装了自来水, 大家也都不需要来我家喝水了, 于是我就把所有的水瓶丢掉(清除引用, 释放闭包的内存),
综上:
怎么理解闭包:
-
我们知道: 执行上下文是一个运行时环境, 当函数执行完毕, 函数上下文会被释放, 局部变量自然也就无法一直保留
-
而闭包的存在, 就是帮助我们把这些会被释放的上下文保存到内存中,使其一直存在
什么才算闭包:
从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
3. 闭包的作用
个人理解: 闭包的主要作用就是保存函数上下文环境, 延长变量的声明周期
4. 闭包的应用
1. 访问函数内部的变量, 也就是上面一直提的需求
function fn() {
var count = 0
function addCount() {
count++
}
return addCount
}
var add = fn()
add()
2. 定时器传值, 本质与上面一致
function func(param) {
return function () {
alert(param)
}
}
var f1 = func(1)
setTimeout(f1, 1000)
3. 利用闭包判断数据类型
function isType(type) {
return function (target) {
return `[object ${type}]` === Object.prototype.toString.call(target)
}
}
const isArray = isType('Array')
console.log(isArray([1, 2, 3])); // true
console.log(isArray({}));