JavaScript闭包通俗解析:从作用域到私有数据暴露

作者:纸飞机

时间:2026-10-22

浏览:518

本文面向JavaScript初学者,通过通俗易懂的比喻和代码示例,深入解析闭包的核心概念。文章首先阐明函数也是数据类型的观点,进而解释作用域链与闭包的定义。重点讲解如何利用闭包将函数内部的私有数据安全地暴露给外部,同时避免全局变量污染,并通过一个模拟jQuery的“紫金葫芦”案例展示闭包的实际应用。

文章目录

JavaScript闭包通俗解析:从作用域到私有数据暴露

本文旨在帮助初学者理解概念,欢迎资深开发者指正。

闭包是什么?

初学者接触JavaScript时,常会遇到“闭包”这一概念,它听起来似乎高深莫测。网络上对此有各种复杂的理论解释,但个人建议不必过度纠结于理论化的定义。

实际上,你每天都在使用闭包,只是可能并未意识到。

例如:


var cheese = '奶酪';
var test = function(){
  alert(cheese);
}

OK,你已经写了一个闭包。

函数也是数据类型

函数也是一个数据类型

变量 cheese 位于全局作用域中。当你定义一个 test 函数时,test 函数与 cheese 变量共享同一个全局作用域。

需要额外明确的一点是,在JavaScript中,函数和变量在本质上是相同的:函数本身也是一种数据类型。

从上述定义中可以观察到这一点。如果不相信,我们可以进行验证。


alert(cheese);
alert(test);

 JavaScript闭包通俗解析:从作用域到私有数据暴露

 JavaScript闭包通俗解析:从作用域到私有数据暴露

让我们再次查看 test 和 cheese 的类型:

alert(typeof test);

 JavaScript闭包通俗解析:从作用域到私有数据暴露

alert(typeof cheese);

 JavaScript闭包通俗解析:从作用域到私有数据暴露

看到了吧,它们只是类型不同,但本质上都是数据类型。

唯一的区别在于,函数类型的 test 可以包含内部逻辑,而字符串类型的 cheese 仅能存储字面量值。这就是它们之间的区别,仅此而已。

一目了然:普通变量如同字面量值,而函数需要加上括号才能执行。

你看,我现在加上括号:

test();

JavaScript闭包通俗解析:从作用域到私有数据暴露

只有加上括号,函数内部的逻辑才会被执行。

作用域与闭包的关系

作用域

让我们回到闭包,对之前的代码做一个微小的改动:


var cheese = '奶酪';
var test = function(){
  alert(cheese);
}
function test2(){
  var cheese = null;
  test();
}
test2();

那么,你认为 alert 弹出的结果是 null 还是奶酪呢?

思考一下。。。

对的,弹出的依然是奶酪。

JavaScript闭包通俗解析:从作用域到私有数据暴露

前文已提及,函数 test 和变量 cheese 处于同一片“天空”下——即同一个作用域。

函数 test 和变量 cheese 共同享有的作用域称为全局作用域,就像地球一样,我们所有人都享有这个地球,可以在这里呼吸、吃饭、玩耍。

对于 test 而言,它能访问的作用域仅限于它自身的闭包和全局作用域:

JavaScript闭包通俗解析:从作用域到私有数据暴露

也就是说,正常情况下它无法访问其他闭包中的内容。在 test2 中定义的变量与它无关,因此弹出的 cheese 依然是全局作用域中的 cheese。

函数可以创造自己的作用域。

我们刚才定义了 test 函数,被 { } 包裹的部分形成了一个新的作用域,也就是所谓的闭包。

其实,如果你深刻理解了作用域的原理,也就理解了闭包。

这就好比地球是一个全局作用域,你自己家的房子是一个函数,你的房子是私人空间,即一个局部作用域,也就是你自己建立了一个闭包!

你透过窗户可以看到外面的景色,比如院子里的一棵芭蕉树,你通过观察看到了芭蕉树的颜色、高度、枝干粗细等。

这一棵芭蕉树相当于一个全局变量,你在自己的闭包内可以访问到它的数据。

因此,在这个例子中,test 就是一个房子,在内部可以通过窗户访问到全局作用域中的奶酪——变量 cheese。

也就是说,当 cheese 被 test 访问到时,它就进入了它的闭包。

这样解释,你是否觉得更容易理解了呢?

现在你是否可以理解一开始我说“闭包这东西其实我们天天都在用”的意思了呢?

闭包的第一种注解:私有作用域

我们给出闭包的第一个注解:

1. 闭包就是在函数被创建的时候,存在的一个私有作用域,并且能够访问所有的父级作用域。

回到刚才的例子:


var cheese = '奶酪';
var test = function(){
  alert(cheese);
}
function test2(){
  var cheese = null;
  test();
}

