1.闭包的概念
所谓闭包,指的是一个函数返回另一个函数,而返回函数作用域中使用了外部函数的变量,导致变量被保存到内存中无法回收的现象。
function fun1() {
let num = 10;
return function() {
console.log(num);
}
}
如上的代码就构成了一个简单的闭包。
变量num
会一直被返回的函数引用,因此其不会被回收。
2.闭包的特点
- 让函数内部变量在外部作用域中被访问
- 被引用变量会常驻于内存里
- 可以一定程度上实现数据封装和私有化
- 由于闭包的存在,导致内存泄漏的可能性增加
3.闭包的原理
1.浅显的变量声明理解
在javascript中,内存分为栈内存和堆内存。我们常见的变量声明都是在栈内存中的,但为变量赋值时的数据值位置是分情况的。简单数据类型的值在栈中声明,然后将其地址赋给变量,引用数据类型的值在堆中声明,最终将引用地址赋值给变量。
2.闭包的问题
但是我们结合闭包来看就会发现一个问题,栈内存中声明的数据在任务执行完出栈之后就会被销毁,但是闭包的变量在函数出栈之后仍然可以被访问到。这让我想到有没有可能闭包的变量并不是在栈中声明,且上述的变量声明的理解本身就存在局限性。
3.变量声明
经过查询资料我得知,上述所理解的变量声明确实存在于局限性,我们都知道js中存在三种作用域(全局作用域,函数作用域,块级作用域,后两者也称为局部作用域),其中只有局部作用域的变量声明是符合上述的变量声明规则的。 js中的变量声明符合以下规律: 1.全局作用域
- 全局对象:在js中全局变量在任何地方都可以访问到,很明显它不是存在栈内存中的,而是存在了堆内存中,并且在函数中会通过一个属性来讲该变量引用,后续即可以通过该属性来访问到全局对象。(这个属性其实是一个数组,其中存的就是该函数作用域可以访问到的变量,这就是作用域链的实质)js引擎会将全局对象(在浏览器中为window,在node中为global)放在作用域链的最顶层,也就是这个数组的最后一项。
function testScope () {
return function scopeFun() {
}
}
console.dir(testScope());
示例:
- 使用var关键字声明的变量:var关键字声明变量本质上是给全局对象挂载了一个属性。
var count = 10
function testScope () {
return function scopeFun() {
}
}
console.dir(testScope());
- 使用let 或const声明: 使用es6引入的这两个关键字声明变量,声明的变量会被挂载到一个对象中,这个对象取决于该变量所在的作用域。
let count1 = 10
const count2 = 10
function testScope () {
return function scopeFun() {
}
}
console.dir(testScope());
有上面的示例可知,全局作用域的变量本质上在之后的所有位都可以访问,其本身是存在于堆空间中。 2.局部作用域 在函数作用域或者是块级作用域中声明的变量是临时变量,其会被保存到栈空间中,待到当前作用域内所有的代码执行完毕后会将这些变量回收。这也是我们平时最常见的变量声明规则。
4.捕获变量
如果一个函数中返回了一个未执行的作用域(可以是函数或者是类),且作用域内访问到外层函数的局部变量,那么改变量就会被称为是捕获变量。
var count = 2
let sum = 0
function testScope () {
let num = 1;
let string = 'string';
let bool = true;
let obj = {
attr1: 2,
attr2: 'string',
attr3: false,
attr4: 'attr4'
}
return function scopeFun() {
return console.log(num, string,bool,obj);
}
}
console.dir(testScope());
、
这些捕获变量被保存到了一个Closure对象上,这也就是闭包变量真正所存在的位置。因为这些对象本质上都是在堆中定义,如果形成闭包,会将原先的局部变量转换为捕获变量并挂到该对象上,我们在函数中访问时本质上就是访问的该对象的属性。所以尽管是函数作用域销毁,其捕获变量仍然可以被访问到。
简单来说闭包中访问的变量本质上就不是局部变量,而是被定义到堆内存中,所以不会因为所在作用域销毁而被回收。