在学习闭包之前,我们先学习两个重要的概念:作用域链和词法作用域。
作用域链
作用域链是JavaScript中一种用于解析变量的方法。当一个函数被调用时,函数在执行前预编译,会创建一个执行上下文对象,这个对象包含了变量环境和函数的外部引用。变量环境中有一个特殊的属性outer
,它指向外层作用域,outer
的指向是根据词法作用域来定的。如下图所示:
bar()
函数内输出a,a在bar()内未找到,outer
将会指向外层作用域去查找,bar()
函数外层是全局作用域,全局作用域中定义了var a = 200
,故bar()
函数输出a的值为200;如果在第六行再添加一个console.log(a)
,则这里输出的a的值为100,因为js引擎在查找变量时,会先在函数中查找,在函数中找到了var a = 100
。
因此我们得到结论:
- 当JavaScript引擎查找变量时,首先会在当前函数的作用域内查找。如果找不到,就会根据
outer
属性的指向逐层向外查找,直到找到所需的变量或达到全局作用域。这种逐层查找变量的过程称为作用域链。
作用域链的作用: - 确保了在不同的作用域中查找变量时,能够正确地找到所需的变量。
词法作用域
词法作用域是指一个函数在定义时所处的作用域。换句话说,它是由代码书写的位置决定的,而不是运行时的位置决定的。在函数定义时,它已经“记住”了外部作用域的环境。例如:
function outer() {
let outerVar = '!!!';
function inner() {
console.log(outerVar); // '!!!'
}
inner();
}
outer();
因为inner
函数是在outer
函数内定义的,它的词法作用域包含了outer
函数的变量环境。
掌握了作用域链和词法作用域,我们就可以来学习今天的主题——闭包
闭包
闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。 换句话说,即使外部函数已经执行完毕,内部函数引用的外部函数中的变量依旧保存在内存中。这些变量的集合被称为闭包。 接下来我将举个例子,带大家更好的理解闭包:
这个例子的执行过程:
① 定义阶段
foo()
函数、age
、baz
被定义;
② age
赋值和调用foo()
age
赋值20后,当执行 const baz = foo();
时,调用了 foo
函数,创建了 foo()
的执行上下文。name
、bar()
、count
、age
被定义,之后name
、count
、age
被初始化,最后,foo
返回 bar
函数,并将其赋值给 baz
;
③ 调用baz
函数
调用 baz()
实际上是在调用 bar
函数,bar
函数是在 foo
的作用域内定义的,因此它形成了一个闭包,能够访问 foo
作用域中的变量 count
和 age
,baz()
被调用时,会输出 count
和 age
的值,输出 1 18
。
在这里,还有另一种形式能形成闭包,如果面试官问你,你就可以拿下面的例子说明:
function foo() {
var a = 1
function bar() {
console.log(a);
}
// return bar
window.fn = bar
}
// const baz = foo()
// baz()
foo()
window.fn()
这里不需要新定义baz。
闭包的作用
- 实现共有变量 (企业的模块开发):闭包可以用于在不同函数间共享变量。
- 做缓存:闭包可以用于缓存计算结果,避免重复计算。
- 封装模块,防止全局变量污染:闭包可以用于创建私有作用域,从而避免变量污染全局作用域。
闭包的缺点
尽管闭包有许多优点,但它也有一些缺点,最主要的是可能导致内存泄漏。由于闭包会保留对其外部作用域变量的引用,这些变量在闭包存在期间无法被垃圾回收,从而占用内存。为避免内存泄漏,开发者需要在不再需要闭包时,显式地解除对外部变量的引用。
闭包的应用(面试)
如果面试时,面试官问你:运用闭包,将下列代码改为输出0,1,2,3,4,5,6,7,8,9
。
var arr=[];
for (var i = 0; i <10; i++) {
arr[i] =function(){
console.log(i);
}
}
arr.forEach(function(item){
item()
});
分析代码
首先,这个代码输出为10,10,10,10,10,10,10,10,10,10
,之所以输出十个10,是因为for循环后,i的值为10,i为var声明,会发生声明提升,i是作用在整个函数作用域内,for循环后i的值就为10,执行到arr.forEach(function(item){ item() });
时,调用每一个item
函数都是同一个i
变量10。
解法 ①
- 将
var i = 0
改为leit i = 0
。 当你使用let
声明变量时,i
是在块级作用域内的。每次迭代时,i
都是一个新的变量,每个闭包都能捕获到当前迭代的i
值。
解法 ②
var arr = [];
for (var i = 0; i <10; i++) {
function foo(){
var j=i
arr[i] =function(){
console.log(j);
}
}
foo();
}
arr.forEach(function(item){
item()
});
在这段代码中,for循环时每一遍都会调用foo()
函数,都会创建一个新的局部变量 j
,并将当前的 i
值赋给 j
,遍历 arr
并调用每个函数,这些函数分别会打印它们各自闭包内捕获的 j
值。
这里最好两个解法都要掌握,毕竟傻瓜才做选择,聪明人两个都要。