在这个例子中,test 和 test2 各自享有一个作用域,对不对?而且它们互相不能访问。例如,我在 test 中定义的一个变量,test2 就无法直接访问。


var test = function(){
  var i = 10;
}
function test2(){
  alert(i);
}
test2();

像这样,一旦执行 test2 函数,编译就不通过,因为在 test2 的闭包内,根本找不到变量 i。它首先会在自己的闭包内寻找 i,找不到的话就去父级作用域里找,这边的父级就是全局作用域,很遗憾,还是没有。这就是所谓的作用域链,它会一级一级往上找。如果找到最顶层,还是找不到的话,就会报错了。

JavaScript闭包通俗解析:从作用域到私有数据暴露

在这里,还有一个需要注意的点就是:如果某一个闭包中对全局作用域(或父级作用域)中的变量进行了修改,那么任何引用该变量的闭包都会受到牵连。

这的确是一个需要注意的地方。

举个例子


var cheese = '奶酪';
var test = function(){
  cheese = '奶酪被偷吃了!'
}
function test2(){
  alert(cheese);
}
test();
test2();

结果是:

JavaScript闭包通俗解析:从作用域到私有数据暴露

很有趣,是不是呢?

当我们在定义一个函数,就产生了一个闭包,如果这个函数里面又有若干的内部函数,就是闭包嵌套着闭包。

像这样:


function house(){
  var footBall = '足球';
  /* 客厅 */
  function livingRoom(){
    var table = '餐桌';
    var sofa = '沙发';
    alert(footBall);
  }
  
  /* 卧室 */
  function bedRoom(){
    var bed = '大床';
  }
  
  livingRoom();
}
house(); 

函数 house 是一个闭包,里面又定义了两个函数,分别是 livingRoom 客厅,和 bedRoom 卧室,它们各自形成自己的闭包。对它们而言,父级作用域就是 house。

如果我们希望在客厅里踢足球,在 livingRoom 函数执行的时候,它会先在自己的闭包中找足球,如果没找到,就去 house 里面找。一层一层往上找,直至找到了为止。当然,这个例子可能不是很恰当。但起码展示了作用域,闭包之间的联系。

再说明一下, 闭包就是在函数被创建的时候,存在的一个私有作用域,并且能够访问所有的父级作用域。因此,从理论上讲,任何函数都是一个闭包!

通过闭包暴露私有数据

2. 如何将私有数据暴露出去

之前有这样一个例子


var test = function(){
  var i = 10;
}
function test2(){
  alert(i);
}
test2();

函数 test 和 test2 各自形成一个闭包,两个闭包之间无法访问对方的私有数据。例如,在 test 中定义的变量,在 test2 里面是无法直接访问到的。

那么问题来了, 当然,这边和挖掘机没关系。这里的问题是,有没有什么办法让 test2 可以访问到其他闭包中的私有变量呢?

办法当然是有的,最直接的想法就是,大不了我定义一个全局变量,在 test 中将私有数据赋给全局变量,然后在 test2 里面就能访问到了。

是的,因为两个函数共同享有一个全局作用域,所以这个办法确实可行。我在很多项目里也的确看到很多人就是这么做的。

那么,有没有一种更好的方法呢?要知道,全局作用域是一个比较敏感的地方,一不小心就会出现变量名重复的问题。顺便说一句,在全局作用域中,尽量不要使用诸如 temp , a , b , c 这一类的大众化变量。

于是,这就牵扯到返回值的相关知识了,你在C语言的教材中肯定见惯了类似于这样的代码


int sum(int a,int b)
{
  return a + b;
}
int all = sum(3,5);

这是一个简单的求和函数,很多人慢慢地养成了这样一个观念,就是函数的返回值就是一个字面值,要么是数字类型,要么是布尔类型,或者是字符串。

在很多强类型的语言,诸如 Java,C,C++, 确实如此。但是 return 在 JavaScript 中却大有来头。

在上一节已经说明了,js 的函数也是一种数据类型,你可以把函数看成是和int , float , double 一样的东西。

那么,既然int可以当做函数的参数或者返回值,函数当然也可以!

请看下面两句话:

在js中

如果函数被当做参数传进去了,它就是所谓的回调函数。

如果函数被当做返回值return出去了,它就是把一个闭包return出去了。

这一章不讲回调函数,如果你不清楚啥叫回调函数,可以去看看有关文章

还是上面的那个例子,我们希望在 test2 中可以访问到 test 里面的变量,可以这样做:


var test = function(){
  var i = 10;
  /* 定义一个函数将变量i暴露出去 */
  var get = function(){
    return i ;
  }
  return get; //将获得i的函数暴露出去
}
function test2(){
  var fn= test();//接收test暴露出来的函数
  alert(fn()); //获得test中的私有数据
}
test2();

