本文深入解析 JavaScript 闭包的核心机制。通过经典的循环点击案例,揭示变量共享引用的陷阱,并对比属性存储与立即执行函数两种解决方案。进一步探讨闭包对 this 对象的影响,阐明通过中间变量保存上下文的方法,并总结闭包在封装私有变量中的实际应用价值。
梳理网络上关于闭包的零散概念,旨在系统性地厘清这一核心机制。
闭包的核心定义与经典循环陷阱
闭包的定义:在 JavaScript 中,内部函数始终有权访问其外部函数声明的参数和变量,即便外部函数已经执行完毕并返回(即生命周期结束)。
这是初次接触闭包概念时的典型困惑场景。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8"/>
<title>闭包循环问题</title>
<style type="text/css">
p {background:#ccc; width: 300px; height: 100px;}
</style>
</head>
<body>
<p id="p0">段落0</p>
<p id="p1">段落1</p>
<p id="p2">段落2</p>
<p id="p3">段落3</p>
<p id="p4">段落4</p>
<script type="text/javascript">
for( var i=0; i<5; i++ ) {
document.getElementById("p"+i).onclick=function() {
alert(i); //访问了父函数的变量i, 闭包
};
};
</script>
</body>
</html>
若未深入理解闭包机制,直觉上会认为点击各个段落会分别弹出对应的索引号 0、1、2、3、4。然而实际运行结果却是每次点击都弹出数字 5。
针对这一现象,网络上有大量讨论文章提供了多种修复方案。
解决循环变量共享的两种方案
第一种解决思路:将循环变量 i 的值存储到对应 DOM 元素的自定义属性中。
var pAry = document.getElementsByTagName("p");
for( var i=0; i< 5; i++ ) {
pAry[i].no = i;
pAry[i].onclick = function() {
alert(this.no);
}
};
第二种解决思路:引入一层闭包,将变量 i 作为参数传递给内部函数。
var pAry = document.getElementsByTagName("p");
for( var i=0; i< 5; i++ ) {
pAry[i].no = i;
pAry[i].onclick = function() {
alert(this.no);
}
};
闭包的内存驻留与地址引用机制
针对此现象,常见解释称“变量 i 以指针或地址形式保存在函数作用域中”。既然涉及底层指针概念,值得进一步探究其内存行为。
探索一:在上述代码逻辑下,所有点击事件均返回数值 10。
(function test() {
var temp =10;
for(var i=0; i< 5; i++ ){
document.getElementById("p"+i).onclick=function() {
alert(temp); //访问了父函数的变量temp, 闭包
}
};
temp=20;
})();
探索二:第一次点击返回 10,后续所有点击均返回数值 20。
(function test() {
var temp =10;
for( var i=0; i< 5; i++ ) {
document.getElementById("p"+i).onclick=function() {
alert(temp); //访问了父函数的变量i, 闭包
}
if(i===1){
alert(temp);
}
};
temp=20;
})();
由探索的1、2,可以得出结论:函数内部访问了与函数同级的变量,那么该变量是常驻内存的。访问该变量实质上是访问的是变量的地址;
此外,参考关于“JS 闭包中 this 对象”的论述,继续探讨 this 关键字在闭包环境下的行为。
// js闭包this对象1
var name = 'The Window';
var object = {
name : 'My Object',
getNameFunc1 : function(){
// return this.name;
console.log(this);//object
return function(){//闭包,访问的便是全局变量的了,this指windows
console.log(this);//windows
return this.name; //The Window
}
},
getNameFunc2 : function(){
return this.name;//访问的是object
},
aa:function(){
alert(22);
}
};
alert(object.getNameFunc1()());//弹出“The Window”
闭包环境下的 this 指向问题
问:为何匿名内部函数无法直接获取其外层作用域的 this 对象? 答:每个函数在调用时会自动生成两个特殊变量:this 和 arguments。内部函数在查找这两个变量时,仅能搜索到其自身的作用域链末端(活动对象),因此无法直接访问外部函数中的 this 和 arguments。 但可通过以下代码实现对外部变量(含 this 指向的对象)的访问:
// js闭包this对象2
var name = 'The Window';
var object = {
name : 'My Object',
getNameFunc : function(){
var that = this;
console.log(this);//输出的是object
return function(){
console.log(this);//输出的仍然是Windows
return that.name;
};
}
};
alert(object.getNameFunc()());//弹出“My Object”
关键区别在于将外部函数的 this 对象赋值给局部变量 that。即使外部函数返回,that 依然引用该对象,从而确保内部函数能正确访问并返回预期对象。 既然已详细阐述闭包原理,顺便探讨其实际应用场景;若闭包仅用于制造问题,则其价值大打折扣。
闭包的实际应用场景与价值
观察一个典型的闭包应用示例:
function A(){
var a=1;
function B(){
return a;
};
return B;
};
var C=A();//C取得A的子作用域B的访问接口
console.log(C());//1 C能访问到B的父级作用域中的变量a
只要其他作用域能够获取子作用域的访问接口,即可通过该接口访问子作用域父级作用域中的变量。这意味着,若未来需要访问某个函数内部的特定值,闭包将提供极大的便利。
上述代码示例多源自网络资料,此处仅为整理个人理解与阅读过程中的总结。