闭包的定义
这个函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域名。
所以闭包的本质其实就是作用域 + 垃圾回收机制,弄懂这两个东西,闭包也就懂了。
所谓作用域
牢记 2 原则 + 1 规则
- var变量作用域只能被限制在函数作用域中,let可以被函数作用域和块作用域限制(⚠️ var存在变量提升,let和const没有变量提升。声明本身会被提升,而赋值或其他运行逻辑会留在原地)
- 词法作用域:在哪定义,词法作用域就在哪
遍历嵌套作用域链的规则
引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。简而言之,作用域往外找。
function foo(a) {
console.log(a + b); // 变量b在函数foo里面找不到,持续往外围找,最后找到b = 2.
}
var b = 2;
foo(2); // 4
作用域分为函数作用域,块作用域
函数作用域
function foo(a) {
var a = 2; //仅能在foo函数里面才能访问
}
块作用域: for循环,条件判断if, switch, while....
for (let i=0; i<10; i++) {
console.log(i);
}
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
垃圾回收机制
垃圾回收机制是 JavaScript 引擎中的一部分,负责自动回收那些不再被使用的内存,确保内存资源得到有效利用,避免内存泄漏。
简单来说,当程序中某些对象不再被引用时,这些对象所占用的内存就会被视为“垃圾”,垃圾回收器会负责释放这些内存,使其可以被重新利用。 具体的算法就不在这里赘述了。
闭包示例
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 朋友,这就是闭包;实际上执行的是返回的bar(),bar能访问foo()内部作用域
//foo()执行后,通常foo()的整个内部作用域都被销毁,但因为闭包的存在,foo内部的作用域在被bar所使用,因此没有被回收
再来个常见的闭包(其实你日常就在写~)
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000 );
//将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。
//在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
}
wait("Hello, closure! ");
最后来一个经典面试题一起回顾一下知识点
for(var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
} //输出?
每秒输出一个数字,分别输出1~5。不,实际上,这段代码在运行时会以每秒一次的频率(当i是1,就是1秒后输出i,当i是2的时候就是2秒后输出i,以此类推,总共5次,最后会显示每秒都在打印i。)输出五次6。 所有的回调函数是在循环结束后才会被执行,而结束条件是i<=5,也就是i=6的时候结束,因此会每次输出一个6.
为什么会跟我们设想的不一样呢?
其实是因为我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。(!!!别忘了作用域原则1 - var变量作用域只能被限制在函数作用域中,let可以被函数作用域和块作用域限制。)
那怎么修改这个函数来产生我们预期的值呢?使用立即函数。
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j*1000 );
})(i);
}//在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
还有一个更简单的方法,使用块作用域。
for (let i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000 );
//另外for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
}