闭包

之前在我执行上下文执行上下文栈这篇文章中,出现了这样一个题目

for (var i=0; i<10; i++){
setTimeout(
()=>{
console.log(i) // 猜猜结果
},
2000
)
}

题目答案是: 大约2s后输出10个10

引发这个问题的原因恰恰就是因为var关键字没有块级作用域,当定时器异步执行时,同步执行的for早已经执行完毕了,此时i早已经变成了10了,所以最终10个定时器输出的i就全部都是10了

我后来使用了一个方法解决了这个问题,这个方法就是本篇文章介绍的一个概念----闭包

for (var i = 0; i < 10; i++) {  // for循环的i由于是var申明没有块级作用域,是全局变量
(function (i) { // 形参i是IIFE的局部变量
setTimeout(
() => {
console.log(i)
},
2000
)
})(i) // 闭包产生
}

闭包如何产生

我们先不管闭包是什么,有什么用。而是先研究一下闭包是如何产生的

function func1 () {
var a = 1 // 断点打在这
function func2 () {
console.log(a)
}
func2() // 无需调用,闭包也已经产生了
} func1() // 执行函数的时候,就会落在断点处

到这,我们就能知道: 当两个函数互相嵌套,内部函数引用外部函数的变量时,就会产生闭包

需要注意的是:

  • 变量值可以是对象也可以是普通类型的值
  • 内部函数不需要调用,只要引用了外部函数的变量,闭包就已经产生

最后,我们总结一下闭包的产生, 总共需要两个条件:

  1. 函数嵌套
  2. 内部函数引用了外部函数的变量

闭包是什么

根据闭包是如何产生所需的两个条件,我们就能顺便说出闭包究竟是什么东西了---- 闭包就是那个包含被引用变量的对象

常见的闭包

常见的闭包有两种

  • 将内部函数作为外部函数的返回值
  • 将函数作为实参传递给另一个函数调用

先看第一种:

function func1() {
var a = 1
function func2() {
a++
console.log(a)
}
return func2
} var getInnerFunc = func1() // 执行外部函数得到其返回值 ---- func2函数
getInnerFunc() // 2
getInnerFunc() // 3
getInnerFunc() // 4

这里插个题外话,如何计算闭包产生的个数?

这就需要我们对前面闭包是如何产生的定义很清楚: 闭包是内部函数被定义的时候产生的(当然这个内部函数还要引用外部函数的变量)

所以,闭包产生的个数就是外部函数被调用的次数,上面例子产生闭包个数是1个。

在看第二种,这种方式可能对初学者比较劝退,python中装饰器概念对应着的就是这种方式:

function fn1 (fn) {
var MyName = 'Fitz'
function wrapper () { // 内部函数嵌套于外部函数
return fn(MyName) // 闭包产生
}
return wrapper
} // 这个函数作为参数
function fn2 (a) {
console.log(a)
} var decorator = fn1(fn2)
decorator()

闭包的作用

讲了这么多,那闭包究竟有什么作用?,既然都是要在最外层(全局)中调用的,为什么不定义在全局中,而是这样多此一举呢?

闭包能够使得函数内部的变量在函数执行完毕后,继续存活于内存中(延长局部变量的生命周期)

function func1() {
var a = 1
function func2() {
a++
console.log(a)
}
return func2
} var getInnerFunc = func1()
getInnerFunc() // 2
getInnerFunc() // 3

根据执行上下文的相关概念: 函数执行上下文在函数调用时产生,函数内的语句执行完(函数调用完毕)后函数内申明的局部变量/函数将会被销毁(回收)

但是上面这个例子很明显,在函数调用结束后,我们仍然能够访问函数内定义的局部变量(函数),这是为什么呢? 我来画图表示一下

究其原因: 还是因为全局中仍然有变量关联着局部变量func2对应那个函数对象,所以能够通过全局变量getInnerFunc访问到这个函数对象

由于func2这个变量对应的函数对象仍被引用着,所以当外部函数func1及其内部的局部变量被垃圾回收器进行回收时,这个被引用着的函数对象将不会被回收(注意:func1里面的局部变量a和func2都会被回收,但是func2变量指向的对象不会被回收),这将会导致下面介绍的内存泄露与内存溢出的问题

闭包除了能够延长局部变量的申明周期,还能在对外部隐藏实现的情况下,让外部安全的操作函数内部的数据,这也是众多编程语言中gettersetter寄存器的基本原理

function func1() {
var a = 1
function getter() {
console.log(a)
}
function setter(val) {
a = val
}
return {
get: getter,
set: setter
}
} var getInnerFunc = func1() // 执行外部函数得到其返回值 ---- func2函数
getInnerFunc.get() // 1
getInnerFunc.set(666)
getInnerFunc.get() //666

闭包的生命周期

产生: 闭包是在函数定义的时候就产生,跟作用域一样,是静态的

死亡: 当内部函数也成为垃圾对象的时候

function func1 () {
var a = 1
function func2 () {
console.log(a)
}
return func2
} var f = func1()
f()
f = null // 这一步释放操作,让内部函数也成为垃圾对象,释放闭包

闭包的应用

