# 词法环境[[environment]]--闭包

# 闭包定义

一些书籍中对于闭包的定义:

  • 闭包是指有权访问另一个函数作用域中的变量的函数---《JavaScript高级程序设计》
  • 闭包允许函数访问并操作函数外部的变量---《JavaScript忍者秘籍》
  • 闭包使得函数可以继续访问定义时的词法作用域---《你不知道的JavaScript》
  • 所有JavaScript函数都是闭包---《JavaScript权威指南》

MDN

  • 函数和对其周围状态(词法环境)的引用捆绑在一起构成闭包
  • 也就是说,闭包可以让你从内部函数访问外部函数作用域(作用域链)。
  • 在JavaScript中,每当函数被创建,就会在函数生成时生成闭包。

# [[Environment]]

[[Environment]]:内见属性

  • 每个函数都有;
  • 存放词法环境的作用;
  • v8引擎中无法读写;

对比之前学过的另一个内见属性[[Prototype]]

  • 每个函数(构造函数)都有;
  • 存放对象原型的作用;
  • 可以使用__proto__读写;

[[Environment]]叫词法环境对象:

  • 整个脚本文件执行前会产生一个
  • 函数实例创建后会产生一个

[[Environment]]属性记录了:当前函数的词法环境对象==>外层函数的词法环境对象==>全局的词法环境对象;这样就形成了作用域链

举例说明:

function fn() {
  const a = 1;
  function fo() {
    console.log('hi')
  }
}
fn()
//此时fn函数的[[Environment]]属性包括:作用域链
//1.当前环境的记录
a:1
fo:()=>console.log('hi')
//2.对外部词法环境的引用(全局词法环境)

闭包的例子:函数与词法环境捆绑

function fn() {
    let a = 1;
    return function fo() {
        console.log(a++)
    }
}
let x = fn()//fn()执行后的返回值fo被引用到了全局变量x中
x();//1
x();//2

//下面两种情况相当于创建了两个实例,
//当前面的函数执行完之后里面的变量和函数就被垃圾回收掉,
//所以不影响后面函数的执行依然打印的是a=1
fn()()//1
fn()()//1

我们分析一下上面函数不考虑词法环境的情况下的执行调用栈:

fn()入栈;变量a入栈;fn()和a一起出栈;x()入栈;(此时出现问题了,x()在执行过程中需要找到a,但是a又没在执行栈中,并不符合我们要的预期)

此时就需要在函数与词法环境捆绑,形成闭包来实现这个问题:函数执行前就形成词法环境写入[[Environment]]属性中:此时fo函数内部词法环境是只有一个console.log函数,外层函数词法环境是a:1,全局词法环境为空。

函数中在执行过程中的内部变量,并不是同函数一样被加入到调用栈中,而是在函数执行前形成词法环境写入[[Environment]]属性中保存。只要有函数就会有闭包,只是v8中有垃圾回收机制,当函数A执行完时,返回另一个函数B,函数B又使用全局变量来保存,且函数B中引用了函数A中的变量,那么函数A中被引用的变量就永远不会被回收掉,需要我们手动进行变量回收。(需要注意的是:在js中定义的全局变量是不会被销毁的,因为随时都可能会用到这个变量,所以不能被销毁)

function f() {
    function f1() {
        var n = 1;
        function f2() {
            return console.log(n = n + 1)
        };
        return f2;
    }
    let x = f1()
    x();
    x();
}
f();
//如果是这样的话,函数f在执行完毕之后,变量n依然会被垃圾回收掉。

全局变量与局部变量生命周期

  • 全局变量: 在页面关闭后结束
  • 局部变量: 在执行的作用域块执行完成后结束
  • 综上, 局部变量会在其函数块执行之后自动解除,对于引用类型的局部作用域其引用关系会自动解除,大多数的引用类型的全局变量需要手动解除引用关系

下面的例子中const a这个变量就是被存放在词法环境对象[[Environment]]中。(下图可以解释为什么要有闭包。)

在这里插入图片描述

在这里插入图片描述

# 理解垃圾回收问题:

js变量回收规则:

  • 在js中定义的全局变量是不会被销毁的,因为随时都可能会用到这个变量,所以不能被销毁。
  • 具体引用关系的不会被销毁。
    • 如果一个对象不被引用,那么这个对象就会被回收;
    • 如果两个对象互相引用,但是没有被第3个对象所引用,那么这两个互相引用的对象也会被回收。

通过下面两段代码进行对比分析:

function a(){
  var b= 10;
  return function(){
    b++;
    console.log(num);
  }
}
a()(); //11
a()(); //11

分析: 在函数a中返回了一个匿名函数,在这个匿名函数中我们num++了一下,然后在函数外面执行了这个匿名函数函数,现在num是11,然后又执行了一次这个函数,你们应该是12吧,为什么不是呢?

原因: js为了让没有必要的变量保存在内存中,(我们写的任何变量都是需要内存空间的)在不需要这个变量的时候它就会被销毁。所以每次执行一遍 a()() 则变量b就会被销毁。下次再执行,就会重新声明变量b,所以两次输出都是11。

function a(){
    var b = 0;
    return function(){
        b ++;
        console.log(b);
    }
}
var d = a();
d();//1
d();//2

原因分析:

  • 函数a 被 变量d 引用,更准确的说是 函数a 里面的 匿名函数 被变量d所引用。
  • 因为变量d 保存的是函数a执行完成后的值,而函数a执行完,返回了那个匿名函数,所以变量d等于匿名函数。 -匿名函数因为使用了 函数a 中的 变量b 并且还被 变量 d所引用,所以就形成了一个闭包。
  • 只要这个变量d不等于null的话,那么那个变量b会一直保存到变量d中不会被销毁。
  • 所以两次执行的结果不一样

# 垃圾回收问题

# 1.什么是js的垃圾回收机制?

答:javascript中垃圾收集机制是自动回收的,不用人工操作,这让我们更专注于编辑代码上。 回收垃圾机制是定时执行的,具有周期性。

# 2.什么时候会有垃圾?

答:在作用域中当整个作用域中的代码执行完后,作用域中的变量和方法都会没用,此时就是被当做垃圾了。比如局部作用域,一个函数执行完,里面的变量就可以被销毁,其占用内存被释放。

# 3.垃圾回收方式?

答:常用的是标记清除:这样操作:一个变量-->进入环境(被标记,有此标记为不能被清除)-->执行-->离开环境(被标记,这个标记告诉机制能被清除)-->回收机制一段周期后,变量被清除。

# 4.为什么还要管理内存?

答:分配给web浏览器的内存比桌面的应用的内存少,这是出于安全考虑,为了防止运行js的网页耗尽系统内存导致系统崩溃。所以,开发者发现一旦数据不再用时,就将其值设为null来释放其引用(解除引用),这做法适用于全局变量和全局对象属性。

参考资料:

js闭包变量回收问题

垃圾回收问题

Last Updated: 8/15/2020, 1:39:46 PM