JavaScript基础——闭包及其使用场景
一、概念
一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
理解:闭包 = 内层函数 + 外层函数的变量
eg:
function output(){
let i = 1
function fn(){
console.log(i)
}
return fn()
}
const fun = output()
fun()
二、作用
封闭数据,提供操作,外部也可以访问函数内部的变量
解析(参照上面代码):因为let是有作用域的,只能在output函数内部使用,要想在外部使用是不可以的,采用闭包的形式,在fn函数中使用 i 数据,然后将fn函数return出去,外部的output() === function fn(){} ,最后用fun()调用函数就可以使用 i 数据了
三、使用场景
闭包应用:实现数据的私有(不会被外部串改)
风险:存在内存泄漏(垃圾回收机制的标记清除法)
使用场景:返回值、函数赋值、函数参数、IIFE(自执行函数)、循环赋值、getter/setter、迭代器、首次区分、缓存、节流函数
以下例子参考:https://blog.csdn.net/dkm123456/article/details/111644638
-
返回值(最常用)
function fn(){ var name = "Hello"; return function(){ return name; } } let fnc = fn(); console.log(fnc()) //Hello
-
函数赋值
在闭包里面给fn2函数设置值,闭包的形式把name属性记忆下来,执行输出Hello
let fn2; function fn(){ let name = "Hello" //将函数赋值给fn2 fn2 = function(){ return name } } fn()//执行后将函数赋给fn2 console.log(fn2())//执行输出fn2
-
函数参数
闭包返回callback函数给到fn1,在fn2函数中充当参数,然后在fn2中执行即可拿到name值
function fn(){ var name="hello"; return function callback(){ return name; } } var fn1 = fn()//执行函数将返回值(callback函数)赋值给fn1, function fn2(f){ //将函数作为参数传入 console.log(f());//执行函数,并输出 } fn2(fn1)//执行输出fn2
-
IIFE【Immediately Invoked Function Expression(立即调用的函数表达式)】 ===> 自执行函数
跟第3点差不多,一样把函数当参数传给fn2,不同的是这个是自执行的不用调用再次赋函数给fn1
(function(){ var name="hello"; var fn1= function(){ return name; } //直接在自执行函数里面调用fn2,将fn1作为参数传入 fn2(fn1); })() function fn2(f){ //将函数作为参数传入 console.log(f());//执行函数,并输出 }
-
循环赋值
需要注意的是setTimeout是循环结束后才执行的,因为var是有变量提升的,每次赋值都会被新值覆盖,如果不使用闭包形式,那么setTimeout打印出的值将为10次11。如果了解垃圾回收机制就容易理解了。闭包可使i值记忆起来,在打印出来就可以依次打印1-10了
//每秒执行1次,分别输出1-10 for(var i=1;i<=10;i++){ (function(j){ //j来接收 setTimeout(()=>{ console.log(j); },j*1000); })(i)//i作为实参传入 }
-
getter/setter
这里体现了闭包的数据私有,在包里面封装getter和setter函数,return出去给外部使用,外部可以在不影响包内的值对name进行获取和修改
function fn(){ var name='hello' setName=function(n){ name = n; } getName=function(){ return name; } //将setName,getName作为对象的属性返回 return { setName, getName } } var fn1 = fn();//返回对象,属性setName和getName是两个函数 console.log(fn1.getName());//getter fn1.setName('world');//setter修改闭包里面的name console.log(fn1.getName());//getter
-
迭代器(执行一次函数往下取一个值)
闭包可以用作记录函数调用次数,迭代器也是利用这个方法,第一次调用函数i为0所以是arr[0],第二次调用i为1所以是arr[1],以此类推!如果不懂i为什么为0和1的可以看看++i和i++的区别(i++:先用后加)
var arr =['aa','bb','cc']; function iterator(arr){ var i=0; return function(){ //这个函数每次被执行都返回数组arr中 i下标对应的元素 return arr[i++] || '数组值已经遍历完'; } } var next = iterator(arr); console.log(next());//aa console.log(next());//bb console.log(next());//cc console.log(next());//数组值已经遍历完
-
首次区分(相同的参数,函数不会重复执行)
这里的fn就是return的函数,因为最后加了()自动执行让fn获取到return的函数,调用函数的时候通过indexOf()判断数组中是否存在,存在则不执行,否则反之。
var fn = (function(){ var arr=[];//用来缓存的数组 return function(val){ if(arr.indexOf(val)==-1){//缓存中没有则表示需要执行 arr.push(val);//将参数push到缓存数组中 console.log('函数被执行了',arr); //这里写想要执行的函数 }else{ console.log('此次函数不需要执行'); } console.log('函数调用完打印一下,方便查看已缓存的数组:',arr); } })(); fn(10); //执行 fn(10); //不执行 fn(1000); //执行 fn(200); //执行 fn(1000); //不执行
-
缓存
通过 tSum = cache[key] 判断是否有缓存,有则返回值,无则undefined。
补充:
-
arguments(通过索引操作数据,也可以获取数据长度,所传递的实参都会在arguments中保存)
-
join():通过连接输入数组中的所有条目来返回一个新字符串。如果数组只有一项,则该项将在不使用分隔符的情况下返回。
-
slice():可从已有的数组中返回选定的元素 ===> array.slice(start, end)
-
call():改变this的指向
所以Array.prototype.slice.call(arguments)最后形成一个数组,可简写为:[].slice.call(arguments)
//求和操作,如果没有缓存,每次调用都要重复计算,采用缓存已经执行过的去查找,查找到了就直接返回,不需要重新计算(能减轻机器压力) var fn=(function(){ var cache = {};//缓存对象 var calc = function(arr){//计算函数 var sum = 0; //求和 for(var i = 0;i<arr.length;i++){ sum += arr[i]; //累加 } return sum; } return function(){ var args = Array.prototype.slice.call(arguments,0);//arguments转换成数组 var key=args.join(",");//将args用逗号连接成字符串 var result , tSum = cache[key]; if(tSum){//如果缓存有 console.log('从缓存中取:',cache)//打印方便查看 result = tSum; }else{ //重新计算,并存入缓存同时赋值给result result = cache[key]=calc(args); console.log('存入缓存:',cache)//打印方便查看 } return result; } })(); fn(1,2,3,4,5); fn(1,2,3,4,5); fn(1,2,3,4,5,6); fn(1,2,3,4,5,8); fn(1,2,3,4,5,6);
-
-
节流函数
目的:降低回调函数的执行频率,节省计算资源
区别:节流函数和防抖函数有点类似,但是不一样。防抖是当一个动作连续触发,只执行最后一次;节流是限制一个函数在一定时间内只能执行一次
补充:apply:与call类似改变this的指向
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GBK">
<style>
*{
margin:0;padding:0;
}
</style>
</head>
<body>
<div class="box" id="div_box" >
<button onclick="fn1()">test</button>
</div>
</body>
<script>
//节流函数
function throttle(fn,delay){
var flag=false;
var timer=null; //用于记录定时器,方便清除
return function(){
var args=[].slice.call(arguments,0);//将参数转成数组
var context=this;
if(flag){//如果在限定的时间内 flag是true 则直接返回,不让执行
return;
}
flag=true; //函数正在控制中
//执行函数
fn.apply(context,args);
clearTimeout(timer);//清除定时器
timer =setTimeout(function(){
flag=false;//延时时间过了以后,放开函数控制
},delay)
}
}
function fn(){
console.log(123);
}
var fn1 = throttle(fn,2000);//绑定节流函数
</script>
</html>
总结:
有数据的私有要求的时候会用到闭包,还有进行数据记录的时候也会用到闭包!