介绍一大堆闭包相关的知识,那这个闭包它在实际中有什么用呢?

闭包其中一个大的作用就是用于编写js模块,最典型的例子就是Jquery,看过Jquery源码的同学都会看到,Jquery是一个巨大的IIFE函数,这个函数里面向全局对象暴露$对象或者说JQuery对象

基于闭包,我们也来简单的做一个js模块

// 模仿jquery源码的方式
// 我们来自定义一个数学工具方法
(function myMath (globalObject) {
var initVal = 1
function add (val) {
return initVal += val
}
function pow (val) {
initVal = initVal ** val
return initVal
} globalObject.$ = globalObject.fakeJquery = {
// es6的对象简写语法
add,
pow
}
})(window)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head> <body>
<script src="./myModule.js"></script>
<script>
console.log(window)
console.log($) console.log($.add(1)) // 2
console.log($.add(2)) // 4
console.log($.pow(2)) // 16
</script>
</body>
</html>

闭包的缺点

闭包在实际运用很广泛,但是它有一个显著的缺点就是: 如果不及时释放闭包,就会造成内存泄露,内存泄漏到了一定程度,最终导致内存溢出

内存泄露

内存在使用完后没用被及时释放,一直被没用的东西占用着

常见的内存泄露的情况

意外的全局变量

function test () {
a = 'Fitz' // 忘记使用关键字申明, 这里的a是全局变量
}
console.log(a) // 'Fitz'

没被及时清理的定时器

setTimeout(()=>{
console.log('hello')
},1000)

操作dom及其回调函数

const btn = document.getElementById('btn01')
btn.onclick = function () {
alert('my name is Fitz')
}
btm = null // 既要清空变量的应用
document.body.removeChild('btn') // 还要清空DOM的引用

最后一种就是我们本篇介绍的闭包,导致的内存泄露了

内存溢出

内存占用超过了可用内存的总大小时,就会产生内存溢出

闭包面试题

题目1

var name = 'the window'
var object = {
name: 'my object',
getNameFunc: function () {
return function () {
return this.name
}
}
}
console.log(object.getNameFunc()()) // 'the window'

解析: 这一题考查的是闭包中this的指向,闭包中的this可能令人有些迷惑,但是只要我们对this的知识比较扎实,就能不被表象所欺骗

这里object.getNameFunc()()其实真正可以写成

var innerFunc = object.getNameFunc()
innerFunc() // this自然指向window
/*
这其实属于this中的隐式绑定丢失的概念
*/

那对于这道题目,如果我们一定要访问object中的name怎么办呢? 那我们就需要防止this的隐式绑定丢失,我们将object的this保存下来

var name = 'the window'
var object = {
name: 'my object',
getNameFunc: function () {
var that = this
return function () {
return that.name
}
}
}
console.log(object.getNameFunc()()) // 'my object'

接着是一道终极无敌蛇皮怪怪锤面试题,这题就是玩弄心态的,各位年轻人耗子尾汁

上菜!

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
/*
输出结果是啥? */ var b = fun(0).fun(1).fun(2).fun(3)
/*
输出结果是啥? */ var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
/*
输出结果是啥? */

答案:

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
/*
输出结果是啥?
- undefined
- 0
- 0
- 0
*/ var b = fun(0).fun(1).fun(2).fun(3)
/*
输出结果是啥?
- undefined
- 0
- 1
- 2
*/ var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
/*
输出结果是啥?
- undefined
- 0
- 1
- 1
*/

解析:

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var a = fun(0) // 由于没有实参给予o,所以o为undefined,输出undefined // 然后得到的一个对象赋值给变量a
/*
a => {
fun: function (m) {
return fun(m, 0) // function(m){...}是闭包
}
}
*/ // 此时实参1赋值给形参m
a.fun(1) // 执行fun(n=1, o=0) 输出o=0
// 此时实参2赋值给形参m
a.fun(2) // 执行fun(n=2, o=0) 输出o=0
// 此时实参3赋值给形参m
a.fun(3) // 执行fun(n=3, o=0) 输出o=0
/*
输出结果是啥?
- undefined
- 0
- 0
- 0
*/

由于a.fun()是三次独立的调用,即产生的是不同的执行上下文,所以函数之间的变量是独立、没有记忆的

接着

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
}
var b = fun(0).fun(1).fun(2).fun(3) /*
fun(0): n=0 o=undefined 输出undefined
fun(0).fun(1): m=1 n=上次的n=0 输出0
fun(0).fun(1).fun(2): m=2 n=上次的m=1 输出1
fun(0).fun(1).fun(2).fun(3): m=3 n=上次的m=2 输出2
*/ /*
输出结果是啥?
- undefined
- 0
- 1
- 2
*/

由于是连续的调用,执行上下文对象始终是同一个,所以前一次调用后的变量/参数,会影响后一次的结果,是有记忆的

最后

