深入理解JavaScript闭包:从原理到模块模式

作者:拾光

时间:2026-10-25

浏览:455

本文深入解析JavaScript中闭包的核心概念,阐述其基于词法作用域的工作原理。通过具体代码案例,揭示闭包在定时器、回调函数及模块模式中的应用,并重点分析循环中常见的闭包陷阱及其解决方案,帮助开发者掌握这一关键特性。

文章目录

前言

在JavaScript语言体系中,闭包是一个既核心又晦涩的概念,常被形容为近乎神话般的存在。对于具备一定JS使用经验却未透彻理解闭包的开发者而言,掌握闭包往往意味着技术认知的某种“重生”。闭包并非遥不可及,它基于词法作用域书写代码时的自然产物,广泛存在于日常开发的代码之中。我们只需学会识别并善用它。

理解闭包

下面通过代码示例来阐释这一定义:

function foo(){
   var a = 2;
   function bar(){
     console.log(a);//2
   }
   bar();
}
foo()

这是否构成闭包?或许是的,但这种写法难以直观观察闭包的定义,也无法清晰展示其工作机制。词法作用域相对容易理解,而闭包则隐藏在代码执行的幕后,显得神秘且不易捉摸。

接下来看一段清晰展示闭包的代码:

function foo(){
   var a = 2;
   function bar(){
     console.log(a)
   }
   return bar;
}
var baz = foo();
baz() //2-------这就是闭包的效果。

函数bar的词法作用域能够访问foo()的内部作用域。我们将bar()函数本身作为值类型进行传递,具体表现为将bar引用的函数对象作为返回值。在foo()执行后,其返回值(即bar函数)被赋值给变量baz并调用baz()。这实际上是通过不同的标识符引用调用了内部函数foo内部的bar(),且bar()显示可以被正常执行。

通常情况下,当foo()执行完毕后,人们会预期其整个内部作用域都被销毁,因为JavaScript引擎具备垃圾回收机制来释放不再使用的内存空间。既然foo()看似不再被利用,大脑自然会认为它会被回收。

然而,闭包的奇妙之处在于它能阻止上述销毁过程。事实上,内部作用域依然存在且未被回收。是谁在使用这个内部作用域?正是bar()本身。由于bar()的声明位置,它拥有覆盖foo()内部作用域的闭包,使得foo()的作用域得以持续存活,供bar()在之后任何时间被调用。

bar()依然持有对该作用域的引用,而这个引用即被称为闭包。

当然,通过函数传递也可以实现间接效果:

var fn;
function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    fn = baz;
}
function bar(){
   fn()
}
foo();
bar(); //2-------这就是闭包!

无论采用何种手段将内部函数传递到其所在的词法作用域之外,它都会持有原始定义作用域的引用。无论该函数在何处执行,都会使用闭包。

升级版闭包

前面的代码片段可能略显刻板,且为了解释闭包用法而写得过于直白。实际上,闭包在实际操作中是一个极具趣味的工具,且开发者一定曾使用过它。现在让我们厘清这一事实:

function wait(message){
    setTimeout(function timer(){
       console.log(message);
    },1000);
}
wait ("Hello closure")

将一个内部函数(名为timer)传递给setTimeout()。timer具有涵盖wait()作用域的闭包,因此还保有对变量message的引用。

wait(...)执行耗时1000毫秒,其内部作用域并不会消失,timer函数依然保有wait()作用域的闭包。这就是闭包。无论你的生活中编写何种代码,在定时器、事件监听器、Ajax请求、跨窗口通信或任何其他异步任务中,只要你使用了回调函数,实际上就是在利用闭包。

var a = 2;
(
  function IIFE(){
    console.log(a)
  }
)()

虽然这段代码可以正常工作,但严格来说它并非典型的闭包应用,因为函数并非在其本身的词法作用域以外执行。它在定义时所在的作用域中执行(而外部作用域,即全局作用域也持有a)。变量a是通过普通的词法作用域查找而非闭包机制被发现的。

循环和闭包

要说明闭包,for循环是最常见的例子。

for(var i = 1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i)
  },i*1000)
}

正常情况下,我们对这段代码的行为预期是分别输出数字1到5,每秒一次,每次一个。

但实际上,这段代码在运行时会以每秒一次的频率输出五次6。

前言

这是为什么?

首先解释6的来源。该循环的终止条件是i不再小于等于5。当条件首次判断为假时,i的值是6。因此,输出显示的是循环结束时i的最终值。

其次需明确,延迟函数的回调会在循环结束后才执行。事实上,即使每个迭代中执行的是setTimeout(..., 0),所有的回调函数依然是在循环结束后才会被执行,因此每次输出一个6。

那为什么没有获得我们预期的结果,即从1到5输出?

根据作用域原理,尽管循环中五个输出函数是被分别定义出来的,但它们都被封闭在一个共享的全局作用域下,实际上还是共享同一个变量i。

既然知道解决方法,那就不共享一个i,让每一次的值都相互独立,每次都获得对应的值。

for(var i = 1;i<=5;i++){
   (
     function(){
       var j = i;
       setTimeout(function timer(){
         console.log(j)
       },j*1000)
     }
   )()
}

这样,将每一次的i值都传给另一个变量,保证i的实时更新,就可以正常输出1到5了!

知道原因了,举一反三,那是不是也会有其他解决办法呢?

仔细思考我们前面的解决方案。我们使用IIFE函数(立即执行函数)每次迭代时都创建了一个新的作用域。换句话说,我们每次迭代都产生新的块作用域。那是不是可以声明块作用域避免变量共享的问题呢?

for(let i = 1;i<=5;i++){
   setTimeout(function timer(){
      console.log(i)
   },i*1000)
}

很酷是吧?块作用域和闭包联手便可“天下无敌”。

模块

function CoolModule(){
  var something = "cool";
  var another = [1,2,3];
  function doSomething(){
    console.log(something);
  }
  function doAnother(){
    console.log(another.join("!"));
  }
  return {
    doSomething:doSomething,
    doAnother:doAnother
  }
}
var foo = CoolModule();
foo.doSomething();//cool
foo.doAnother();//1!2!3

这个模式在JavaScript中被称为模块,最常见的实现模块模式的方法通常被称为模块暴露。首先,CoolModule()只是一个函数,必须要通过它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。

其次,CoolModule()返回一个用对象字面量语法{key:value,...}表示的对象。这个返回的对象中含有对内部对象而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上模块的公共API。

doSomething()和doAnother()函数具有涵盖模块实例内部作用域的闭包(通过调用CoolModule()实现)。

简单描述一下,模块模式的闭包需要具备两个必要条件。

(1)必须有外部的封闭函数,该函数必须至少被调用一次。(每次调用都会创建一个新的模块实例)。

(2)封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

另外,模块也是普通的函数,因此可以接受参数:

function CoolModule(id){
  function identify(){
    console.log(id)
  }
  return {
    identify:identify
  }
}
var foo1 = CoolModule("foo1");
var foo2 = CoolModule("foo2");
foo1.identify();//"foo1"
foo2.identify();//"foo2"

小结

闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里,但实际上他只是一个标准,显然就是关于如何在函数作为值按需传递的此法环境中书写代码的。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。

如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。

模块主要有两个特征:

(1)为创建内部工作域而调用了一个包含函数。

(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

以上就是一文了解你不知道的JavaScript闭包篇的详细内容,更多关于JavaScript 闭包的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章