JavaScript闭包原理与用法详解:从基础到性能优化

作者:极速慢跑

时间:2026-10-23

浏览:409

本文深入解析JavaScript闭包的原理与实现机制,阐述函数如何通过作用域链形成封闭空间以保护内部数据。通过对象模型与函数模型展示闭包在封装私有变量中的应用,并结合沙箱模式与斐波那契数列缓存案例,说明闭包在实际开发中的价值。最后分析闭包导致的内存占用问题,提供释放引用的最佳实践。

文章目录

本文通过具体实例解析JavaScript中闭包的核心原理及其实际应用场景,旨在帮助开发者深入理解并掌握这一关键特性。

为了更透彻地理解闭包概念,建议读者在阅读本文前,先阅读上一篇关于《JavaScript词法作用域与作用域链》的文章,以建立扎实的基础知识。

1. 什么是闭包

“闭包”一词源于“闭合”与“包裹”,形象地描述了一种具备封闭性与包裹性的结构。简而言之,闭包是一个对外部不可见、内部数据被包裹起来的封闭空间或结构。

在JavaScript中,函数天然构成了闭包。函数作为一种代码结构,具有包裹特性;同时依据作用域规则,函数内部可以访问外部数据,但外部无法直接访问函数内部数据,这种“对外不公开”的封闭特性使得函数能够形成闭包。

总结而言,闭包是具备封闭与包裹功能的结构,而函数正是构成闭包的基本单元。函数内部定义的数据对外部不可见,体现了封闭性;函数能够封装代码块,体现了包裹性,因此函数可以构成闭包。

2. 闭包有什么用(解决什么问题)?

  1. 闭包的核心机制之一是限制外部直接访问内部数据。
  2. 其主要解决的问题是如何在受控的前提下间接访问这些数据。

既然函数可以构成闭包,那么核心问题便转化为:如何安全地访问函数内部的数据?


function foo () {
 var num = 123;
 return num;
}
var res = foo();
console.log( res ); // =>123

通过上述方式确实可以访问到函数中的数据。然而,该数据并非永久有效或可重复直接访问;若需再次访问,必须重新调用foo函数,这意味着每次调用都会生成一个新的num = 123实例。

虽然函数内部的数据无法直接在外部访问,但如果我们在函数内部定义另一个函数,那么在这个内部函数中可以直接访问外层函数的数据。


function foo() {
 var num = Math.random();
 function func() {
  return mun;
 }
 return func;
}
var f = foo();
// f 可以直接访问这个 num
var res1 = f();
var res2 = f();

我们可以利用之前学到的绘制作用域链结构图的方法,来绘制闭包的作用域链结构图,具体如下图所示:

1. 什么是闭包

3. 闭包使用举例

接下来探讨如何设计闭包以获取多个数据,以及如何实现数据的读取与修改操作。


function foo () {
 var num1 = Math.random();
 var num2 = Math.random();
 return {
  num1: function () {
   return num1;
  },
  num2: function () {
   return num2;
  }
 }
}

通过闭包结构,我们可以封装数据并提供读取和修改接口。


function foo () {
 var num = Math.random();
 return {
  get_num : function () {
   return num;
  },
  set_num: function( value ) {
   return num = value;
  }
 }
}

4. 基本的闭包结构

解决闭包问题的关键在于简洁地获取函数内部数据的使用权,基于此可以总结出两种基本使用模型。

  1. 模型一:编写一个函数,在其内部定义一个新函数并返回该新函数,通过调用返回的新函数来获取内部数据。
  2. 模型二:编写一个函数,在其内部定义一个对象,将多个函数(方法)绑定到该对象上并返回对象,通过调用对象的方法来访问内部数据。

5. 闭包的基本用法

闭包的主要用途是实现具有私有访问空间的函数,确保数据的安全性。

这种机制同样适用于带有私有访问数据的对象设计。


function Person() {
 this.name = "张三";
 // setName( '' )
}

