From 83a0ca5c6c4c5f6592db202d21354eca849b161f Mon Sep 17 00:00:00 2001 From: qianguyihao Date: Sat, 6 Aug 2022 18:27:23 +0800 Subject: [PATCH] =?UTF-8?q?update:=E9=97=AD=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 04-JavaScript基础/25-闭包.md | 445 +++++++++++++++++++++++++++++++ 04-JavaScript基础/26-闭包.md | 500 ----------------------------------- 2 files changed, 445 insertions(+), 500 deletions(-) create mode 100644 04-JavaScript基础/25-闭包.md delete mode 100644 04-JavaScript基础/26-闭包.md diff --git a/04-JavaScript基础/25-闭包.md b/04-JavaScript基础/25-闭包.md new file mode 100644 index 0000000..3819ad7 --- /dev/null +++ b/04-JavaScript基础/25-闭包.md @@ -0,0 +1,445 @@ +--- +title: 25-闭包 +--- + + + +## 闭包的引入 + +我们知道,变量根据作用域的不同分为两种:全局变量和局部变量。 + +- 函数内部可以访问全局变量和局部变量。 + +- 函数外部只能访问全局变量,不能访问局部变量。 + +- 当函数执行完毕,本作用域内的局部变量会销毁。 + +比如下面这样的代码: + +```js +function foo() { + let a = 1; +} + +foo(); +console.log(a); // 打印报错:Uncaught ReferenceError: a is not defined +``` + +上方代码中,由于变量 `a` 是函数内的局部变量,所以外部无法访问。 + +但是,在有些场景下,我们就是想要在函数外部访问**函数内部作用域的局部变量**,那要怎么办呢?这就引入了闭包的概念。 + +## 什么是闭包 + +### 闭包(closure)的概念 + +**闭包**:如果**外部作用域**有权访问另外一个**函数内部**的**局部变量**时,那就产生了闭包。这个内部函数称之为闭包函数。注意,这里强调的是访问**局部变量**。 + +闭包代码举例: + +```js +function fun1() { + const a = 10; + return function fun2() { + console.log(a); + }; +} +fun1(); +// 调用外部函数,就能得到内部函数,并用 变量 result 接收 +const result = fun1(); +// 在 fun1函数的外部,执行了内部函数 fun2,并访问到了 fun2的内部变量a +result(); // 10 +``` + +打印结果: + +``` +10 +``` + +上方代码中,外部作用域(即全局作用域) 访问了函数 fun1 中的局部变量,那么,在 fun1 中就产生了闭包,函数 fun1是闭包函数。 + +全局作用域中,并没有定义变量a。正常情况下作为函数内的局部变量 a,无法被外部访问到。但是通过闭包,我们最后还是可以在全局作用域中拿到局部变量 a 的值。 + +注意,闭包函数是fun1,不是fun2。fun2在这里的作用是让全局作用域访问到变量a,fun2只是一个桥梁。 + +### 闭包的生命周期 + +1. 产生:内部函数fn1被声明时(不是被调用时)就产生了。 + +2. 死亡:嵌套的内部函数成为垃圾对象时。(比如fun1 = null,就可以让 fun1 成为垃圾对象) + +### 在 chrome 浏览器控制台中,调试闭包 + +上面的代码中,要怎么验证,确实产生了闭包呢?我们可以在 chrome 浏览器的控制台中设置断点,当代码执行到 `console.log(a)`这一行的时候,如下图所示: + +![](http://img.smyhvae.com/20200703_2055.png) + +上图中, Local 指的是局部作用域,Global 指的是全局作用域;而 Closure 则是**闭包**,fn1 是一个闭包函数。 + +## 闭包的表现形式 + +- 1. 将一个函数作为另一个函数的返回值 + +- 2. 将函数作为实参传递给另一个函数调用。 + +### 形式1:将一个函数作为另一个函数的返回值 + +```javascript + function fn1() { + var a = 2 + + function fn2() { + a++ + console.log(a) + } + return fn2 + } + + var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2 + f() // 3 //执行fn2 + f() // 4 //再次执行fn2 +``` + + +当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。 + +上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。 + +也就是说,要看闭包对象创建了一个,就看:**外部函数执行了几次**(与内部函数执行几次无关)。 + +### 形式2:将函数作为实参传递给另一个函数调用 + +在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。 + +```javascript + function showDelay(msg, time) { + setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg + alert(msg) + }, time) + } + showDelay('qianguyihao', 2000) +``` + +上面的代码中,闭包是里面的function,因为它是嵌套的子函数,而且引用了外部函数的变量msg。 + + +## 闭包的作用 + +- 作用1:延长局部变量的生命周期。 + +- 作用2:让函数外部可以操作(读写)到函数内部的数据(变量/函数) + +代码演示: + +```javascript +function fun1() { + let a = 2 + + function fun2() { + a++ + console.log(a) + } + return fun2; +} + +const foo = fun1(); //执行外部函数fn1,返回的是内部函数fn2 +foo() // 3 //执行fun2 +foo() // 4 //再次执行fun2 +``` + +上方代码中,foo 代表的就是整个 fun2 函数。当执行了 `foo()` 语句之后,也就执行了fun2()函数,fun1() 函数内就产生了闭包。 + +**作用1分析**: + +一般来说,在 fn1() 函数执行完毕后,它里面的变量 a 会立即销毁。但此时由于产生了闭包,所以 **fun1 函数中的变量 a 不会立即销毁,仍然保留在内存中,因为 fn2 函数还要继续调用变量 a**。只有等所有函数把变量 a 调用完了,变量 a 才会销毁。 + +**作用2分析:** + +在执行 `foo()`语句之后,竟然能够打印出 `3`,这就完美通过闭包实现了:全局作用域成功访问到了局部作用域中的变量 a。 + +达到的效果是:**外界看不到变量a,但可以操作a**。当然,如果我真想看到a,我可以在fn2中将a返回即可。 + +## 闭包的应用场景 + +### 场景1:高阶函数 + +题目:不同的班级有不同成绩检测标准。比如:A班的合格线是60分,B 班的合格线是70分。已知某个人班级和分数,请用闭包函数判断他的成绩是否合格。 + +思路:创建成绩检测函数 checkStandard(n),检查成绩 n 是否合格,函数返回布尔值。 + +代码实现: + +```js +// 高阶函数:判断学生的分数是否合格。形参 standardTemp 为标准线 +function createCheckTemp(standardTemp) { + // 形参 n 表示具体学生的分数 + function checkTemp(n) { + if (n >= standardTemp) { + alert('成绩合格'); + } else { + alert('成绩不合格'); + } + } + return checkTemp; +} + +// 创建一个 checkStandard_A 函数,它以60分为合格线 +var checkStandard_A = createCheckTemp(60); +// 再创建一个 checkStandard_B 函数,它以70分为合格线 +var checkStandard_B = createCheckTemp(70); + +// 调用函数 +checkStandard_A(65); // 成绩合格 +checkStandard_B(65); // 成绩不合格 +``` + +对于A班来说,它的闭包函数是createCheckTemp(),闭包范围是 checkTemp()函数和参数`standardTemp = 60`。对于B班来说,它的闭包函数是全新的createCheckTemp(),闭包范围是全新的checkTemp()函数和全新的参数`standardTemp = 70`。 + +因为有闭包存在,所以,并不会因为 createCheckTemp() 执行完毕后就销毁 standardTemp 的值;且A班和B班的standardTemp参数不会混淆。 + +备注:关于“高阶函数”的更多解释,我们在以后的内容中讲解。 + +### 场景2:封装JS模块 + +闭包的第二个使用场景是:定义具有特定功能的JS模块,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或方法。模块的调用者,只能调用模块暴露的对象或方法来实现对应的功能。 + +比如有这样一个需求:定义一个变量a,要求a只能被进行指定操作(比如加减),不能进行其他操作(比如乘除)。在 Java、C++ 等语言中,有私有属性的概念,但在JS中只能通过闭包模拟。 + +我们来看看下面的代码,如何通过闭包封装JS模块。 + +写法1: + +(1)myModule.js:(定义一个模块,向外暴露多个方法,供外界调用) + +```javascript +function myModule() { + //私有数据 + var msg = 'Qinguyihao Haha' + + //操作私有数据的函数 + function doSomething() { + console.log('doSomething() ' + msg.toUpperCase()); //字符串大写 + } + + function doOtherthing() { + console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写 + } + + //通过【对象字面量】的形式进行包裹,向外暴露多个函数 + return { + doSomething1: doSomething, + doOtherthing2: doOtherthing + } +} +``` + +上方代码中,外界只能通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到里面的具体实现。 + +(2)index.html: + +```html + + + + + 闭包的应用-自定义JS模块 + + + + + + + +``` + +写法2: + +同样是实现上面的功能,我们还采取另外一种写法,写起来更方便。如下: + +(1)myModule2.js:(是一个立即执行的匿名函数) + +```javascript +(function () { + //私有数据 + var msg = 'Qinguyihao Haha' + + //操作私有数据的函数 + function doSomething() { + console.log('doSomething() ' + msg.toUpperCase()) + } + + function doOtherthing() { + console.log('doOtherthing() ' + msg.toLowerCase()) + } + + //外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象 + window.myModule = { + doSomething1: doSomething, + doOtherthing2: doOtherthing + } +})() +``` + + +(2)index.html: + +```html + + + + + 闭包的应用-自定义JS模块 + + + + + + + + + + +``` + +上方两个文件中,我们在`myModule2.js`里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。 + +**小结:** + +写法1和写法2都采用了闭包。 + +## 内存溢出和内存泄露 + +> 这里我们额外讲两个概念,这两个概念和闭包有些联系。 + +### 内存泄漏 + +**内存泄漏**:**占用的内存**没有及时释放。 + +注意,内存泄露的次数积累多了,就容易导致内存溢出。 + +**常见的内存泄露**: + +- 1.意外的全局变量 + +- 2.没有及时清理的计时器或回调函数 + +- 3.闭包 + + +情况1举例: + +```javascript + // 意外的全局变量 + function fn() { + a = new Array(10000000); + console.log(a); + } + + fn(); +``` + +情况2举例: + +```javascript + // 没有及时清理的计时器或回调函数 + var intervalId = setInterval(function () { //启动循环定时器后不清理 + console.log('----') + }, 1000) + + // clearInterval(intervalId); //清理定时器 +``` + +情况3举例: + +```html + +``` + +### 内存溢出 + +**内存溢出**:一种程序运行出现的错误。当程序运行**需要的内存**超过了剩余的内存时,就出抛出内存溢出的错误。 + +代码举例: + +```javascript + var obj = {}; + for (var i = 0; i < 10000; i++) { + obj[i] = new Array(10000000); //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间 + console.log("-----"); + } +``` + +### 闭包是否会造成内存泄漏 + +一般来说,答案是否定的。因为内存泄漏是非预期情况,本来想回收,但实际没回收;而闭包是预期情况,一般不会造成内存泄漏。 + +但如果因代码质量不高,滥用闭包,也会造成内存泄漏。 + +## 闭包面试题 + +代码举例: + +```js +function addCount() { + let count = 0; + return function () { + count = count + 1; + console.log(count); + }; +} + +const fun1 = addCount(); +const fun2 = addCount(); +fun1(); +fun2(); + +fun1(); +fun2(); +``` + +打印结果: + +``` +1 +1 +2 +2 +``` + +代码解释: + +(1)fun1 和 fun2 这两个闭包函数是互不影响的,因此第一次调用时,count变量都是0,最终各自都输出1。 + +(2)第二次调用时,由于闭包有记忆性,所以各自会在上一次的结果上再加1,因此输出2。 diff --git a/04-JavaScript基础/26-闭包.md b/04-JavaScript基础/26-闭包.md deleted file mode 100644 index de2031b..0000000 --- a/04-JavaScript基础/26-闭包.md +++ /dev/null @@ -1,500 +0,0 @@ ---- -title: 26-闭包 ---- - - - - -## 闭包的引入 - -我们知道,变量根据作用域的不同分为两种:全局变量和局部变量。 - -- 函数内部可以访问全局变量和局部变量。 - -- 函数外部只能访问全局变量,不能访问局部变量。 - -- 当函数执行完毕,本作用域内的局部变量会销毁。 - -比如下面这样的代码: - -```js -function foo() { - let a = 1; -} - -foo(); -console.log(a); // 打印报错:Uncaught ReferenceError: a is not defined -``` - -上方代码中,由于变量 `a` 是函数内的局部变量,所以外部无法访问。 - -但是,在有些场景下,我们就是想要在函数外部访问函数内的局部变量,那要怎么办呢?这就需要引入闭包的概念。 - -## 闭包的概念和代码举例 - -### 闭包的概念 - -**闭包**(closure):指有权**访问**另一个函数作用域中**变量**的**函数**。 - -上面这个概念,出自《JavaScript 高级程序设计(第 3 版)》这本书。上面的概念中指出,闭包是一种函数;当然,你可以**把闭包理解成是一种现象**。具体解释如下: - -如果**这个作用域可以访问另外一个函数内部的局部变量**,那就产生了闭包。注意,这里强调的是访问**局部变量**哦。 - -闭包代码举例: - -```js -function fun1() { - const a = 10; - return function fun2() { - console.log(a); - }; -} -fun1(); -var result = fun1(); -result(); // 10 -``` - -打印结果: - -``` -10 -``` - -上方代码中,函数 fun2 的作用域访问了 fun1 中的局部变量,那么,在 fn1 中就产生了闭包。 - -正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,我们最后还是可以拿到 a 变量的值。 - -### 产生闭包的条件 - -当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量或函数时, 就产生了闭包。 - -- 条件1.函数嵌套 - -- 条件2.内部函数引用了外部函数的数据(变量/函数)。 - - - -来看看条件2: - -```javascript - function fn1() { - function fn2() { - - } - - return fn2; - } - - fn1(); -``` - -上面的代码不会产生闭包,因为内部函数fn2并没有引用外部函数fn1的变量。 - - -PS:还有一个条件是**外部函数被调用,内部函数被声明**。比如: - -```javascript - - function fn1() { - var a = 2 - var b = 'abc' - - function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数) - console.log(a) - } - - } - - fn1(); - - function fn3() { - var a = 3 - var fun4 = function () { //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前 - console.log(a) - } - } - - fn3(); - -``` - - - -### 在 chrome 浏览器控制台中,调试闭包 - -上面的代码中,要怎么验证,确实产生了闭包呢?我们可以在 chrome 浏览器的控制台中设置断点,当代码执行到 `console.log(a)`这一行的时候,如下图所示: - -![](http://img.smyhvae.com/20200703_2055.png) - -上图中, Local 指的是局部作用域,Global 指的是 全局作用域;而 Closure 则是**闭包**,fn1 是一个闭包函数。 - - - -### 闭包的作用:延伸变量的作用范围 - -我们来看看下面这段闭包的代码: - -```js -function fn1() { - let a = 20; - - function fn2() { - console.log(a); - } - return fn2; -} - -const foo = fn1(); // 执行 fn1() 之后,会得到一个返回值。foo 代表的就是 fn2 函数 -foo(); -``` - -上方代码中,foo 代表的就是整个 fn2 函数。当执行了 `foo()` 语句之后(相当于执行了 ),fn1 函数内就产生了闭包。 - -一般来说,在 fn1 函数执行完毕后,它里面的变量 a 会立即销毁。但此时由于产生了闭包,所以 **fn1 函数中的变量 a 不会立即销毁,因为 fn2 函数还要继续调用变量 a**。只有等所有函数把变量 a 调用完了,变量 a 才会销毁。 - -而且,可以看出, 在执行 `foo()`语句之后,竟然能够打印出 `20`,这就完美通过闭包实现了:全局作用域成功访问到了局部作用域中的变量 a。 - -因此,我们可以看出,闭包的主要作用就是:延伸了变量的作用范围。 - -上面的代码也可以简写成: - -```js -function fn1() { - let a = 20; - - return function () { - console.log(a); - }; -} - -const foo = fn1(); // 执行 fn1() 之后,会得到一个返回值。这个返回值是函数 -foo(); -``` - - - -## 闭包的表现形式 - -- 1. 将一个函数作为另一个函数的返回值 - -- 2. 将函数作为实参传递给另一个函数调用。 - -### 闭包1:将一个函数作为另一个函数的返回值 - -```javascript - function fn1() { - var a = 2 - - function fn2() { - a++ - console.log(a) - } - return fn2 - } - - var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2 - f() // 3 //执行fn2 - f() // 4 //再次执行fn2 -``` - - -当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。 - -上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。 - -也就是说,要看闭包对象创建了一个,就看:**外部函数执行了几次**(与内部函数执行几次无关)。 - -### 闭包2. 将函数作为实参传递给另一个函数调用 - -在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。 - - -```javascript - function showDelay(msg, time) { - setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg - alert(msg) - }, time) - } - showDelay('qianguyihao', 2000) -``` - -上面的代码中,闭包是里面的function,因为它是嵌套的子函数,而且引用了外部函数的变量msg。 - - -## 闭包的作用 - -- 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期) - -- 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数) - -我们让然拿这段代码来分析: - -```javascript - function fn1() { - var a = 2 - - function fn2() { - a++ - console.log(a) - } - return fn2; - } - - var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2 - f() // 3 //执行fn2 - f() // 4 //再次执行fn2 - -``` - -**作用1分析**: - -上方代码中,外部函数fn1执行完毕后,变量a并没有立即消失,而是保存在内存当中。 - - -**作用2分析:** - -函数fn1中的变量a,是在fn1这个函数作用域内,因此外部无法访问。但是通过闭包,外部就可以操作到变量a。 - -达到的效果是:**外界看不到变量a,但可以操作a**。 - -比如上面达到的效果是:我看不到变量a,但是每次执行函数后,让a加1。当然,如果我真想看到a,我可以在fn2中将a返回即可。 - - - -回答几个问题: - -- 问题1. 函数执行完后, 函数内部声明的局部变量是否还存在? - -答案:一般是不存在, 存在于闭包中的变量才可能存在。 - -闭包能够一直存在的根本原因是`f`,因为`f`接收了`fn1()`,这个是闭包,闭包里有a。注意,此时,fn2并不存在了,但是里面的对象(即闭包)依然存在,因为用`f`接收了。 - - -- 问题2. 在函数外部能直接访问函数内部的局部变量吗? - -不能,但我们可以通过闭包让外部操作它。 - - -## 闭包的生命周期 - -1. 产生: 嵌套内部函数fn2被声明时就产生了(不是在调用) - -2. 死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了) - - - -## 闭包的应用:定义具有特定功能的js模块 - -- 将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。 - -- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能。 - -### 方式一 - -(1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用) - -```javascript -function myModule() { - //私有数据 - var msg = 'Smyhvae Haha' - - //操作私有数据的函数 - function doSomething() { - console.log('doSomething() ' + msg.toUpperCase()); //字符串大写 - } - - function doOtherthing() { - console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写 - } - - //通过【对象字面量】的形式进行包裹,向外暴露多个函数 - return { - doSomething1: doSomething, - doOtherthing2: doOtherthing - } -} -``` - - -上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。 - -(2)index.html: - -```html - - - - - 05_闭包的应用_自定义JS模块 - - - - - - - -``` - - -### 方式二 - -同样是实现方式一种的功能,这里我们采取另外一种方式。 - -(1)myModule2.js:(是一个立即执行的匿名函数) - -```javascript -(function () { - //私有数据 - var msg = 'Smyhvae Haha' - - //操作私有数据的函数 - function doSomething() { - console.log('doSomething() ' + msg.toUpperCase()) - } - - function doOtherthing() { - console.log('doOtherthing() ' + msg.toLowerCase()) - } - - //外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象 - window.myModule = { - doSomething1: doSomething, - doOtherthing2: doOtherthing - } -})() -``` - - -(2)index.html: - -```html - - - - - 05_闭包的应用_自定义JS模块2 - - - - - - - - - - -``` - -上方两个文件中,我们在`myModule2.js`里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。 - -**总结:** - -当然,方式一和方式二对比后,我们更建议采用方式二,因为很方便。 - -但无论如何,两种方式都采用了闭包。 - - -## 闭包的缺点及解决 - -缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。 - - -解决:能不用闭包就不用,及时释放。比如: - -```javascript - f = null; // 让内部函数成为垃圾对象 -->回收闭包 -``` - -总而言之,你需要它,就是优点;你不需要它,就成了缺点。 - - -## 内存溢出和内存泄露 - -### 内存溢出 - -**内存溢出**:一种程序运行出现的错误。当程序运行**需要的内存**超过了剩余的内存时, 就出抛出内存溢出的错误。 - -代码举例: - -```javascript - var obj = {}; - for (var i = 0; i < 10000; i++) { - obj[i] = new Array(10000000); //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间 - console.log("-----"); - } -``` - -### 内存泄漏 - -**内存泄漏**:**占用的内存**没有及时释放。 - -注意,内存泄露的次数积累多了,就容易导致内存溢出。 - -**常见的内存泄露**: - -- 1.意外的全局变量 - -- 2.没有及时清理的计时器或回调函数 - -- 3.闭包 - - -情况1举例: - -```javascript - // 意外的全局变量 - function fn() { - a = new Array(10000000); - console.log(a); - } - - fn(); -``` - -情况2举例: - -```javascript - // 没有及时清理的计时器或回调函数 - var intervalId = setInterval(function () { //启动循环定时器后不清理 - console.log('----') - }, 1000) - - // clearInterval(intervalId); //清理定时器 -``` - -情况3举例: - -```html - -```