MDN: 函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在创建时生成闭包。
闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量和函数。
什么是闭包?
能够读取其他函数内部变量的函数。在Js中,只有函数内部的函数才能读取局部变量。所以可以说闭包一个定义在函数内部的函数。
示例:
function makeFunc() {
var name = "Mozilla"; // name是makeFunc的局部变量
// displayName()函数和它的词法环境,组成了一个闭包
function displayName() {
alert(name); // 使用了父函数中声明的变量
}
return displayName;
}
//执行makeFunc,返回结果是函数displayName
var myFunc = makeFunc();
//执行displayName,此时displayName仍能访问makeFunc的局部变量name
myFunc();
displayName
可以访问makeFunc
的内部变量,外部无法访问makeFunc
的内部变量,所以将displayName
作为返回结果,在外部就可以通过displayName
访问makeFunc
的内部变量name
了。
在一些编程语言中,函数中的局部变量仅在函数的执行期间可用,一旦函数执行完了,那么它的内部变量将不能被访问。 但是在js中,函数在执行完以后,它的内部变量还可以被它的内部函数访问。
为什么会这样呢?
原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后被垃圾回收机制(garbage collection)回收。
私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):
var makeCounter = function() {
//私有变量
var privateCounter = 0;
//私有方法
function changeBy(val) {
privateCounter += val;
}
return {
//Counter.increment,Counter.decrement 和 Count共享一个词法环境,
//也就是说,外部函数的变量privateCounter 和函数changeBy以下三个函数是都可以访问的
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上面示例创建了一个词法环境和三个函数。
环境包含两个私有项:名为 privateCounter
的变量和名为 changeBy
的函数。
三个函数:Counter.increment
,Counter.decrement
和 Counter.value
。
这三个函数是共享同一个环境的闭包。由于JavaScript 的词法作用域,它们都可以访问 privateCounter
变量和 changeBy
函数。
为什么上面Counter2的value并没有受到影响?
每个闭包都是引用自己词法作用域内的变量 privateCounter
。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
为什么?
闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
闭包会在父函数外部,改变父函数内部变量的值。
如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。
为什么?
原因是这将导致每次构造器被调用时,方法都会被重新赋值一次。
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
//构造器MyObject被调用时,this.getName和this.getMessage方法都会被重新赋值一次
let obj = new MyObject('shang','123');
//改造
function MyObject2(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject2.prototype.getName = function() {
return this.name;
};
MyObject2.prototype.getMessage = function() {
return this.message;
};
//调用MyObject2,它属性中的两个方法并不会被影响
let obj = new MyObject2('shang','123');
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
//这里的this指向的是window
console.log(this.name);
};
}
};
object.getNameFunc()(); //The Window
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
//闭包中的this指向的是定义它的函数,即object
var that = this;
return function(){
//这里的name就是object.name
console.log(that.name);
};
}
};
object.getNameFunc()(); //My Object
this在最终调用时才确定,而不是定义时确定。
思考题解释:
object.getNameFunc()()
。
getNameFunc
是由object
调用,所以getNameFunc
的this
指向object
。
第一个示例中,执行第二个方法时,由于this
指向是不能像作用域一样存在链式的,所以执行第二个方法时其实是window
在调用,所以输出是The Window
。
第二个示例中,我们将this
(this
指向object
)转存that
,在第二个方法中输出that.name
相当于输出object.name
,所以输出My Object
。