本文深入解析 JavaScript 中闭包的概念、形成条件及核心作用。通过作用域链和垃圾回收机制的原理,阐明闭包如何访问外部变量并延长其生命周期。文章结合具体代码示例,详细讲解利用闭包解决 for 循环中索引输出错误的三种方法:立即执行函数、let 块级作用域及自定义属性,帮助开发者正确理解并应用闭包。
1. 闭包的概念
首先通过一般函数的执行过程来引入话题:
function stop() {
var num = 0;
console.log(num);
}
stop(); // 打印出来num是0
console.log(num); // 报错 函数未定义
1. 在常规情况下,函数外部的代码无法直接访问函数内部定义的变量。
2. 函数内部定义的变量通常不具备持久性,它们会随着函数的执行结束而被销毁。
闭包的概念:
1. 闭包本质上是一个函数,该函数拥有访问另一个作用域中变量的权限。
2. 另一种表述方式是:当内部函数的生命周期超过外部函数的声明周期,并且内部函数以某种方式被外部作用域访问时,闭包便由此产生。
接下来通过具体的闭包代码及其解释来进行分析:
function fn() {
var num = 10;
// function fun() {
// console.log(num);
// }
// return fun;
return function () {
console.log(num); // 10
}
}
var f = fn();
f();
我们可以将上述代码拆解为以下几个关键部分:
1. 函数 fn 内部包含一个返回值,且该返回值本身也是一个函数。
2. 返回的这个内部函数负责打印变量 num。之所以能够访问并打印 num,是因为 JavaScript 的作用域链访问机制,关于作用域和作用域链的具体知识点将在后文补充。
3. 我们在外部使用变量 f 接收了 fn() 的调用结果,即接收了 fn 返回的内部函数。
4. 随后调用变量 f,实际上就是调用了 fn 内部返回的那个函数。最终执行结果是打印出数字 10。
知识点的补充:
1. 作用域:
变量在特定的范围内生效,超出该范围则失效,这个范围即为作用域。作用域在函数定义时就已经确定,而非在函数调用时才产生。
2. 作用域链:
简单来说:基于【内部函数可以访问外部函数变量】这一特性,采用就近原则层层向上查找变量,这种机制被称为作用域链。
若函数 A 包含了函数 B,则函数 B 为函数 A 的内部函数,
当内部函数需要使用某个变量时,首先检查自身内部是否存在该变量,
如果不存在,则向紧邻的上一级作用域查找,遵循【就近原则】
如果层层向上都未找到,最后才会去全局作用域中查找。
var a = 1;
var b = 11;
function fn1() {
var a = 2;
var b = '22';
fn2();
function fn2() {
var a = 3;
fn3();
function fn3() {
var a = 4;
console.log(a); // 4
console.log(b); // '22'
}
}
}
fn1();
3. 垃圾回收机制
关于 JS 垃圾回收机制的详细描述,可以参考以下链接:
//www.jb51.net/article/229425.htm
结合上述三个概念,我们可以更好地理解闭包的作用。
2. 闭包的作用:
我们将外层函数称为函数 A,该函数内部包含一个函数 B。
在外部,使用一个变量 f 来接收函数 A 的返回值(即函数 B)。
而函数 A 作用域内的变量称为 num。
1. 闭包使得在函数外部能够访问函数内部的变量【搭建起外部访问内部作用域的通道】
原理:上文已有解释。
首先需要理解作用链的原理,如前所述。函数 B 能够调用函数 A 的变量 num。
其次需要理解,函数 A 的返回值是函数 B【内部函数】,且该返回值在外部被变量 f 接收。接收后调用函数 B,函数 B 便会访问函数 A 的变量 num。这个内部函数 B 即为闭包函数。
2. 能够延长函数内部变量的生命周期。
第一个作用自然导致第二个作用。JavaScript 存在垃圾回收机制,通常函数执行完毕后,变量会被清除,内存也会被释放。但如果利用闭包,变量可以不被立即清除。
原因在于,外部的变量 f 接收了函数 A 的内部函数 B,而这个内部函数访问了函数 A 作用域的变量 num。只要函数 B 被执行且变量 f 一直存在,那么变量 num 就会一直驻留在内存中,不会因为函数 A 执行结束而消失。
参考了下面的文章,讲解非常详细,推荐阅读。
JavaScript 闭包详解
3. 闭包示例
后文会补充闭包的一些具体应用场景。
我们需要明确在什么场合使用闭包,切忌滥用闭包。
3.1 点击 li 元素,输出当前 li 的索引号
<ul class="nav">
<li>榴莲</li>
<li>臭豆腐</li>
<li>鲱鱼罐头</li>
<li>大猪蹄子</li>
</ul>
<script>
// 闭包应用-点击li输出当前li的索引号
// 1. 我们可以利用动态添加属性的方式
var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
console.log(i); // 四个4
}
}
</script>
原理:如上图所示,如果直接这样写,打印出来的 i 永远都是 4。原因是,此时处于非严格模式,在非严格模式下,for 循环是同步执行的任务,而按钮点击事件的处理是异步任务。同步任务执行完毕后,i 已经增加到 4,再执行异步任务打印 i 时,结果自然都是 4。
改法 1:使用闭包
1. for 循环生成四个立即执行函数。
2. 立即执行函数是闭包的一种典型应用。立即执行函数内部的所有函数,包括【点击回调】函数,都可以使用立即执行函数传递的形参。
for (var i = 0; i < lis.length; i++) {
(function (i) {
// console.log(i);
lis[i].onclick = function () {
console.log(i);
}
})(i);
}
改法 2:将 var 改为 let
点击对应的 li 元素,打印出的 i 即为对应的索引号。使用 let 是 ES6 语法,此时 for 循环具有块级作用域。
var lis = document.querySelector('.nav').querySelectorAll('li');
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
// console.log(i);
console.log(i);
}
}
改法 3:使用设置自定义属性 index 的方法
var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) { // 注意这里是var不是let
lis[i].index = i; // 注意这里是lis[i]不是this.index,此时没有点击,哪里来的this
lis[i].onclick = function () {
console.log(this.index);
}
}
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!