直观的说,一个函数使用了它外部的变量,这种用法就是闭包,闭包可以理解为函数中的函数(也是对象)。所以说,之所以有闭包概念,其实就是因为JS的作用域规范和其他编程语言有差别。为什么要使用闭包呢?闭包常常被用来间接地访问一个变量,这个变量在闭包外面,该变量由于闭包的间接访问可能一直存在于内存中。这里需要注意一点,闭包会不会引起内存泄漏?首先我们要明确内存泄漏是什么,内存泄漏指的是我们用不到的变量依然占据内存空间,无法被回收再利用。而闭包使用的变量正是我们需要的,所以不会引起内存泄漏。
值得一提的是所有函数本质上说都是闭包,但是我们平常不会这么说,因为没有实际意义,只有嵌套在函数内部的闭包才能真正发挥作用。通过闭包,可以访问和修改函数内部的变量,还能使之一直存在于内存中,调用后也不会被系统自动回收。
举个例子来说:
function f1(){
var n = 999;
function f2(){
return n = n + 1
}
return f2;
}
var a = f1();
a(); //1000
a(); //1001
a(); //1002
经典的闭包面试题,以下这段代码输出的结果是什么?
for(var i = 0; i < 10; ++i){
setTimeout(function(){
console.log(i);
}, 0)
}
这段代码的输出是十个10,其实算老生常谈了,JS中的var关键字有提升的效果,同时没有死区的概念,所以for循环中的变量i溢出了,使得setTimeout中的闭包也可以访问到,而此时i的值是10,所以定时器函数输出10个10。那么为什么i的值是10呢?这是因为JS的异步机制发挥了作用,注意我们这里使用了setTimeout函数,这个函数会异步的执行,当JS主线程的执行栈中所有任务完成后,JS引擎会查看异步的任务队列中是否有任务需要执行,也就是说只有在for循环循环完毕后,此时i的值为10,才会触发setTimeout函数,而定时器设置时间为0,所以会在0s后立即执行,打印结果。
很显然,这不是我们想要的结果,那么如何修改呢?修改的方法有两种。
- 使用立即执行函数
将i作为参数传递,并利用IIFE的特性构造封闭的作用域,从而达到输出0-9的效果。for(var i = 0; i < 10; i++){ setTimeout(function(i){ return function(){ console.log(i); } }(i), 0); } - 使用关键字Let
for(let i = 0; i < 10; i++){
setTimeout(function(){
console.log(i);
}, 0)
}
ES6的关键字let直接构造了一个封闭的作用域,使得变量i只能存在于当前for循环中,不会使得i溢出循环。因此每次循环都是一个新的的变量i,而每个循环中的变量i的块级作用域因为存在于闭包的作用域链中,所以不会销毁。