本文深入解析JavaScript闭包的核心概念,阐述其作为函数与作用域对象组合的本质。详细讨论闭包对局部变量的保留机制、垃圾回收的影响,以及每次调用外部函数生成新闭包的特性。重点解决闭包在循环变量中常见的陷阱,并提供使用立即执行函数(IIFE)绑定变量的标准解决方案。
闭包的基本概念与原理
闭包是JavaScript中较为晦涩难懂的概念之一,尤其对于缺乏编程经验的初学者而言。
掌握闭包的关键要点其实有限,一旦理清这些核心逻辑,理解它便不再困难。接下来我们将探讨闭包的基本原理。
闭包的概念
闭包本质上是一个函数与其被创建时所处的作用域对象的组合(关于作用域对象的具体定义后文详述)。
通俗来说,只要一个函数内部嵌套了一个或多个函数,这些函数共同构成了闭包。
其结构大致如下:
function A() {
var i = 5;
return function() {
console.log('i = '+i);
}
}
var a = A();
a(); // i = 5
闭包的原理
第一点:若外部函数的局部变量被内部闭包函数引用,则该变量不会在外部函数执行结束后立即被回收。
所有编程语言通常都具备垃圾回收机制,用于回收不再使用的内存空间以优化性能。函数的生命周期始于调用,终于返回,期间创建的局部变量在函数执行完毕后通常会被回收。
以前文示例为例,当外部函数A执行完毕后,其局部变量i理论上应被操作系统回收。然而,由于使用了闭包,变量i并未被回收。如果i被回收,返回的函数在尝试访问i时将得到undefined。
变量i为何未被回收?
JavaScript在执行函数时会创建一个作用域对象,用于存储函数内的局部变量(包括形参)及传入的变量。
因此,当调用函数A时,会创建一个名为Aa的作用域对象,其结构类似:Aa { i: 5; }。在函数A返回一个内部函数后,A执行结束。此时Aa对象本应被回收,但由于返回的内部函数引用了Aa中的属性i,该内部函数保存了对Aa的引用,导致Aa无法被回收。
理解作用域对象的工作机制,就能明白为何闭包存在时,局部变量不会在函数调用结束后立即被销毁。
再看一个示例:
function A(age) {
var name = 'wind';
var sayHello = function() {
console.log('hello, '+name+', you are '+age+' years old!');
};
return sayHello;
}
var wind = A(20);
wind(); // hello, wind, you are 20 years old!
你能识别出该代码的作用域对象Ww是什么吗?
Ww{ age: 20; name: 'wind'; };
闭包的创建与状态保留特性
第二点:每次调用外部函数都会创建一个新的闭包,之前创建的闭包依然有效且互不干扰。
第三点:同一个闭包会保留上一次调用的状态,再次调用时会在之前的基础上继续执行。
每次调用外部函数生成的作用域对象都是独立的。以上述代码为例,每次传入的age参数不同,因此生成的作用域对象也各不相同。
每次调用外部函数,都会生成一个新的作用域对象实例。
function A() {
var num = 42;
return function() { console.log(num++); }
}
var a = A();
a(); // 42
a(); // 43
var b = A(); // 重新调用A(),形成新闭包
b(); // 42
这段代码揭示了两个现象:首先,连续调用两次a(),num值在原有基础上自增,说明同一个闭包会保留上一次的状态,后续调用基于此状态进行。其次,b()的结果为42,表明它创建了一个新的闭包,且不受其他闭包状态的影响。
可以将其类比为吹肥皂泡:每次调用外部函数(吹一下),就会生成一个新的肥皂泡(闭包)。多个肥皂泡可以同时存在,且彼此之间互不影响。
第四点:在外部函数中定义的多个函数“同生共死”,共享同一作用域。
以下三个函数同时声明,且均可访问和操作作用域对象中的属性(即局部变量)。
var fun1, fun2, fun3;
function A() {
var num = 42;
fun1 = function() { console.log(num); }
fun2 = function() { num++; }
fun3 = function() { num--; }
}
A();
fun1(); // 42
fun2();
fun2();
fun1(); // 44
fun3();
fun1(); //43
var old = fun1;
A();
fun1(); // 42
old(); // 43 上一个闭包的fun1()
由于JavaScript函数只能返回一个值,此处使用了全局变量来演示。我们可以再次看到,第二次调用A()时创建了一个新的闭包实例。
闭包在循环变量中的陷阱与解决
当闭包遇到循环变量
讨论闭包时,无法避开循环变量这一经典场景,请看以下代码:
function buildArr(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + arr[i])} );
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0](); // item2 undefined
fnlist[1](); // item2 undefined
fnlist[2](); // item2 undefined
为何会出现这种情况?我们预期的三个输出应该是 item0 1、item1 2、item2 3。但实际结果是返回的result数组中存储的三个函数均输出 item2 undefined。
原因在于,当闭包遇到循环变量时,变量值是在循环结束后统一保存的。以上述代码为例,i是循环变量,当循环全部结束时,i的值已变为3(执行完i++后)。由于arr[3]不存在,故输出undefined。有人可能疑惑为何item是item2而非item3。需注意,在最后一次循环中(i=2时),item被赋值为'item2'。随后执行i++,i变为3,循环条件不满足,循环结束。此时item的值已固定为'item2',而arr[i]即arr[3]为undefined。这样解释是否清晰?若将代码修改如下,逻辑便通顺了:
function buildArr(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
result.push( function() {console.log('item' + i + ' ' + arr[i])} );
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[1](); // item3 undefined
那么,如何解决这个问题呢?请看以下代码:
function buildArr(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
result.push( (function(n) {
return function() {
var item = 'item' + n;
console.log(item + ' ' + arr[n]);
}
})(i));
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0](); // item0 1
fnlist[1](); // item1 2
fnlist[2](); // item2 3
我们可以利用自执行函数(IIFE)将变量i绑定,这样i的每一个状态都会被独立存储,输出结果便符合预期。
因此,在使用闭包处理循环变量时,应习惯性使用自执行函数来绑定当前变量值。
以上是对闭包机制的个人理解,如有任何意见或建议,欢迎在评论区交流。感谢阅读,共勉。