闭包:是指在一个函数内部定义的函数能够“记住”其所在的词法作用域,即使这个外部函数已经执行完毕。这意味着内部函数可以访问外部函数的变量,即使外部函数已经结束。重点是思想是:函数内部定义的函数可以访问外部函数的变量
举个例子,代码如下
function outerFunction() {let outerVar = 'I am outside!';function innerFunction() {console.log(outerVar); // 访问外部函数的变量}return innerFunction;
}const inner = outerFunction();
inner(); // 这会输出 'I am outside!'
在这个例子中,innerFunction
是在 outerFunction
内部定义的,并且它能够访问 outerFunction
中定义的 outerVar
变量。即使 outerFunction
执行完毕并返回后,innerFunction
仍然“记住”了 outerVar
的值,这就是闭包的特性
下面来分析一下实际项目中遇到的有关闭包的问题,代码如下(因为是直接从项目里截取出来的,所以格式有些问题)
selectGroups.forEach(group => {// 每个组里面副标题只会有一个secondsTitle = group.querySelector('.arrow-right');// 让当前循环到的副标题展示出来secondsTitle.classList.toggle('allowAll'); console.log(secondsTitle); // 给所有副标题添加点击事件// 点击副标题展示当前副标题下的子内容secondsTitle.addEventListener('click',()=>{secondsTitle.classList.toggle('arrow-right'); secondsTitle.classList.toggle('arrow-top');console.log(secondsTitle);items = group.querySelector('.select-items');items.classList.toggle('allowAll');});})
首先结构是循环每个selectGroups,每个selectGroups里都有一个secondsTitle,希望为当前这个secondsTitle添加点击事件。遇到的问题是,在当前只有两个selectGroup的情况下,每次点打印出来的都是第二个selectGroup里的secondsTitle。
排查了一会发现了问题所在。准确来说,问题是每次打印出来的都是最后一个selectGroup里的secondsTitle,也就是说每次循环的secondsTitle被覆盖,导致只有最后一个点击事件有效果。
我的理解:我看不出来有问题,理解中每一层循环secondsTitle,然后分别给当前secondsTitle添加点击事件,接下来一步步分析。
首先,secondsTitle没有定义,js引擎解析secondsTitle变量的时候,因为没有在当前作用域中找到相关声明,那么会向上寻找父级作用域直到全局作用域,如果还是没有就默认为全局变量var。
接下来,要知道闭包这个概念,前面已经说了它的一个特点,接下来是另一个特点,闭包会“记住”其所在的词法作用域中的变量引用,而不是变量的值。使用var声明的是全局变量,每次secondsTitle = group.querySelector('.arrow-right')实际上只改变了这个变量的值而没有改变引用,可以理解为在循环中给每个按钮(secondsTitle)分配了一个标记(点击事件)。但由于 var 的作用域特性,这些标记都指向同一个记号(secondsTitle 变量)。每次你循环时,记号上的内容(即变量的值)会被擦掉并写上新的内容,那么最后,只会有一个按钮,并且按钮上面的内容只会是最后一个循环到的文本,而不是预想中的每一个文本有一个按钮。
解决方法:为了让每个闭包捕获独立的变量,可以使用 let,因为它是块级作用域,每次循环都会创建一个独立的变量实例,闭包捕获的就是不同的变量引用,结合上面的例子思考就是创建了新按钮,并填写了当前文本,符合预期。