前端面试之js闭包和内存泄漏问题
日期:2020-07-06
来源:程序思维浏览:3120次
JavaScript中的闭包是一个面试中经常被考到的问题,大家可能都对这个概念多多少少都有一些模糊的概念或者一点都不了解,那么今天就来给大家讲解一下。
什么是闭包?
闭包是指有权访问另一个函数作用中的变量的函数,常见的闭包形式就是一个函数的内部再创建另一个函数。
想必这个概念听起来很懵,那我们接下来就来体验一个闭包吧。
来用一个小案例,来体验一下闭包是什么
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div class="show">0</div>
<button>增加</button>
<script>
let show = document.querySelector('.show')
let btn = document.querySelector('button')
function func() {
let n = 0
btn.onclick = function () {
n ++
show.innerHTML = n
}
}
f()
</script>
</body>
</html>
我们可以看到,在函数 func 中定义了一个变量 n,而我们在该函数内部,对按钮 btn 绑定了一个点击函数,该点击函数将变量 n + 1,然后展示在页面上。了解过作用域链的小伙伴会知道,当我们点击按钮的时候,处罚了点击的处理函数,此时该函数内部的作用域链是这个样子的。
引用了变量 n ,首先在作用域链的第一个变量对象中寻找变量 n,没有找到;然后去作用域链的第二个里寻找变量 n,找到了,也就是在函数 func 内部定义的变量 n。所以该点击处理函数每次引用变量 n 时,都是从函数 func 内部去寻找的变量 n ,这也就是我们所说的,一个函数有权访问另一个函数内部的变量。
使用闭包的注意事项
上面我们了解了闭包的基本使用,那么我们再用一个例子来给大家介绍在使用闭包时容易犯的错误。
function create() {
var arr = []
for(var i=0; i<10; i++) {
arr[i] = function () {
return i
}
}
return arr
}
let result = create()
result[0]() //返回10
result[1]() //返回10
……
result[9]() //返回10
这个例子就是在函数 create 中通过 for 循环定义10个匿名函数,每个函数都返回变量 i,最终将每个匿名函数保存到数组 arr 中并返回数组 arr,然后我们在收到数组 arr 后依次调用每个匿名函数,发现每个返回的都是数字10,而我们最初的目的是依次返回的是 0~9。
这是因为,我们调用匿名函数的时候需要返回变量 i ,而匿名函数内部没有该变量,所以去往下一个变量对象,也就是定义匿名函数时所处的函数环境 create 中寻找变量 i ,但此时的变量 i 已经通过循环变成了10,所以当我们调用每个匿名函数时,返回的全部都是10.
为此,我们的代码可以写成这样:
function create() {
var arr = []
for(var i=0; i<10; i++) {
arr[i] = (function (num) {
return function () {
return num
}
})(i)
}
return arr
}
let result = create()
result[0]() //返回 0
result[1]() //返回 1
……
result[9]() //返回 9
这样做就直接在定义最内部的匿名函数时,把当前循环的变量 i 放在了最内部匿名函数外部的那个匿名函数内,这样的话,我们之后调用匿名函数时,寻找变量 i 就会从该匿名函数外部的那个匿名函数的变量对象中找到相应的变量。
内存泄漏
相信面试过的小伙伴都知道,在面试时,如果面试官问到你闭包,可能会跟你提一下内存泄漏。
首先我要打假一个说法,很多人都说闭包会引起内存泄漏,这一半真一半假,因为只有在IE9之前才会因为闭包出现内存泄露的问题,所以以后千万别在别人面前说闭包就会引起内存泄露了哈。
那么内存泄露到底是什么呢?
简单来说一下,就是当一个闭包的作用域链内有一个HTML元素时,该元素就无法被垃圾回收机制给销毁。
这里不懂JavaScript垃圾回收机制的小伙伴可以花2分钟看一下这篇文章,下面会讲解到,以防听不懂——JavaScript的垃圾回收机制,清除无用变量,释放多余内存,展现更好的性能
function handle() {
let element = document.querySelector('#app')
element.onclick = function () {
console.log(element.id)
}
}
在函数 handle 中,给HTML元素 element 创建了一个点击事件的匿名函数,该函数内部引用了变量 element ,所以变量 element 的引用次数为1,这样的话垃圾回收机制一直都不会清除该元素了,这就是一个内存泄露的情况。
所以我们可以这样做,来解决内存泄露的问题
function handle() {
let element = document.querySelector('#app')
let id = element.id
element.onclick = function () {
console.log(id)
}
element = null
}
将元素 element 的 id 值保存在一个变量 id 内,然后在该元素的点击处理事件中引用变量 id , 并且在最后通过把变量 element设置为 null ,以解除对DOM元素的引用,这样引用次数就变为0,而不再是1了,垃圾回收机制就可以对其进行清除了。
闭包的私有变量
顾名思义,私有变量的意思就是说,闭包拥有自己的变量,别人都无法访问,无法使用。
很明显,了解过作用域链就能清楚得知道,当函数调用后,作用域链是先从最内部开始,然后向外依次排列。所以只有内部访问外部变量的说法,而没有说外部访问内部变量的道理。
就比如这个简单的例子
let m = 1
let n = 4
function func() {
let n = 2
alert(m) //返回 1
return function () {
let m = 3
alert(n) //返回 2
}
}
func()
在该例子中可以看到,函数 func 本意想访问匿名函数中的变量 m 值为3,但却只访问到全局中的变量 m 值为1;而匿名函数就成功访问到了函数 func 内部定义的变量 n 值为2
这就是通过闭包实现的私有变量的例子
什么是闭包?
闭包是指有权访问另一个函数作用中的变量的函数,常见的闭包形式就是一个函数的内部再创建另一个函数。
想必这个概念听起来很懵,那我们接下来就来体验一个闭包吧。
来用一个小案例,来体验一下闭包是什么
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div class="show">0</div>
<button>增加</button>
<script>
let show = document.querySelector('.show')
let btn = document.querySelector('button')
function func() {
let n = 0
btn.onclick = function () {
n ++
show.innerHTML = n
}
}
f()
</script>
</body>
</html>
我们可以看到,在函数 func 中定义了一个变量 n,而我们在该函数内部,对按钮 btn 绑定了一个点击函数,该点击函数将变量 n + 1,然后展示在页面上。了解过作用域链的小伙伴会知道,当我们点击按钮的时候,处罚了点击的处理函数,此时该函数内部的作用域链是这个样子的。
引用了变量 n ,首先在作用域链的第一个变量对象中寻找变量 n,没有找到;然后去作用域链的第二个里寻找变量 n,找到了,也就是在函数 func 内部定义的变量 n。所以该点击处理函数每次引用变量 n 时,都是从函数 func 内部去寻找的变量 n ,这也就是我们所说的,一个函数有权访问另一个函数内部的变量。
使用闭包的注意事项
上面我们了解了闭包的基本使用,那么我们再用一个例子来给大家介绍在使用闭包时容易犯的错误。
function create() {
var arr = []
for(var i=0; i<10; i++) {
arr[i] = function () {
return i
}
}
return arr
}
let result = create()
result[0]() //返回10
result[1]() //返回10
……
result[9]() //返回10
这个例子就是在函数 create 中通过 for 循环定义10个匿名函数,每个函数都返回变量 i,最终将每个匿名函数保存到数组 arr 中并返回数组 arr,然后我们在收到数组 arr 后依次调用每个匿名函数,发现每个返回的都是数字10,而我们最初的目的是依次返回的是 0~9。
这是因为,我们调用匿名函数的时候需要返回变量 i ,而匿名函数内部没有该变量,所以去往下一个变量对象,也就是定义匿名函数时所处的函数环境 create 中寻找变量 i ,但此时的变量 i 已经通过循环变成了10,所以当我们调用每个匿名函数时,返回的全部都是10.
为此,我们的代码可以写成这样:
function create() {
var arr = []
for(var i=0; i<10; i++) {
arr[i] = (function (num) {
return function () {
return num
}
})(i)
}
return arr
}
let result = create()
result[0]() //返回 0
result[1]() //返回 1
……
result[9]() //返回 9
这样做就直接在定义最内部的匿名函数时,把当前循环的变量 i 放在了最内部匿名函数外部的那个匿名函数内,这样的话,我们之后调用匿名函数时,寻找变量 i 就会从该匿名函数外部的那个匿名函数的变量对象中找到相应的变量。
内存泄漏
相信面试过的小伙伴都知道,在面试时,如果面试官问到你闭包,可能会跟你提一下内存泄漏。
首先我要打假一个说法,很多人都说闭包会引起内存泄漏,这一半真一半假,因为只有在IE9之前才会因为闭包出现内存泄露的问题,所以以后千万别在别人面前说闭包就会引起内存泄露了哈。
那么内存泄露到底是什么呢?
简单来说一下,就是当一个闭包的作用域链内有一个HTML元素时,该元素就无法被垃圾回收机制给销毁。
这里不懂JavaScript垃圾回收机制的小伙伴可以花2分钟看一下这篇文章,下面会讲解到,以防听不懂——JavaScript的垃圾回收机制,清除无用变量,释放多余内存,展现更好的性能
function handle() {
let element = document.querySelector('#app')
element.onclick = function () {
console.log(element.id)
}
}
在函数 handle 中,给HTML元素 element 创建了一个点击事件的匿名函数,该函数内部引用了变量 element ,所以变量 element 的引用次数为1,这样的话垃圾回收机制一直都不会清除该元素了,这就是一个内存泄露的情况。
所以我们可以这样做,来解决内存泄露的问题
function handle() {
let element = document.querySelector('#app')
let id = element.id
element.onclick = function () {
console.log(id)
}
element = null
}
将元素 element 的 id 值保存在一个变量 id 内,然后在该元素的点击处理事件中引用变量 id , 并且在最后通过把变量 element设置为 null ,以解除对DOM元素的引用,这样引用次数就变为0,而不再是1了,垃圾回收机制就可以对其进行清除了。
闭包的私有变量
顾名思义,私有变量的意思就是说,闭包拥有自己的变量,别人都无法访问,无法使用。
很明显,了解过作用域链就能清楚得知道,当函数调用后,作用域链是先从最内部开始,然后向外依次排列。所以只有内部访问外部变量的说法,而没有说外部访问内部变量的道理。
就比如这个简单的例子
let m = 1
let n = 4
function func() {
let n = 2
alert(m) //返回 1
return function () {
let m = 3
alert(n) //返回 2
}
}
func()
在该例子中可以看到,函数 func 本意想访问匿名函数中的变量 m 值为3,但却只访问到全局中的变量 m 值为1;而匿名函数就成功访问到了函数 func 内部定义的变量 n 值为2
这就是通过闭包实现的私有变量的例子
- 闭包就是指有权访问另一个函数作用中的变量的函数,常见的闭包形式就是一个函数的内部再创建另一个函数。
- 闭包就是为了隐藏变量,使外部无法访问到
- 闭包可以将变量定义在内部,使内部拥有自己的变量,同时可以不污染全局变量
精品好课