对JS闭包的理解及常见应用场景(闭包的作用)
使用闭包主要为了设计私有的方法和变量
优点
- 可以避免变量的污染
缺点
- 闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露
在js中,函数即闭包,函数才会产生作用域的概念:
变量作用域
-
变量作用域两种:全局变量、局部变量
-
js中函数内部可以读取全局变量,函数外部不能读取函数内部的局部变量
-
如何从外部读取函数内部的变量?
function f1(){ var n = 123; function f2(){ //f2是一个闭包 alert(n) } return f2; }
-
js链式作用域:子对象会一级一级向上寻找所有父对象的变量,反之不行
-
f2可以读取f1中的变量,只要把f2作为返回值,就可以在f1外读取f1内部变量
闭包概念
能够读取其他函数内部变量的函数,或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用
闭包用途
- 读取函数内部的变量,让这些变量的值始终保持在内存中
- 不会在f1调用后被自动清除
- 方便调用上下文的局部变量
- 利于代码封装
- 原因:f1是f2的父函数,f2被赋给了一个全局变量,f2始终存在内存中,f2的存在依赖f1,因此f1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的理解
来几个例子:
function init() {
var name = "Chrome"; //创建局部变量name和局部函数alertName
function alertName() { //alertName()是函数内部方法,是一个闭包
alert(name); //使用了外部函数声明的变量,内部函数可以访问外部函数的变量
}
alertName();
}
init();
-
一个变量在源码中声明的位置作为它的作用域,同时嵌套的函数可以访问到其外层作用域中声明的变量
function outFun(){ var name = "Chrome"; function alertName(){ alert(name); } return alertName; //alertName被外部函数作为返回值返回了,返回的是一个闭包 } var myFun = outFun(); myFun();
-
闭包有函数+它的词法环境,词法环境指函数创建时可访问的所有变量
-
myFun引用了一个闭包,闭包由alertName()和闭包创建时存在的“Chrome”字符串组成
-
alertName()持有了name的引用,myFunc持有了alertName()的的访问,因此myFunc调用时,name还是处于可以访问的状态
function add(x){ return function(y){ return x + y; }; } var addFun1 = add(4); var addFun2 = add(9); console.log(addFun1(2)); //6 console.log(addFun2(2)); //11
-
add接受一个参数x,返回一个函数,它的参数是y,返回x+y
-
add是一个函数工厂,传入一个参数,就可以创建一个参数和其他参数求值的函数
-
addFun1和addFun2都是闭包。他们使用相同的函数定义,但词法环境不同,addFun1中x是4,后者是5
闭包应用场景
-
setTimeout传参
//原生的setTimeout传递的第一个函数不能带参数 setTimeout(function(param){ alert(param) },1000) //通过闭包可以实现传参效果 function func(param){ return function(){ alert(param) } } var f1 = func(1); setTimeout(f1,1000);
-
回调
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <link rel="stylesheet" href=""> </head> <style> body{ font-size: 12px; } h1{ font-size: 1.5rem; } h2{ font-size: 1.2rem; } </style> <body> <p>哈哈哈哈哈哈</p> <h1>hhhhhhhhh</h1> <h2>qqqqqqqqq</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>
-
封装变量
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>闭包模拟私有方法</title> <link rel="stylesheet" href=""> </head> <body> <script> //用闭包定义能访问私有函数和私有变量的公有函数。 var counter = (function(){ var privateCounter = 0; //私有变量 function change(val){ privateCounter += val; } return { increment:function(){ //三个闭包共享一个词法环境 change(1); }, decrement:function(){ change(-1); }, value:function(){ return privateCounter; } }; })(); console.log(counter.value());//0 counter.increment(); counter.increment();//2 //共享的环境创建在一个匿名函数体内,立即执行。 //环境中有一个局部变量一个局部函数,通过匿名函数返回的对象的三个公共函数访问。 </script> </body> </html>
-
为节点循环绑定click事件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <link rel="stylesheet" href=""> </head> <body> <p id="info">123</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 showContent(content){ document.getElementById('info').innerHTML = content; }; function setContent(){ var infoArr = [ {'id':'email','content':'your email address'}, {'id':'name','content':'your name'}, {'id':'age','content':'your age'} ]; for (var i = 0; i < infoArr.length; i++) { var item = infoArr[i]; document.getElementById(item.id).onfocus = function(){ showContent(item.content) } } } setContent() </script> </body> </html>
-
上述代码原本想实现,点击不同的框显示不同的信息,结果现在都只会显示最后一项,“your age”
-
分析:
- 循环中创建了三个闭包,他们使用了相同的词法环境item,item.content是变化的变量
- 当onfocus执行时,item.content才确定,此时循环已经结束,三个闭包共享的item已经指向数组最后一项。
-
解决:
//解决方法1 通过函数工厂,则函数为每一个回调都创建一个新的词法环境 function showContent(content){ document.getElementById('info').innerHTML = content; }; function callBack(content){ return function(){ showContent(content) } }; function setContent(){ var infoArr = [ {'id':'email','content':'your email address'}, {'id':'name','content':'your name'}, {'id':'age','content':'your age'} ]; for (var i = 0; i < infoArr.length; i++) { var item = infoArr[i]; document.getElementById(item.id).onfocus = callBack(item.content) } } setContent()
// 解决方法2 绑定事件放在立即执行函数中 function showContent(content){ document.getElementById('info').innerHTML = content; }; function setContent(){ var infoArr = [ {'id':'email','content':'your email address'}, {'id':'name','content':'your name'}, {'id':'age','content':'your age'} ]; for (var i = 0; i < infoArr.length; i++) { (function(){ var item = infoArr[i]; document.getElementById(item.id).onfocus = function(){ showContent(item.content) } })()//放立即执行函数,立即绑定,用每次的值绑定到事件上,而不是循环结束的值 } } setContent()
// 解决方案3 用ES6声明,避免声明提前,作用域只在当前块内 function showContent(content){ document.getElementById('info').innerHTML = content; }; function setContent(){ var infoArr = [ {'id':'email','content':'your email address'}, {'id':'name','content':'your name'}, {'id':'age','content':'your age'} ]; for (var i = 0; i < infoArr.length; i++) { let item = infoArr[i]; //限制作用域只在当前块内 document.getElementById(item.id).onfocus = function(){ showContent(item.content) } } } setContent()
闭包的成熟使用
- 防抖:
- 参考链接:https://blog.csdn.net/qq_43569680/article/details/123665410
- 节流
- 参考链接:https://blog.csdn.net/qq_43569680/article/details/123665410
优缺点及解决办法
- 优点
- 避免全局变量的污染
- 能够读取函数内部的变量
- 可以在内存中维护一个变量
- 缺点
- 闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值
- 闭包,不会在调用结束后被垃圾回收机制回收
垃圾回收机制
当内存不再需要使用时,需要将其释放,这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了。这就需要垃圾回收机制来判定了。
-
引用计数垃圾收集
- 这是最初级的垃圾收集算法,此算法把对象是否不再需要简化定义为对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收
- 例子:
var o = { a: { b:2 } }; // 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o // 很显然,没有一个可以被垃圾收集 var o2 = o; // o2变量是第二个对“这个对象”的引用 o = 1; // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有 var oa = o2.a; // 引用“这个对象”的a属性 //现在,“这个对象”有两个引用了,一个是o2,一个是oa o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了 // 但是它的属性a的对象还在被oa引用,所以还不能回收 oa = null; // a属性的那个对象现在也是零引用了 // 它可以被垃圾回收了
- 限制:循环引用
- 该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o return "azerty"; } f();
- 循环引用实际例子
- IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:
var div; window.onload = function(){ div = document.getElementById("myDivElement"); div.circularReference = div; div.lotsOfData = new Array(10000).join("*"); };
- 在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放。
-
标记清除垃圾收集
- 这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”
- 这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象
- 这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”
- 从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
- 循环引用不再是问题了
- 在上面引用计数的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。
-
限制
- 那些无法从根对象查询到的对象都将被清除
- 尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制
文章参考:https://blog.csdn.net/qq_39903567/article/details/115010640