深入理解JavaScript闭包:原理、特性与循环变量陷阱

作者:小括号时间:2026-10-30 17:02:03浏览:556

本文深入解析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的每一个状态都会被独立存储,输出结果便符合预期。

因此,在使用闭包处理循环变量时,应习惯性使用自执行函数来绑定当前变量值。

以上是对闭包机制的个人理解,如有任何意见或建议,欢迎在评论区交流。感谢阅读,共勉。

作者最新文章