**回答:**什么是闭包,为什么要用它?
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途。
闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理
1、定义:闭包是指有权访问另一个函数作用域中变量的函数。有局部变量才会有闭包。简单理解:如果一个函数内部访问了外部的变量,那么就是闭包
2、举个闭包的例子:
function sayHi(name) {
return () => {
console.log(`Hi! ${name}`)
}
}
const test = sayHi('xiaoming')
test() // Hi! xiaoming
虽然sayHi函数已经执行完毕,但是其活动对象也不会被销毁,因为test函数仍然引用着sayHi函数中的变量name,这就是闭包。
但也因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。
3、怎么解决内存泄露的问题:
在不使用闭包时,把被引用的变量设置为null,即手动清除变量,这样下次js垃圾回收机制回收时,就会把设为null的量给回收了。
以上面代码为例,需设置name=null。
javascript垃圾回收机制原理:
解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存。
现在各大浏览器通常采用的垃圾回收机制有两种方法:标记清除,引用计数。
标记清除:
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在一个函数中声明一个变量,就将这个变量标记为"进入环境",从逻辑上讲,永远不能释放进入环境变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为"离开环境"。
function test(){
var a = 10;
//被标记"进入环境"
var b = "hello";
//被标记"进入环境"
}
test();
//执行完毕后之后,a和b又被标记"离开环境",被回收 垃圾回收机制在运行的时候会给存储再内存中的所有变量都加上标记(可以是任何标记方式),然后,它会去掉处在环境中的变量及被环境中的变量引用的变量标记(闭包)。而在此之后剩下的带有标记的变量被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾回收机制到下一个周期运行时,将释放这些变量的内存,回收它们所占用的空间。
引用计数:
语言引擎有一张"引用表",保存了内存里面所有资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
4、闭包的用途:
-
(第一种情况,需要)访问函数内部的变量(时)
-
(第二种情况)防止函数内部的变量执行完成后被销毁,使其一直保存在内存中。
5、闭包使用场景
-
循环注册点击事件,点击获取li的索引
<body> <ul class="nav"> <li>苹果</li> <li>香蕉</li> </ul> <script> var lis=document.querySelector('.nav').querySelectorAll('li') ; //以前的方式,通过动态添加属性的方式 for(var i=0;i<lis.length;i++){ lis[i].index=i; lis[i].onclick=function(){ console.log(this.index); } } //利用闭包的方式得到当前的小li,使用小闭包立即执行函数 for(var i=0;i<lis.length;i++){ //利用循环创建四个立即执行函数,(function(){})(),最后一个括号相当于调用函数,我们在里面传入一个值i。 (function(i){ lis[i].onclick=function(){ console.log(i); } })(i); } </script> </body>
情景题
div里 多个a标签 让这些标签console.log 让窗口不跳转如何设置?
当页面中a标签不需要任何跳转时,从原理上来讲,可分如下两种方法:
1.标签属性href,使其指向空或不返回任何内容。如:
<a href="javascript:void(0);" >
点此无反应javascript:void(0)
</a>
<a href="javascript:;" >点此无反应javascript:</a>
2.标签事件onclick,阻止其默认行为。如:
<a href="" onclick="return false;">return false;</a>
<a href="#" onclick="return false;">return false;</a>
注意:只有一个href=”#”是不可以的。 不用for循环该怎么做(用冒泡) 假设有个ul,它下面有5000个li;
需求是给这5000个li添加一个点击事件,;
有些人会去这样做
var oUl = document.getElementById('oUl');
var aLi = oUl.getElementsByTagName('li');
var len = aLi.length;
for(var i =0;i<len;i++){
(function(index){
aLi[i].onclick = function(){
fn()
};
})(i)
}
这样做的弊端显而易见,遍历的基数越大,性能开销越大。
因此正确的做法应该是这样
var oUl = document.getElementById('oUl');
oUl.onclick = function(ev){
//IE或者火狐浏览器 兼容问题
var oEv = ev || event;
var oSrc = oEv.srcElement || oEv.target;
if(oSrc.tagName=='li'){
fn();
}
}
利用时间冒泡的特性进行事件委托。避免了大量的循环遍历,减少了性能开销。
jQuery下可以利用on添加事件的方法达到同样的效果;
var $ul = $('ul');
$ul.on('click','li',function(){
fn();
})
-
循环中的setTimeout()
var lis=document.querySelector('.nav').querySelectorAll('li') ; /利用闭包的方式--几秒钟之后,打印所有li的内容 for(var i=0;i<lis.length;i++){ //利用循环创建四个立即执行函数,(function(){})(),最后一个括号相当于调用函数,我们在里面传入一个值i,因为setTimeout中使用了立即执行函数的变量i,因而产生了闭包 (function(i){ setTimeout(function(){ console.log(lis[i].innerHTML) ; },3000) })(i); }
for(var i=1;i<4;i++){
setTimeout(function(){
console.log(i);
},1000);
}
上面的代码会在1秒之后输出3个4,原因是i为全局变量,会先执行完循环(js是单线程机制,先执行同步代码,最后执行异步代码,setTimeout是异步调用),此时i的值已经为4,之前每次循环放入异步队列中的函数就会依次打印出i的值,即连续打印3次4
问题:如何将上面的改为1秒之后一次输出1,2,3?(一种是es6中的let声明变量i,此处利用闭包实现
for(var i=1;i<4;i++){
(function(j){
setTimeout(function(){
console.log(j);
},1000);
})(i);
}
上面的代码即可实现1秒后依次打印出1,2,3;此处利用闭包每次将i的值传入进去,里面使用参数j接受传过来的值,然后setTimeout异步调用,进入异步队列,循环代码很快执行完,在1秒后,从异步队列里返回执行后的结果,依次打印出1,2,3.
问题:如何使每隔1秒依次打印一个结果?(设置延迟时间依次增加1秒即可)
for(var i=1;i<4;i++){
(function(j){
setTimeout(function(){
console.log(j);
},1000*j);
})(i);
}
上面代码在延迟执行时间那块加了一个参数j,j的值每次都会增加1,使时间每次扩大比上次大一倍,从而实现了每隔一秒就打印出一个结果。
-
防抖和节流函数,
-
函数节流: 指定时间间隔内只会执行一次任务;应用场景:判断页面是否滚动到底部为例 节流注重开关锁
-
函数防抖: 任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。应用场景:用在注册模块中,用户注册时验证用户名是否被占用,或者游戏中监测鼠标的位置,或者登录注册等按钮。。
-
防抖注重清零
-
简单来说,函数的节流就是通过闭包保存一个标记(
canRun = true
),在函数的开头判断这个标记是否为true
,如果为true
的话就继续执行函数,否则则 return 掉,判断完标记后立即把这个标记设为false
,然后把外部传入的函数的执行包在一个setTimeout
中,最后在setTimeout
执行完毕后再把标记设置为true
(这里很关键),表示可以执行下一次的循环了。当setTimeout
还未执行的时候,canRun
这个标记始终为false
,在开头的判断中被 return 掉。function throttle(fn, interval = 300) { let canRun = true; return function () { if (!canRun) return; canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, interval); }; } 法二:用时间戳实现
function throttle(fn,interval){
let last=0; return function(){ var now=Date.now(); if(now-last>interval){ fn.apply(this,arguments); last=now; } }
}
-
- 其实函数防抖的原理也非常地简单,通过闭包保存一个标记来保存
setTimeout
返回的值,每当用户输入的时候把前一个setTimeout
clear 掉,然后又创建一个新的setTimeout
,这样就能保证输入字符后的interval
间隔内如果还有字符输入的话,就不会执行fn
函数了。 -
``` function debounce(fn, interval = 300) { let timeout = null; return function () { clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(this, arguments); }, interval); }; } ```
6、闭包优点和缺点
- 优点:可以延伸变量的作用范围
- 缺点:闭包使用过多,会占用较多的内存,容易造成内存泄露。
7、有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参函数相似,但是这个函数只能执行3次,再次执行无效,如何实现?
function sayHi(){
console.log('hi');
}
function threeTimes(fn){
let times=0;
return ()=>{
if(times++<3){
fn();
}
}
}
const newfn=threeTimes(sayHi);
newfn();
newfn();
newfn();
newfn();//后面两次执行都无任何反应