一 . 闭包
1.什么是闭包:
什么是闭包?引用JS书中的定义,当一个函数能够记住并且访问其所在的作用域,即使这个函数是在作用域之外被执行的,这就是闭包。
1 | function a() { |
在a() 函数中声明了一个私有变量 a , b() 函数是 a() 函数内部的函数,此时 a 内部的所有局部变量,对 b 都是可见的,但是a()中的变量对于外部是不可见的,b 内部的局部变量,对 a 就是不可见的。这就是JS的”作用域链“。其中,a()在执行之后,将 b 返回,并赋值给了c ,通过c从外部访问了 a() 的内部变量a。
闭包是一种保护私有变量的机制,它在函数执行时创建一个私有作用域,从而保护内部的私有变量不受外界干扰,同时允许外界访问内部的私有变量。
直观地说,闭包就像是一个不会被销毁的栈环境。
2.闭包的优点:
从上面的例子可以注意到,a是a()中的一个私有变量,但是我们却在全局作用域中访问了函数的私有变量,从中可以看出了闭包的几个优点:
- 保护私有变量
- 延长变量寿命
- 实现变量共享
为什么这么说呢?可以来看看接下来的几个例子。
1 |
|
上述代码中,a是a()中的一个私有变量,因此外部无法直接访问它。即使在全局声明了一个同名变量a,也无法影响到函数内部,a变量的值只能在函数内部被修改和访问,这样就保护了闭包中的私有变量。
1 |
|
一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被垃圾回收,但是在这个例子中,f1()函数作为fn的返回值被赋值给了add,这时候相当于add=function f1(){ … },并且f1()内部引用着fn里的变量a,所以变量a无法被销毁,而变量n是每次被调用时新创建的,所以每次f1执行完后它就把属于自己的变量连同自己一起销毁,但是把a保留了下来,实现了延长变量寿命,但这又出现了一个新问题,后面再讲。
1 | function a() { |
还是上面的例子,即使a是**a()**中的私有变量,但仍然从外部访问了它,这就是变量共享。
3. 闭包的作用
3.1节流
节流是指限制函数的执行频率,即在一定时间间隔内最多只执行一次**,即使事件被多次触发。
1 | function throttle(fn, delay) { |
3.2 防抖
防抖是指在事件触发后,一定时间内不再触发,才执行函数。
如果在等待时间内再次触发,则重新计时。
1 | function debounce(fn, delay) { |
3.3 函数柯里化
柯里化 是指将一个多参数函数转换成多个单参数函数的调用链。
1 | function curry(fn) { |
3.4 模块化
模块化是指将代码拆分成独立的模块,每个模块负责不同的功能。
1 | const CounterModule = (function () { |
现在,再返回前面的问题,为什么fn()中的a被保留下来,而f1()中的n被销毁了呢?这就引出了JS的垃圾回收机制。
二 . 垃圾回收
1. 什么是垃圾:
什么是垃圾呢?垃圾是如何定义的呢?
很简单,JS中的函数,变量,对象等都需要占用一定的内存,当这些东西不再被使用的时候,就变成了垃圾。
垃圾可以分为以下几类:
- 已经调用完毕的函数作用域及其内部的值
- 无法被访问到的值
正如之前所说,JS中的所有的变量都会占用内存,当这些变量变成垃圾的时候,如果不进行回收,内存就会被一直占用,随着程序的运行,垃圾也会越来越多,总有一刻,内存会被占满,程序也就无法运行了。
2. 垃圾回收机制:
首先,根据JS的内存分配机制,基本数据类型保存在固定的占空间中,可以直接通过值进行访问,但是复杂数据类型的大小不固定,所以其会通过引用地址保存在栈空间,而引用的指向保存的值保存在堆空间,通过引用访问,比如函数,对象等。
栈内存中的基本数据类型,可以直接通过操作系统来进行处理,但是堆内存中的复杂数据类型的值的大小不确定,这就需要JS的引擎通过垃圾回收机制进行处理。
2.1 可达性:
在JS中,内存管理的主要概念是可达性,”可达性“值就是那些以某种方式可访问的值,他们被保证存储在内存中。
1 | const me = { |
在这里,创建了一个全局变量me,它指向了一个对象{name:“奶龙”},因此这个这个对象是可达的。
1 | const me = { |
此时,对象{name:“奶龙”}处于不可达的状态,由于无法访问它,垃圾回收器将回收这个数据并释放内存。
2.2 引用计数和标记清除:
在浏览器的历史上,有两种垃圾回收的方法:
- 标记清除
- 引用计数
引用计数
引用计数是一种几乎被淘汰的垃圾回收机制,它通过对每个复杂数据类型的变量进行计数,比如:
- 当变量声明并赋值之后,值得引用数为1
- 当同一个值被赋值给另一个变量时,引用数加1
- 当保存该值引用的变量被其它值覆盖时,引用数减1
- 当一个值的引用数变为0时,就表示无法再访问该值,此时垃圾回收机制就会将其清除并回收内存
看起来很完美,但只是看起来而已,引用计数存在一个严重的缺陷:
1 | function problem(){ |
可以看到,虽然没有其他引用指向objA和objB之前分别指向的两个对象,但它们相互引用,导致它们的引用数永远不会为零,也就永远不会被回收了。因此,现代浏览器采用了另一种新的回收机制。
标记清除
相对于引用计数,标记清除就简单的多了,无非就是打不打标记的问题,就跟二进制的0和1十分相似,具体步骤如下:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
但是,标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片
,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题,这里不过多介绍。
回过头来,再来解释上面的问题
1 | function fn(){ |
fn
执行:
- 当调用
fn()
时,局部变量a
被创建并赋值为1
。 - 然后返回的是内部函数
f1
,并且这个函数内部会访问外部的a
变量。
闭包形成:
- 函数
f1
是一个 闭包,它访问了fn
中的a
变量。因此,a
被保存在f1
的作用域中。 - 这意味着即使
fn
执行完毕,fn
中的a
变量依然存在于f1
的 闭包环境中,并不会被销毁。
**调用 add()
**:
add
是fn()
的返回值,即f1
函数。- 每次调用
add()
时,f1
执行并访问了闭包中的a
变量。 - 尽管
fn
执行完毕,a
变量的作用域并没有被销毁,因为f1
(闭包)仍然在访问它。闭包让a
保持在内存中,直到闭包本身不再被引用。
省流:
根对象:
window
add
(全局变量,指向f1
)
可达对象:
add
指向f1
f1
通过闭包作用域 引用了a
a
仍然可达
三. 内存泄漏
1.什么是内存泄露
在介绍闭包的时候,我们发现了一个问题,现在再来看看:
1 | function fn(){ |
由于我们使用了闭包,因此延长了局部变量a的寿命,使得我们能够在函数执行完毕时访问它,但这样也导致了a会一直存活,无法被回收,这就是内存泄露。
内存泄漏(Memory Leak)是指程序在运行时无法释放不再使用的内存,导致内存逐渐耗尽,甚至造成程序崩溃或性能下降。
2. 怎样会引起内存泄露
2.1 闭包
略
2.2 意外的全局变量
全局变量具有非常长的生命周期,一直到页面被关闭,它都会存活着,所以全局变量上的内存一直都不会被回收。
2.3 角落的定时器
定时器是我们经常使用的一个功能,如果我们在某个页面使用了定时器,但在关闭定时器时没有清除定时器的话,定时器还是活着的。换句话说,定时器的生命周期并不挂靠在页面上,如果在当前页面的JS通过定时器注册了某个回调函数,而该回调函数又持有当前页面的某个变量或某个DOM元素,就会导致即使页面销毁,由于定时器持有该页面部分引用而造成页面无法正常回收,导致内存泄漏。
1 | function clearTimeoutExample() { |
2.4 被遗忘的DOM元素
DOM元素的生命周期是取决于是否可以挂载在DOM树上,当从DOM树上移除时,也就可以被销毁移除了。但是,如果某个DOM元素在JS中也持有它的引用时,那么它的生命周期就由JS和是否在DOM树上二者决定了,只有将两个地方都去清理才能正常回收它
1 | <div id="myElement">Hello, World!</div> |
上述代码中,即使我们从DOM树中移除了#myElement
元素,但element
变量仍然持有对 #myElement
的引用,导致内存泄露。
3.如何解决内存泄露
1 | function big() { |
1 | function a() { |
1 | function clearTimeoutExample() { |
1 | <div id="myElement">Hello, World!</div> |
Author: John Doe
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.