什么是闭包
要解释闭包,可以从广义和狭义上去理解。
从广义上来讲,所有的函数就是闭包。
从狭义上来讲,必须要同时满足 2 个条件:
-
一个函数中要嵌套一个内部函数,并且内部函数要访问外部函数的变量
-
内部函数要被外部引用
下面我们来举个例子:
function example(){
var say = "hello word"
return function(){
console.log(say)
}
}
let doSomeSth = example()
doSomeSth (); // hello word
doSomeSth (); // hello word
在这个 example 函数中,返回了一个函数,并且在这个内部函数中访问了 say 这个局部变量。调用 example 函数并将结果赋给 doSomeSth 变量,这个 doSomeSth 指向了 example 函数中的内部函数,然后调用它,最终输出 say的值hello word。
按照对函数的理解,这个 say变量应该当 example函数调用完后就销毁了,后续为什么还能通过调用 doSomeSth 方法访问到这个变量呢?
这就是因为闭包起了作用。返回的内部函数和它外部的变量 say 实际上就是一个闭包。
闭包的实质,就是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使离开了创造它的环境也不例外,这里的say就是自由变量。
自由变量可以理解成跨作用域的变量,比如子作用域访问父作用域的变量。
总结一下:变量 say 属于 example 函数的局部变量,并被内部函数使用,同时该函数也被返回出去。在外部,通过调用 example得到了内部函数的引用,后面多次调用这个内部函数,仍然能够访问到 say 变量。这样 say 作为自由变量被内部函数引用,即使创造它们的函数 example 执行完了,变量 say 依然存在,因此,这就是闭包。
为什么要使用闭包
举个简单的例子:
function example(){
var say = "hello word"
console.log(say);
}
example(); // hello word
console.log(say); // 报错
在这个例子中有一个名为 example 的函数,对它进行调用后。JavaScript 引擎会创建一个 example 函数的执行上下文,在其中声明 say 变量并赋值。
当该方法执行完后,上下文被销毁,say 变量也会跟着消失。这是因为 say 变量属于 example 函数的局部变量,它作用于 example 函数中,会随着 example 的执行上下文创建而创建,销毁而销毁。
这个例子和上面的那个例子形成了对比,继而也说明了,闭包可以将一些函数的局部变量保存下来,供之后的代码使用,为什么要使用这样的方法而不直接使用全局变量声明呢,这就涉及到全局命名变量的污染问题。
闭包的原理
那么闭包为什么会有这样的不符合一般函数知识的原理呢?这里需要从两个角度来认识闭包的原理
1.函数内部为什么可以访问外部函数的变量?
这个问题很好回答,在函数创建的时候,在其内部会产生一个 scope 属性,该属性指向创建该函数的执行上下文中的作用域链对象。作用域链对象包含了该上下文中的 VO/AO 对象,还有 scope 对象,当内部函数中找不到对应的变量,它就会到 scope 指向的对象中找。该对象保存着外部上下文中的作用域链对象,从该作用域链中就能找到对应的变量。这就是为什么函数内部可以访问到外部函数变量的原因。
2.为什么当外部函数的上下文执行完以后,其中的局部变量还是能通过闭包访问到呢?应该销毁了,找不到了才对啊。
其实函数在执行上下文时, scope 属性在函数创建的时候就已经确定下来了,所以即使外部函数的上下文结束了,但内部的函数只要不销毁(被外部引用了,就不会销毁),它当中的 scope 就会一直引用着刚才上下文的作用域链对象,那么包含在作用域链中的变量也就可以一直被访问到。
所以,所有函数都是拥有 scope 属性的,换一句话就是文章开头提到的广义定义,所有的函数就是闭包
闭包的优缺点
闭包的优点,主要有以下 2 点:
-
通过闭包可以让外部环境访问到函数内部的局部变量。
-
通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁。
举个例子:
不使用闭包:
let count = 0; // 全局计数器
let compute = function () { // 将计数器加1
count++;
console.log(count);
}
for (let i = 0; i < 100; i++) {
compute(); // 循环 100 次
}
使用闭包:
var compute = function () {
var count = 0; // 局部变量
return function () {
count++; // 内部函数访问外部变量
console.log(count);
}
}
var func = compute(); // 引用了内部函数,形成闭包
for (var i = 0; i < 100; i++) {
func();
}
在这个例子中就不再使用全局变量,其中 count 这个局部变量依然可以被保存下来。
闭包的缺点:
其实闭包本身并没有什么明显的缺点,我们选择闭包的一部分原因是我们想主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量。把这些变量放在闭包中和放在全局作用域中,对内存方面的影响是一样的,局部变量本来应该在函数退出时被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。当然也不排除变量间的相互循环引用,导致垃圾处理机制无法及时回收这些变量,我们只需要把循环引用中的变量设为 null 即可。
本文章取自本人JS语言导师谢老师的学习总结,同时也感谢谢老师对我的谆谆教诲,感谢他带我走上前端这条道路,并让我为之不断向前