对JS闭包的理解及常见应用场景(闭包的作用)
使用闭包主要为了设计私有的方法和变量。
- 优点是可以避免变量的污染
- 缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
在js中,函数即闭包,函数才会产生作用域的概念。
1.变量作用域
变量作用域两种:全局变量、局部变量。js中函数内部可以读取全局变量,函数外部不能读取函数内部的局部变量。
2.如何从外部读取函数内部的变量?
function f1(){
var n = 123;
function f2(){ //f2是一个闭包
alert(n)
}
return f2;
}
- js链式作用域:子对象会一级一级向上寻找所有父对象的变量,反之不行。
- f2可以读取f1中的变量,只要把f2作为返回值,就可以在f1外读取f1内部变量
3.闭包概念
- 能够读取其他函数内部变量的函数。
- 或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用。
4.闭包用途
- 读取函数内部的变量
- 让这些变量的值始终保持在内存中。不会在f1调用后被自动清除。
- 方便调用上下文的局部变量。利于代码封装。
原因:f1是f2的父函数,f2被赋给了一个全局变量,f2始终存在内存中,f2的存在依赖f1,因此f1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收。
5.闭包的理解
来几个例子:
/**
* [init description]
* @return {[type]} [description]
*/
function init() {
var name = "Chrome"; //创建局部变量name和局部函数alertName
function alertName() { //alertName()是函数内部方法,是一个闭包
alert(name); //使用了外部函数声明的变量,内部函数可以访问外部函数的变量
}
alertName();
}
init();
一个变量在源码中声明的位置作为它的作用域,同时嵌套的函数可以访问到其外层作用域中声明的变量
/**
* [outFun description]
* @return {[type]} [description]
*/
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还是处于可以访问的状态。
/**
* [add description]
* @param {[type]} x [description]
*/
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
6.闭包应用场景
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()
7.优缺点及解决办法
优点:
- 避免全局变量的污染
- 能够读取函数内部的变量
- 可以在内存中维护一个变量
缺点:
- 1.闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 2.闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
闭包,不会在调用结束后被垃圾回收机制回收,这里拓展一下垃圾回收机制吧
8.拓展·垃圾回收机制
当内存不再需要使用时,需要将其释放,这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。这就需要垃圾回收机制来判定了。
1.引用计数垃圾收集
这是最初级的垃圾收集算法。
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。
如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
例子:
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 属性),而这个数据占用的内存将永远不会被释放。
2.标记-清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
循环引用不再是问题了 在上面引用计数的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。
限制: 那些无法从根对象查询到的对象都将被清除 尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!