作用域

作用域 指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限

  • 全局作用域:全局作用域为程序的最外层作用域,一直存在。
  • 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。

    每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量

在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链

静态作用域 (词法作用域)
嵌套关系是分析代码就可以得出的,不需要运行,按照这种顺序访问变量的链就是静态作用域链,这种链的好处是可以直观的知道变量之间的引用关系

  1. function func() {
  2. const guang = 'guang';
  3. function func2() {
  4. const ssh = 'ssh';
  5. {
  6. function func3 () {
  7. const suzhe = 'suzhe';
  8. }
  9. }
  10. }
  11. }

但是,JavaScript 除了静态作用域链外,还有一个特点就是函数可以作为返回值

  1. function func () {
  2. const a = 1;
  3. return function () {
  4. console.log(a);
  5. }
  6. }
  7. const f2 = func();

这就导致了一个问题,本来按照顺序创建调用一层层函数,按顺序创建和销毁作用域挺好的,但是如果内层函数返回了或者通过别的暴露出去了,那么外层函数销毁,内层函数却没有销毁,这时候怎么处理作用域,父作用域销不销毁? (比如这里的 func 调用结束要不要销毁作用域)

闭包

  1. function func() {
  2. const guang = 'guang';
  3. function func2() {
  4. const ssh = 'ssh';
  5. function func3 () {
  6. const suzhe = 'suzhe';
  7. }
  8. return func3;
  9. }
  10. return func2;
  11. }
  12. const func2 = func();

当调用 func2 的时候 func1 已经执行完了,这时候销不销毁 ?于是 JavaScript 就设计了闭包的机制。

闭包设计

父作用域的销毁问题:如果不销毁的话,由于子函数不结束将会一直常驻内存,影响性能,所以要销毁父函数,但是由于父函数被销毁会导致影响子函数。因此新建一个变量将 子函数引用父作用域的变量 打包起来给子函数

设计个独特的属性,比如 [[Scopes]] ,用这个来放函数打包带走的用到的环境。并且这个属性得是一个栈,因为函数有子函数、子函数可能还有子函数,每次打包都要放在这里一个包,所以就要设计成一个栈结构,就像饭盒有多层一样。
我们所考虑的这个解决方案:销毁父作用域后,把用到的变量包起来,打包给子函数,放到一个属性上。这就是闭包的机制

闭包并不是封闭当前函树,封闭内部状态; 而是封闭外部状态。即外部状态失效的时候,内部状态能有一份外部状态的备份

闭包最少会包含全局作用域

打包的只是环境内没有的,也就是闭包只保存外部引用。然后是在创建函数的时候保存到函数属性上的,创建的函数返回的时候会打包给函数,但是 JS 引擎怎么知道它要用到哪些外部引用呢,需要做 AST 扫描,很多 JS 引擎会做 Lazy Parsing,这时候去 parse 函数,正好也能知道它用到了哪些外部引用,然后把这些外部用打包成 Closure 闭包,加到 [[scopes]] 中。
所以,闭包是返回函数的时候扫描函数内的标识符引用,把用到的本作用域的变量打成 Closure 包,放到 [[Scopes]] 里。
每一个作用域闭包时会被打包打不同的Closure包,一个作用域一个Closure包

闭包是在函数创建的时候,让函数打包带走的根据函数内的外部引用来过滤作用域链剩下的链。它是在函数创建的时候生成的作用域链的子集,是打包的外部环境。
evel 因为没法分析内容,所以直接调用会把整个作用域打包(所以尽量不要用 eval,容易在闭包保存过多的无用变量),而不直接调用则没有闭包

  1. JavaScript 是静态作用域的设计,闭包是为了解决子函数晚于父函数销毁的问题,我们会在父函数销毁时,把子函数引用到的变量打成 Closure 包放到函数的 [[Scopes]] 上,让它即使**父函数销毁了也随时随地能访问外部环境** <br />但是如果一个很大的对象被函数引用,本来函数调用结束就能销毁,但是现在引用却被通过闭包保存到了堆里,而且还一直用不到,那这块堆内存就一直没法使用,严重到一定程度就算是**内存泄漏**了