一、什么是闭包?
闭包出现的原因:js
允许函数嵌套。由于在Javascript
语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成定义在一个函数内部的函数。
正常函数嵌套:
function f(){
var i = 0;
function s(){
console.log(i++); //0
}
s();
}
f();
在js
中,正常的函数嵌套,其父函数f()
调用时,父函数会在内存中产生;执行期间,子函数s()
被调用,并在内存产生,执行完毕后,子函数会自动注销回收,随后父函数执行完毕也注销回收。
闭包的产生:
父函数f()
调用时,体内创建的子函数s()
并未被立即调用,而是返回函数指针,这便导致子函数不会被系统回收注销,由于js
存在链式机制的存在,子函数在执行时又会访问的父函数数据,产生函数作用域链,进而导致父函数在链式机制的作用下无法进行内存释放。这种在链式机制作用下,导致父函数为了维持函数作用域链而无法注销的子函数,即为“闭包函数”。
function f(){
let n=0;
function s(){
n++;
}
return s();
}
f();
二、闭包函数的用途
-
读取函数内部的变量,让这些变量的值始终保持在内存中;
-
方便调用上下文的局部变量,利于代码封装。
注意:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题
三、闭包的几种形式
-
函数返回值:返回函数
function(y)
,外界保持对makeAdder()作用域引用。function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); // 调用闭包函数 console.log(add5(2)); // 7 console.log(add10(2)); // 12
在这个示例中,
makeAdder(x)
函数,它接受一个参数x
,并返回一个新的函数。返回的函数接受一个参数y
,并返回x+y
的值。add5
和add10
都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在add5
的环境中,x
为 5。而在add10
中,x
则为 10。 -
函数赋值:
1)将F作用域里的函数N赋值给全局作用域的inner,所以F执行之后,inner保持了对F作用域里的引用
var inner; var F = function(){ var b = 'local'; var N = function(){ return b; }; inner = N; }; F(); console.log(inner()); // local
2)IIFE中给getter()和setter()函数提供将要操作的变量,并使其保存在闭包函数内部,防止其暴露在外部,类似函数模块化:
var getValue,setValue; (function(){ var secret = 0; getValue = function(){ return secret; } setValue = function(v){ if(typeof v === 'number'){ secret = v; } } })(); console.log(getValue());//0 setValue(1); console.log(getValue());//1
-
函数参数:将F里的N作为参数给外界的Inner,F执行之后,外界Inner保持了对F里的引用
var Inner = function(fn){ console.log(fn()); //local } var F = function(){ var b = 'local'; var N = function(){ return b; } Inner(N); } F();
-
IIFE:由前面的示例代码可知,函数F()都是在声明后立即被调用,因此可以使用IIFE来替代。但是,要注意的是,这里的Inner()只能使用函数声明语句的形式,而不能使用函数表达式
function Inner(fn){ console.log(fn()); //local } (function(){ var b = 'local'; var N = function(){ return b; } Inner(N); })()
-
循环赋值:通过循环产生多个闭包函数
function foo(){ var arr = []; for(var i = 0; i < 2; i++){ arr[i] = (function fn(j){ return function test(){ return j; } })(i); } return arr; } var bar = foo(); console.log(bar[0]()); //0
-
迭代器
// 累加器 var add = (function(){ var counter = 0; return function(){ return ++counter; } })(); console.log(add()); //1 console.log(add()); //2 console.log(add()); //3 // 迭代器 function setup(x){ var i = 0; return function(){ return x[i++]; } } var next = setup(['a','b','c','d']); console.log(next()); // 'a' console.log(next()); // 'b' console.log(next()); // 'c' console.log(next()); // 'd'
-
缓存机制:
通过闭包加入缓存机制,使得相同的参数不用重复计算,来提高函数的性能。
var mult=(function(){ var cache={}; var calculate=function(){ var a=1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; } return function(){ var args=Array.prototype.join.call(arguments,','); if(cache[args]){ return cache[args]; }else{ var ss=calculate.apply(null,arguments); return cache[args]=ss; } } })(); console.log( mult( 1,2,3 ) ); //结果6 console.log( mult( 1,3,3 ) );//结果9
四、闭包应用
- 闭包-函数回调
JavaScript
代码都是基于事件的定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
</head>
<style>
body{
font-size: 10px;
}
h1{
font-size: 1.8rem;
}
h2{
font-size: 1.5rem;
}
</style>
<body>
<p>body字体</p>
<h1>一级标题</h1>
<h2>二级标题</h2>
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
<script>
function changeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
var size12 = changeSize(12);
var size14 = changeSize(14);
var size16 = changeSize(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
</script>
</body>
</html>
文本尺寸调整按钮可以修改 body
元素的 font-size
属性,由于我们使用相对单位,页面中的其它元素也会相应地调整。
- 闭包-模块化/封装
JavaScript
不支持私有方法声明,但可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
// 实例化counter1 和 counter2
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 */
// counter1的操作对counter2的运行不产生影响
console.log(Counter2.value()); /* logs 0 */
这次我们只创建的函数,为三个函数:Counter.increment
,Counter.decrement
和 Counter.value
提供相同的作用域。函数体内包含两个私有项:名为 privateCounter
的变量和名为 changeBy
的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
把这个函数储存在另外一个变量makeCounter
中,并用变量makeCounter
创建Counter1
和Counter2
,访问 privateCounter
变量和 changeBy
函数。请注意两个计数器 Counter1
和 Counter2
是如何维护它们各自的独立性的,每个闭包都是引用自己词法作用域内的变量 privateCounter
。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量,即实现了JavaScript
函数的模块化处理,适合于进行数据隐藏和封装。
五、闭包踩坑
循环生成闭包函数时容易出现的错误,请看下一段代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
</head>
<body>
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{ 'id': 'name', 'help': 'Your full name'},
{ 'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
</script>
</body>
</html>
我们想要实现的效果时点击相应对话框时,出现对应的提醒,数组 helpText
中定义了三个有用的提示信息,每一个都关联于对应的文档中的input
的 ID。通过循环这三项定义,依次为相应input
添加了一个 onfocus
事件处理函数,以便显示帮助信息,如图:
但真正实现的均无论焦点在哪个input
上,显示的都是关于年龄的信息:
分析
原因是赋值给 onfocus
的是闭包函数。
通过for循环创建的三个闭包,共享一个作用域。在这个作用域中存在一个变量item
声明使用的是var item
,由于变量提升,为setuphelp()
内变量,所以具有函数作用域。当onfocus
的回调执行时,item.help
的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item
(被三个闭包所共享)已经指向了helpText
的最后一项,而这是由Js
机制决定的:Js
是单线程的,一个时间点只能做一件事,优先处理同步任务,之后再执行异步任务;按照代码从上往下执行,遇到异步(事件、定时器等)时,就挂起并放到异步任务里,继续执行同步任务,只有同步任务执行完了,才去看看有没有异步任务,然后再按照顺序执行!
这里for
循环是同步任务,onfocus
是异步任务,所以等for
循环执行完了,此时i=3,item={ 'id': 'age', 'help': 'Your age (you must be over 16)'}
,所以每次点击输入框时,均提示年龄。
解决办法:
-
使用更多闭包:
这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境,
makeHelpCallback
函数为每一个回调创建一个新的词法环境。在这些环境中,help
指向helpText
数组中对应的字符串
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
// 添加的闭包
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
- 使用匿名闭包,使得
item
作用域限制在for
循环内部的匿名闭包函数域内,item
仅被本次for
循环产生的匿名闭包享有。
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
// 使用匿名闭包函数(函数每次循环时立即执行)
// 当前循环项的item与事件回调相关联起来,而不是循环结束的值
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})();
}
}
setupHelp();
- let变量声明
使用let进行闭包变量声明,限制变量尽在当前代码块有效。
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
六、闭包与性能
由于闭包会使一些数据无法被及时销毁,要尽量减少闭包的使用,在程序开发中,选择主动把一些变量封闭在闭包中,主要考虑的是在不污染全局环境的前提下,保存后期需要使用的变量。从提高性能的角度考虑,一旦全局和闭包中的数据不再有用,最好通过将其值设置为 null 来释放其引用,以便垃圾收集器下次运行时将其回收。
function fnTest(_i) {
var i = _i;
function fnAdd() {
console.log(i++);
}
return fnAdd;
}
var fun = fnTest(100);
fun(); //100,i常驻内存
fun(); //101,i常驻内存
fun(); //102,i常驻内存
// 闭包回收
fun=null;