很多人学 JS 刚学到闭包的时候会比较懵, 特别是从强类型语言 (如 Java) 转而学 JS 的人, 更是觉得这都啥跟啥呀. 本文也就只针对这些刚学的新手, 所以不会去谈闭包的原理, 只谈闭包的基本使用, 新手可以放心食用. 只有在知道如何使用之后, 你再深入了解就会得心应手, 在用都不知道用的情况下就想对一个知识点了解的很透彻, 这是不可能的.
了解闭包的使用之前, 先得捋清一下一些基本的知识点, 咱们一个知识点一个知识点慢慢往下捋, 到最后你就会发现你已经知道如何使用闭包了.
思路梳理
JS 中, 函数内声明的变量其作用域为整个函数体, 在函数体外不可引用该变量, 听起来很玄乎, 一看代码大家就很清楚了:
- function outter() {
- // 在函数内部声明一个变量
- let x = 3;
- // 在函数体内使用这个变量, 这个肯定没有什么问题
- console.log('我在本函数里使用变量:' + x);
- }
- outter(); // 输出内容: 我在本函数里使用变量: 3
- console.log(x); // 报错, 因为函数外部这样拿不到函数内部的变量
这个知识点相信大家都可以理解, 我在这里说的再浅显一些, 函数内部声明的变量, 就只能在声明该变量的大括号内使用, 大括号外就用不了. 所以看函数内的变量作用域, 直接找大括号就好了.
我们再继续前进, 由于 JS 的函数可以嵌套, 此时内部函数可以访问外部函数定义的变量, 反过来则不行:
- // 外部函数
- function outter() {
- let x = 3;
- // 内部函数
- function inner() {
- let y = x + 1; // 内部函数可以访问外部函数的变量 x
- }
- let z = y + 1; // 报错, 外部函数访问不了内部函数的变量 y
- }
这一点也很好理解, 和前面一个知识点是完全一致的, 内部函数 inner()因为在外部函数 outter()的大括号内, 当然就可以使用变量 x, 而外部函数 outter()在内部函数 inner()的大括号外面, 自然就用不了变量 y.
了解上面的基本知识点后就可以开始了解闭包了. 假设现在我们有一个需求, 我就是想在 outter()外面拿到变量 x 怎么办? 好办呀, 直接在 outter()里将 x 当做返回值返回就好了:
- function outter() {
- let x = 3;
- return x;
- }
- let y = outter(); // 3
OK, 这样是拿到了变量 x, 但是, 严格的来说这只是拿到了变量的值, 并没有拿到变量. 啥意思呢, 就是说你无法对变量 x 的值进行修改, 如果我想将变量 x 的值自增 1 呢? 你是无法修改的, 你就算修改变量 y 的值, x 的值也不会被改变:
- function outter() {
- let x = 3;
- return x;
- }
- let y = outter(); // 3
- y++;
- console.log(y); // 4, y 的值确实被修改了
- console.log(outter()); // 3, 函数内部 x 并没有被修改
有可能你会想到, 那我在函数内部将 x 自增, 然后再返回不就可以了?
- function outter() {
- let x = 3;
- x++;
- return x;
- }
- console.log(outter()); // 4
OK, 没问题, 但是我想每次调用函数的时候, x 都会自增, 就像一个计数器一样, x 的值会随着我的调用次数动态增加. 我们可以按照上面的代码来演示一下看能否达到要求:
- function outter() {
- let x = 3;
- x++;
- return x;
- }
- console.log(outter()); // 4
- console.log(outter()); // 4, 但我想要的是 5
- console.log(outter()); // 4, 但我想要的是 6
会发现每次调用都是 4, 因为当你调用 outter()的时候, x 在最开始都会被重新赋值为 3 然后自增, 所以每次拿到的值都是固定的, 并不会动态增加. 那这时该咋办呢? 这里闭包就能派上用场了!
闭包的最基本演示
还记得之前所说的吗, 内部函数可以调用外部函数内声明的变量, 我们先看一下在内部函数操作一番后, 我们能否拿到 x 的值
- // 外部函数
- function outter() {
- let x = 3;
- // 内部函数
- function inner() {
- // 在内部函数操作 x
- x++;
- }
- // 调用一次内部函数, 将 x 进行更新
- inner();
- // 最后将 x 进行返回
- return x;
- }
- console.log(outter()); // 4
- console.log(outter()); // 4
- console.log(outter()); // 4
这样是可以获得 x 的值, 但这样还是达不到我们计数器的要求, 因为每次调用 outter()时, x 的值都会被重新赋值为 3. 我们应当绕过 outter()函数重新赋值的步骤, 只需要获得 x 自增的操作就可以了. 怎么只获取自增的操作呢, 现在自增的操作是在内部函数 inner()里, 我们能否只拿到内部函数? 当然可以啦!!
JS 是一个弱类型语言, 并且支持高级函数. 就是说, JS 里函数也可以作为一个变量来进行操作! 我们在外部函数 outter()里将内部函数作为变量进行返回, 就可以拿到内部函数了 . 接下来要仔细理解代码, 这种操作就是闭包:
- // 外部函数
- function outter() {
- let x = 3;
- // 内部函数
- function inner() {
- // 在内部函数里操作 x
- x++;
- // 每次调用内部函数的时候, 会返回 x 的值
- return x;
- }
- // 将 inner()函数作为变量返回, 这样当别人调用 outter()时就可以拿到 inner()函数了
- return inner;
- }
- let fun = outter(); // 此时拿到是函数 inner(), 就是说 fun 此时是一个函数
- // 我们接下来调用 fun 函数(就等于在调用 inner 函数)
- console.log(fun()); // 4
- console.log(fun()); // 5
- console.log(fun()); // 6
可以看到上面代码完美拿到了内部函数 inner(), 并实现了需求. 内部函数对外部函数的变量 (环境) 进行了操作, 然后外部函数将内部函数作为返回值进行返回, 这就是闭包. 上面代码的思路步骤就是:
调用外部函数 outter() ---> 拿到内部函数 inner() ---> 调用 inner()函数 ---> 成功对 outter()函数内的变量进行了操作.
看到这有人可能会说, 我为啥要多一节步骤, 要先拿到内部函数, 再对变量进行操作啊? 不能直接在外部函数里对变量进行操作, 省了中间两个步骤吗? 哥, 之前不是演示了吗, 如果直接从外部函数操作, 变量值是 "死" 的, 你是无法实现动态操作变量的. 因为外部函数每次调用完毕后, 会销毁变量, 如果再重新调用则会重新为变量开辟空间并重新赋值. 内部函数的话则会将外部函数的变量存放到内存中, 从而实现动态操作
通过闭包实现装饰模式
上面演示的是内部函数可以操作外部函数的变量, 其实不仅仅是某个变量这么简单, 内部函数可以操作外部函数所拥有的环境, 并可以携带整个外部函数的环境. 这句话如果不能理解也完全没关系, 丝毫不影响你平常使用闭包, 使用的多了自然而然就会明了. 我们接下来继续演示闭包, 更加加深理解:
现在我有一个需求, 我想让一些函数运行的同时并打印日志. 这个打印日志的操作并不属于函数本身的逻辑, 需要剥离开来额外实现, 这种 "扩展功能" 的需求就是典型的装饰模式. 我们先来看一下普通的做法是怎样的:
- function fun() {
- console.log('fun 函数的操作');
- }
- fun(); // fun 函数的操作
我们要对 fun()函数进行扩展功能, 最直接的办法当然是修改 fun()函数的源代码咯:
- function fun() {
- console.log('额外功能: 在运行函数前打印日志');
- console.log('fun 函数的操作'); // fun()函数本身的功能
- console.log('额外功能: 在运行函数后打印日志');
- }
这样是达到了需求, 但是如果我有几十个函数需要扩展功能呢, 岂不是要修改几十次函数源代码? 上面只是为了做演示, 将扩展功能写的很简单只有两句代码, 可往往很多扩展功能可不止几行代码那么简单. 况且, 很多时候就不允许你修改函数的源代码! 所以上面这种做法, 是完全不行的.
这时候, 我们就可以用到闭包来实现了. 函数可以当做变量并进行返回, 那么函数自然也可以当做变量作为参数进行传递. 这就非常非常灵活, 方便了. 我将需要扩展的函数当做参数传递进来, 然后在我的函数里进行额外的操作就可以了:
- // 需要被扩展的函数
- function fun() {
- console.log('fun 函数的操作');
- }
- // 闭包的外部函数, 需要接收一个是函数的参数
- function outter(f) {
- // 此时 f 就是外部函数的一个成员变量, 内部函数理所应当的可以操作这个变量
- function inner() {
- console.log('额外功能: 在运行函数前打印日志');
- f(); // 调用外部函数的变量 f, 也就是说调用需要被扩展的函数
- console.log('额外功能: 在运行函数后打印日志');
- }
- // 外部函数最后将内部函数 inner 返回出去
- return inner;
- }
- // 调用外部函数, 并传递参数进去. 这样就可以拿到已经扩展后的函数: inner()
- let f = outter(fun);
- f(); // 此时 f 函数已经将原来的 fun()函数功能扩展了, 就相当于是 inner()
- // 一般装饰模式都是将原函数给覆盖:
- fun = outter(fun);
- fun(); // 此时再调用原函数的话, 其实就是在调用 inner(), 是包含了扩展功能的
- /*
- 输出内容:
- 额外功能: 在运行函数前打印日志
- fun 函数的操作
- 额外功能: 在运行函数后打印日志
- */
通过闭包就完美实现了装饰模式, 如果还有其他函数需要扩展的话, 直接调用 outter()函数即可, 简单方便. 如果上面这个操作看不明白, 千万不要想复杂了, 第一个闭包演示是操作变量 x, 这个闭包演示也是操作变量, 只不过这个变量 f 是一个函数罢了. 本质没有任何区别.
闭包的总结
现在我们来对闭包进行总结一下, 原理方面就不谈了, 就只谈使用.
使用的思路是:
调用外部函数 outter() ---> 拿到内部函数 inner() ---> 调用 inner()函数 ---> 成功对 outter()函数内的变量 (环境) 进行了操作.
闭包是啥呢 ? 就是将内部函数作为返回值返回, 内部函数则对外部函数的变量 (环境) 进行操作.
为啥要通过内部函数这一步骤呢? 因为内部函数可以将外部函数的环境存放到内存里, 从而实现提供了更为灵活, 方便的操作.
闭包的使用不难, 当你使用熟练之后, 再去了解背后原理就会非常轻松了.
小扩展
本文只终于讲解闭包的基本使用, 其他稍微深一点的东西就不讲了, 有兴趣的可以去扩展一下:
在面向对象 (OOP) 的设计模式中, 比如 Java, 装饰模式是需要通过继承和组合来实现, 装饰者和被装饰者必须都继承了同一个抽象组件. 而 JS 中, 则通过闭包非常灵活的实现了装饰模式, 任何函数都可以被装饰从而扩展功能. 不过这还不算最方便, 在 python 里直接是从语法层面提供了装饰模式, 即装饰器. JS 在 ES6 也通过语法层面实现了装饰器, 不过和 python 的有些不太一样, 有兴趣的可以去了解一下.
内部函数可以操作外部函数的变量, 上面样式的那些变量都是固定的值, 如果变量是一个引用的值(比如引用了外面的一个数组, 在外部函数的外面也可以直接对数组进行修改), 会产生什么后果.
运用闭包的好处上面已经演示了, 那闭包的坏处是什么? 提示: 内存
来源: http://www.jianshu.com/p/c22832ae9eb0