function fun(n, o) {
console.log(o)
return {
fun: function (m) {
return fun(m, n)
}
}
} var c = fun(0).fun(1)
/*
fun(0): n=0 o=undefined 输出undefined
fun(0).fun(1): m=1 n=上次的n=0 输出0 此时c是一个对象:
c = {
fun: function (m) {
return fun(m, 1)
}
}
*/ c.fun(2)
/*
相当于执行:
function (2) {
return fun(2, 1)
}
输出1
*/ c.fun(3)
/*
相当于执行:
function (3) {
return fun(3, 1)
}
输出1
*/ /*
输出结果是啥?
- undefined
- 0
- 1
- 1
*/

这个例子是上面两种的结合,连续调用得到c对象,然后在对c对象进行独立的调用,考查的是执行上下文对象以及显而易见的闭包

让你弄懂js中的闭包的更多相关文章

  1. JavaScript中的this详解(彻底弄懂js中的this用法)!

    要想学好js,那么其中那些特别令人混淆迷惑的知识点,就一定要弄清楚.this关键字就是其中让初学者比较迷惑的知识点之一,不过灵活运用this可以提升代码的性能和复用性,那么今天我就和大家一起来了解th ...

  2. 彻底弄懂JS中的this

    首先,用一句话解释this,就是:指向执行当前函数的对象. 当前执行,理解一下,也就是说this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定.this到底指向谁?this的最终指向的 ...

  3. 详解js中的闭包

    前言 在js中,闭包是一个很重要又相当不容易完全理解的要点,网上关于讲解闭包的文章非常多,但是并不是非常容易读懂,在这里以<javascript高级程序设计>里面的理论为基础.用拆分的方式 ...

  4. JS中的闭包(closure)

    JS中的闭包(closure) 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面就是我的学习笔记,对于Javascript初学者应该是很有用 ...

  5. js进阶 12-2 彻底弄懂JS的事件冒泡和事件捕获

    js进阶 12-2 彻底弄懂JS的事件冒泡和事件捕获 一.总结 一句话总结:他们是描述事件触发时序问题的术语.事件捕获指的是从document到触发事件的那个节点,即自上而下的去触发事件.相反的,事件 ...

  6. js中的闭包之我理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  7. 浅谈JS中的闭包

    浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...

  8. js中的“闭包”

    js中的“闭包” 姓名:闭包 官方概念:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. ( ⊙o⊙ )!!!这个也太尼玛官方了撒,作为菜鸟的 ...

  9. 彻底弄懂AngularJS中的transclusion

    点击查看AngularJS系列目录 彻底弄懂AngularJS中的transclusion AngularJS中指令的重要性是不言而喻的,指令让我们可以创建自己的HTML标记,它将自定义元素变成了一个 ...

随机推荐

  1. 多线程(四) AQS底层原理分析

    J.U.C 简介 Java.util.concurrent 是在并发编程中比较常用的工具类,里面包含很多用来在并发 场景中使用的组件.比如线程池.阻塞队列.计时器.同步器.并发集合等等.并 发包的作者 ...

  2. sqlmap 详解

    sqlmap 使用总结   0x01 需要了解 当给 sqlmap 这么一个 url 的时候,它会:1.判断可注入的参数 2.判断可以用那种 SQL 注入技术来注入 3.识别出哪种数据库 4.根据用户 ...

  3. js var & let & const All In One

    js var & let & const All In One js var & let & const 区别对比 var let const 区别 是否存在 hois ...

  4. 如何重置电信悦 me 智能网关

    如何重置电信悦 me 智能网关 重置电信网关密码 电信悦 me 智能网关密码忘记了怎么办? 首先,得要知道默认终端配置地址和默认终端配置密码. 可以从无线路由器背面标签得知. 如果不知道密码了,可以通 ...

  5. vue技术栈

    1 vue 说明:vue生命周期:技术点:1:常用的API:computed,methods,props,mounted,created,components 2vue-cli说明:vue绞手架,用于 ...

  6. redis源码之dict

    大家都知道redis默认是16个db,但是这些db底层的设计结构是什么样的呢? 我们来简单的看一下源码,重要的字段都有所注释 typedef struct redisDb { dict *dict; ...

  7. 01、初识Java

    目录 前言 一.认识Java 历史介绍 Java介绍 二.认识及安装JDK 1.认识JDK 2.安装JDK 配置与测试 配置注意及不生效解决 3.认识Java虚拟机 三.Java的工作方式 四.jav ...

  8. 自己的Scrapy框架学习之路

    开始自己的Scrapy 框架学习之路. 一.Scrapy安装介绍 参考网上资料,先进行安装 使用pip来安装Scrapy 在开始菜单打开cmd命令行窗口执行如下命令即可 pip install Scr ...

  9. 第34天学习打卡(GUI编程之组件和容器 frame panel 布局管理 事件监听 多个按钮共享一个事件 )

    GUI编程 组件 窗口 弹窗 面板 文本框 列表框 按钮 图片 监听事件 鼠标 键盘事件 破解工具 1 简介 GUi的核心技术:Swing AWT 1.界面不美观 2.需要jre环境 为什么要学习GU ...

  10. Docker-compose编排微服务顺序启动

    一.概述 docker-compose可以方便组合多个 docker 容器服务, 但是, 当容器服务之间存在依赖关系时, docker-compose 并不能保证服务的启动顺序.docker-comp ...