在理解闭包之前,你需要先了解的是执行上下文,当然了你了解V8执行过程就更好了。你可以通过上一篇的文章中学习相关知识,这里详细讲解了V8的执行过程以及执行上下文是如何创建的。
什么是闭包?
闭包
(Closure),又称词法闭包
(Lexical Closure)或函数闭包
(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。 闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。
闭包
(closure)是一个函数以及其捆绑的周边环境状态(lexical environment
,词法环境
)的引用的组合闭包能让开发者可以从内部函数访问外部函数的作用域。 。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
总的来说,闭包就是能够读取其他函数内部变量的函数。
LHS查询和RHS查询
在开始闭包之前,我们再来补充一下两个概念,就是什么是## LHS查询和RHS查询,待会要用到。
LSH查询
;LHS查询会在当前作用域链进行查询,如果当前作用域查询不到,就会沿着作用域链一层一层,找到的话就会将值赋值给这个变量,如果到达作用域顶端仍然找不到,就会在作用域链顶端创建这个变量。RHS查询
;RHS查询会在当前作用域链进行查询,如果当前作用域查询不到,就会沿着作用域链一层一层,,找到的话就会取得这个值并返回,如果到达作用域顶端仍然找不到,就会抛出错误(比如TypeError、ReferenceError
)。
闭包的产生
JavaScript三大特性,而闭包产生的原因也正是因为这些特性:
可以在JavaScript函数内部定义新的函数; 内部函数中访问函数中的定义; 在JavaScript中,函数是一等公民,所以函数中既可以传入一个函数又可以作为参数返回一个函数。
function foo() {
var moment = 18;
var test = 111;
function bar() {
const may = moment + 777;
return may;
}
console.log(test);
return bar;
}
var baz = foo();
baz(); // 嗨,朋友,这就是闭包
复制代码
通过观察上面的代码,我们在foo函数中定义了bar函数,并返回bar函数,同时在bar函数中访问了foo函数中的变量moment。 上面的代码大概的执行流程为:
当调用 foo
函数时,foo
函数会将它的内部函数 bar
返回给全局变量 baz
; 等到 foo
函数执行结束时,执行上下文会被 V8
销毁;
按照正常的情况来说,变量 moment
已经被 V8 销毁了,因为我们知道 V8 引擎有垃圾回收期用来释放不再使用的内存空间,但是由于存活的函数 bar
依然引用了 foo
函数作用域中的变量 moment
,这样就会带来两个问题:
当 foo
执行结束时,变量 moment
该不该被销毁?如果不应该被销毁,那么他应该在什么时候销毁,而又应该采用什么策略? 我们都知道 V8 引擎采用的是惰性解析的方案,那么当执行到 foo
函数时, V8 只会解析 foo
函数,并不会解析内部的 bar
函数,仅仅知识对 bar
函数进行了标记,在这时 V8 引擎并不知道 bar
函数中是否引用了 外层函数作用域中的变量 moment
;
由于 JavaScript
是一门基于堆和栈的语言。在执行全局代码时当执行,V8
会将全局执行上下文压入到调用栈中,然后进入执行 foo
函数的调用过程。 这时候 V8
引擎会为 foo
函数创建执行上下文,执行上下文中包括了变量 moment
,然后将 foo
函数的执行上下文压入栈中,foo 函数执行结束之后,foo 函数执行上下文从栈中弹出,这时候 foo
执行上下文中的变量 moment
也随之被销毁。 正常的处理方式应该是 foo
函数的执行上下文被销毁了,但是 bar
函数引用的 foo
函数中的变量却不能被销毁。 在执行 foo
函数的阶段,虽然采取了惰性解析,不会解析和执行 foo
函数中的的 bar
函数中的 bar
函数,但是 V8
还是需要判断 bar
函数是否引用了 foo
函数中的变量。 V8
引擎引入了预解析器,当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该书做一次快速的预解析,其中主要的目的主要有两个:
判断当前函数是否是不是存在一些语法上的错误,如果发现语法错误,那么就会向 V8 抛出语法错误; 判断 foo
函数是否有被 bar
函数引用的变量,如果有,就会把该变量复制一份到堆内存中,同时 bar
函数本身也是一个对象,也会被存放到内存当中,这样即使 foo
函数即使执行完成,内存被释放以后,bar
函数在执行的时候,依然可以从堆内存中访问复制过来的变量;
第二点钟说的复制一个变量,实际上是复制了一个闭包函数(Closure (foo)
),但是此函数只有被 bar
函数引用的值,foo
函数中的 test
变量并没有被复制过去,如下图所示:
Other Example
function foo() {
var moment = 777;
function baz() {
console.log(moment);
}
bar(baz);
}
function bar(fn) {
fn(); // 这也是一个闭包
}
foo();
复制代码
把内部函数 baz
传递给 bar
函数,当调用这个内部函数时(这个时候叫作 fn
),它涵盖的 foo
()内部作用域的闭包就可以观察到了,因为他能够访问。
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("hello world");
```js
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("hello world"); // 这也是一个闭包
复制代码
将一个内部函数 timer
传递给settimeout
(…)。timer函数依然保存有wait
(…)作用域的闭包。 在引擎内部,内置的工具函数settimeout
(…)持有对一个参数的引用,这个参数也许叫作fn或者func,又或者其他类型的名字。引擎会调用这个函数,在这个例子中就是内部的timer函数,而词法作用域在这个过程中保持完整。
经典永不过时
for (var i = 0; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 10000);
}
复制代码
正常情况下,我们对这段代码行为的预期分别是输出1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次输出的频率输出五次6。因为这个循环的终止条件是 i
不再<=5
,条件时 i
的值为6,因此输出显示的是循环结束时 i
的最终值。 这是因为 setTimeout
是异步的,而for循环是同步的,延迟函数的回调会在循环结束时才执行,当循环结束时 i
已经是 6了,所有的回调函数才会开始执行,因此会每次输出一个 6
来。 那么有什么办法可以让这个循环一次输出数字呢? 用 let
关键字代替 var
? 答案当然是可以的 , 你会看到 0 1 2 3 4 5
成功输出。
for (let i = 0; i <= 5; i++) {
setTimeout(() => {
### 最后
小编综合了阿里的面试题做了一份前端面试题PDF文档,里面有面试题的详细解析
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
![](https://img-blog.csdnimg.cn/img_convert/45f2869b91b538dd3bb3290ba13bc806.png)
![](https://img-blog.csdnimg.cn/20210419193354991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0tlcGxlcl9JSQ==,size_16,color_FFFFFF,t_70)
虽只说了一个公司的面试,但我们可以知道大厂关注的东西并举一反三,通过一个知识点延伸到另一个知识点,这是我们要掌握的学习方法,小伙伴们在这篇有学到的请评论点赞转发告诉小编哦,谢谢大家的支持!