JavaScript闭包总结

  闭包可以说在JavaScript的函数表达式方面是一个很重要的知识点。有不少初学者总搞不清楚匿名函数闭包这两个概念,这里都一一做一个总结,巩固自己,希望对初学者有帮助。

闭包的概念

  闭包是指有权去访问另一个函数作用域中的变量的函数。这句话很好理解,首先它是一个函数,其次是有权访问另一个函数作用域中的变量。我们知道局部方法可以访问外部父类方法的属性。

1
2
3
4
5
var firstValue = 'JX';
function f1(){
console.log(firstValue);
}
f1();

输出

1
JX

而从父类去获取子函数方法的属性的值是否也可以呢?

1
2
3
4
function f2(){
var secondValue = 'love';
}
console.log(secondValue);

输出

1
Uncaught ReferenceError: secondValue is not defined

结果显示参数获取不到的错误,secondValue未定义。也就是说函数外的区域是获取不到里面的变量的,这是函数作用域所决定的。

那如果我们需要在外部获取到这个变量应该怎么办呢?

我们可以利用函数可以获取外部变量这一个特性,再把这个函数做为返回值return出来,这一思路就可以轻松实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function f3(){
var thirdValue = 'you';
var fourthValue = '!';
function f4(){
var get_thirdValue = thirdValue;
var get_fourthValue = fourthValue;
var data = {
thirdValue : get_thirdValue,
fourthValue : get_fourthValue
}
return data;
}
return f4;
}
var getMan = f3();
console.log(getMan().thirdValue);

输出

1
you

而我们返回的这个函数,即f4()就是一个闭包,是不是很简单。
在上面的代码中,函数f4就被包括在函数f3内部,这时f3内部的所有局部变量,对f4都是可见的。但是反过来就不行,f4内部的局部变量,对f3就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

闭包的特性

  1. 嵌套在函数里的函数
  2. 有权访问函数外面的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

看到这,前两个特性已经很清楚了,那么说说第三个。我们都知道JavaScript和大多数语言一样也有垃圾回收机制。在什么情况下会被回收呢?如下

  • 如果一个对象不再被引用,那么这个对象就会被GC回收;
  • 如果两个对象互相引用,而不再被第三者所引用,那么这两个互相引用的对象也会被回收。

(垃圾回收不是本篇的重点,不做详细解读,更多可以传送到JavaScript垃圾回收总结

看之前的代码f3是f4的父函数,而f4被赋给了一个全局变量,这导致f4始终在内存中,而f4的存在依赖于f3,因此f3也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

优缺点

优点

  1. 希望一个变量长期驻扎在内存中
  2. 避免全局变量的污染
  3. 私有成员的存在
  4. 使用闭包可以大大减少我们的代码量

缺点

  1. 变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除,把值置null。
  2. 被保存在内存中的变量可以在函数外部改变。所以如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包的使用

说到它的使用就不得不提及到匿名函数

匿名函数

函数是JavaScript中最常见也最灵活的一种对象。顾名思义匿名函数就是没有函数名的函数。

匿名函数的定义

JS中函数的常见定义大致有三种。其中

1
2
3
var anonymousFun = function(arg){
return arg++;
}

=号右边的函数就是一个匿名函数。
第二种方式,接触过jQuery的博友,就非常常见到。形如()();,第一个括号放匿名函数体,第二个括号放调用的参数,可为空。

1
2
3
(function(arg1,arg2){
console.log(arg1+arg2);
})(7,7);

匿名函数最多的用途是创建闭包,并且还可以构建命名空间,以减少全局变量的使用。

1
2
3
4
5
6
7
8
9
10
11
12
var namespace = {};
(function(){
var firstValue = 'JX';
var secondValue = 'love';

var firstEvent = function(){
return firstValue+' ' +secondValue;
};

namespace.firstEvent = firstEvent;
})();
namespace.firstEvent();

输出

1
"JX love"

最后小测试

1
2
3
4
5
6
7
8
9
10
11
12
13
//在页面上添加自定义的新节点
var htmlChild="<ul><li>L</li><li>O</li><li>V</li><li>E</li></ul>";
var newNode = document.createElement("div");
newNode.innerHTML =htmlChild;
document.body.appendChild(newNode);

//添加js事件监听
var lists = document.getElementsByTagName('li');
for(var i=0;i<lists.length;i++){
lists[i].onmouseover = function(){
console.log(i);
}
}

输出

1
4

你会发现当鼠标移过每一个li元素时,控制台总是显示4,而不是我们期待的元素下标。这是为什么呢?当mouseover事件调用监听函数时,首先在匿名函数( function(){ console.log(i);})内部查找是否定义了 i,结果是没有定义;因此它会向上查找,查找结果是已经定义了,并且i的值是4(循环后的i值)。所以最终每次显示的都是4。

解决方法一:

1
2
3
4
5
6
7
8
var lists = document.getElementsByTagName('li');
for(var i=0;i<lists.length;i++){
(function(index){
lists[ index ].onmouseover = function(){
console.log(index);
};
})(i);
}

i的值是4,index的值0-3保存在内存中了。

解决方法二:

1
2
3
4
5
6
7
var lists = document.getElementsByTagName('li');
for(var i=0;i<lists.length;i++){
lists[ i ].$$index = i;
lists[ i ].onmouseover = function(){
console.log(this.$$index);
};
}

通过在Dom元素上绑定$$index属性记录下标

解决方法三:

1
2
3
4
5
6
7
8
9
10
11
//定义一个闭包函数 index 会被保存在内存中
function listenEvent(list,index){
list.onmouseover = function(){
console.log(index);
}
}

var lists = document.getElementsByTagName('li');
for(var i=0;i<lists.length;i++){
listenEvent(lists[ i ] , i);
}
0%