JS中你必须要了解的闭包
前言:
它是指函数与其周围的词法环境的组合。这意味着函数可以访问其定义时所在的词法作用域中的变量,即使函数在定义之后被移到了其他地方执行也是如此。闭包在 JavaScript 中经常用于创建私有变量、模块化设计以及异步编程等方面。
正文:
作用域链:
JavaScript 中的作用域链是指在函数嵌套结构中,每个函数都会创建自己的作用域,而在函数内部可以访问外部函数中声明的变量。作用域链的形成是由函数在定义时捕获其周围的词法环境所决定的。
在 JavaScript 中,当函数被调用时,会创建一个执行环境(execution context),该执行环境包括了该函数的作用域。当函数在内部访问一个变量时,JavaScript 引擎会首先在当前函数的作用域中查找该变量,如果找不到,则会向上一级作用域链中查找,直到找到该变量或者到达全局作用域。
function bar() {
console.log(myName);
}
function foo() {
var myName = 'Tom'
bar()
}
var myName = 'Jerry'
foo()
// 这种关系叫做作用域链
解析:
V8引擎先编译再执行:
- 创建一个全局作用域,这里用全局上下文来讲,形成词法环境和变量环境两块。词法环境由let和const定义构成,所以没有。变量环境中bar,foo赋值后为function,myName赋值是Jerry。
- 再是foo的调用。
- 创建一个foo的执行上下文,形成词法环境和变量环境。变量环境中myName重新的赋值是Tom,再到bar的调用。
- 同理,创建bar的执行上下文,词法环境和变量环境均为空,再是myName 的执行。
- bar中没有myName的赋值,由outer指向bar的词法作用域,即全局作用域,跳到myName 的赋值为Jerry,最后打印Jerry。
更加形象的可以根据我绘制的图片来理解:
这里解释一下outer声明的作用域,可以通俗的理解是函数声明所在位置的作用域,因为foo和bar均在全局声明,所以foo和bar的词法作用域均是全局作用域,上述我们把这种关系称作作用域链。
词法作用域的位置
let count = 1
let a = 1
function main() {
let count = 2
let a = 2
function bar() {
let count = 3
function foo() {
let count = 4
console.log(a);
}
foo()
}
bar()
}
main()
根据作用域链的关系,依次形成全局,main,bar,foo的作用域,逐层递增递增的寻找a值,a最后的值为2。
这里能更清晰的确认词法作用域和全局作用域的关系,就是这样一个简单的逻辑。
闭包(重点)
在JavaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量,当内部函数被拿到外部函数之外调用时, 即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。
这里可能比较拗口,用代码来解释:
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i);
}
}
for(var j = 0; j < arr.length; j++) {
arr[j]()
}
可以先自己想想这段代码的执行结果。
结果是十个10,在for的循环下,依次打印十个
function() { console.log(i); }
再是到第二个for循环,把数组函数体取出来再调用,十个函数都执行打印,因为i值最后是10,最后打印十个10。所以为什么不是0-9?又该如何形成?
解法一:
函数function中的outer指向全局,function声明在全局中,因为for不构成作用域。若是让function不声明在全局,就让for形成作用域。这里就可以用let和const取代var,let和const和{}形成块级作用域,那么function就声明在了let形成的作用域中。
此时形成的第一个块级作用域包含了第一个i的值,第二个块级作用域就包含了第二个i值...依次形成十个不同的块级作用域且i值均不同,而function声明在了每个块级作用域之中,此时log打印的值就是0-9了。
var arr = []
for (let i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i);
}
}
for(var j = 0; j < arr.length; j++) {
arr[j]()
}
根据图片更形象的理解:
解法二:
先看这段代码:
function foo() {
function bar() {
var age = 18
console.log(myName);
}
var myName = 'Tom'
return bar
}
var myName = 'Jerry'
var fn = foo()
fn()
bar的词法作用域是foo的作用域,var fn = foo()
,将fn这个变量赋值为foo的执行结果,因为foo最终的执行结果是返回一个bar,所以此时的fn的值即为bar。
14行代码fn的调用,即为bar的调用,最终执行打印myName。
根据作用域链的关系,先后生成全局,foo,bar的执行上下文,词法环境没有,变量环境中依次存入对应值。当执行完foo的执行上下文时,为了不占用V8引擎中的内存,就要在执行完毕的时候消除这段执行上下文,最后执行到bar的调用打印myName。本应该 bar中outer的指向是foo的作用域,由于foo的作用域被清除,outer没有对象指定,此时按道理打印会报错。但是,最终的结果并没有报错,这是为什么?
此时长安把上面的思路整理成图片,更具大家形象化的理解:
插入对解法二的原因解释:
域中的上下文执行完毕时,这里你可以想象一个清洁工就会过来访问上下文:“兄弟,你执行完毕没?OK的话那我就将你执行完剩下的空壳清扫了。”一般正常的上下文对象都会来声OK。但是当问到foo的执行上下文时,foo确是这样说的:“哥们,不是我不让你清扫,而是我自己都不知道我有没有执行完,虽然我体内诞生了bar的函数体,但是被放到外面调用了,它并没有受我的掌控,更主要的是,它依旧访问了我【foo】体内的变量。”清洁工哥们就很疑惑,我到底该不该会回收你呢,不回收又会造成空间的浪费?
V8此时这样设定的:这时判断到底是哪些如bar这样的崽子,它到底调用了哪些变量,直接就让清洁工留下这些变量的空间,其他的清理掉,此时就是最优解。
所以此时foo的执行上下文依旧销毁,但是在原来的位置放上了新的空间,防止报错。这个空间小背包储存了myName = 'Tom', 所以bar的outer最终找到了foo中的这个小背包,访问了myName 的值打印Tom。
这个小背包空间就叫闭包,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。
闭包的作用:实现变量私有化
function add() {
let count = 0
function fn() {
count++
return count
}
return fn
}
var res = add()
console.log(res());
console.log(res());
console.log(res());
根据闭包关系,最终打印出1 2 3
总结:
了解好作用域链的关系,知道oute指向的作用域,这样才能更好的理解闭包的关系,内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。
转载自:https://juejin.cn/post/7365793739891884032