JS底层运行原理:闭包机制

859 阅读5分钟

闭包:函数执行,形成私有上下文,来保存和保护私有变量的机制,称之为闭包=>它是一种机制

我们用一道题来分析

例1:
    let x = 1;
    function A(y){
       let x = 2;
       function B(z){
           console.log(x+y+z);
       }
       return B;
    }
    let C = A(2);
    C(3);
    
    1.首先创建了EC Stack 执行环境栈,并且创建了一个供全局代码执行的EC(G)执行环境上下文,并且进栈执行

    2.在里面形成GO全局变量以及VO(G)全局变量对象
        回顾:
            GO:全局对象window 在堆内存中,里面放置的是浏览器内置的API
            VO(G):全局变量对象,在上下文中的空间,里面放置的是上下文中创建的变量
            二者不想等,但是有关联:
                基于var/function在全局上下问中声明的全局变量也会给GO赋值一份(映射机制)
                但是就let/const等ES6方式在全局上下文中创建的全局变量和GO没有关系
   3.然后就是代码部分了
    首先进行变量提升,function  A(){},同时也会映射到window中一份
    由于老版本浏览器带function的会声明+定义,所以就确定了其作用域,也会创建一个堆内存,同时确定了形参"y",用来存放函数的属性和方法,并且会有一个16进制的地址存放在栈内存当中供别人a指针指向
        堆内存中包含:函数内容的字符串以及名字、形参个数、原型....


开始执行代码

1.let x = 1;在栈内存中创建一个变量x和值1,并把它们关联起来
    let/const等ES6方式不会映射给全局GO
2.遇到function A(y){},由于变量提升,已经声明定义过了,所以略过
3.let C = A(2);先把函数A执行,把执行完返回的结果赋值给C

函数A执行前的事情

1.函数每执行一次就创建一个堆内存,也会形成一个私有上下文EC(A),里面会有私有变量对象AO(A)
2.初始化作用域链:<EC(A),EC(G)>
  初始化this
  初始化arguments:[0:2]
  形参赋值 y = 2
  变量提升 function B(z){},还是像上面函数A一样,创建一个堆内存...

开始执行函数A

1.let x = 2;在栈内存中创建一个变量x和值1,并把它们关联起来
2.遇到function B(z){},由于变量提升,已经声明定义过了,所以略过
3.return B;把私有变量B的值作为返回值返回(B的堆内存地址)
4.此时全局变量C的值为B的地址

函数执行的过程为:

1.形成的私有上下文
2.进栈执行
3.一系列操作(代码执行前和代码执行)
4.正常情况下
    代码执行完,私有上下文会出栈(出栈后被释放,以此解约栈内存的空间);
    但是有特殊情况:如果当前私有上下文中的某个东西(一般是一个堆)被上下文以外的事物占用了,则上下文不会再出栈释放,也就是形成不销毁的上下文;
    不能销毁的话,就会被一直放在栈内存底部全局上下文中的上面,如果还有没被销毁的上下文则在栈内存中继续往上摞

浏览器的垃圾回收机制(浏览器自己内部处理)

[谷歌等浏览器是“基于引用查找“来进行垃圾回收的]
1. 开辟的堆内存,浏览器自己默认会在空闲的时候,查找所有内存的引用,把那些不被引用的内存释放掉
2. 开辟的栈内存(上下文)一般在代码执行完都会出栈释放,如果遇到上下文中的东西被外部占用,则不会释放

[IE等浏览器是“基于计数器”机制来进行内存管理的]
1. 创建的内存被引用一次,则计数1,在被引用一次,计数2...  移除引用减去1...  当减为零的时候,浏览器会把内存释放掉

=>真实项目中,某些情况导致计数规则会出现一些问题,造成很多内存不能被释放掉,产生“内存泄漏”;查找引用的方式如果形成相互引用,也会导致“内存泄漏“

比如:let aa = {name:"哈哈"}
      let pp = aa
      那么此时对象就不会释放
      
      怎么让才能让其释放呢?
      aa = null;
      pp = null;
      null代表空对象指针,是不会占用内存的,

回到上题

开始执行C(3)

与函数A执行钱一样,进行一系列的操作
执行代码的时候,遇到变量先看是否是自己私有的,如果不是私有的按照作用域链向上查找,不是上级的再往上查找,直到EC(G)为止

x为EC(A)的x   2
y为EC(A)的y   2
z自己私有的   3
所以结果为 7

EC(C)执行完就被释放了,因为没有被占用,但是EC(A)还继续被C占用着,所以EC(A)还没有被释放

如果想让EC(A)释放
C = null;

总结:

函数执行会形成全新的私有上下文,这个上下文可能被释放,也可能不被释放,不论是否被释放,它的作用是:

  1. 保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,而用到的私有变量和其它区域中的变量不会有任何的冲突(防止全局变量污染)
  2. 保存:如果上下文不被销毁,那么存储的私有变量的值也不会被销毁,可以被其下级上下文中调取使用

我们把函数执行,形成私有上下文,来保存和保护私有变量的机制,称之为“闭包” =>它是一种机制

可以理解为:其实函数本身就是一种闭包,只是存在时间太短了就被释放了

市面上一般认为只有形成的私有上下文不被释放,才算是闭包(因为如果一但释放,之前的东西也就不存在了);还有人认为,只有一下级上下文用到了此上下文中的动西才算闭包;