闭包指的是函数和对其词法作用域的引用。也就是说,闭包可以让你从内部函数访问外部函数的作用域。在JavaScript中,函数被创建时就会生成闭包。
词法作用域
function fn(){
let str = "Hello World";
function getStr(){
console.log(str);
}
getStr();
}
fn(); //"Hello World"
fn()创建了一个局部变量str和一个具名函数getStr()。getStr()是定义在fn()里的内部函数,仅在fn()函数体内部可用。请注意,getStr()没有自己的局部变量,但是他可以访问外部函数的变量,所以getStr()可以使用父函数fn()中声明的变量str。
这个词法作用域的例子描述了分析器如何在函数嵌套的情况下解析变量名。词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。
闭包
闭包是有函数以及声明该函数的词法环境组合而成。该环境包含了这个闭包创建时作用域内的任何局部变量。
function add(x){
return function(y){
return x + y;
};
}
let add_5 = add(5);
let add_10 = add(10);
console.log( add_5(2) ); //7
console.log( add_10(2) ); //12
在这个示例中,定义了add()函数,接收一个参数x,并返回一个新的函数。返回函数接收一个参数y,返回x+y的值。
本质上,add是一个函数工厂,他创建了一种将指定的指和他的参数相加求和的函数。在上面示例中,我们创建了两个函数,一个将其参数与5求和的函数,一个是将其参数与10求和的函数。
add_5和add_10都是闭包。他们共享相同的函数定义,但是保存了不同的词法环境。在add_5的词法环境中,x=5。在add_10中,x=10.
用闭包模拟私有方法
闭包许将函数与其所操作的词法环境关联了起来,这很类似面向对象编程。在面向对象编程中,对象允许我们将对象的属性与方法相关联。因此,通常你使用一个方法的对象的地方,就可以使用闭包。
在面向对象编程语言中,比如Java,是支持将方法声明为私有的,即他们只能被同一个类中的其他方法调用。
而JavaScript没有这种原生支持,但可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问,还能提供管理全局命名空间的能力,避免非核心的方法弄乱了代码的核心公共接口部分。
下面示例展示了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为模块模式。
let count = (function() {
let value = 0;
function change(val) {
value += val;
}
return {
add : function() {
change(1);
},
decr : function() {
change(-1);
},
getVal : function() {
return value;
}
}
})();
console.log(count.getVal()); //0
count.add();
count.add();
console.log(count.getVal()); //2
count.decr();
console.log(count.getVal()); //1
这个示例,创建了一个词法环境,为三个函数共享:count.add(),count.decr(),count.getVal()。该共享环境创建于一个立即执行的匿名函数体中。这个环境中包含局部变量value和函数change(),类似私有变量和私有方法。这两个都无法在匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。对亏了JavaScript的词法作用域,他们都可以访问变量value和函数change()。
实际上上面的示例定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他赋值给变量count。我们可以把这个函数存储在另一个变量中,并用他来创建多个计数器。
let makeCount = function(){
let value = 0;
function change(val){
value += val;
}
return{
add : function() {
change(1);
},
decr : function() {
change(-1);
},
getVal : function() {
return value;
}
}
}
let count_1 = makeCount();
let count_2 = makeCount();
console.log(count_1.getVal()); //0
count_1.add();
count_1.add();
console.log(count_1.getVal()); //2
count_1.decr();
console.log(count_1.getVal()); //1
console.log(count_2.getVal()); //0
count_1和count_2各自独立,每个闭包都是引用自己的词法作用域内的变量value。每次调用其中一个计数器,通过改变这个变量的值,会改变这个闭包的词法环境。然而在这个闭包内对变量的修改,不会影响到其他闭包中的变量。
以这种方式使用闭包,提供了徐国与面向对象编程相关的好处,例如数据隐藏和封装。
性能问题
如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对性能具有负面影响。
例如在创建新的对象时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是每个对象的创建)。
考虑以下示例:
function myObject(name,msg){
this.name = name.toString();
this.message = msg.toString();
this.getName = function(){
return this.name;
};
this.getMessage = function(){
return this.message;
}
}
在上面代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改如下:
function myObject(name,msg){
this.name = name.toString();
this.message = msg.toString();
}
myObject.prototype.getName = function(){
return this.name;
}
myObject.prototype.getMessage = function(){
return this.message;
}
继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法。