本文深入解析JavaScript闭包的原理与实现机制,阐述函数如何通过作用域链形成封闭空间以保护内部数据。通过对象模型与函数模型展示闭包在封装私有变量中的应用,并结合沙箱模式与斐波那契数列缓存案例,说明闭包在实际开发中的价值。最后分析闭包导致的内存占用问题,提供释放引用的最佳实践。
本文通过具体实例解析JavaScript中闭包的核心原理及其实际应用场景,旨在帮助开发者深入理解并掌握这一关键特性。
为了更透彻地理解闭包概念,建议读者在阅读本文前,先阅读上一篇关于《JavaScript词法作用域与作用域链》的文章,以建立扎实的基础知识。
1. 什么是闭包
“闭包”一词源于“闭合”与“包裹”,形象地描述了一种具备封闭性与包裹性的结构。简而言之,闭包是一个对外部不可见、内部数据被包裹起来的封闭空间或结构。
在JavaScript中,函数天然构成了闭包。函数作为一种代码结构,具有包裹特性;同时依据作用域规则,函数内部可以访问外部数据,但外部无法直接访问函数内部数据,这种“对外不公开”的封闭特性使得函数能够形成闭包。
总结而言,闭包是具备封闭与包裹功能的结构,而函数正是构成闭包的基本单元。函数内部定义的数据对外部不可见,体现了封闭性;函数能够封装代码块,体现了包裹性,因此函数可以构成闭包。
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();
我们可以利用之前学到的绘制作用域链结构图的方法,来绘制闭包的作用域链结构图,具体如下图所示:

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. 基本的闭包结构
解决闭包问题的关键在于简洁地获取函数内部数据的使用权,基于此可以总结出两种基本使用模型。
- 模型一:编写一个函数,在其内部定义一个新函数并返回该新函数,通过调用返回的新函数来获取内部数据。
- 模型二:编写一个函数,在其内部定义一个对象,将多个函数(方法)绑定到该对象上并返回对象,通过调用对象的方法来访问内部数据。
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程序设计有所帮助。