闭包
闭包是什么
- 当内部函数被保存到函数外时,就会产生闭包。
- 闭包会使得原有作用域链不释放,导致内存泄漏。
内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
举个栗子:
function a () {
function b() {
var bbb = 234;
console.log(aaa);
}
var aaa = 123;
return b;
}
var glob = 100;
var demo = a();
demo();
联系上文作用域知识:
- a undefined --> 0 : GO
- a doing --> 0 : aAO
--> 1 : GO - b undefined --> 0 : aAO
--> 1 : GO - b doing --> 0 : bAO
--> 1 : aAO
--> 2 : GO
demo = a ();使得b函数保留在了demo中,所以可以在函数外部,对b进行访问。
同时,函数的return是这个传送门,可以将一个内部函数送出外部函数。 即使无法直接访问到外部函数内部的变量,也可以通过return出的内部函数去访问外部函数的变量。
这样的做法,被总结称为 闭包。
由于闭包会导致内存泄露,所以使用闭包时一定要注意,在编码时更要注意无意产生闭包的情况,过多的闭包会导致加载慢。
闭包的作用
实现公有变量
- 累加器
function add(){
var count = 0;
function demo(){
count ++;
console.log(count);
}
return demo;
}
var counter = add();
counter();//执行一次,count就会累加一次
存储结构
function eater(){
var food = "";
var obj = {
eat:function () {
console.log("I am eating" + food);
food = "";
},
push:function(myFood){
food = myFood;
}
}
return obj;
}
var eater1 = eater();
eater1.push("banana");
私有化变量
function person(name,age){
this.name = name;
this.age = age;
var friend = 'john';
this.sayFriendName = function(){
console.log(friend);
}
}
var person1 = new person('merry',21);
此时在函数外部调用friend时不能返回值。如person.friend返回Undefined.
而调用包含friend元素的方法时,可以引用到friend并返回值。
立即执行函数
当我们理解了 闭包 的概念后,就会出现一种需求,我们并不需要多次调用闭包函数,甚至并不需要闭包的返回值,仅仅是需要它执行一次。那上述函数最简单的写法是否如下:
function a(){/* code */}() //SyntaxError: Unexpected token ( 解析错误
很可惜,结果却是报错。
原因:
当JavaScript在读取代码时,看到 function 关键字后,就已经认为这是一个函数声明了,而在函数声明之后,是不可以直接加()来执行的。我们要做的就是让js知道这不是一个函数声明即可。
解决方法:
可以用圆括号将整个函数包起来再(),更可以使用简单的判断符号来处理。这样的做法,就称为 立即执行函数 。
(function a(){/* code */})()
而一般情况下,只对匿名函数使用这种立即执行函数,通常步骤为:
- 声明一个匿名函数。
- 马上调用这个匿名函数,执行完立即释放
(function(){alert('我是匿名函数')});
上面是一个典型的立即执行函数。
- 首先声明一个匿名函数 function(){alert(‘我是匿名函数’)}。
- 然后在匿名函数后面接一对括号 (),调用这个匿名函数。
那么为什么还要用另一对括号把匿名函数包起来呢?
如果我们不加另一对括号,直接写成
function(){alert('我是匿名函数')}()
会显示解析错误。
在js中,只有表达式才能被执行符号执行:
如:
var a = function test(){
var aaa = 123;
console.log(aaa);
}
语法可以被执行,但是被执行的函数将会被忽略函数名,
因此test将变成未知量。
同样,以下的语法都可以被执行:
- (function(){alert(‘我是匿名函数’)} ()) // 用括号把整个表达式包起来
- (function(){alert(‘我是匿名函数’)}) () //用括号把函数包起来
- !function(){alert(‘我是匿名函数’)}() // 求反,我们不在意值是多少,只想通过语法检查。
- +function(){alert(‘我是匿名函数’)}()
- -function(){alert(‘我是匿名函数’)}()
- ~function(){alert(‘我是匿名函数’)}()
- void function(){alert(‘我是匿名函数’)}()
- new function(){alert(‘我是匿名函数’)}()
同样的,()也是运算符,可以将原式转换成表达式。
因此存在两种括号的写法。
( function ( ) { } ( ) )
( function ( ) { } ) ()
立即执行函数的作用
- 不必为函数命名,避免污染全局变量;
- 内部形成单独的块级作用域,封装一些私有变量;
- 内部变量执行完即销毁,不会占用更多的内存。
以一个著名的面试题为例:
var liList = ul.getElementsByTagName('li');
for(var i = 0; i < 6; i ++){
liList[i].onclick = function(){
alert(i) // 为什么 alert 出来的总是 6,而不是 0、1、2、3、4、5
}
}
为什么 alert 的总是 6 呢,因为 i 是贯穿整个作用域的,而不是给每个 li 分配了一个 i,如下:
那么怎么解决这个问题呢?用立即执行函数给每个 li 创造一个独立作用域即可:
var liList = ul.getElementsByTagName('li')
for(var i=0; i<6; i++){
!function(ii){
liList[ii].onclick = function(){
alert(ii) // 0、1、2、3、4、5
}
}(i)
}
在立即执行函数执行的时候,i 的值被赋值给 ii,此后 ii 的值一直不变。
i 的值从 0 变化到 5,对应 6 个立即执行函数,这 6 个立即执行函数里面的 ii 「分别」是 0、1、2、3、4、5。