JavaScript函数之闭包
什么是闭包
闭包是JavaScript的难点,闭包产生的原因也是因为函数作用域的特性,函数作用域的内容可以回顾上一篇文章JavaScript函数之作用域。闭包是什么呢,很多文档资料对闭包的讲解也是非常的抽象,比如引用Mozilla对闭包的解释:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。简单的理解是:闭包其实是一个被引用的函数作用域。这个作用域是函数的一个私有空间,由于私有空间的变量被引用,从而形成闭包不让它销毁。
回顾作用域
在JavaScript中,作用域无非两种:局部和全局作用域。作用域的特点也很明显,内层可以访问外层的作用域。比如下面的示例:
function myFun() {
var num = 10;
function inFun(){
console.log(num);
}
inFun();//10
}
myFun();
当执行外层函数myFun时,该函数产生了一个内部变量num和一个内部函数inFun,所以在myFun函数外部无法访问到内部的作用域,原因是作用域创建的顺序有先后。同理,嵌套函数也是一样。
闭包
上面的示例中,myFun的嵌套函数inFun执行后成功输出了num的值,这是我们意料之中的事,因为嵌套函数可以访问外部函数的作用域,但是如果我们要在myFun外面执行inFun函数呢,看下面的例子
function myFun() {
var num = 10;
function inFun(){
console.log(num);
}
return inFun;
}
var pubFun = myFun();
pubFun();//10
该示例中我们没有直接调用inFun函数,而是将它作为返回值返回了。在外层定义了一个全局变量pubFun,其值为myFun函数的调用,注意这里在声明变量pubFun时已经调用了myFun函数,所以再执行pubFun的时候输出了num的值。
在本示例中,由于myFun执行完毕,num变量可能会被回收销毁,但是由于内部函数对其有引用,所以myFun函数形成了一个闭包,阻止局部变量被销毁,所以闭包可以理解为是一个作用域,包括该作用域的所有引用。
计数器
再看一个闭包的示例:
function add() {
var num = 0;
function addx(x){
return num+=x;
}
return addx;
}
var myAdd = add();
console.log(myAdd(1));//1
console.log(myAdd(1));//2
console.log(myAdd(1));//3
由于闭包,num变量不会被销毁或重置,所以每次执行都会累加,这样就形成了计数器。我们也可以让它的计数梯度为其它的值,比如下面:
function add() {
var num = 0;
function addx(x){
return num+=x;
}
return addx;
}
var myAdd = add();
console.log(myAdd(2));//2
console.log(myAdd(2));//4
console.log(myAdd(2));//6
当然,这样的应用就有很多了,其核心就是JS函数形成的闭包。
闭包模拟私有方法
学过Java的同学都知道,Java可以用private关键字来修饰私有方法,而原生的js并没有提供支持,我们可以使用闭包来模拟私有方法,私有方法只能被同一个类所调用。
function math(x) {
var num = 0;
return {
add : function (y) {
return x + y;
},
multi : function (y) {
return x * y;
}
};
}
var myMath = math(5);
console.log(myMath.add(5));//10
console.log(myMath.multi(5));//25
例子简单,够用就好,不过这个示例还就可以使用立即执行函数优化,下一篇文章讲解立即执行函数。
闭包引发的性能问题
因为闭包中的作用域引用无法释放,大量的使用闭包会严重的消耗内存,如果不是特定任务需要使用闭包,在其它函数中创建函数不是一个明智选择。比如下面的例子:
function MyObj(name){
this.name = name.toString();
this.getName = function(){
return this.name;
}
}
类似这种示例,每次构造器被调用时,方法都会被重新赋值一次。应该关联于对象的原型,而不是定义到对象的构造器中。
function MyFun(name){
this.name = name.toString();
}
MyFun.protoype = {
getName : function(){
return this.name;
}
}
但是也不建议重新定义原型,而是推荐在原型对象上添加原型方法。
function MyFun(name){
this.name = name.toString();
}
MyFun.protoype.getName = function(){
return this.name;
}
这里就涉及到构造函数和原型的知识点了,先混个眼熟吧。