什么是闭包?
函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。
闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。
简要来讲,闭包就是一个函数引用另外一个函数的变量,因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量。
MDN 上面这么说:闭包是一种特殊的对象。
词法作用域
废话不说,上代码:
function init() {
var name = "first"; // name 是一个被 init 创建的局部变量
function consoleName() { // displayName() 是内部函数,一个闭包
console.log(name); // 使用了父函数中声明的变量
}
consoleName();
}
init(); //输出 first
函数init创建了一个局部变量name,和一个名为consoleName的内部函数。consoleName没有自己的局部变量,然而它可以访问到外部函数的变量,于是输出‘first’;
闭包
理解闭包的关键在于:外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象,这就是闭包的重要概念。
function outer(){
var a = 1;
return function (){
return a;
}
}
const b = outer();
console.log(b()); // 1
以上就是一个闭包,b得到的是outer函数内部返回回来的函数,因而在调用b时,可以访问到outer的内部变量a。创建闭包最常见方式,就是在一个函数内部创建另一个函数。
常见的陷阱,闭包只能取得包含函数中的任何变量的最后一个值
下面这个例子:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
console.log(funcs[i]());
}
输出10个10
这里的陷阱就是:函数带()才是执行函数! 单纯的一句 var f = function() { alert(‘Hi’); }; 是不会弹窗的,后面接一句 f(); 才会执行函数内部的代码。
上面的代码翻译过来就是
var result = new Array(), i;
result[0] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
result[1] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
...
result[9] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
i = 10;
funcs = result;
result = null;
console.log(i); // funcs[0]()就是执行 return i 语句,就是返回10
console.log(i); // funcs[1]()就是执行 return i 语句,就是返回10
...
console.log(i); // funcs[9]()就是执行 return i 语句,就是返回10
为什么只垃圾回收了 result,但却不收了 i 呢?
因为 i 还在被 function 引用着。当createFunctions执行完毕后,其作用域被销毁,但它的变量对象仍保存在内存中,得以被匿名访问,这时i的值为10。
要想保存在循环过程中每一个i的值,需要在匿名函数外部再套用一个匿名函数,在这个匿名函数中定义另一个变量并且立即执行来保存i的值。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return function(){
return i;
}(i);
};
}
return result;
}
var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
console.log(funcs[i]());
}
这时最内部的匿名函数访问的是num的值,所以数组中10个匿名函数的返回值就是1-10。
闭包中的this对象
var name = 'window';
var obj = {
name:'obj',
getName:function(){
return function(){
return this.name;
}
}
}
console.log(obj.getName()())
此处调用obj.getName()后返回的是一个匿名函数,所以this指向window。
函数名与函数功能是分割开的,不要认为函数在哪里,其内部的this就指向哪里。
window才是匿名函数功能执行的环境。
如果想使this指向外部函数的执行环境,可以这样改写:
var name = 'window';
var obj = {
name:'obj',
getName:function(){
const s = this;
return function(){
return s.name;
}
}
}
console.log(obj.getName()())
在闭包中,arguments与this也有相同的问题。下面的情况也要注意:
var name = 'window';
var obj = {
name:'obj',
getName:function(){
return this.name;
}
}
obj.getName(); //obj;
(obj.getName = obj.getName)(); //window
obj.getName();这时getName()是在对象obj的环境中执行的,所以this指向obj。
(obj.getName = obj.getName)赋值语句返回的是等号右边的值,在全局作用域中返回,所以(obj.getName = obj.getName)();的this指向全局。要把函数名和函数功能分割开来。
闭包的注意事项
通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。
因此需要及时释放闭包内的函数。
function makeAdd(x){
return function(y){return x+y}
}
var md5 = makeAdd(5);
var md10 = makeAdd(10);
console.log(md5(2)); //7
console.log(md10(2)); //12
//释放对闭包的引用
md5 = null;
md10 = null;
在javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收;
如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。
函数内部的定时器
当函数内部的定时器引用了外部函数的变量对象时,该变量对象不会被销毁。
(function(){
var a = 0;
setInterval(function(){
console.log(a++);
},1000);
})();
闭包的应用
应用闭包的主要场合是:设计私有的方法和变量。
闭包运用的关键
闭包引用外部函数变量对象中的值;
在外部函数的外部调用闭包。