JavaScript闭包通俗解析:从作用域到私有数据暴露

test 函数中的 get 方法是一个内部函数,它自己也形成了一个闭包, test 是他的父级作用域,因此它可以获取i的值。

i 进入 get 方法的闭包,被包了起来,然后最终被返回了出去。

而对于 test2 来说,是可以访问到 test函数的,因此可以调用并执行 test 函数,从而获取其返回值。

你可能会说,我直接在test中把i给return出去就好了嘛,干嘛这么麻烦。

是的,言之有道理。

可是,如果我要访问 test 中多个私有数据咋办捏?

这下你可明白了吧!

现在,我们给出关于闭包的第二个注解:

从应用的角度来看,闭包可以将函数或者对象的私有数据暴露出去,而不影响全局作用域。

JavaScript闭包通俗解析:从作用域到私有数据暴露

通过这张图,是不是好理解一些了呢?我们这一节单说函数里的私有数据。

将私有数据包装为JSON对象

2. 将私有数据包装成json对象

刚才的例子说明,在js中,return出去的可以是基本数据类型,也可以是函数类型。

其实,JavaScript是一种基于对象的语言,也有对象的概念,所以,我们可以把你需要的东西包裹成一个对象返回出去!

上代码:


var test = function(){
  var apple = '苹果';
  var pear = '梨子';
  /* 定义一个函数将水果暴露出去 */
  var getFruit = {
    apple : apple ,
    pear : pear
  }
  return getFruit; //将获得i的函数暴露出去
}
function test2(){
  var getFruit = test();//接收test暴露出来的函数
  console.log(getFruit);
}
test2();

像这样用 { } 括起来的东西就是一个js对象,也就是所谓json。你可能经常会听到json这个词,觉得还挺高大上的。其实它就是一个用 { } 包起来的数据而已。

里面是键值对的形式,非常类似于Java里面的HashMap。

在这个例子中,我们可以直接把需要暴露的私有数据用一个 { } 包起来,构成一个json对象return出去就可以啦。

因为是 js 对象,alert 不能看到里面的具体内容,所以我们使用 console.log() ,结果如下:

JavaScript闭包通俗解析:从作用域到私有数据暴露

展开后:

JavaScript闭包通俗解析:从作用域到私有数据暴露

这样是不是也可以了?多出来的 proto 是原型链,以后会讲到。

应用案例:模拟jQuery的紫金葫芦

3. 我们来做一个紫金葫芦

大家都还记得西游记里孙悟空用遮天的把戏骗来的紫金葫芦吗,只要你拿着这个葫芦,叫一声别人的名字,如果答应了,别人就会被吸进去。

OK,这个紫金葫芦里面不正如一个闭包吗?

对不对嘛,所以,我们用闭包的知识来做一个好玩的东西吧。


<body>
  <div id='box' style='width:50px;height:50px;background:#333;color:#fff;text-align:center;line-height:50px'>小妖</div>
</body>

JavaScript闭包通俗解析:从作用域到私有数据暴露

紫金葫芦里面的源码大概是这样的:


var 紫金葫芦 = function(id){
  var domElement = document.getElementById(id);
  var returnObject = {
    domElement : domElement ,
    backgroundColor : function(color){
      domElement.style.backgroundColor = color;
    },
    click : function(fn){
      domElement.onclick = fn;
    }
  };
  return returnObject;
}

注:我纯粹是为了看起来方便而采用中文定义变量,在实际开发中,千万不要使用中文变量。

我们在返回出去的对象上加了三个东西:

1.domElement

你传进来一个id,我就用 document.getElementById 来包一下,得到一个dom元素,最终要操作的也就是这个dom元素。也就是说:


var box1 = 紫金葫芦('box').domElement;
var box2 = document.getElementById('box');
alert(box1 === box2);
Paste_Image.png

JavaScript闭包通俗解析:从作用域到私有数据暴露

他们是一个东西,一样的。

紫金葫芦('box');

这行代码一旦执行,紫金葫芦就会返回 returnObject 对象,也就是说。我们喊一声 “box”,那个id为box的小妖一答应,就被装进来了,然后我们可以对它为所欲为!

比如,给它换一个背景色:

2.backgroundColor 给元素添加背景色的方法


var box = 紫金葫芦('box');
box.backgroundColor('red');

JavaScript闭包通俗解析:从作用域到私有数据暴露

3.click 给元素添加点击事件,需要传入一个回调函数


var box = 紫金葫芦('box');
box.click(function(){
  alert('就没人吐槽这个无聊的作者么,小妖也有尊严的好么,啊喂!!');
});

结果:

JavaScript闭包通俗解析:从作用域到私有数据暴露

也许你已经发现了,这些方法是不是和jQuery有点类似呢?

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持本站!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章