所谓私有数据,指的是仅能在函数内部访问,或由对象内部方法访问的数据,外部无法直接触及。

最简单的实现方式如下所示:


function createPerson() {
 var __name__ = "";
 return {
  getName: function () {
   return __name__;
  },
  setName: function( value ) {
   // 如果不姓张就报错
   if ( value.charAt(0) === '张' ) {
    __name__ = value;
   } else {
    throw new Error( '姓氏不对,不能取名' );
   }
  }
 }
}
var p = createPerson();
p.set_Name( '张三丰' );
console.log( p.get_Name() );
p.set_Name( '张王富贵' );
console.log( p.get_Name() );

对于带有私有数据的函数,可以通过闭包封装数据并提供访问接口。


var func = function () {}
function func () {}
var foo = (function () {
 // 私有数据
 return function () {
  // 可以使用私有的数据
  ...
 };
});

6. 闭包基本模型

对象模型通过返回包含方法的对象来实现数据封装。


function foo () {
 // 私有数据
 return {
   method : function(){
    // 操作私有数据
   }
 }
}

函数模型则通过返回内部函数来实现数据访问。


function foo(){
 // 私有数据
 return function(){
   // 可以操作私有数据
 }
}

7. 沙箱模式(闭包应用的一个典范)

7.1 沙箱的概念

沙箱如同沙盘与盒子,允许在一个独立的空间内模拟现实世界。其特点是执行效果与现实世界完全一致,但模拟环境与现实世界相互隔离,互不影响。

7.2 沙箱模式

沙箱模式本质上是一个自调用函数(IIFE)。代码在函数内部执行,但不会与外部全局环境产生任何副作用或冲突。

例如,在jQuery库的实现中广泛使用了这种模式:


(function () {
 var jQuery = function () { // 所有的算法 }
 // .... // .... jQuery.each = function () {}
 window.jQuery = window.$ = jQuery;
})();
$.each( ... )

8. 带有缓存功能的函数

以Fibonacci数列为例,我们可以改进传统的递归计算方法,通过引入缓存机制来提升性能。回顾传统递归求斐波那契数列的方法,我们定义一个count变量来统计递归调用的次数:


var count = 0;
function fibo( n ){
 count++;
 if( n ==0 || n == 1 ) return 1;
 return fibo( n - 1 ) + fibo( n - 2 );
}
fib1( 20 );
console.log( count1 );
// 5: 15
// 6: 25
// ...
// 20: 21891

当n = 5时,count = 15;而当n = 20时,count高达惊人的21891次,这表明传统递归方法的性能极低。

性能低下的根本原因在于重复计算。如果每次计算结果都能被存储起来,就能避免重复劳动。

  • 因此,每次需要计算时,先检查缓存中是否已存储该数据;如果存在,直接返回缓存结果。
  • 如果缓存中不存在,则进行递归计算,并将计算结果存储到缓存中,以便下次直接使用。

改进后的代码实现如下:


var data = [ 1, 1 ];
var count = 0;
function fibo( n ) {
 count++;
 var v = data[ n ];
 if( v === undefined ){
   v = fibo( n - 1 ) + fibo( n - 2 );
   data[ n ] = v;
 }
 return v;
}
fibo( 100 );
console.log( count ); // 199

经过改进,当n = 100时,递归次数仅为199次,性能得到了极大提升。

9. 闭包的性能问题

函数执行会占用内存,通常函数执行结束后,其内部定义的变量会被自动回收。然而,如果存在闭包结构,且被引用的数据仍有变量指向它,那么这些数据将不会被垃圾回收机制释放。

因此,在使用闭包时,如果不再需要某些数据,务必将其赋值为null,以显式释放引用,避免内存泄漏。


var f = (function () {
 var num = 123;
 return function () {
  return num;
 };
})();
// f 引用着函数,函数引用着变量num
// 因此在不适用该数据的时候,最好写上
f = null;

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容可查看本站专题:《JavaScript常用函数技巧汇总》、《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章