本文深入解析JavaScript闭包的核心机制,阐述其通过访问外部函数作用域变量而形成的特性。文章详细讲解了作用域链的工作原理,分析了闭包在保护变量、维持状态以及避免全局污染方面的实际应用。同时,针对闭包可能引发的内存泄漏问题提供了具体的解决方案,并探讨了闭包中this指向的特殊行为及块级作用域的模拟方法。
引言
引言
闭包的基本概念与示例
闭包是指有权访问另一个函数作用域中变量的函数。作为JavaScript中较为晦涩难懂的概念之一,许多高级编程技巧都依赖于闭包来实现。首先通过一个基础示例来直观理解其运作方式:
function outer() {
var i = 100;
function inner() {
console.log(i);
}
}
在上述代码中,依据变量作用域规则,函数outer内部的局部变量对嵌套在其中的函数inner完全可见;然而,函数inner内部的局部变量在outer外部是不可见的,这意味着在outer函数外部无法直接读取inner的局部变量。
既然函数inner能够访问函数outer的局部变量,那么只需将inner作为返回值返回,即可在outer函数外部直接访问inner的局部变量。
function outer() {
var i = 100;
function inner() {
console.log(i);
}
return inner;
}
var rs = outer();
rs();
该函数结构具备两个显著特征:
- 函数inner被嵌套定义在函数outer内部;
- 函数outer执行后返回函数inner的引用。
执行语句var rs = outer()后,变量rs实际上指向了函数inner。这段代码即构成了一个闭包。换言之,当函数outer内部的函数inner被outer外部的一个变量引用时,便创建了一个闭包。
JavaScript中的作用域与作用域链
作用域概念简述为变量与函数的可访问范围,它控制着变量与函数的可见性及生命周期。在JavaScript中,变量的作用域主要分为全局作用域和局部作用域两类。
全局作用域
var num1 = 1;
function fun1 (){
num2 = 2;
}
上述代码中的num1、num2和fun1均处于全局作用域中。需要注意的是,未声明直接赋值的变量会自动被声明为拥有全局作用域;
局部作用域
function wrap(){
var obj = "我被wrap包裹起来了,wrap外部无法直接访问到我";
function innerFun(){
//外部无法访问我
}
}
作用域链机制。JavaScript中一切皆对象,这些对象拥有一个[[Scope]]属性,该属性包含了函数被创建时所在作用域中对象的集合,这被称为函数的作用域链(Scope Chain),它决定了函数可以访问哪些数据。
function add(a,b){
return a+b;
}
当函数创建时,其[[scope]]属性会自动包含全局作用域。
var sum = add(3,4);
当函数被调用时,会创建一个称为运行期上下文(execution context)的内部对象,该对象定义了函数执行时的环境。它拥有自己的作用域链用于标识符解析,且该作用域链初始化为当前运行函数的[[Scope]]所包含的对象。

在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程,以决定从哪里获取和存储数据。该过程从作用域链头部(即活动对象)开始搜索同名标识符,若找到则使用对应变量;若未找到,则继续搜索作用域链中的下一个对象;若遍历完所有对象(最后一个为全局对象)仍未找到,则判定该标识符未定义。
闭包的深入解析
闭包定义。闭包简单来说就是一个函数访问了其外部变量。
var quo = function(status){
return {
getStatus: function(){
return status;
}
}
}
变量status保存在quo函数中,quo返回了一个对象,该对象中的方法getStatus引用了这个status变量,即getStatus函数访问了其外部变量status;
var newValue = quo('string');//返回了一个匿名对象,被newValue引用着
newValue.getStatus();//访问到了quo的内部变量status
假如没有getStatus这个方法,当quo('string')执行结束后,status变量会自动被回收。正是因为返回的匿名对象被一个全局变量引用,而该匿名对象又依赖于status,所以会阻止status被释放。
示例分析:
//错误方案
var test = function(nodes){
var i ;
for(i = 0;i<nodes.length;i++){
nodes[i].onclick = function(e){
alert(i);
}
}
}
匿名函数创建了一个闭包,其访问的i是外部test函数中的i,因此每个节点实际上引用的是同一个i变量。

