闭包是一种现象。基于js引擎OC的一种运用作用域的一种手段。
这种现象是,有权访问另一个函数作用域中的变量。
也就是说闭包保存了函数执行完毕之后的执行上下文, 上下文按照我的理解就是此时的AO和GO, 即也就是说闭包保存了函数执行完毕之后的执行上下文, 上下文按照我的理解就是此时的AO和GO, 即[[scope]]
一旦产生了闭包,那么函数的上级作用域就可以访问到,所以说闭包产生的原因 存在上级作用域的引用。
引入下面这张图,完整的描述和闭包相关的知识点。
那么怎么样才能够产生闭包呢?
一、何时会产生闭包
当内部函数被返回到外部并保存时,一定会产生闭包,闭包会产生原来的作用域链而不会销毁. 即使自己的被执行完毕,也只是会清除自己的AO,而它的上文作用域不会有影响,依旧在内存当中.
表现的形式,可以是return Function 有可以是return [Function1, Function2] ,甚至是对象函数也算在内.return {handle: Function}
二、栗子🌰
1. 闭包带来的作用
- 保护私有变量不受外部的干扰
- 形成不被销毁的栈内存
- 将上级作用域引用保存下来,实现方法和属性的私有化
var n = 10;
function fn() {
var n = 20;
function f() {
n++;
console.log(n);
}
f();
return f;
}
var x = fn();
x();
x();
console.log(n);
结果如下:
// 21
// 22
// 23
// 10
n 由于被闭包所引用,闭包没有在销毁,它一直存在,且不被全局的n = 10 所影响;
每调用一次,n就自增1,说明n的变量没有被销毁, 将本来存在栈内存中的变量给变成了堆内存中,这个在预编译部分有很好的解释;
闭包引用的内存存储在堆内存当中
f()的上级作用域的n = 20 很好的被保存了下来,这个时候就随便你进行任何操作;
2.自执行函数也是闭包(IIFE)
var n = '蚂蚱'
(function p() {
console.log(n) // 蚂蚱
})()
循环赋值产生的闭包问题:
我们可以使用闭包来解决for循环中全局变量的问题:
对照组:
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(j)
}, 1000)
}
i是全局作用域的变量,而且setTimeout是宏任务,所以需要等同步的任务运行完毕之后才放入到同步队列当中。结果如下:
// 10
// 10
.... 10次10
for(var i = 0; i < 10; i++) {
(function() {
setTimout(function() {
console.log(i)
}, 1000)
})()
}
- 结果
// 1 ,2 ,3 ,4 ,5, 6, 7, 8,9 , 10
立即执行函数形成了闭包,闭包存在引用变量的保护,所以每一次的循环都产生了一个新的作用域,而这个作用域中的变量放入了堆内存当中,并没有被立即销毁. 所以产生了如上的现象.
以前我一直以为产生这个的原因是因为定时器是宏观任务, 循环中是微观任务,所以宏观任务是需要等到微观任务完结了才能够继续下去.现在才意思到不是这么一回事.应该是由于产生了闭包的副作用. 因为对照组出现的时机10个10.
3.使用回调函数就是使用闭包
window.name = 'www'
setTimeout(function timeHandle() {
console.log(window.name);
}, 100)
3.1 函数数组
var data = []
for (var i = 0; i < 3; i++) {
data[i] = function() {
console.log(i)
}
}
data[0]()
data[1]()
data[2]()
-
结果
3 、3、3
造成上面这样的结果,是应该i在这里面是全局变量.
为了解决这个问题,有如下两种方式
解决的办法1: let,能够形成块级作用域
var data = []
for (let i = 0; i < 3; i++) {
data[i] = function() {
console.log(i)
}
}
data[0]()
data[1]()
data[2]()
解决的办法2: 使用自执行函数
var data = []
for (var i = 0; i < 3; i++) {
(function (j) {
data[j] = function () {
console.log(j)
}
})(i)
}
data[0]()
data[1]()
data[2]()
上面的i传进行重新什么为j.外面的data[]一直在引用,所以没有对里面的j进行了销毁了,形成了闭包,形成了不销毁的私有作用域
4. 对于闭包的副作用,对引用的变量使其存储到了堆内存中的理解
var result = [];
var a = 3;
var total = 0;
function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a;
console.log(total);
}
}
}
foo(1);
result[0]();
result[1]();
result[2]();
-
结果
3、6、9
这里有五个变量;
- 值得注意的是,foo函数内部的形参自形成了作用域,所以它在里面是1;
- 当在foo循环内部被外部的变量给存储了函数之后,就产生了外部变量能够引用内部函数的现象,故而形成了闭包.
- 在foo()函数内部,i相对于data[index]是全局变量,所以data[index]任何参数,i都为3
- 所以此时在堆内存的变量唯有total中,它被闭包所引用,所以不会销毁,所以造成了如此的结果.
三、使用闭包需要注意
容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。
四、其他容易混淆的点
很多时候你看到的例子都是return 一个函数来构成闭包. 是不是说闭包一定要return一个函数出去呢?
function test() {
var a = 1
function add() {
a++
console.log(a)
}
this.add = add
}
test()
add() // 2
add() // 3
add() // 4
这里并没有return一个函数出去,但是依旧能够保存test()上面的a值.
在立即执行函数中可以更加优雅,这也是es5时代做插件的入口方法.
;(function() {
var a = 1
function add() {
a++
console.log(a)
}
windows.add = add
})()
add() // 2
add() // 3
add() // 4
上面也实现了闭包.
上面两个例子已经很直接的告诉你,return一个函数出去,只是实现闭包的一个手段.其目的是为了把内部函数作用域挂载在相对全局的作用域中.