目录
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,该怎么改造代码
可以访问 外部作用域 中变量的内部函数(在 函数内部 或者 {} 块内部 定义一个函数,就创建了闭包)
举个栗子~~
- // log()、simple() 是嵌套在 autorun() 函数里面的函数
-
- (function autorun() {
- let x = 1;
-
- // 闭包示例
- function log() {
- // 在 log() 内部,可以访问到 外部作用域 中的变量 x,此时 log() 就是闭包
- console.log(x);
- }
-
- // 纯函数示例
- function simple() {
- return 2 === Number(2);
- }
-
- log();
- })();
关于内部函数,内部函数有两种(闭包、纯函数):
也可以这么理解,随着函数得执行完毕,某个变量因为被引用着(内部函数/闭包引用着)导致不能够被垃圾回收
即使 外部函数 / 外部块 已经执行完毕,内部函数仍然可以访问 外部函数 / 外部块 中定义的变量、接收的传参;让 局部变量 的值,始终保留在 内存 中
举个栗子~~
- // 外部函数
- (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
- })();
- })();
- })();
当外部作用域执行完毕后,内部函数还存活(仍在其他地方被引用)时,闭包才真正发挥其作用
timer
定时器,事件处理,Ajax
请求中被作为回调变量 x 将一直保留,直到以下两种情况出现:
- (function autorun() {
- let x = 1;
- setTimeout(function log() {
- // 变量 x 将一直存活着直到定时器的回调执行或者 clearTimeout() 被调用
- // 如果用 setInterval() ,那么变量 x 将存活到 clearInterval() 被调用。
- console.log(x);
- }, 10000);
- })();
变量 x 将一直保留,直到 事件处理函数 被移除
- (function autorun() {
- let x = 1;
- // 当变量 x 在事件处理函数中被使用时,它将一直存活直到该事件处理函数被移除。
- $("#btn").on("click", function log() {
- console.log(x);
- });
- })();
变量 x 将一直保留,直到 接收到接口返回数据,执行回调函数
- (function autorun() {
- let x = 1;
- fetch("http://").then(function log() {
- // 变量 x 将一直存活到接收到后端返回结果,回调函数被执行。
- console.log(x);
- });
- })();
除了 Timer 定时器,Event 事件处理,Ajax 请求等常见的异步任务,还有其他的异步 API(比如 HTML5 Geolocation,WebSockets , requestAnimationFrame())
他们都使用到闭包的这一特性 —— 变量的生命周期取决于闭包的生命周期,被闭包引用的外部作用域中的变量,将一直存活直到闭包函数被销毁。
如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁
- // 原始题目
- 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 呢?
立即执行函数,就是个闭包,它可以让局部变量的值,始终保持在内存中,对内部变量进行保护,外部访问不到
- for (var i = 0; i < 5; i++) {
- (function(j) {
- setTimeout(function timer() {
- console.log(j);
- }, 1000);
- })(i);
- }
- function initEvents() {
- for(let i = 1; i <= 3; i++){
- setTimeout(function timer() {
- console.log(i);
- }, 1000);
- }
- }
第三个参数将作为 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);
- }
在 Javascript 中,局部变量会随着 “函数的执行完毕” 而被销毁,除非还有指向他们的引用;
当闭包本身也被垃圾回收之后,闭包中的 私有状态 也会被垃圾回收;
不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后,未被回收)
由浅入深,一步步讲解,受益很多,点赞👍
- 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%的人,及格了
法一 —— 立即执行函数
- 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);
代码执行时,立即输出 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);
如果把这次的需求抽象为:在系列异步操作完成(每次循环都产生了 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);
- })();