//改进方案
var test = function(nodes){
var i ;
for(i = 0;i<nodes.length;i++){
nodes[i].onclick = function(i){
return function(){
alert(i);
};
}(i);
}
}
每个节点绑定了一个事件,该事件接收一个参数并立即运行,传入i。由于参数是按值传递的,所以每次循环都会为当前i产生一个新的备份。

闭包的作用与变量特性
闭包的主要作用。
function outer() {
var i = 100;
function inner() {
console.log(i++);
}
return inner;
}
var rs = outer();
rs(); //100
rs(); //101
rs(); //102
在上述代码中,rs是闭包inner函数。rs共运行了三次,第一次输出100,第二次101,第三次102,这说明在函数outer中的局部变量i一直保存在内存中,并没有在调用结束后自动清除。
闭包的作用在于,当outer执行完毕并返回后,闭包使得JavaScript的垃圾回收机制(garbage collection)不会回收outer所占用的内存,因为outer的内部函数inner的执行依赖于outer中的变量。(另一种解释:outer是inner的父函数,inner被赋给了一个全局变量,导致inner会一直在内存中,而inner的存在依赖于outer,因此outer也始终存在于内存中,不会在调用结束后被垃圾收集回收)。
闭包有权访问函数内部的所有变量。当函数返回一个闭包时,这个函数的作用域将会一直在内存中保存,直到闭包不再存在为止。
闭包与变量的关系。
由于作用域链的机制,闭包只能取得包含函数中任何变量的最后一个值。请看下面的例子:
function f() {
var rs = [];
for (var i=0; i <10; i++) {
rs[i] = function() {
return i;
};
}
return rs;
}
var fn = f();
for (var i = 0; i < fn.length; i++) {
console.log('函数fn[' + i + ']()返回值:' + fn[i]());
}
函数会返回一个数组,表面上看,似乎每个函数都应该返回自己的索引值,实际上,每个函数都返回10。这是因为每个函数的作用域链上都保存着函数f的活动对象,它们引用的都是同一个变量i。当函数f返回后,变量i的值为10,此时每个函数都保存着变量i的同一个变量对象。我们可以通过创建另一个匿名函数来强制让闭包的行为符合预期。
function f() {
var rs = [];
for (var i=0; i <10; i++) {
rs[i] = function(num) {
return function() {
return num;
};
}(i);
}
return rs;
}
var fn = f();
for (var i = 0; i < fn.length; i++) {
console.log('函数fn[' + i + ']()返回值:' + fn[i]());
}
在这个版本中,我们没有直接将闭包赋值给数组,而是定义了一个匿名函数,并将立即执行匿名函数的结果赋值给数组。这里匿名函数有一个参数num,在调用每个函数时,我们传入变量i,由于参数是按值传递的,所以就会将变量i复制给参数num。而在这个匿名函数内部,又创建了并返回了一个访问num的闭包,这样,rs数组中每个函数都有自己num变量的一个副本,因此就可以返回不同的数值了。
闭包中的this与内存管理
闭包中的this对象指向问题。
var name = 'Jack';
var o = {
name : 'bingdian',
getName : function() {
return function() {
return this.name;
};
}
}
console.log(o.getName()()); //Jack
var name = 'Jack';
var o = {
name : 'bingdian',
getName : function() {
var self = this;
return function() {
return self.name;
};
}
}
console.log(o.getName()()); //bingdian
内存泄漏风险。
function assignHandler() {
var el = document.getElementById('demo');
el.onclick = function() {
console.log(el.id);
}
}
assignHandler();
上述代码创建了作为el元素事件处理程序的闭包,而这个闭包又创建了一个循环引用。只要匿名函数存在,el的引用数至少为1,因此它所占用的内存将永远不会被回收。
function assignHandler() {
var el = document.getElementById('demo');
var id = el.id;
el.onclick = function() {
console.log(id);
}
el = null;
}
assignHandler();
将变量el设置为null能够解除对DOM对象的引用,确保正常回收其占用的内存。
模拟块级作用域与应用场景
模仿块级作用域。
任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
(function(){
//块级作用域
})();
闭包的实际应用。
保护函数内的变量安全。如前面的例子,函数outer中i只有函数inner才能访问,而无法通过其他途径访问到,因此保护了i的安全性。在内存中维持一个变量。如前面的例子,由于闭包,函数outer中i一直存在于内存中,因此每次执行rs(),都会给i加1。