难度:容易
前言
闭包是JS中非常重要的内容,理解闭包有利于我们深入掌握JS语言以及学习其它前端框架源代码,同时闭包也是面试中必考的知识点。我在学习闭包时,花了不少功夫,看了许多大佬的博客,一开始死记硬背,看的时候以为掌握了,然后过几天又忘了,反反复复,耗时费力。本文尝试从js代码运行时的角度,来解释闭包的基本原理,希望看完之后可以将闭包知识刻进记忆里。
定义
我们先看下闭包的定义:
一个函数和对其周围状态( lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是 闭包( closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。—— 摘自MDN
定义比较抽象,看完疑窦丛生,闭包是组合!?那什么是词法环境?什么又是引用捆绑?什么又是函数的作用域?
我们再来看看大佬们怎么说:
闭包就是能够读取其他函数内部变量的函数。
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。——摘自某位财经博主
看完大佬说法更晕了,闭包是函数!?函数内部的函数!?
算了,姑且不论谁对谁错,我们先从一个例子出发,看看JS引擎咋说。
举例
我们先看第一个例子:
function makeFunc() {
var stevenx911_name = "StevenX911";
function displayName() {
console.log(stevenx911_name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
我们将断点放在console.log(stevenx911_name);
这一行,观察Chrome DevTools调试面板,结果如下:
从调试面板中我们可以清晰的看到,在Scope
下显示着Closure (makeFunc)
,这就是我们的闭包(Closure)所在!同时在这个闭包中,我们也可以看到在执行内部函数displayName
时访问到了定义在外层函数makeFunc
中定义的变量stevenx911_name
,换句话说,闭包Closure(makeFunc)
在内部函数执行时
的作用域(Scope
)链上,所以,对比上述的两种定义,不难看出,MDN的定义更为严谨、更为靠谱!(此处没有diss大佬之意~)
我们再来看一个例子:
// 这是一道经典的面试题,结果输出什么?怎么改进?
function test() {
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
})
}
}
test();
我们将断点放置在console.log(i)
这一行,观察调试面板,结果如下:
从调试面板上我们可以看到结果都是10,这里的闭包是由setTimeout
回调函数和外层函数组合形成的,当主线程循环结束时,闭包中的变量i=10
,所以定时器触发的回调函数只能拿到10,因此10次EventLoop的结果是一样的。至于怎么改进就留给各位看官了。
接着,我们再来看一个例子:
// 这也是经常会考到面试题,高阶函数
function Add(x) {
return function (y) {
return function (z) {
console.log(x + y + z);
}
}
}
Add(1)(2)(3);
我们将断点放在console.log(x + y + z);
这一行,然后运行它,打开调试面板,我们看到结果如下:
从上图可以看到最内层的函数一直可以通过Scope(Closure)
链访问到最外层的参数变量,并最终输出结果。除此之外,我们还发现:
- 闭包是可以嵌套的
- 形成闭包的函数不一定需要具名,匿名也可以
通过上述三个小例子,相信诸位对闭包一定有了一个感性的认识,但是我们还是很难用自己的话对闭包下一个准确的定义,这里还是搬出MDN的定义,再看上一遍:
一个函数和对其周围状态( lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是 闭包( closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。—— 摘自MDN
闭包的应用
几乎所有的框架都用了闭包,所以只有深刻理解并掌握闭包,才能看懂框架、用好框架:
- 包装私有变量/方法
- 回调函数(含异步)
- 函数Currying柯里化
- ...
常见闭包面试题
- 什么是闭包?
- 闭包的应用有哪些?
- 闭包的变量存在栈上?还是堆上?
- JS为什么要设计闭包?
- 阅读代码给出输出结果,尝试给出两种以上的改进方案
function test() {
var arr = [];
for(var var_i = 0; var_i < 10; var_i++){
arr[var_i] = function () {
console.log(var_i + " ");
}
}
// 改进方案1 var -> let 利用ES6新增的块级作用域
// for(let let_i = 0; let_i < 10; let_i++){
// arr[let_i] = function () {
// console.log(let_i + " ");
// }
// }
// 改进方案2 利用立即执行函数形成新的闭包
// for(var var_i = 0; var_i < 10; var_i++){
// (function(f_var_i){
// arr[f_var_i] = function () {
// console.log(f_var_i + " ");
// }
// })(var_i)
// }
return arr;
}
var myArr = test();
for(var j = 0; j < 10; j++) {
myArr[j]();
}
参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
https://developers.google.com/web/tools/chrome-devtools/memory-problems?hl=zh-cn