理解闭包

知识小储备

ECMAScript 的数据有两种类型:基本类型值和引用类型值,基本类型指的是简单的数据段,引用类型指的是可能由多个值构成的对象。

Undefined、Null、Boolean、Number 和 String 是值类型,其他都是引用类型。

垃圾回收

我们创建的原始类型、对象、函数等等,都会占用内存。为了防止溢出,我们就需要对不用的数据进行删除。这就是垃圾回收。

可触及(Reachability)

JavaScript 内存管理的关键概念是可触及(Reachability)。
我的理解就是还处于被引用状态。
将全局(无论是window还是global)比作树根,我们创建的原始类型、对象、函数等等比作一个个枝杈。如果可以从window不断层的知道某个变量,那这个变量就是可触及的,不可回收的。

举个例子:

// user has a reference to the object
let user = {
  name: "John"
};

图片描述

箭头代表的是对象引用。全局变量 "user" 引用了对象{name: "John"}(简称此对象为 John)。John 的 "name" 属性储存的是一个原始值,所以无其他引用。

如果覆盖 user,对 John 的引用就丢失了:

user = null;

clipboard.png

现在 John 变得不可触及,垃圾回收机制会将其删除并释放内存。

内存机制

js的内存空间分为栈 (stack)、堆 (heap);其中栈存放变量,堆存放复杂对象。
借用一张图直观感受一下
图片描述

栈内存

只能存放基本数据类型的数据和对象类型的引用地址也叫哈希码。里面的数据后进先出。
对栈内数据进行复制修改时:
图片描述

堆内存

是用来存储 “数组类型” 和“对象类”的数据。特点是存储空间大。
对堆内数据进行复制修改时:
图片描述

理解闭包

有了前面的铺垫,我们再来看看闭包是怎么回事。还是举个例子:

//决策层开会决定生产新一代phone手机,就弄了个叫PhoneFactory的企划案
let Proposal = function(){
    //新一代手机信息被封装在企划案中
    let  version = "XX", money = 10000
    return function (){
        //生产新的手机
        return {
            version: version,
            money: money
        }
    }
}
//执行者根据策划案建成了一个工厂, 工厂方法每执行一次就产出一个手机
let Factory = new Proposal();
let phone1 = Factory();
console.log(phone1.version)

对于手机的version, money而言, 它是策划书Proposal的内部变量,而Proposal的同级phone1应该是访问不到。但实际上我们还是拿到了手机的版本和价格数据。这种反常的现象我们就叫它Closure,中文名闭包。

原因

我们不管它为什么叫这个名字,先看看具体是什么原因产生的。
根据上面说的垃圾回收机制。函数Proposal在执行过之后(第16行)就没有引用。那么Proposal久应该被回收。里面的所有内部变量也应该被回收了。
但实际上 Proposal返回了一个新的函数Factory。而这个Factory是要能够访问到它生成时的同级以及父祖辈变量。而Proposal内部变量version, money就有了新的引用。因而阻止了被回收。就像Factory生成了一个新的泡泡把它能访问到的作用域包裹了起来。这就是闭包形成的原因了。

基于上面的js内存机制的知识,我们可以画出下面这张图:
图片描述

图简陋了点。。。。
但也可以看出,对于变量version,money而言。虽然他们本身在proposal的黄色作用域中。但也在fatory生成的时候也被包含在了fatory打的可访问的作用域气泡内。不仅他们,甚至更外层的也都被包含在内。
在proposal这个泡泡破碎之后,只有当打的红色泡泡也破了,这些变量才会真的被回收。这也是为什么闭包用多了会影响性能的原因。

引用:

前端基础进阶:详细图解 JavaScript 内存空间
垃圾回收

阅读 381

推荐阅读
前端成长之路
用户专栏

记录前端成长路上的心得体会。

7 人关注
11 篇文章
专栏主页