函数和对其词法环境 (lexical environment) 的引用捆绑在一起构成闭包 (closure),也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript,函数在每次创建时生成闭包。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
定义闭包
为了定义一个闭包,首先需要一个函数来套一个匿名函数。闭包是需要使用局部变量的,定义使用全局变量就失去了使用闭包的意义,最外层定义的函数可实现局部作用域从而定义局部变量,函数外部无法直接访问内部定义的变量。
function student() {
var name = "Bronya";
var sayMyName = function() { // sayMyName 作为内部函数,有权访问父级函数作用域 student 中的变量
console.log(name);
}
console.log(sayMyName);
return sayMyName; // return 是为了让外部能访问闭包,挂载到 window 对象也可以
}
var stu = student();
stu(); // Bronya
可以看到定义在函数内部的 name 变量并没有被销毁,我们仍然可以在外部使用函数访问这个局部变量,使用闭包,可以把局部变量驻留在内存中,从而避免使用全局变量。全局变量污染会导致应用程序不可预测性,每个模块都可调用必将引来灾难。
词法环境
闭包共享相同的函数定义,但是保存了不同的词法环境 lexical environment。
function student(name) {
var sayMyName = function() {
console.log(name);
}
return sayMyName;
}
var stu1 = student("Bronya");
var stu2 = student("Kiana");
stu1(); // Bronya
stu2(); // Kiana
模拟私有方法
在面向对象的语言中,例如 Java、PHP 等,都是支持定义私有成员的,即只有类内部能够访问,而无法被外部类访问。JavaScript 并未原生支持定义私有成员,但是可以使用闭包来模拟实现,私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
function student() {
var HP = 100;
var addHP = function() {
return ++HP;
}
var decHP = function() {
return --HP;
}
return {
addHP,
decHP
};
}
var stu = student();
console.log(stu.HP); // undefined 不允许直接访问
console.log(stu.addHP()); // 101
console.log(stu.decHP()); // 100
回调机制
Js 的闭包为回调机制提供了支持,无论函数是否立马被调用,这个闭包都不会被释放。而且在 Js 里,无论把 callback 函数作为参数传递给其他函数,或者作为返回值返回,以便于之后调用,都是合法的。
function localContext() {
var localVal = 1;
var callback = function() {
console.log(localVal);
}
console.log(callback);
setTimeout(callback, 1000); // 1
}
localContext();
在本例中,callback 函数与其词法环境构成了闭包,其词法环境中存在的变量 localVal = 1 在函数 callback 作为回调函数传递时并没有被立即释放,而可以在回调执行时继续使用,这就是闭包为回调机制提供了支持。
循环创建闭包问题
在 ECMAScript 2015 引入 let 关键字之前,只有函数作用域和全局作用域,函数作用域中又可以继续嵌套函数作用域,在 for 并未具备局部作用域,于是有一个常见的闭包创建问题:
function counter() {
var arr = [];
for (var i = 0; i < 3; ++i) {
arr[i] = function() {
return i;
}
}
return arr;
}
var coun = counter();
for (var i = 0; i < 3; ++i) {
console.log(coun[i]()); // 3 3 3
}
解决方法一:使用匿名函数新建函数作用域
function counter() {
var arr = [];
for (var i = 0; i < 3; ++i) {
(function(i) {
arr[i] = function() {
return i;
}
})(i);
}
return arr;
}
var coun = counter();
for (var i = 0; i < 3; ++i) {
console.log(coun[i]()); // 0 1 2
}
解决方法二:使用 let 关键字
function counter() {
var arr = [];
for (let i = 0; i < 3; ++i) {
arr[i] = function() {
return i;
}
}
return arr;
}
var coun = counter();
for (var i = 0; i < 3; ++i) {
console.log(coun[i]()); // 0 1 2
}
内存泄漏
内存泄露是指你用不到(访问不到)的变量,依然占据着内存空间,不能被再次利用起来。
闭包引用的变量应该是需要使用的,不应该属于内存泄漏,但是在 IE8 浏览器中 JScript.dll 引擎使用会出现一些问题,造成内存泄漏。
性能考量
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次。