闭包(closure)被认为是必不可少的语言特性,,也是所有传统语言都想添加的语言特性。
闭包很难学?实际上我们早就学习过了((((((P6函数环境与词法作用域),只是那时我们没有称之为闭包而已。
ps. 作为闭包的前置知识,务必先学习环境与词法作用域
要点
- 自由变量指的是在函数体内未绑定的变量
- 闭包指的是函数及其引用的环境。 闭包捕获其创建时所处作用域内的变量(包括自由变量)的值(将它们保存在环境中)
- JavaScript中的闭包可以简单理解成【在函数内部定义的嵌套函数】,使用闭包能够读取其他函数内的局部变量,作为连接函数内部和外部的桥梁
- 闭包的本质源自两点:词法作用域和函数作为值传递。
- 创建闭包的方法是:
在某函数的内部创建嵌套函数,且嵌套函数中使用了外围的自由变量
(自由变量 即 没有在嵌套函数本地定义的变量) - 在创建闭包的上下文外部执行它时,闭包函数的自由变量的值由其环境决定
- 通常使用闭包来为事件处理程序捕获状态
- 闭包包含的是实时的实际环境,而非环境的(静态的)副本
复习:函数与环境、词法作用域
- 函数的环境包含了在其作用域内出现的所有局部变量(包括在函数内定义的变量、包括没有在函数内定义的变量;包括函数内的嵌套函数、包括传入函数的形参,但不包含全局变量)
- 调用函数时,实际上是在与其相关联的环境中执行这个函数
- 无论何时调用函数,函数都优先使用其环境中的有定义的变量,若环境中找不到相应的变量,再向外查找同名变量
- 函数及其环境创建后,无法直接访问(无法改变)环境中的变量值,必须通过那个函数来访问和改变(私有性)
- 关于函数环境,详见((((((((((((P6
什么是闭包(closure)
- 我们知道,从函数返回函数时,返回的是函数引用(指向函数及其环境)
- 如果在函数内部创建嵌套函数,并返回这个嵌套函数,就得到了闭包
(结合使用闭包的使用场景来说,闭包多指嵌套函数及其环境,该环境包含了嵌套函数外部的变量值)
图:闭包是函数及其环境,但这不是一般情况下的“函数及其环境”,这里的环境包含了自由变量(即:在函数外部定义的变量,也称外围变量)
闭包:简单地说就是函数和引用环境
闭包(closure)与敲定(close,动词)函数有关
函数内部的变量有两种:
- 局部变量:在函数体中定义的变量
- 自由变量(本文也称之为“外围变量”):不是在本地定义的变量
称其“自由”是因为:在函数体内,自由变量没有绑定到任何值(即:它们不是在本地声明的)
有了给每个自由变量都提供了值的环境后,便将函数敲定了;而函数和环境一起被称为闭包。
因此我们对闭包下定义:
闭包 = 含有外围变量的函数 + 为这些外围变量提供了值(提供了变量绑定,将变量绑定到某个值)的环境
可以参照正式的闭包定义,加深理解
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起,这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
为什么函数中会出现不是在函数本地定义的变量(自由变量)呢?
这与JavaScript的特性有关,此情况多出现在嵌套函数中,详见((((P6 环境中的例子
为什么说闭包(closure)与敲定(close)函数有关?敲定有什么用?
对于自由变量(未在函数内定义),有两种情况:①它是全局变量;②它是“外层函数”定义的变量(当前的函数是一个嵌套函数)。
函数可以被敲定,就意味着函数内所有自由变量都是情况②(环境中不存储全局变量),意味着函数体内出现的所有变量,都能在其关联的环境中找到,在函数中处理变量时,不会发生“在本地找不到变量,需要查找全局变量”的情况,这契合了使用闭包的目的——利用环境保护局部变量,从而不与任何代码冲突(有利于多人合作开发代码,保证每个人定义的变量不冲突)
因此,函数能否被敲定与闭包的概念息息相关
闭包有什么好处
若不使用闭包,我们创建一个计数器的代码如下:
var count = 0;
function counter() {
count = count + 1;
return count;
}
console.log(counter());//输出1
console.log(counter());//输出2
console.log(counter());//输出3
这种做法的缺点是是,使用全局变量count
在代码中我们应该尽量避免使用全局变量
协作开发代码时,大家帝常会使用相同的变量名,进而导致冲突。
如果使用闭包,就能用受保护的局部变量实现计数器。这样,计数器将不会与任何代码发生冲突,且只能通过调用相应的函数(也叫闭包)来增加计数器的值。
用闭包实现“神奇”的计数器
从函数返回的函数携带了其周边环境,可利用这一点来创建闭包
function makeCounter() {
var count = 0;
function counter() {
count = count + 1;
return count;
}
return counter;//从函数返回函数(counter及其环境一同返回)
}
var doCount = makeCounter();
console.log(doCount());//输出1
console.log(doCount());//输出2
console.log(doCount());//输出3
分析(很关键!):
var doCount = makeCounter();
我们调用makeCounter,它创建函数counter,并将其与包含自由变量count的环境一起返回。换句话说,它创建了一个闭包。从makeCounter返回的函数存储在doCount中。
- 从函数返回函数,实际上返回的是函数引用,此函数引用指向函数及其环境
因此,这里makeCounter()返回一个函数引用,指向counter函数及其环境 - 函数的环境保存了在函数中出现的所有局部变量(全局变量除外)
因此,这里与counter函数关联的环境中存储了count变量的值 - 根据词法作用域,count变量未在本地定义,因此环境保存的count源自于“外层”函数的count变量,其值为0
console.log(doCount());//输出1
我们调用函数doCount,这将执行函数counter的函数体。
遇到变量count时,我们在环境中查找并获取它的值。我们将count的值加1,将结果存回到环境中,再将结果返回到调用doCount的地方。
- 函数及其环境创建后,无法直接访问(无法改变)环境中的变量值,必须通过那个函数来访问和改变(私有性)
因此,这里通过doCount调用counter时(它们实际上指向的是同一个函数),其环境中的count变量的值会改变(+1),并且没有其他方法能改变count的值 - 最终doCount函数返回值为count(count=1)
console.log(doCount());//输出2
console.log(doCount());//输出3
- 后两次与上面同理
总结:
调用makeCounter时,我们获得的是一个闭包:一个函数及其环境(保存了外围变量count)
调用doCount(指向函数counter的引用),进而需要获取count的值时,我们使用这个闭包的环境中的变量count。在外部世界(全局作用域中),根本看不到变量count,但调用doCount时可以使用它,此外没有其他任何办法能够获取count。
对闭包的理解
闭包就是[能够读取其他函数内的局部变量的函数]
本质上,闭包就是连接函数内部和外部的桥梁
在JavaScript中,只有函数内的嵌套函数才能读取函数的局部变量;
因此JavaScript中的闭包可以简单理解成【在函数内部定义的嵌套函数】
JavaScript闭包的本质源自两点,词法作用域和函数作为值传递。
代码解释执行时按照词法作用域的规则,可以访问外围变量,这些外围变量(相对于嵌套函数而言就是“自由变量”)保存在环境中
对于嵌套函数的环境,自由变量值(外围变量)在外层函数执行时创建,外层函数执行完毕时理应销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性
并且,因为自由变量(外围变量)不是在函数内定义的,却保存在函数的环境中,导致在外部世界(全局作用域中),根本看不到这些变量(自由变量),除了调用该函数,没有其他任何办法能够获取这些自由变量
闭包不是“封闭内部状态”,而是“封闭外部状态”:当外部状态的scope失效时,其状态还保存了一份在内部状态中(即:外围变量的值保存在嵌套函数的环境中)
「闭包是词法作用域的体现」
因为闭包可以大大的增强函数的处理能力,函数可以作为first-class的这一优点才能更好的发挥出来。
创建闭包
怎样创建一个闭包?
- 注意定义,闭包指函数使用了自由变量,并且将它们保存在环境中(这意味着这些变量不是全局变量,即他们是外围变量)
因此要创建闭包,要在函数内部创建嵌套函数且使用外围变量,从而产生闭包 - 另一种理解:闭包的核心在于,函数的环境中保存了自由变量的值,从而得到了受保护的局部变量(并且它们只能通过调用函数来访问,不能直接访问)。
可见,创建闭包,要在嵌套函数中使用外围变量的值(这些外围变量在外层函数中定义)
综上,创建闭包的方法是:
在某函数的内部创建嵌套函数,且嵌套函数中使用了外围的自由变量
(自由变量 即 没有在嵌套函数本地定义的变量)
法一 从函数返回函数来创建闭包
function makeMultiplierN(n) {
return function multBy(m) {//返回一个函数,它会将传入的参数乘以n
return n*m;
};
}
var multBy2 = makeMultiplierN(2);
console.log("3 multiply by 2 = " + multBy2(3));
法二 将函数表达式用作实参,创建闭包
function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
setTimeout(function() {
alert(doneMessage);
}, n);
}
makeTimer("Cookies are done!", 1000);
法三 用事件处理程序创建闭包
网页上有两个元素:
<button id="clickme">Click me!</button>
和<div id="message"></div>
每单击一次按钮,<div>
的内容显示为用户单击按钮的总次数
不使用闭包的代码如下:
var count = 0;
//count必须声明为全局的,因为如果将其作为handleClick的局部变量,
//则用户每次单击时,它都将被初始化。
window.onload = function() {
var button = document.getElementById("clickme");
button.onclick = handleClick;
};
function handleClick() {
var div = document.getElementById("message");
count++;
div.innerHTML = "You clicked me " + count + " times!";
}
使用闭包的代码如下:
将内层的嵌套函数所需的所有变量,在外层函数中声明
从而将它们作为自由变量(外围变量)保存在环境中
window.onload = function() {
var count = 0;
//现在,所有变量都是在一个函数中定义的,消除了名称冲突的问题
var div = document.getElementById("message");
var button = document.getElementById("clickme");
//函数表达式创建闭包(函数有自由变量,因此创建了闭包)
button.onclick = function() {
count++;
div.innerHTML = "You clicked me " + count + " times!";
};
};
//加载事件处理程序window.onload执行完毕后,变量button就消失了,
//但button对应得按钮对象还在DOM中,其属性onclick存储了我们的闭包
//加载事件处理程序window.onload执行完毕后,变量count、div理应消失,
//但由于闭包作为值赋给了属性onclick,闭包(函数和环境)中的自由变量被保存下来
//而且无法直接访问,必须通过闭包函数访问(即:调用事件处理程序才能访问)
//这个闭包将一直存在,直到关闭网页。它已准备就绪,每当单击按钮时就会行动起来
闭包的环境中保存了自由变量count、div的值,需要时直接从环境中获取、访问并修改
(而不用再次从DOM中获取这个div对象,提高了效率)
(count也不再是全局变量,避免了名称冲突)
每当调用事件处理程序(闭包函数),闭包的环境中的count变量都会因为执行了闭包函数中的代码count++
而改变
可见,虽然完全可以不使用闭包,但闭包有很多优点
闭包的优缺点
优点:
- 跨作用域访问变量:在函数外部也可以访问函数内的局部变量
- 避免名称冲突:将自由变量保存在环境中,需要时直接从环境中获取,减少了全局变量,避免了名称冲突
- 创建一个安全的环境:得到受保护的局部变量(私有性),它们不能直接访问
- 提高代码执行速度:由于闭包函数的环境中保存了自由变量(未在本地定义),执行时遇到自由变量,直接从环境中获取它的值,而不用到外层查找
- 让代码更简洁
缺点:内存消耗大,需要保存环境中的自由变量
注意 闭包包含的是实时的实际环境,而非环境的(静态的)副本
环境是实时更新的,访问和改变环境中的自由变量(外围变量)值,要么直接改变环境中的自由变量值,要么改变(嵌套函数的)外层函数中的相应变量
换句话说,要访问和改变环境中的自由变量,有两种方法:
- 直接改变环境中的自由变量值:函数及其环境创建后,无法直接访问环境中的变量值,必须通过调用那个函数来访问和改变(如上面的Eg3所述)
- 改变(嵌套函数的)外层函数中的相应变量:在外层函数中,修改相应的自由变量的值
在闭包函数(嵌套函数)外面的外层函数代码中改变了变量,闭包函数的环境将引用改变后的新值
为证明这一点,有如下的示例
function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
setTimeout(function() {
alert(doneMessage);
}, n);
doneMessage = "OUCH!";
}
makeTimer("Cookies are done!", 1000);
输出"OUCH!"
下面是分析
function makeTimer(doneMessage, n) {//设置定时器,它在n毫秒后提示doneMessage
...
}
makeTimer("Cookies are done!", 1000);
首先调用了makeTimer,并传入参数
function makeTimer(doneMessage, n) {
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
setTimeout(function() {
alert(doneMessage);
}, n);
...
}
在这里,向setTimeout传递事件处理程序时,使用了函数表达式创建闭包
在此刻,环境中doneMessage的值为"Cookies are done!"
function makeTimer(doneMessage, n) {
//创建一个函数(使用了自由变量doneMessage),并作为setTimeout的事件处理程序
setTimeout(function() {
alert(doneMessage);
}, n);
doneMessage = "OUCH!";
}
紧接着又在闭包外面修改doneMessage的值为"OUCH!"
最终,1000毫秒后调用事件处理程序时,由于闭包函数的环境中,doneMessage变量的值为"OUCH!",提示框中显示的是"OUCH!"