JavaScript高级 |深入闭包

本文已收录于专栏
⭐️ 《JavaScript》⭐️

闭包

闭包是JavaScript中非常容易让人迷惑的知识点。
《在你不知道的JavaScript》一书中,笔者这样评价闭包的重要性:“理解闭包可以看做是某种意义上的重生,但是需要付出非常的多的努力和牺牲才能理解这个概念”。
本篇文章将会详细带你了解闭包,深入闭包!

基本概念

我们先看闭包在计算机科学中的定义:

  • 闭包(Closure),又称为词法闭包(Lexical Closure)或函数闭包(function closure)。
  • 是在支持头等函数的编程语言中,实习词法绑定的一种技术。
  • 闭包在实现上是一个结构体,它存储了一个函数和一个关联环境
  • 闭包跟函数最大的区别在于当捕捉闭包的时候,它的自由变量会在捕捉是被确定,这样即使脱离了捕捉时的上下文,它也能照常运行。

我猜读者读到这里对闭包的概念还是一头雾水,别担心上面的定义过于官方和晦涩。
别急让我们举一个例子看看。

var name = "shenqi";
var age = 18;
var height = 180;

function foo(){
    
    
	var message = "Hello World";
  console.log(message,name,age,height);
}

在上面的例子中,我声明了三个变量和一个函数。
在函数中我有定义了一个变量并很自然的将定义的四个变量打印了出来。
那么问题就来了。
如果读者有其他编程语言基础的话会发现,像C、C++、java这样的语言,在函数中要想使用函数之外定义的变量,必须在调用时将以实参的形式传入函数。
比如:

var name = "shenqi";
var age = 18;
var height = 180;

function foo(name,age,height){
    
    
	var message = "Hello World";
  console.log(message,name,age,height);
}

但JavaScript则不同,即使不将外部的变量通过实参的方式传入函数,也照样能使用函数体之外的变量。

而这就是闭包发挥的作用。

无论深度、子函数是否被调用只要内部有用到外部的变量,就会把它们保存到同一个闭包上,而这些变量实际上是通过作用域链获取且绑定的。
小结:

  • 一个函数和对其周围状态(词法环境)的引用捆绑在一起,或者是函数被引用捆绑在一起这样的组合就是闭包。
  • 在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • 闭包可以在一个内层函数中访问到其外层函数的作用域。

再通俗的说:

一个普通函数function,如果它可以访问外层作用域的自由变量,那么这个函数与其作用域就是一个闭包。

内存管理

编程语言,在代码的执行过程中都需要给他分配内存的,一般分为需要手动管理内存和自动管理内存的两种类型。

手动管理内存:C、C++,包括早起的OC都需要手动来管理内存的申请和释放(malloc和free函数)

自动管理内存:Java、JavaScript、python、Swift都有自动帮助我们管理内存的机制。
但无论以什么方式来管理内存,内存的管理都会有如下的生命周期:

  • 分配申请你需要的内存
  • 使用分配的内存(存放一些东西如对象等)
  • 不需要时,对其进行释放

JavaScript的内存管理

JS会在定义数据时,给数据分配内存。
根据数据类型不同,JS的内存分配方式也有所区别。

  • 对于原始数据类型:直接在栈空间进行分配。
  • 对于复杂数据类型:会在堆内存开辟一块空间,并且将这块空间的指针返回值变量引用。

image.png

垃圾回收

手动释放内存的方式对开发者的要求极高,一不小心就会产生内存泄露,并且影响我们编写逻辑的代码效率。
所以现代的编程语言都是有自己的垃圾回收机制。

  • 垃圾回收的英文是(Garbage Collection),简称GC
  • 对于那些不再使用的对象,我们都称之为垃圾,它需要被回收,以释放更多的内存空间。
  • 很多的语言运行环境,都会自带垃圾回收器。
    • 如Java的JVM,JavaScript的运行环境JS引擎都会有内存垃圾回收器。
  • 垃圾回收器,我们也简称GC,所以大部分情况下GC指的就是垃圾回收器。

GC算法-标记清除

标记清除的核心思想是可达性
这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找到从根开始有引用的对象,对于哪些没有引用到的对象,就认为是不常用的对象,然后通过系统的机制将其清除。
作用:可以很好的解决循环引用的问题。

GC算法-标记整理

和标记清除相似。
不同的是,回收期间同时会将保存的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化;

GC算法-分代收集

对象被分成两组:新生代和旧生代
新生代:许多对象出现,完成他们的工作之后就会闲置下来,闲置的这些很快就会在检查的时候被清除。
旧生代:旧生代是在新生代时期没有被清除的一部分,会移动到内存专门存放旧生代的一篇区域,这片区域一般存放长期存放的对象,被检查的频率也会减少。

V8为了提供内存的管理效率,对内存进行非常详细的划分。 image.png

GC算法-增量收集

  • 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。
  • 所以引擎试图将垃圾收集工作分为即部分来做,然后将这几部分会逐一进行处理,这样一个明显的延迟就会被转换为许多微小的延迟。

GC算法-闲时收集

垃圾收集器只会在CPU空闲时尝试运行,以减少可能对代码的影响。

内存泄露

闭包很容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。
过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。
内存泄露:对于一些我们之后永远都不会再使用的对象,但是因为他们满足可达性等GC算法,所以GC它不知道要进行内存释放,而这些对象所占有的内存依旧保留着。
解决方法:将需要释放的对象,手动赋值成null即可。
我们需要将arr8手动回收。

  1. 此时 addr8 仍然具有可达性,所以GC无法将其清除。

image.png

arr8 = null;
  1. 当我们手动将arr8赋值为null时,VO对象原本储存的arr8的内存地址也会被置换为null。

image.png

  1. 于此同时,由GO对象指向arr8AO对象的作用域链也会消失。

image.png

  1. 由于arr8不再具有可达性,所以GC就自动将arr8以及关联的AO对象清除内存。

image.png

浏览器的优化

首先我们来思考一个问题
AO对象在没有销毁时,里面的所有属性是否都不会被释放?

function makeAdder(count){
    
    
	var name = "shenqi";
  var age = 18;
  var height = 1.88;
  
	function bar(){
    
    
    console.log(name,height);
  }
  
  return bar;
}

在上面的代码中,我定义了三个变量,但在函数中只引用了其中的nameheight属性,并未使用age。
然后通过debugger我们发现:
声明后bar函数后JS引擎创建的对应的AO对象中,并没有age的身影。
这就是浏览器针对不使用的属性做的优化,将其释放,使得age被销毁。

完结散花

ok以上就是对 JavaScript高级 |深入闭包 的全部讲解啦,很感谢你能看到这儿。如果有遗漏、错误或者有更加通俗易懂的讲解,欢迎小伙伴私信我,我后期再补充完善。

参考文献

coderwhy老师JS高级视频教程

猜你喜欢

转载自blog.csdn.net/m0_66139206/article/details/128346257
今日推荐