在前面的文章中,我们分别介绍了
它们也都是了解闭包知识的前置概念,在我们平常的面试中,也经常会考察闭包的相关的知识,以此来考察面试者对JavaScript的掌握程度。
今天让我们来深入理解闭包。
闭包
闭包没有官方固定的概念,简单来说闭包是,函数嵌套函数时,内层函数引用外层函数作用域下的变量,并且内层函数在全局环境下可访问,这就是闭包。
下面我们来看一个简单的闭包例子
function numGen() {
let n = 1
n++
return () => {
console.log(n)
}
}
let getNum = numGen()
getNum()
在上面例子中,numGen函数内创建了一个变量n,然后返回一个匿名函数打印n的值,在外部我们通过返回的这个函数getNum来访问变量n,所以当numGen函数执行完毕后,相关调用出栈后,变量n不会消失,仍可继续被外部访问。
通过断点,我们可通过Chrome开发工具清楚的看到JavaScript引擎的执行过程
变量n的值被标记为Closure,也称为闭包变量。
再回过头来看我们前面所说的闭包概念,就更容易理解了。
正常情况下在全局作用域中我们是无法访问到函数内部的变量的,在函数执行过后,函数上下文被销毁。但如果在函数中,返回了另一个函数,且这个函数使用了上层函数的变量,那么在全局作用域中我们就能通过这个函数来获取它上层函数里的变量值。这也就是闭包的原理。
知道了什么是闭包之后,那么你可能就会有疑问,这个闭包到底有什么用呢?
首先我们知道闭包可以让我们在外部访问到函数内部的变量,所以我们可以通过利用闭包来实现“模块化”。
同时在许多框架的源码中,比如Redux源码的中间件实现,其中也会大量使用闭包。
内存管理
内存管理计算机的基本概念,不论使用什么语言开发,对内存的管理无外乎也就是分配内存
,读写内存
,释放内存
。
var foo = 'bar' // 分配内存
alert(foo) // 读写内存
foo = null // 释放内存
内存管理基本概念
内存空间可分为栈空间和堆空间
-
栈空间:由操作系统自动分配释放,用来存放函数的参数值、局部变量值等,操作方式类似数据结构中的栈
-
堆空间:一般由开发者分配释放,这部分空间要考虑垃圾回收的问题
在JavaScript中,主要包括基本数据类型和引用类型
- 基本数据类型:undefined、null、number、boolean、string等
- 引用类型:object、array、function等
一般来说,基本数据类型按照值的大小保存在栈空间中,占有固定大小的内存空间;引用类型保存在堆空间中,内存空间大小并不固定,需按引用情况来访问。
var a = 1 // 基本数据类型
var b = 'hello' // 基本数据类型
var c = [1,2,3] // 引用类型
var d = { a: 10 } // 引用类型
内存分配示意图:
栈空间
变量 | 值 |
---|---|
a | 1 |
b | hello |
c | 0X0012ff76 |
d | 0X0012ff7c |
堆空间
内存地址 | 值 |
---|---|
0X0012ff76 | [1,2,3] |
0X0012ff7c | { a: 10 } |
对于分配内存和读写内存,所有语言基本一致,但释放内存不同语言之间有区别。JavaScript依赖浏览器的垃圾回收机制,一般不用我们操心。但还有些特殊情况需要我们注意,稍不注意可能就会造成内存泄漏。
内存泄漏一半是我们使用的内存不再使用但未能及时释放,导致程序运行缓慢,甚至崩溃。
内存泄漏的场景
我们来举几个经典的内存泄漏的例子
示例一:
打开掘金的首页,在控制台执行下面的代码
var ele = document.getElementById('juejin')
ele.mark = 'marked'
// 移除元素
function remove() {
ele.parentNode.removeChild(ele)
}
remove()
在上面代码中,我们把id为juejin的元素删除了,但是ele变量依然存在,该元素所占用的内存无法被释放。那么要解决这个问题,我们需要在remove函数内加上一句ele = null
。
示例二:
var ele = document.getElementById('juejin')
ele.innerHTML = '<button id="btn">点击</button>'
var btn = document.getElementById('btn')
btn.addEventListener('click', function() {
console.log('btn clicked')
})
ele.innerHTML = ''
上面代码中,在ele.innerHTML = ''
后,button元素已经从DOM中被移除了,但由于该元素的事件处理句柄还在,所以该节点变量依然无法被回收。所以我们还要添加removeEventListener
函数,防止内存泄漏。
示例三
function foo() {
var name = 'shuai'
setInterval(function() {
console.log(name)
}, 1000)
}
foo()
上面代码中,由于存在setInterval函数,所以name的内存空间始终无法被释放,所以在实际情况中要在合适的时机使用clearInterval来对其清理。
浏览器的垃圾回收
浏览器的垃圾回收会依赖标记清楚、引用计数两种算法进行回收。
感谢的小伙伴可以自行查阅这方面的文章。
内存泄漏和垃圾回收的注意事项
从上面的例子中可以看到,如果我们借助闭包来保存变量,由于变量需要被全局使用,所以始终不会被垃圾回收。如果使用不当的话很容易引发内存泄漏,所以在使用闭包时要格外注意。
我们来看几个例子
示例一
function foo() {
let value = 123
function bar() { console.log(value) }
return bar
}
var bar = foo()
在上面例子中,变量value保存在内存中,如果加上bar = null,则随着bar不再使用,value也会被垃圾回收清除。
我们对上面代码进行修改
function foo() {
let value = Math.random()
function bar() { debugger }
return bar
}
var bar = foo()
bar()
在Chrome浏览器执行上面代码,会发现value没有被引用
我们在bar函数中加入对value的引用
function foo() {
let value = Math.random()
function bar() {
console.log(value)
debugger
}
return bar
}
var bar = foo()
bar()
这时我们会发现闭包变量的value值。
下一部分内容将介绍通过Chrome devtool工具来排查内存泄漏和一些闭包的经典面试题目来深入理解闭包的知识,做到有问必答。
欢迎我的公众号【小帅的编程笔记】,让我们在前端的路上越走越远