闭包在 JavaScript 代码中随处可见, 但有时由于对它的了解不够深刻, 因此它每每带有一丝神秘色彩。虽然在写代码时随手就会建立而且使用闭包, 但有时它对咱们来讲仍然像一个黑盒,因此有必要探究一下这个黑盒子里面到底包的什么馅。闭包
正如引言中所说, 咱们随手就能建立一个闭包:函数
// eg.1
var variable = 1;
function fn(){
console.log(variable);
}
fn();
复制代码
这段代码中的函数 fn 其实就是一个闭包。这可能看起来很诡异, 不过 fn 确实是一个最简单的闭包,为何它能被称为一个闭包呢? 下面给出闭包的定义以后就能解释这个问题了。ui
闭包的定义是:在本身的做用域内引用了外层(也称上层)做用域中的变量的函数称为闭包。 这句话中的关键词有两个: 1. 外层(也称上层)做用域; 2. 函数。这说明了闭包是个函数,并且这个函数引用了外层做用域里的变量。this
这样看来上面 eg.1 中的例子就明朗了: fn 是个函数, 并且 fn 内输出(引用)了外层做用域中的变量 variable
。也许有的同窗不太清楚 JavaScript 中做用域的概念, 如今来回顾一下。spa
注: MDN英文版中说"闭包是一个函数以及这个函数定义时所在的词法环境的组合", 这比笔者给出的闭包是个函数的说法更准确, 在读彻底这篇文章后相信读者会有清晰的认识。设计
做用域是代码生存的环境,它限制着代码的活动范围。JavaScript 中有三种做用域, 分别是 1. 全局做用域; 2. 函数做用域 3. 块做用域。后二者也被称为局部做用域。3d
做用域之间能够并列,也能够互相嵌套。如下面的代码为例:code
// 这是全局做用域
function fn1(){
// 这里面是函数做用域
}
function fn2(){
// 这里面也是函数做用域
}
复制代码
上面的代码中有全局做用域和两个函数做用域, 全局做用域比较好理解, 函数在定义的时候就有了本身的做用域, 那就是本身的内部。上面的全局做用域包含了两个函数做用域, 称为做用域嵌套, 即做用域里还套着其余的做用域, 而全局做用域能够称为外层(或者上层)做用域, 两个函数做用域称为内层(或者下层)做用域。相似于俄罗斯套娃, 大娃套中娃, 中娃套小娃, 一层一层的套。cdn
而嵌套着的做用域有个特色: 内层做用域能够直接访问外层做用域里的变量, 可是外层做用域没法直接访问内层做用域内的变量, 例如:blog
// eg.2
// 这是全局做用域, 相对于两个函数做用域来讲是全局做用域就是外层做用域
// 在外层做用域中定义一个变量
var outerVariable = 1;
function fn(){
// 这里面是函数做用域, 相对说也是内层做用域
// 在内层做用域内定义一个变量
var innerVariable = 2;
// 在内层做用域内访问外层做用域中的变量 outerVariable
console.log(outerVariable); // 1
}
fn(); // 调用函数, 输出 1, 说明在函数(内层)做用域里能够访问到全局(外层)做用域里的变量
// 在外层做用域里尝试访问内层做用域的变量
console.log(innerVariable); // ReferenceError: innerVariable is not defined
// 报了引用错误, 说明外层做用域中没有 innerVariable 这个变量, 也就是说明外层没法访问到内层做用域中定义的变量
复制代码
上面的代码中的外层做用域为全局做用域, 内层做用域为函数做用域。在尝试在内层做用域中访问外层做用域时中定义的变量时是能够的,可是想在外层做用域中直接访问内层做用域里定义的变量就不会成功了。
能够将做用域嵌套进行具象化以便于理解, 以上面的例子 eg.2 为例:
上图中能够看出外层做用域(绿色)内定义了一个函数 fn, 这个函数 fn 有本身的做用域(棕色), 这两个做用域是嵌套关系, 就产生了内外层做用域这个说法。
上面只介绍了最简单的闭包例子, 下面介绍更加复杂, 也是更加常见的闭包例子, 以便加深理解。
关于 JavaScript 做用域的更多内容能够查阅《JavaScript高级程序设计》的 4.2节。
在文章的第 1 部分中, 为了引入闭包的概念, 只给出了最简单的闭包例子。 下面以一个稍微复杂的闭包例子来引出关于闭包的更多内容。
一个稍复杂一些的闭包例子:
1 // eg.3
2 function outer(){
3 let outerVariable = 0;
4
5 let inner = function(){
6 console.log('outerVariable === ', outerVariable);
7 }
8
9 return inner;
10 }
11
12 let closure = outer(); // 调用 outer 函数, 获得 inner 函数, 也就是获得了一个闭包实例
13
14 // 运行这个闭包, 在这个闭包里仍然能够访问它引用的外层做用域里的变量 outerVariable
15 closure(); // outerVariable === 0
复制代码
上面代码中定义了 outer 和 inner 两个函数, 其中 inner 函数是定义在 outer 函数中的, 并且引用了 outer 函数中定义的 outerVariable 变量。
同时能够发现, inner 函数的做用域被嵌套在了 outer 函数的做用域中, 则此时能够认为 outer 是 inner 函数的外层做用域。 而 inner 访问了 外层做用域中的 outerVariable 变量, 此时 inner 就是一个闭包。
又由于函数是一等公民, 能够做为其余函数的返回值, 上面代码中的 outer 函数就将 inner 函数返回了, 因此调用 outer 函数就是获得了一个 inner 闭包的一个实例, 上面的代码将这个实例命名为 closure。
调用这个闭包, 就运行了 console.log(outerVariable);
这行代码, 就读取到了闭包环境中保存的外层做用域中的 outerVariable。
能够用嵌套的做用域图片更具象的解释上面这几段话:
由上图可见, 内层做用域的 inner 函数中引用了外层做用域的 outerVariable 变量, 因此 inner 函数成为了一个闭包。
在实例化一个闭包(例如上面eg.3示例代码中的第12行)的时候, 会建立一个环境, 这个环境保存了定义这个闭包(例如上面eg.3示例代码中的第5~7行)时的做用域信息, 这个做用域信息能够被抽象为上面图片中所示的。
请注意的是, 每实例化一个闭包时, 就会建立一个环境, 每一个闭包能够在本身的环境中操做, 却不能够访问其余闭包实例对应的环境。并且只要一个闭包实例存在, 它的对应的环境就会存在。
将上面的例子 eg.3 修改为下面的代码:
// eg.4
function outer(){
let outerVariable = 0;
let inner = function(){
outerVariable ++; // 增长的代码, 在输出 outerVariable 以前, 先让他自增 1
console.log('outerVariable === ', outerVariable);
}
return inner;
}
let closure1 = outer(); // 修改的代码, 实例化一个闭包并命名为 closure1
let closure2 = outer(); // 修改的代码, 再实例化第二个闭包并命名为 closure2
// 增长的代码, 调用三次 closure1, 观察 closure1 引用的变量 outerVariable 的变化
closure1(); // outerVariable === 1
closure1(); // outerVariable === 2
closure1(); // outerVariable === 3
// 增长的代码, 调用一次 closure2, 观察 closure2 引用的变量 outerVariable 的值
closure2(); // outerVariable === 1
/* 发现闭包 closure2 引用的 outerVariable 值没有受到闭包 closure1 的影响*/
复制代码
上面的代码在闭包 inner 输出 outerVariable 以前使它自增一下, 而后实例化了两个闭包, 从代码的运行结果中能够看出, 这两个闭包引用的 outerVariable 并非一个, 说明这两个闭包各自保存了一份 outerVariable, 而且每一个闭包中保存的 outerVariable 的初始值就是定义闭包 inner 时候的值.
这个过程一样能够经过图形表达出来:
上面的图片能够说明, 每一个闭包操做的都是本身环境中的 outerVariable, 与其余的闭包无关, 上面 eg.4 中的代码的运行结果页说明了这个结论.
下面举一个使用了闭包的经典应用, 使用闭包模拟私有变量:
function Person(){
let age = 24;
this.getAge = function(){
return age; // 引用了外层的变量
}
this.grow = function(){
age ++; // 引用了外层的变量
}
}
let xm = new Person(); // 实例化小明 xm
// 没法经过 xm.age 来访问 xm 的年龄;
// 获取 xm 的年龄
let ageThisYear = xm.getAge();
console.log('小明今年的年龄是 ' + ageThisYear); // 小明今年的年龄是 24
// 过了一年, xm 涨了一岁
xm.grow();
// 获取小明过年以后的年龄
let ageNextYear = xm.getAge();
console.log('小明明年的年龄是', ageNextYear); // 小明明年的年龄是 25
复制代码
如上面的代码所示, 在两个函数 getAge 和 grow 中引用了上层做用域里的 age 变量, 这两个函数就是闭包,接着对这两个闭包进行调用。 就实现了一个简单版的模拟了私有变量。
MDN对英文版对闭包的定义是: "A closure is the combination of a function and the lexical environment within which that function was declared". 即:"闭包是一个函数以及这个函数定义时所在的词法环境的组合"。这个定义比本文章内的说法更加准确, 请参考.
完. 若有错误, 感谢指出!