什么是闭包?
闭包回答:
1:闭包具有封闭性不对外公开的结构,在js中函数可以构成闭包,根据作用域链规则 函数访问只允许外部数据,外部不能访问内部数据
2:闭包了解决外界允许访问,可以间接的通过一个函数返回新的函数或者对象的方法
代码举例
总结
闭包的实质是因为函数嵌套而形成的作用域链
比如说:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包
用途:使用闭包主要是为了设计私有的方法和变量
优点:可以避免变量被全局变量污染
缺点:函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包
解决方法:在退出函数之前,将不使用的局部变量全部删除
答案:闭包是一个函数,什么函数呢?一个定义在函数内部的函数。例如:
var add = (function () {
var counter = 0;
return function () {
return counter += 1;
}
})();
复制代码在上面的例子中,函数内部return的这个函数function () {return counter += 1;},它就是一个闭包。我们直接把add在控制台打印出来:
我们可以看到,在这里全局定义的这个变量add,它实际上是一个函数,但是这个函数却是在另一个函数内部创建的,这个就是定义在函数内部的函数。
闭包有什么作用?
1、闭包可以访问到局部变量
即我们要解读这句话:能够读取其他函数内部变量的函数。
我们知道,在JavaScript中,有全局变量和局部变量。顾名思义,全局变量它是公有的、共享的,程序内的所有函数都可以直接调用这个全局变量,即它的作用域是全局性的;而局部变量,在函数内部声明的变量,它是私有的,只在函数内部起作用,在该函数之外的地方是无法被调用的,它的作用域是局部性的。
function add() {
var counter = 0;
return counter += 1;
}
console.log(add()); // 1
console.log(add()); // 1
console.log(add()); // 1
// 本意是想输出 3, 但事与愿违,输出的都是 1
复制代码以上代码将无法正确输出,每次调用add()函数,计数器都会设置为1。可以将它写成闭包的形式,使counter = 0只执行一次,并返回函数表达式,然后得到我们想要输出的结果。
function add() {
var counter = 0;
return function () {
return counter += 1;
}
}
var closure = add();
console.log(closure()); // 1
console.log(closure()); // 2
console.log(closure()); // 3
闭包它可以访问函数上一层作用域的局部变量,它使得函数拥有私有变量变成可能。
2、闭包可以保护里面的局部变量,使它们不会随着函数的结束而销毁
百度百科里面还有这么一段话:“闭包”一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。 这句话含有一个意思,就是在闭包内部的变量它是不会被销毁的,会一直存在内存中,并且不会受到外界的干扰。在上面的例子中counter的值就是这样受到闭包的保护,只能通过closure方法去修改。
使用闭包需要注意的点
1、闭包会使得函数中的变量都被保存在内存中,会增大内存的使用量,所以不能滥用闭包,否则会造成网页的性能问题,在IE9中可能会导致内存泄露。但这个导致内存泄露是IE早期的BUG,闭包本身并不会导致内存泄露。
因为,在JavaScript的垃圾自动回收机制中有这么一句话:从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。因为进入闭包的变量是有可能继续使用的,即它们不会被自动回收,这个时候需要我们手动回收清除。
2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。 最后,推荐观看下面关于闭包的这篇文章: developer.mozilla.org/zh-CN/docs/…
例子1
例子2
function foo () {
var a = 2
function bar () {
console.log(a)
}
return bar
}
var baz = foo()
baz() //2
function test () {
var result = new Array()
for (var i = 0; i < 6; i++) {
result[i] = function () {
return i
}
}
console.log(result)
return result
}
test () //
拓展
function test () {
var result = new Array()
for (var i = 0; i < 6; i++) {
let a=function () {
return i
}
result[i] = a()
}
return result
}
test ()
例子:****
常见闭包场景:
判断公式:这个局部变量,是否直接 or 间接被 window 对象引用(牵连)?
常见案例分析:
1.定时器回调函数
(function(){
var num = 0;
setInterval(function(){ //注意setInterval的回调函数,是注册在window下的
console.log(++num)
},500)
})()
//1.setInterval的回调函数 被 window 引用,所以不能回收
//2.回调函数执行引用匿名函数的num,所以匿名函数的局部变量对象,不能回收
2.事件处理函数
var num = 0;
document.onclick = function(){ //注意事件处理函数,注册在 window 下
console.log(++num)
}
})()
num会同上述情况一样,无法被回收
3.利用闭包写法保存 变量 i
var arr1 = []
var arr2 = []
for(var i=0;i<10;i++){
//不使用不闭包
arr1[i] = ()=>{
console.log(i) //这里的 i,在函数执行时,访问的是 10(循环条件i<10不满足,最后的保留值)
}
//使用闭包
arr2[i] = ((i)=>{ //这里的 i 保存在 匿名函数的局部变量对象中
return ()=>{
console.log(i) //访问 局部变量对象 保存的 i
}
})(i)
}
arr1.forEach((f)=>{console.log(f())}) // 都是 10 ,因为访问的是全局的i,都指向一个值
arr2.forEach((f)=>{console.log(f())}) // 0~9 ,每次执行,访问的是自身所处作用域 专属的 i
关于闭包优化:
避开全局引用牵连: 在日常开发过程中,尽量避免把函数局部变量赋值给全局 window
主动释放局部变量: 对于已知体积较大的局部变量,必要时设置 null 清空其内存占用
var responseData = [{}, ...] //例如这个变量存储了10万条数据
var num = 0;
setInterval(function(){
console.log(++num)
if(num === 8){//在确定业务代码完成任务,不再需要 responseDate 时
responseData = null //解除内存占用
}
},500)
})()
回顾什么闭包
回顾闭包的作用
- 可以读取函数内部的变量。看上面代码就是 c 可以操作 i
- 让这些变量的值始终保持在内存中。i 一直在内存里
- 避免变量污染全局
- 把变量存到独立的作用域,作为私有成员存在
- 模拟私有方法,模块封装
回顾闭包缺点
- 增大内存消耗。闭包里的引用无法被垃圾回收,增大内存用量,使用不当会导致内存泄漏。
- 对处理速度有影响。闭包的层级决定了引用在查找时经过的作用
回顾应用场景
应用场景一
典型应用是模块封装,在各模块规范出现之前,都是用这样的方法防止变量污染全局。
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
对于模块封装有两个要点:
- 使用立即执行函数包裹,该函数必须被至少调用一次
- 必须返回至少一个函数形成闭包
回顾应用场景二
在循环中创建闭包,防止取到意外的值
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
破解前端面试闭包
题一
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
只要你对 JS 中同步和异步代码的区别、变量作用域、闭包等概念有正确的理解,就知道正确答案是 C,代码的实际输出是:
首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 5了,所以会输出一堆 5。
2017-03-18T00:43:45.873Z 5 2017-03-18T00:43:46.866Z 5 2017-03-18T00:43:46.868Z 5 2017-03-18T00:43:46.868Z 5 2017-03-18T00:43:46.868Z 5 2017-03-18T00:43:46.868Z 5
拓展1
接下来我会追问:如果我们约定,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?会有下面两种答案: 正确答案B
A. 60% 的人会描述为:5 -> 5 -> 5 -> 5 -> 5,即每个 5 之间都有 1 秒的时间间隔;
B. 40% 的人会描述为:5 -> 5,5,5,5,5,即第 1 个 5 直接输出,1 秒之后,输出 5 个 5;
这就要求候选人对 JS 中的定时器工作机制非常熟悉,循环执行过程中,几乎同时设置了 5 个定时器,一般情况下,这些定时器都会在 1 秒之后触发,而循环完的输出是立即执行的,显而易见,正确的描述是 B。
拓展2
如果这道题仅仅是考察候选人对 JS 异步代码、变量作用域的理解,局限性未免太大,接下来我会追问,如果期望代码的输出变成:5 -> 0,1,2,3,4,该怎么改造代码?
for (var i = 0; i < 5; i++) {
//立即执行函数
(function(j) { // j = i
setTimeout(function() {
console.log(new Date, j);
}, 1000);
})(i);
}
console.log(new Date, i); // 5
巧妙的利用 IIFE(Immediately Invoked Function Expression:声明即执行的函数表达式)来解决闭包造成的问题,确实是不错的思路,但是初学者可能并不觉得这样的代码很好懂,至少笔者初入门的时候这里琢磨了一会儿才真正理解。
有没有更符合直觉的做法?答案是有,我们只需要对循环体稍做手脚,让负责输出的那段代码能拿到每次循环的 i 值即可。该怎么做呢?利用 JS 中基本类型(Primitive Type)的参数传递是按值传递(Pass by Value)的特征,不难改造出下面的代码:
var output = function (i) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i); // 这里传过去的 i 值被复制了
}
console.log(new Date, i);
拓展三 for 闭包结合es6 promise 方法
接着上文继续追问:如果期望代码的输出变成 0 -> 1 -> 2 -> 3 -> 4 -> 5,并且要求原有的代码块中的循环和两处 console.log 不变,该怎么改造代码?新的需求可以精确的描述为:代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5(这里使用大概,是为了避免钻牛角尖的同学陷进去,因为 JS 中的定时器触发时机有可能是不确定的,具体可参见 How Javascript Timers Work)。
粗暴的方法(不友好)
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(new Date, j);
}, 1000 * j)); // 这里修改 0~4 的定时器时间
})(i);
}
setTimeout(function() { // 这里增加定时器,超时设置为 5 秒
console.log(new Date, i);
}, 1000 * i);
promise的方法(推荐的方法)
const array=[];
for(var i=0;i<5;i++){
((j)=>{
let promise= new Promise((resolve)=>{
setTimeout(()=>{
console.log(new Date,j)
resolve()
},1000*j)
})
array.push(promise)
})(i)
}
Promise.all(array).then((res)=>{
setTimeout(()=>{
console.log(new Date,i)
},1000)
})
拓展四,结合es7 sync await 实现
,既然 Promise 已经被拿下,如何使用 ES7 中的 async await 特性来让这段代码变的更简洁?你是否能够根据自己目前掌握的知识给出答案?请在这里暂停 1 分钟,思考下。
// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { // 声明即执行的 async 函数表达式
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();
经典回顾闭包面试题:
for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。
解决办法两种,第一种使用闭包
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
第二种就是使用 setTimeout 的第三个参数
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
第三种就是使用 let 定义 i 了
for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
因为对于 let 来说,他会创建一个块级作用域,相当于
{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}