JavaScript深刻系列第八篇,介绍理论上的闭包和实践上的闭包,以及从做用域链的角度解析经典的闭包题。git
MDN 对闭包的定义为:github
闭包是指那些可以访问自由变量的函数。面试
那什么是自由变量呢?闭包
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。app
由此,咱们能够看出闭包共有两部分组成:函数
闭包 = 函数 + 函数可以访问的自由变量post
举个例子:ui
var a = 1;
function foo() {
console.log(a);
}
foo();复制代码
foo 函数能够访问变量 a,可是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,因此 a 就是自由变量。this
那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……spa
还真是这样的!
因此在《JavaScript权威指南》中就讲到:从技术的角度讲,全部的JavaScript函数都是闭包。
咦,这怎么跟咱们平时看到的讲到的闭包不同呢!?
别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让咱们看看汤姆大叔翻译的关于闭包的文章中的定义:
ECMAScript中,闭包指的是:
接下来就来说讲实践上的闭包。
让咱们先写个例子,例子依然是来自《JavaScript权威指南》,稍微作点改动:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();复制代码
首先咱们要分析一下这段代码中执行上下文栈和执行上下文的变化状况。
另外一个与这段代码类似的例子,在《JavaScript深刻之执行上下文》中有着很是详细的分析。若是看不懂如下的执行过程,建议先阅读这篇文章。
这里直接给出简要的执行过程:
了解到这个过程,咱们应该思考一个问题,那就是:
当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 做用域下的 scope 值呢?
以上的代码,要是转换成 PHP,就会报错,由于在 PHP 中,f 函数只能读取到本身做用域和全局做用域里的值,因此读不到 checkscope 下的 scope 值。(这段我问的PHP同事……)
然而 JavaScript 倒是能够的!
当咱们了解了具体的执行过程后,咱们知道 f 执行上下文维护了一个做用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}复制代码
对的,就是由于这个做用域链,f 函数依然能够读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即便 checkscopeContext 被销毁了,可是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然能够经过 f 函数的做用域链找到它,正是由于 JavaScript 作到了这一点,从而实现了闭包这个概念。
因此,让咱们再看一遍实践角度上闭包的定义:
在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:
This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.
闭包在计算机科学中也只是一个普通的概念,你们不要去想得太复杂。
接下来,看这道刷题必刷,面试必考的闭包题:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();复制代码
答案是都是 3,让咱们分析一下缘由:
当执行到 data[0] 函数以前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}复制代码
当执行 data[0] 函数的时候,data[0] 函数的做用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}复制代码
data[0]Context 的 AO 并无 i 值,因此会从 globalContext.VO 中查找,i 为 3,因此打印的结果就是 3。
data[1] 和 data[2] 是同样的道理。
因此让咱们改为闭包看看:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();复制代码
当执行到 data[0] 函数以前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}复制代码
跟没改以前如出一辙。
当执行 data[0] 函数的时候,data[0] 函数的做用域链发生了改变:
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}复制代码
匿名函数执行上下文的 AO 为:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}复制代码
data[0]Context 的 AO 并无 i 值,因此会沿着做用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即便 globalContext.VO 也有 i 的值(值为3),因此打印的结果就是 0。
data[1] 和 data[2] 是同样的道理。
若是想了解执行上下文的具体变化,不妨按部就班,阅读这六篇:
《JavaScript深刻之从ECMAScript规范解读this》
JavaScript深刻系列目录地址:github.com/mqyqingfeng…。
JavaScript深刻系列预计写十五篇左右,旨在帮你们捋顺JavaScript底层知识,重点讲解如原型、做用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎star,对做者也是一种鼓励。