循环中的闭包

目录

1. 什么是闭包?闭包的作用? 

1.1 可以访问 外部作用域 中变量的内部函数

1.2 闭包可以访问外部作用域中的变量及传参

2. 异步操作中 变量 的生命周期,取决于 闭包 的生命周期

2.1 Timer 定时器(保留到 定时器回调执行完毕、定时器被清空)

2.2 Event 事件处理函数(保留到 事件处理函数 被移除)

2.3 Ajax 请求数据(保留到 接收到接口返回数据,执行回调函数)

2.4 其他异步 API 使用闭包

3. 循环中的闭包

3.1 利用闭包(利用立即执行函数)

3.2 在 for 循环中使用 let 声明变量

3.3 利用 setTimeout 第三个参数

4. 闭包的垃圾回收机制

5. 推荐一篇很值得学习的面试题分析文章

5.1 文章

5.2 文章题目概述

5.2.1 基础题目 / 初步及格标准

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 中,局部变量会随着 “函数的执行完毕” 而被销毁,除非还有指向他们的引用;

当闭包本身也被垃圾回收之后,闭包中的 私有状态 也会被垃圾回收;

不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后,未被回收)
 

[译]发现 JavaScript 中闭包的强大威力 - 掘金闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。 作用域决定这个变量的生命周期及其可见性。 当我们创建了一个函数或者 {} 块,就会生成一个新的作用域。需要注意的是,通过 var 创建的变量只有函数作用域,而通过 let 和 const 创建的变量既有函…https://juejin.cn/post/6844903769646317576

5. 推荐一篇很值得学习的面试题分析文章

5.1 文章

由浅入深,一步步讲解,受益很多,点赞👍

破解前端面试(80% 应聘者不及格系列):从闭包说起 - 掘金修订说明:发布《80% 应聘者都不及格的 JS 面试题》之后,全网阅读量超过 6W,在知乎、掘金、cnodejs 都引发了很多讨论,还被多个前端微信公号和技术媒体转载。酝酿许久之后,笔者准备接下来撰写前端面试题系列文章,内容涵盖 DOM、HTTP、浏览器、框架、编码、工程化等方…https://juejin.cn/post/6844903474212143117

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);
})();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lyrelion

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值