三、重学js — 闭包

260 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

大家好,我是小二,今天来简单介绍一下闭包。面试官经常会问到js中的闭包是什么? 我们先来看官方是怎么回答的,下面摘抄于 mdn闭包

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

怎么都是汉字,组合起来就不理解了呢?

其实,根据前面一篇文章函数执行过程和作用域(0080ADC5.png点击查看文章)的介绍,我们也大概知道闭包是啥了。我们在回顾一下,看下一段代码:

function foo() {
  var message = "foo";
  

  function bar() {
  	console.log('bar',message)
	}

  return bar
}

var fn = foo()
fn()

下面我们画个图,来看一下代码的执行过程

1、首先在堆内存创建全局对象GO

2、编译代码,创建执行栈,并创建全局执行上下文GECS,放入执行栈中

3、 开始执行代码, 执行foo之前,创建函数执行上下文并创建AO,然后对foo内进行解析。

这里会遇到bar函数,这时才对bar函数进行解析,而不是代码一开始的时候就解析

4、解析完,执行foo函数内代码

这里会更新foo函数里的AO数据

5、代码执行完,函数上下文出栈。

这里对应的FECS被销毁。至于为什么AO没有被销毁,这个问题我们留到后面在说。

接着把foo函数的返回值赋值给fn,则把bar函数地址赋值给fn

6、执行fn函数。

我们从go中发现fn对应的地址是bar函数地址。在执行fn函数之前,我们会做什么呢? 在心里想一下答案。

当然是创建函数执行上下文,这里的bar作用域就是父级作用域加上自己的AO对象,而父级作用域就是foo的作用域

7、开始执行fn函数内部代码,在父级作用域中找到message,打印结果。

好了一整套代码的执行过程就走完了。我们应该可以对mdn这句话有了一定的李姐了。我们来总结一下:

⭐ 闭包: 就是一个函数和他本身可以访问上层作用域中的变量组成的组合就是闭包

上面留了一个问题:

在foo函数执行完之后,为啥foo的AO没有被销毁

这里就要引出 闭包的内存泄漏 问题

我们在上一篇文章中了解到了js代码垃圾回收机制,其中现在最主要的回收机制是标记清除法。以全局对象为根节点,来往下寻找,如果不在根节点下面,说明没有代码中没有用到,可以清除,用到了就不清除。

在go中有fn的地址引用,fn中存放着bar函数的地址,bar函数中的parent scope指向了foo中的AO对象,所以AO没有被清除。

如果有很多的变量没有被使用,也没有被消除,这样就造成了内存泄漏。那如何解决内存泄漏呢?

很简单,我们只需要把fn的执行为null就可以了,这样后面就断了。

闭包不注意使用会造成意外的内存泄漏问题,但是闭包除了日常的使用之外也有很多的优点,其中就有

🌍 用闭包来模拟私有方法

比如,我们可以在函数内定义一些私有变量,然后定义函数用来访问私有变量

function Counter () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: () => {
      changeBy(1);
    },
    decrement: () => {
      changeBy(-1);
    },
    value: () => {
      return privateCounter;
    }
  }
})

🌏 局部函数可访问全局变量,不会造成变量污染

好了,到这里闭包的介绍就结束了,之后会更新关于this的知识