目录
2. 异步操作中 变量 的生命周期,取决于 闭包 的生命周期
2.1 Timer 定时器(保留到 定时器回调执行完毕、定时器被清空)
2.2 Event 事件处理函数(保留到 事件处理函数 被移除)
2.3 Ajax 请求数据(保留到 接收到接口返回数据,执行回调函数)
5.2.2 如果期望代码的输出变成:5 -> 0,1,2,3,4,该怎么改造代码
5.2.3 如果期望代码的输出变成:0 -> 1 -> 2 -> 3 -> 4 -> 5,该怎么改造代码
1. 什么是闭包?闭包的作用?
1.1 可以访问 外部作用域 中变量的内部函数
可以访问 外部作用域 中变量的内部函数(在 函数内部 或者 {} 块内部 定义一个函数,就创建了闭包)
举个栗子~~
// log()、simple() 是嵌套在 autorun() 函数里面的函数
(function autorun() {
let x = 1;
// 闭包示例
function log() {
// 在 log() 内部,可以访问到 外部作用域 中的变量 x,此时 log() 就是闭包
console.log(x);
}
// 纯函数示例
function simple() {
return 2 === Number(2);
}
log();
})();
关于内部函数,内部函数有两种(闭包、纯函数):
- 闭包 —— 引用了外部作用域中变量的函数
- 纯函数 —— 没有引用外部作用域中变量的函数,它们通常返回一个值,并且没有副作用
也可以这么理解,随着函数得执行完毕,某个变量因为被引用着(内部函数/闭包引用着)导致不能够被垃圾回收
1.2 闭包可以访问外部作用域中的变量及传参
即使 外部函数 / 外部块 已经执行完毕,内部函数仍然可以访问 外部函数 / 外部块 中定义的变量、接收的传参;让 局部变量 的值,始终保留在 内存 中
举个栗子~~
// 外部函数
(function autorun(p) {
// 即使外部函数已经执行完毕
let x = 1;
setTimeout(function log() {
// 内部函数仍然可以访问 外部函数 中定义的变量
console.log(x); // 1
// 内部函数仍然可以访问 外部函数 中J接收的参数
console.log(p); // 10
}, 10000);
})(10);
// 外部块
{
let x = 1;
setTimeout(function log() {
// 内部函数仍然可以访问 外部块 中定义的变量
console.log(x);
}, 10000);
}
闭包的外部作用域,在闭包定义的时候已决定,而不是执行的时候;闭包可以访问其外部(父)作用域中的定义的所有变量;
举个栗子~~
let x0 = 0;
(function autorun1() {
let x1 = 1;
(function autorun2() {
let x2 = 2;
(function autorun3() {
let x3 = 3;
// 闭包可以访问其外部(父)作用域中的定义的所有变量
console.log(x0 + " " + x1 + " " + x2 + " " + x3); // 0 1 2 3
})();
})();
})();
2. 异步操作中 变量 的生命周期,取决于 闭包 的生命周期
当外部作用域执行完毕后,内部函数还存活(仍在其他地方被引用)时,闭包才真正发挥其作用
- 在异步任务例如
timer
定时器,事件处理,Ajax
请求中被作为回调 - 被外部函数作为返回结果返回,或者返回结果对象中引用该内部函数
2.1 Timer 定时器(保留到 定时器回调执行完毕、定时器被清空)
变量 x 将一直保留,直到以下两种情况出现:
- 定时器的回调执行
- clearTimeout() 被调用
(function autorun() {
let x = 1;
setTimeout(function log() {
// 变量 x 将一直存活着直到定时器的回调执行或者 clearTimeout() 被调用
// 如果用 setInterval() ,那么变量 x 将存活到 clearInterval() 被调用。
console.log(x);
}, 10000);
})();
2.2 Event 事件处理函数(保留到 事件处理函数 被移除)
变量 x 将一直保留,直到 事件处理函数 被移除
(function autorun() {
let x = 1;
// 当变量 x 在事件处理函数中被使用时,它将一直存活直到该事件处理函数被移除。
$("#btn").on("click", function log() {
console.log(x);
});
})();
2.3 Ajax 请求数据(保留到 接收到接口返回数据,执行回调函数)
变量 x 将一直保留,直到 接收到接口返回数据,执行回调函数
(function autorun() {
let x = 1;
fetch("http://").then(function log() {
// 变量 x 将一直存活到接收到后端返回结果,回调函数被执行。
console.log(x);
});
})();
2.4 其他异步 API 使用闭包
除了 Timer 定时器,Event 事件处理,Ajax 请求等常见的异步任务,还有其他的异步 API(比如 HTML5 Geolocation,WebSockets , requestAnimationFrame())
他们都使用到闭包的这一特性 —— 变量的生命周期取决于闭包的生命周期,被闭包引用的外部作用域中的变量,将一直存活直到闭包函数被销毁。
如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁
3. 循环中的闭包
// 原始题目
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 1s 后,打印出 5,5,5,5,5
}, 1000);
}
如何 将上述题目改成 1s 后,打印 0,1,2,3,4 呢?
3.1 利用闭包(利用立即执行函数)
立即执行函数,就是个闭包,它可以让局部变量的值,始终保持在内存中,对内部变量进行保护,外部访问不到
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, 1000);
})(i);
}
3.2 在 for 循环中使用 let 声明变量
function initEvents() {
for(let i = 1; i <= 3; i++){
setTimeout(function timer() {
console.log(i);
}, 1000);
}
}
3.3 利用 setTimeout 第三个参数
第三个参数将作为 setTimeout 第一个参数 fn 的参数
// 利用 setTimeout 的第三个参数,第三个参数将作为 setTimeout 第一个参数 fn 的参数
for (var i = 0; i < 5; i++) {
setTimeout(function fn(i) {
console.log(i);
}, 1000, i); // 第三个参数i, 将作为 fn 的参数
}
// 将上述题目改成每间隔 1s 后,依次打印 0,1,2,3,4
for (var i = 0; i < 5; i++) {
setTimeout(function fn(i) {
console.log(i);
}, 1000 * i, i);
}
4. 闭包的垃圾回收机制
在 Javascript 中,局部变量会随着 “函数的执行完毕” 而被销毁,除非还有指向他们的引用;
当闭包本身也被垃圾回收之后,闭包中的 私有状态 也会被垃圾回收;
不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后,未被回收)
5. 推荐一篇很值得学习的面试题分析文章
5.1 文章
由浅入深,一步步讲解,受益很多,点赞👍
5.2 文章题目概述
5.2.1 基础题目 / 初步及格标准
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i); // 5,5,5,5,5,5
用 箭头 表示其前后的两次输出之间有 1 秒的时间间隔,逗号 表示前后的两次输出之间的时间间隔可以忽略,则打印结果 —— 5 -> 5,5,5,5,5
如果上面的可以理解,就战胜了80%的人,及格了
5.2.2 如果期望代码的输出变成: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);
法二 —— setTimeout 第三个参数
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(new Date, j);
}, 1000, i);
}
console.log(new Date, i);
法三 —— 利用 JavaScript 中基本类型(Primitive Type)的参数传递是按值传递
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);
法四 —— 使用 es6 的 let
这个题目里不能用 let,因为会报错,最后一行执行时无法访问到 let
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
// 下面这行报错,访问不到 i
console.log(new Date, i);
5.2.3 如果期望代码的输出变成:0 -> 1 -> 2 -> 3 -> 4 -> 5,该怎么改造代码
代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4
,循环结束后在大概第 5 秒的时候输出 5(这里使用大概,因为 JS 中的定时器触发时机有可能是不确定的,具体可参见 How Javascript Timers Work)
比较简单粗暴的写法(木有加分效果):
- 把最后一行的 console.log 设置定时器,放到最后执行;
- 循环中的 console.log 定时器时间分别增加 1s
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);
如果把这次的需求抽象为:在系列异步操作完成(每次循环都产生了 1 个异步操作)之后,再做其他事情,代码该怎么组织?聪明的你是不是想起了什么? —— Promise 【ES6 中的新特性】
// 这里存放异步操作的 Promise
const tasks = [];
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(new Date, i);
resolve(); // 这里一定要 resolve,否则代码不会按预期 work
}, 1000 * i); // 定时器的超时时间逐步增加
});
// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date, i); // 注意这里只需要把超时设置为 1 秒
}, 1000);
});
如何使用 ES7 中的 async/await 特性来让上面的代码变的更简洁?
// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
// 声明即执行的 async 函数表达式
(async () => {
for (var i = 0; i < 5; i++) {
if (i > 0) {
await sleep(1000);
}
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
})();