首先先看下概念:
红宝书:闭包 指的是那些引用 了另一个函数作用域 中变量的函数。
MDN: 闭包 是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。
csharp
function func(){
const a = 1;
return function foo(){
return a + 2;
}
}
const bar = func();
bar()
理解闭包,我们需要先从作用域链来入手。
作用域链
先看一道例题,这段代码输出什么呢?
javascript
function bar() {
console.log(myName)
}
function foo() {
var myName = "掘金"
bar()
}
var myName = "掘金前端"
foo()
聪明的你肯定会知道这段代码输出的是"掘金前端";
那JS在查询这段代码的时候,是如何查找的呢? 查找的这个顺序 也就是作用域链。这个作用域链是由词法作用域决定的。
词法作用域
词法作用域是指作用域 是由代码中函数声明的位置决定的。和函数怎么调用是没有关系的。
了解了词法作用域之后, 就可以解释上面这个例题了。
- 变量提升 :
function bar
、function foo
以及var myName
都会被提升到全局作用域的顶部。函数bar
和foo
会完整地被提升,而myName
变量虽被提升但初始值为undefined
。 - 变量赋值:代码开始执行,myName被赋值为
"掘金前端"
。 - foo执行:创建foo的执行上下文,
var myName
会被提升,然后被赋值为"掘金"
。不过这个myName
是foo
函数内部的局部变量。 - bar执行:创建bar的执行上下文,先在当前作用域中查找,找不到沿着作用域链向上查找, 根据词法作用域的解释,作用域是声明位置 决定的,所以bar函数的上层作用域是全局作用域 ,和foo没有关系 。所以找到了全局作用域中的
"掘金前端"
前置知识了解完我们就可以看看开篇的例题了。
csharp
function func(){
const a = 1;
return function foo(){
return a + 2;
}
}
const bar = func();
bar()
根据词法作用域的规则,内部函数foo总能访问外部函数func中的变量。所以虽然func函数执行完成之后,他的执行上下文弹出,但是由于foo总是需要使用变量a,所以这个变量依然存在内存中。
右边 Scope 项就体现出了作用域链的情况:Local 就是当前的 foo 函数的作用域,Closure(func) 是指 func 函数的闭包,最下面的 Global 就是指全局作用域,从"Local-->Closure(func)-->Global"就是一个完整的作用域链。
这样我们就可以理解了闭包的概念: 指的是那些引用 了另一个函数作用域 中变量的函数。
闭包了解到这里还远远不够。开始下一话题:
v8是如何实现闭包的呢?
首先是JavaScript的三个特性,这让闭包实现有了可能
- JS允许在函数内部定义新的函数。
- 可以在内部函数访问父函数中定义的变量。
- 函数可以作为返回值。
我们再回过头来看,V8执行JS代码的时候,需要先编译再执行。但是V8并不会一次性将所有的JS解析为要执行的代码。如果一次性将所有代码解析完,第一是可能会造成卡顿,第二会占用很大的内存。
所以v8实现了惰性解析。
惰性解析
指再解析的过程中,如果遇到函数声明就会跳过函数内部的代码,不会为函数内部代码进行AST转换。
是不是看完这句话内心中有疑问了:你说函数会跳过,那我问你,如果函数解析被跳过了,那V8是如何知道这个函数有没有对外部的变量有引用呢?Tell me why? look in my eyes!
虽然采用了惰性解析,但是V8还是判断了当前函数是否引用了外部函数的变量, 这个事情是交给与解析器来做了。
预解析器
当开始解析代码的时候,预解析器做了两件事情:
- 判断是否有语法错误
- 检查函数内部是否引用了外部变量,如果有引用,就会将栈中的变量复制到堆中。
闭包有哪些作用呢?
封装私有化数据
ini
function createCounter() {
let count = 0;
return {
increment: function () {
count++;
console.log(count);
}
};
}
const counter = createCounter();
counter.increment(); // 输出 1
counter.increment(); // 输出 2
解决异步带来的问题
javascript
for (var i = 0; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// 6 6 6 6 6 6
for (var i = 0; i <= 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}
// 0 1 2 3 4 5 成功输出
闭包的注意事项
- 由于闭包可以创建私有变量,如果私有变量占的内存很大,或者闭包函数很多,会增大内存的消耗量。
- 可能会造成内存泄漏,
javascript
function createLargeArray() {
// 创建一个包含大量元素的数组
const largeArray = new Array(1000000).fill(0);
return function() {
console.log(largeArray)
};
}
let closureFunction = createLargeArray();
const button = document.querySelector('button');
button.addEventListener('click',closureFunction,{
once:true
})
上面这段代码,执行完一次之后,createLargeArray就不会再被访问到了,所以对于我们来说这段代码是不会在使用到了,也就变成了垃圾,但是不会被回收。
改进
ini
closureFunction = null
面试官:说一说对闭包的理解
闭包的是指那些引用了另一个函数作用域中变量的函数。表现为调用一个函数,返回了一个函数。因为JS的词法作用域,这样就让闭包可以存储私有变量,带来的问题是可能造成内存增加或者内存泄漏。之所以V8可以实现闭包,首先因为JS的特性,允许函数中声明函数,允许函数作为参数,允许函数作为返回值。其次JS在惰性解析的过程中,会使用预加载器对函数进行分析,分析出函数中是否引用了外部的函数中的变量,如果有就将这个变量复制到堆中,这样再函数执行完之后,也不会释放该变量。
结尾
- 新手前端,请多多指教。