返回

关于闭包

何为闭包

先看看定义:闭包,指的是词法表示包括不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量。函数是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数,所以闭包可以理解为函数的嵌套。 其实闭包概念不是js中特有的,类似的动态语言比如Python、Ruby都有闭包特性。

    //闭包示例    
    function foo() {
          var name = "我是闭包";
          function bar() { console.log(name) }
          return bar;
        }
    var baz=foo();
    baz(); //我是闭包

为啥叫闭包哪,因为函数bar的作用域被封闭在了foo函数里,所以就有了这个名字。 那既然封闭了,为啥还能访问外面的foo的作用域哪,这就需要了解**“词法作用域”**这个概念。先看下图//1: 词法.png

  • 气泡1 :即全局作用域,包含变量foo
  • 气泡2 :foo的作用域,包含变量a,bar,b
  • 气泡3 :bar的作用域,包含变量c

查找过程:执行console.log,然后由内向外查找变量a,b,c的引用,其中c在自己的作用域中找到,a,b在自己的作用域中找不到,于是向上级作用域中查找,在foo的作用域中找到,并且调用。作用域向上查找的过程中,匹配第一次查找到的变量,也就是说如果foo的作用域中也定义了c,但bar函数只调用自己作用域里的c。

再回到我们的闭包示例中,现在我们知道了其实是bar的此法作用域能访问到foo()的作用域,之后我们利用return返回bar函数,来执行它。很显然我们的bar函数能在词法作用域之外执行。

最后,一般的函数执行完了,内部作用域会被浏览器的垃圾回收机制(GC)销毁回收,但是在这里由于我们的bar函数还在使用foo的作用域,这就导致了执行完foo后他不会被销毁。既然不会被销毁,那么foo的作用域就会一直存活,都怪bar在里面赖着不走啊。

归根到底,啥叫闭包,bar()对foo()作用域的引用就叫闭包。

现在我们就能总结出闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收

闭包的应用

一、加强模块化

var Foo = (function(){
        var a = 1;
        function sum(){ a++; console.log(a);}
        function product(){ a--; console.log(a);}
          return {
                s:sum,             //json结构
                p:product
        }
});
Foo().s(); //2
Foo().p(); //0 

看看代码是不是很简约,很优雅。 另外,你肯定注意到了这个return,我们一般用return来实现外部调用闭包。当然,无论通过何种方法,只要将内部函数传递到词法作用域之外,他都会拥有对原始定义的引用,只要执行这个函数就会使用闭包。一句总结:Javascript中的函数“在定义它们的作用域里运行,而不是在执行它们的作用域里运行”

二、模拟私有变量

function Counter(start) {
    var count = start;
    return {
        increment: function() { count++; },
        get: function() { return count; }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

这里,Counter 函数返回两个闭包,函数 increment 和函数 get。 这两个函数都维持着 对外部作用域 Counter 的引用,因此总可以访问此作用域内定义的变量 count.

三、闭包与自执行函数

(function(){
  var foo = 'Hello';
  var bar = ' World!'
  function baz(){ return foo + bar; }
  function foo(){ return foo + bar; }
  
  window.baz = baz; //将baz赋值给window对象,或者外部变量
})();

console.log(baz()); //Hello World
console.log(foo()); //错误

首先关于自执行函数的语法我们一般用( function(){} )();如果你看过一些js库的源码你还会发先其他的语法比如+ function() {}();。如果你不明白其与function(){} 的区别,你可以去了解一下函数声明与函数表达式。 在以上面代码中foo不会执行而baz会执行。因为baz赋值给了window,这样baz就暴露在了全局作用域,而foo没有所以只能在匿名函数内部调用。显然,自执行匿名函数结合闭包特性可以有效实现有选择性地对外暴露接口,我们想让谁出来谁就出来,是不是很方便,而且特别是我们有多个js文件,能够有效避免变量、函数命名冲突。

四、循环中的闭包 先来看一段代码:

for(var i=0;i<4;i++)
    setTimeout(function(){console.log(i)},0)    //输出4,4,4,4

for(var i=0;i<4;i++)
    setTimeout(function(j){console.log(j)}(i),0)    //输出0,1,2,3

所以,返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。为了得到想要的结果,我们在每次循环中创建变量 i 的拷贝 j 就避免了引用错误。

闭包带来的问题

由于浏览器自身的缺陷,使用闭包时候很可能会造成内存泄露现象,这种现象在IE中尤为突出,内存泄露是一个比较严重的问题,轻则会影响浏览器的响应速度,降低用户体验,重则会造成浏览器无响应等现象。JavaScript的脚本解释器具备一种垃圾回收机制,一般采用的是引用计数的形式,如果一个对象的引用计数为零,垃圾回收机制就会将其回收,这个过程是自动的。但是一旦当垃圾回收机制碰到了闭包,这个过程就变得复杂起来了。在闭包中,因为局部变量可能在将来的某些时刻需要被使用,因此垃圾回收机制不会处理这些被外部引用到的局部变量,因此,倘若一旦出现循环引用,就会容易造成内存泄漏。 其实前端的小伙伴不用太担心,但是在后端的node上内存泄漏了,服务器是会崩溃的。

//1.例子和图片来自《你不知道的JavaScript》

Licensed under CC BY-NC-SA 4.0