一、什么是闭包
二、闭包的几种写法
三、闭包的用途
四、使用闭包的注意点
一个拥有许多变量和绑定了这些变量的环境的表达式,因而这些变量也是该表达式的一部分。闭包的特点:
1. 作为一个函数变量的一个引用,当函数返回时,其处于激活状态。
2. 一个闭包就是当一个函数返回时,一个没有释放资源的栈区。
简单的理解就是:函数里面包含内部函数,内部函数可以访问它们所在外部函数中声明的所有局部变量、参数等,当内部函数所在的外部函数被调用时,就会形成闭包。与普通函数不同是,普通函数执行完毕之后,就会销毁局部活动对象,内存不会再保存。而闭包则不同,外部函数执行完毕之后,其执行环境的作用域链会被销毁,但是它的活动对象仍然留在内存,直到内部的返回函数被销毁之后,外部函数的活动对象才会被销毁。
通俗的讲就是:闭包也是一个函数,它能够访问它所在作用域的外部函数中的变量。
注意:很多人都有一个误区,闭包并不是特指那些必须要返回嵌套函数中的内部函数的行为才叫闭包。在《高级程序设计》中对闭包定义是这样的:“闭包是指有权限访问另一个函数作用域中的变量的函数”。这里没有提到这个函数必须要return出来。我们再来看看《JavaScript语言精粹》中对闭包的定义,它是用一段很误导人的代码例子来解释闭包的,代码如下
var func = function(status) {
return {
getStatus: function() {
return status;
}
}
}
var myFunc = func("amazed");
document.writeln(myFunc.getStatus());
即使func对象返回了,但getStatus函数仍然享有访问func对象的status属性的特权。getStatus方法并不是访问该参数的一个副本,它访问的是该参数本身,只是该函数可以访问它被创建时所处的上下文环境(涉及JS的解析原理)。
这是很多解释闭包的文章最常用的解释案例,所以导致新手第一次看这种解释产生一个误导:必须要return这个函数。但在《JavaScript语言精粹》这段解释中最后强调的是“该函数可以访问被创建时所处的上下文环境”,强调的是访问外部函数的局部变量。
而用这个例子,是因为这个例子是闭包的更为复杂的应用,因为你在函数嵌套中,内部函数的运行只能在外部函数中执行,要在全局变量中运行不了,如果我们要在全局运行一个比较容易理解的方法是:
var getStatus;
var func = function(status) {
getStatus = function() {
return status;
}
}
func("amazed");
document.writeln(getStatus());
那这种是不是闭包呢?对上面代码进行优化利用 JS 可以return函数代码简化了很多。所以个人理解的是,只要调用了外部函数变量的函数都是闭包,而之所以对闭包的介绍都用那个案例,是因为那个算是闭包的经典复杂的应用,所以基本介绍闭包的都会介绍那个案例,这样反而误导了刚学习闭包的同学。
知道了闭包的基本概念之后,就要知道闭包长什么样。下面来看看闭包常见4种写法是怎么样的。
//【第1种写法】
//在JS中函数也是对象,这种写法没什么特别的,只是给函数添加y一些属性。
function Circle(r) {
this.r = r;
}
Circle.PI = 3.14159;
Circle.prototype.area = function() {
return Circle.PI * this.r * this.r;
}
var c = new Circle(1);
alert(c.area());
//【第2种写法】
//这种写法是声明一个变量,将一个函数当作值赋给变量。
var Circle = function() {
var obj = new Object();
obj.PI = 3.14159;
obj.area = function(r) {
return this.PI * r * r;
}
return obj;
}
var c = new Circle();
alert(c.area(1));
//第3种写法
//这种方法最好理解,就是new 一个对象,然后给对象添加属性和方法。
var Circle = new Object();
Circle.PI = 3.14159;
Circle.Area = function(r) {
return this.PI * r * r;
}
alert(Circle.Area(1));
//第4种写法
//这种方法使用较多,也最为方便。
var Circle = {
"PI": 3.14159,
"area": function(r) {
return this.PI * r * r;
}
};
alert(Circle.area(1));
上面的第一种写法中,我们创建了一个函数对象,并将其赋予一个变量。在JavaScript种函数存在prototyp属性,而对象则没有。
没有使用prototype属性定义的对象方法是静态方法,只能直接用类名进行调用,并且无法使用this变量来调用其他属性。而使用了prototype属性定义的对象则相反,它是非静态方法,只能在实例化之后才能调用方法,也可以使用this变量来引用自身中的其他属性。例如下面的例子:
function funcObj(x){
this.x = x;
}
funcObj.func1 = function(){
console.log("fun1函数")
}
funcObj.prototype.func2 = function(){
console.log("fun2函数")
}
//函数直接调用
funcObj.func1() //控制台输出 "fun1函数" 和 undefined
funcObj.func2() //控制台报错
//创建对象之后再调用
var f = new funcObj(5);
f.func1() //控制台报错
f.func2() //控制台输出 "fun2函数" 和 5
我们再来看看上面的第二中写法,我们在一个匿名函数里面创建另外一个匿名函数,并分别赋值给一个对象和对象属性。我们先来看看下面这段代码:
ar dom = function() {
var Name = "LaoYe";
this.Sex = "male";
this.intraduce = function() {
alert("intraduce");
};
};
console.log(dom.Name); //undefined
console.log(dom.Sex); //undefined
控制台中打印出的结果都是undefiend,这是为什么?由于在JavaScript中每个函数都会形成一个作用域,变量在函数内部声明时,声明的变量就处于这个函数的作用域中,外部无法访问。如果想要访问该变量,就必须通过创建一个实例对象,通过该对象进行访问。例如对上诉的代码进行改进之后如下:
var dom = function() {
var Name = "LaoYe";
this.Sex = "male";
this.intraduce = function() {
return "My name is " + Name + ",and sex is " + this.Sex;
};
};
var newObj = new dom();
console.log(newObj.intraduce())
注意:通过函数表达式创建的内部属性,外部想要访问就必须对该函数进行实例化,通过对象去访问函数的内部属性。但是,实例化的对象不能访问该函数的所有局部变量,只能访问含有this关键字的属性,因为this指向引用的对象。
下面再来看看第四种的写法,这种写法就是在一个对象里面创建嵌套函数。其实这是JavaScript的一个“语法糖”。例如下面的这个例子:
var Func= {
Name: 'Object',
Success: function() {
this.Say = function() {
alert("Hello,world");
};
}
};
这种写法相当于
var Func = new Object();
Func.Name = 'Object';
Func.Success = function() {
this.Say = function() {
alert("Hello,world");
};
}
变量Func是一个对象,不是函数,所以就没有prototype属性,其方法也都是共有方法,而且也不能被实例化(因为它已经是由Object函数实例化出来的对象而不是函数)。还有就是内部属性变量也可以直接访问。
闭包可以用在许多地方。它的最大用处有两个:一是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
1、匿名自执行函数
我们知道所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处。
比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。
除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护。
比如UI的初始化,那么我们可以使用闭包:
var data = {
table: [],
tree: {}
};
(function(dm) {
for(var i = 0; i < dm.table.rows; i++) {
var row = dm.table.rows[i];
for(var j = 0; j < row.cells; i++) {
drawCell(i, j);
}
}
})(data);
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在函数执行完后会立刻释放资源,关键是不污染全局对象。
2、结果缓存
我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,
那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。
var CachedSearchBox = (function() {
var 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();
}
}
};
})();
CachedSearchBox.attachSearchBox("input");
3、封装
var person = function() {
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
}();
console.log(person.name); //undefined
console.log(person.getName()); //default
person.setName("abruzzi");
console.log(person.getName()); //abruzzi
4、实现类和继承
function Person() {
var name = "default";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
};
var p = new Person();
p.setName("Tom");
alert(p.getName());
var Jack = function() {};
//继承自Person
Jack.prototype = new Person();
//添加私有方法
Jack.prototype.Say = function() {
alert("Hello,my name is Jack");
};
var j = new Jack();
j.setName("Jack");
j.Say();
alert(j.getName());
我们定义了Person,它就像一个类,我们new一个Person对象,访问它的方法。下面我们定义了Jack,继承Person,并添加自己的方法。
1> 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是:在退出函数之前,将不使用的局部变量全部删除。
2> 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。