闭包,就是将不在自己作用域范围内的变量通过包装,然后作为另一个函数的入参或者返回值,使得在其他的作用域也能被访问。由于实现了跨作用域的变量访问,所以除非变量被释放,否则的话变量占用的内存一直都不会被回收。闭包跟作用域之间其实息息相关。所以在聊闭包之前,先看看下面的代码。
- 案例一
使用表达式函数的时候,此时dom就是一个普通的函数对象的实例,跟普通的对象没什么区别。Display函数是prototype的属性,所以直接通过dom是调用不到的。通过new创建对象d,通过d.Display()的时候会到原型对象中去查找,所以打印‘Property Message’,但是通过对象实例调用Show方法,d对象自己本身没有这个方法,在原型对象中也找不到该方法,所以会报错。
var dom = function(){
};
dom.Show = function(){
console.log("Show Message");
};
dom.prototype.Display = function(){
console.log("Property Message");
};
dom.Display(); // 报错,函数不存在
dom.Show();// Show Message
var d = new dom();
d.Display();// Property Message
d.Show();// 报错
- 案例二
由于js的作用域是函数级别的,所以在函数外面是访问不到函数中的变量的。
var dom = function(){
var Name = "Default";
this.Sex = "Boy";
this.success = function(){
console.log("Success");
};
};
console.log(dom.Name);// undefined
console.log(dom.Sex);// undefined
闭包
存在的意义
如果想在一个函数中访问另一个函数的变量,直接访问是访问不到的,因为js是函数作用域,函数中的变量对于函数外部来说是不透明的。如果要解决这种需要,那就可以使用闭包。当然这只是闭包可以解决的一个小方面。利用闭包可以解决上面作用域的问题。
闭包的例子
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将 baz 分配给全局变量
}
function bar() {
fn(); // 这就是闭包!在bar的作用域中访问了foo作用域中的a变量
}
foo();
bar(); // 2
应用
- 控制变量的作用域
如果定义一个函数表达式 let fn = function(){},如果此时要执行该函数,则是fn()。由于fn与function(){}的含义是一样的,所以可以用function(){}替换fn,则是function(){}(),如果只是这样写的话会报错,所以需要使用一个括号将函数的定义包起来,则是(function(){})(),也就是下面的代码。
由于js是函数作用域,所以对于content中的变量,对于外部来说是不可见的,所以起到了隔离的效果,不会造成全局污染,利用闭包来实现作用域的控制。
(function (argument) {
// content
})(); // 其实从这个角度来看,闭包其实就是为来解决作用域问题的
- 实现共享变量,数据的存储
利用返回值,向外暴漏一个对象,在对象中实现了添加和获取缓存的方法,则外部可以实现缓存,这实际也是共享的一个实现方式。
var CachedSearchBox = (function(){
var cache = {},// 由于所有调用返回函数的时候都是使用同一个cache,也就是同一份存储空间,所以对于cache来说相当于一个全局存储器
count = [];
return {
attachSearchBox : function(dsid){
if(dsid in cache){//如果结果在缓存中
return cache[dsid];//直接返回缓存中的对象
}
var fsb = new uikit.webctrl.SearchBox(dsid);//新建
cache[dsid] = fsb;//更新缓存
if(count.length > 100){//保正缓存的大小<=100
delete cache[count.shift()];
}
return fsb;
},
clearSearchBox : function(dsid){
if(dsid in cache){
cache[dsid].clearSelection();
}
}
};
})();
- 作为回调函数
function setupBot(name, selector) {
$(selector).click( function activator() { // 这里就利用了闭包,name变量的作用域本来是在setupBot中,函数执行完毕之后,name占用的内存不会被回收,因为闭包传递给了回调函数的缘故。
console.log( "Activating: " + name );
});
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );
带来的问题
由于js存在变量提升,所以变量i的声明会被提升到全局作用域,并且执行初始化,则是var i = undefined;当循环执行结束之后,i并不会被销毁,因为回调函数中引用了i变量,此时i的值为6,当5个触发器执行的时候,其实共用的都是同一个全局的内存变量i,所以会打印出5个6.这里的问题就是所有的回调函数都共用了同一个内存变量,所以打印的结果都是一样的。
for (var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log( i );
}, i*1000 );
}
解决的办法其实有很多。
1、利用作用域来进行规避
for (var i=1; i<=5; i++) {
(function(i){ // 由于js是函数作用域,这里使用一个立即执行函数,将回调函数中的i与外部的全局变量分隔开,使用了局部变量。所以在执行回调的时候不会被外部的i变量影响。其实这里也可以理解为创建了多个变量,将变量传递给函数的时候,相当于在函数内重新声明一个变量,然后将每一个变量都传递给回调函数,所以彼此之间不会相互影响。
setTimeout(function() {
console.log( i );
}, i*1000 );
})(i);
}
2、使用let来声明变量。其实对于let变量来说,也存在变量提升的问题,则是let i; 但是并没有执行初始化,所以如果此时打印i的话会报错,与var不一样,var是打印undefined。其实看到这里可能还不明白为什么使用let就能规避这个问题,因为对于所有的回调函数来讲其实还是共用一个let变量。但其实这种写法默认隐藏了一行很重要的代码,请看代码二。在代码二中,标记出来的那行代码,其实就是解决问题的关键所在,在执行每一次的循环的时候,都会重新定义一个let变量,由于作用域的关系,此时回调函数中使用的变量就变成了这个新定义的变量i,由于let是块作用域,所以let的声明不会提升到循环块的最外面,这也是很关键的,不然的话变量提神,这样的话又只是声明一次变量。由于每次都定一个新的变量,所以回调函数之间相互不影响,这种其实也跟上面的解决方案大同小异。
for (let i=1; i<=5; i++) {
setTimeout(function() {
console.log( i );
}, i*1000 );
}
代码二:
for (let i=1; i<=5; i++) {
let i = i; // 很重要!!!
setTimeout(function() {
console.log( i );
}, i*1000 );
}