学习 JavaScript 的同学都会面对闭包,总觉得很难,你会看到闭包各种版本的定义,即使把闭包的概念背会也不一定的能够理解闭包。想要掌握闭包,需要看很多关于闭包的资料,加上对 JavaScript 执行过程的理解才能掌握闭包。如果你看到“一文读懂闭包”,“十分钟读懂闭包”这类文章后,想「投机取巧」花几分钟时间能够理解闭包,其实是不可能的,一切需要脚踏实地,慢慢就理解了。
「闭包的出现能给编程带来哪些便捷呢?」,这是我们学习闭包首先需要面对的问题,也就是说闭包出现的背景是什么。
函数是无状态的,比如下面的函数:
function call() {
var name = 'suyan';
var age = 20;
console.log(name + ' age is ' + age);
}
call();
当 call 函数执行完后 name 和 age 占用的内存空间将会被释放,在函数外部无法访问变量 name 和 age 。如果想要在函数 call 外访问变量 age,且函数执行完后保留 age 的值,咋么办?
想要解决这个问题,可以使用闭包(colsure)
function call() {
var name = 'suyan';
var age = 20;
console.log(name + ' age is ' + age);
return {
getAge: function () {
return age;
},
setAge: function (newValue) {
age = newValue;
}
};
}
const ageObj = call();
console.log(ageObj.getAge()); // 20
// 修改 age 的值为 30
ageObj.setAge(30);
console.log(ageObj.getAge()); // 30
通过 Chrome 调试工具可以查看 call 这个函数捕获的闭包中的变量:
闭包一大重要特征就是可以「保存函数执行环境中的变量」,使其延迟释放,比如下面的函数:
function createCounter() {
let counter = 0;
const myFunction = function() {
counter = counter + 1;
return counter;
};
return myFunction;
}
// increment 是一个函数
const increment = createCounter();
const c1 = increment();
const c2 = increment();
const c3 = increment();
console.log(c1, c2, c3); // 1,2,3
increment 这个函数使用了 createCounter 中的变量 counter,每次调用 increment 这个函数,变量 counter 一直保存在执行环境中,并不会被释放。再创建一个 increment2,这是 c11 的值为 1。可见 increment 和 increment2 使用的执行环境互不影响。
const increment2 = createCounter();
const c11 = increment2();
console.log(c11); // 1
闭包使得一个函数可以访问另一个函数作用域中的变量。我们在看一下 JavaScript 内置对象数组 这节课程中的一道关于闭包的面试题:
(function () {
var numbers = [];
for (var i = 0; i < 4; i++) {
numbers.push(function () {
return i;
});
}
var result = numbers.map(function (e) {
return e();
});
console.log(result); // 值是什么?
})();
最终打印的值是 4、4、4、4。在函数中通过 var 声明的变量 i 属于函数作用域,当代码执行到第 8 行后, i 的值是 4。此时 numbers 中保存为 4 个函数,当这些函数被执行的时候会使用当前函数执行环境中的变量 i,此时值为 4,故最终 result 中的值都是 4。
如果把上面代码中的 var i = 0;修改成 let i = 0;结果是什么?
总之,闭包可以延长变量的释放,你可以把闭包看做是带有执行环境的函数。
推荐阅读:
看透变量提升与块级作用域实现的原理
执行上下文与调用栈
推荐我精心准备的 JavaScript 学习资源