1. 什么是闭包
在JavaScript编程中,闭包是一个重要的概念,是指函数在定义时可以访问其所在词法作用域内的变量,即使在函数被调用(执行)时,仍然可以访问。换句话说,闭包就是将函数内部和函数外部连接起来的一座桥梁。
1.1 闭包的定义
JavaScript中的闭包是由函数和与其相关的引用环境组合而成的实体,即:在创建一个函数时,该函数可以访问到它所在的词法环境中的变量,而在执行该函数时,仍然可以访问到该变量。
1.2 闭包的作用
闭包的作用主要有两个,一是可以实现变量的私有化,二是可以实现对函数内部变量的保护。
对于第一个作用,闭包可以将函数内部的变量和函数外部的变量隔离开来,使得外部无法访问函数内部的变量,从而增强了程序的安全性。
对于第二个作用,闭包可以保护函数内部的变量不受外部的干扰,从而避免了变量的意外修改,造成程序出错的情况。
2. 闭包的实现
实现闭包的关键在于理解JavaScript中的作用域链机制。在JavaScript程序中,每个函数都有一个作用域链Scope Chain。作用域链是一条由当前执行环境(全局环境或函数环境)和所有外层环境中的变量对象(VO)构成的链。作用域链的作用在于保证变量的有序访问。
2.1 使用闭包实现变量私有化
使用闭包实现变量私有化的方法比较简单,就是在函数内部定义一个变量,然后再定义一个函数,该函数可以访问到内部定义的变量,并返回该函数。这样,外部就无法访问到函数内部的变量了。
function counter() {
var count = 0;
return function() {
count++;
console.log(count);
};
}
var c = counter();
c(); //1
c(); //2
c(); //3
上述示例中,函数counter内部定义了一个变量count,然后返回了一个函数,该函数可以访问到内部的变量count。在调用counter函数时,将返回的函数保存在变量c中,然后依次调用c函数,每次调用都会打印出变量count的值,并将count的值加1。
2.2 使用闭包保护函数内部变量
使用闭包保护函数内部变量的方法也比较简单,就是在函数内部定义一个函数,该函数可以访问到内部的变量,然后返回一个闭包函数,该闭包函数可以访问到内部函数的变量,但无法访问到外部函数的变量。
function createPerson(name) {
var age = 0;
function updateAge() {
age++;
}
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
celebrateBirthday: function() {
updateAge();
console.log('Happy birthday!');
}
};
}
var person = createPerson('Tom');
console.log(person.getName()); //"Tom"
console.log(person.getAge()); //0
person.celebrateBirthday(); //"Happy birthday!"
console.log(person.getAge()); //1
上述示例中,函数createPerson内部定义了三个闭包函数(getName、getAge和celebrateBirthday),这些闭包函数都可以访问到内部的变量(name和age),但无法访问到外部的变量。在调用createPerson函数时,将返回一个包含三个闭包函数的对象,并将该对象保存在变量person中,然后就可以通过调用对象的方法来访问到相应的变量了。
3. 闭包的注意事项
使用闭包要注意一些问题,否则可能会导致内存泄露或者其他问题。
3.1 循环引用
循环引用指的是两个对象互相引用对方,这种情况下就会形成循环链表,导致内存泄露。
function createNodes() {
var nodes = document.getElementsByTagName('div');
var i, node;
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
node.onclick = function() {
console.log(node.innerHTML);
};
}
}
createNodes();
上述示例中,函数createNodes用于获取页面中所有的div元素,并为每个div元素绑定click事件。但由于每个click事件都会访问到外部函数的变量node,而node是循环遍历过程中定义的变量,因此每个事件都会访问到最后一个div元素。这就导致了循环引用,从而可能导致内存泄露。
为了避免循环引用的问题,可以通过定义一个内部函数,将需要访问的变量传递给该函数,并返回一个闭包函数,该闭包函数可以访问到传入的变量。这样,就可以避免循环引用的问题。
function createNodes() {
var nodes = document.getElementsByTagName('div');
var i, node;
function bindEvent(node) {
node.onclick = function() {
console.log(node.innerHTML);
};
}
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
bindEvent(node);
}
}
createNodes();
上述示例中,将需要访问的变量node作为参数传递给内部函数bindEvent,并且将bindEvent返回的闭包函数赋值给node.onclick,这样就可以避免循环引用的问题了。
3.2 内存泄露
使用闭包可能会导致内存泄露,在使用闭包时要特别注意。
function createNodes() {
var nodes = document.getElementsByTagName('div');
var i, node;
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
node.onclick = function() {
console.log(node.innerHTML);
};
}
}
createNodes();
上述示例中,如果将页面中的所有div元素删除,但是没有取消点击事件的绑定,就会导致内存泄露。
为了避免内存泄露的问题,可以对绑定的事件进行解绑。
function createNodes() {
var nodes = document.getElementsByTagName('div');
var i, node;
function bindEvent(node) {
node.onclick = function() {
console.log(node.innerHTML);
};
}
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
bindEvent(node);
}
function clearEvents() {
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
node.onclick = null;
}
}
return {
clearEvents: clearEvents
};
}
var obj = createNodes();
obj.clearEvents();
上述示例中,为绑定事件的元素定义了清除事件的方法,并返回了一个包含该方法的对象,这样就可以在需要清除事件时调用该方法了。
4. 总结
闭包是JavaScript中一个重要的概念,是将函数内部和函数外部连接起来的一座桥梁。它可以实现变量的私有化,保护函数内部变量不受外部干扰,从而增强了程序的安全性和稳定性。
使用闭包时要注意循环引用和内存泄漏的问题,对使用闭包时可能产生的问题进行充分的了解和预防,可以避免出现不必要的错误。