作用域与闭包

首先在讲之前,需要掌握以下几个概念:

  1. javaScript具有两种作用域:全局作用域函数作用域,另外还有块作用域,比如:try …catch…语句中,catch 分句是块级作用域,还有with语句等。
  2. ES6的let关键字,可以用来在任意代码块中声明变量。
  3. IIFE(立即执行函数表达式)是什么?它有什么作用?

    什么是闭包

闭包:函数可以记住并访问所在的词法作用域时,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

function foo(){
    var a=2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz=foo();
baz();// 这里就是闭包的效果

函数bar()词法作用域能够访问foo()的内部作用域,然后我们将bar()函数本身当作一个 值类型 进行传递。在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用了内部的函数bar();
bar()显然可以被正常执行,但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在foo()执行后,桐城期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来解释不再使用的内存空间,由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事的发生。事实上内部作用域依然存在,因此没有被回收,谁在使用这个内部作用域?其实是bar()本身在使用。
由于bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫做闭包

 当然,无论使用何种方式对函数类型的值`进行传递`,当函数载别处被调用时都可以观察到闭包。


function foo()    {
    var a = 2;
    function baz()    {
        console.log(a); //2
    }
    bar(baz);
}
function bar(fn){
    fn(); //看吧,这就是闭包
}

当然也可以间接的传递参数,无论用过何种手段将内部函数传递到所载词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
接下来,我们看一个常见的代码:

functon wait(message) {
    setTimeout(function timer () {
        console.log(message);
    },1000);
}
wait('Hi,closure!');

分析一下该码:将一个内部函数(名为timer)传递给setTimeout(…)。timer具有涵盖wait(…)作用域的闭包,因此还保有对变量message的引用。
wait(…)执行1000ms后,它的内容作用域并不会消失,timer函数依然保有wait(…)作用域的闭包。
在引擎内部,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数也许叫作fn或者func,引擎会调用这个函数,例子中就是内部的timer函数,而词法作用域在这个过程中保持完整
这就是闭包。

闭包在哪些函数中应用?

其实包括定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际就是在使用闭包

这里我吗看一个特别典型的闭包例子,但是严格来说它并不是闭包 ,为什么呢?

var a = 2;
(function IIFE(){
   console.log(a);
})();

因为函数(例子中的IIFE)并不是在它本身的词法作用域以外执行的。它却是在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a通过普通的词法作用域查找而非闭包被发现的。

尽管从技术讲,闭包时发生在定义时的,但它并不非常明显,就如IIFE本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是常用来创建可以被封闭起来的闭包的工具。因此IIFE的确同作用域息息相关,即使本身并不会真的创建作用域。

循环和闭包

要说明闭包,for循环时最常见的例子。

for(var i=1;i<=5;i++) {
    setTimeout(function timer(){
        console.log(i);
    },i*1000);
}

我想新手大多数都被这段代码给坑了吧,为何结果输出的却是5个6呢?
6怎么来的这个不用解释吧(6是循环终止条件),但是5个6事怎么来的就得解释一下了,原因在于延迟函数的回调函数在循环结束时才执行。就算是setTimeout(..,0)定时时间为0,所有的回调函数依然是在循环结束后才会被执行,因此才会每次输出都是6;

这里引申出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?

缺陷就是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数式在各个迭代中分别定义的,但是它们都是被封闭在一个共享的全局作用域中,因此实际上只有一个i。

实际上这个是这个样子,图解:
QQ20170224-1
而我们实际想要的却是这样的:
344C442F-CDE7-4343-A2AD-10377485832F

下面回到正题。既然明白了缺陷是什么,那么要怎样做才能达到我们想象中的样子呢?答案是我们需要在每一次迭代的过程中都创建一个闭包作用域。在上文中我们已经有所铺垫,IIFE会通过声明立即执行一个函数来创建作用域。我们可以将代码改成下面的样子:

for(var i=1; i<=5; i++){
    (function(){
        setTImeout(function time(){
            console.log(i)
        }, i*1000)
    })();
}

这样每一次迭代我们都创建了一个封闭的作用域(你可以想象为上图中黄色的矩形部分)。但是这样做仍旧不行,为什么呢?因为虽然每个延迟函数都会将IIFE在每次迭代中创建的作用域封闭起来,但我们封闭的作用域是空的,所以必须传点东西过去才能实现我们想要的结果。

for(var i=1; i<=5; i++){
    (function(){
        var j = i
        setTImeout(function time(){
            console.log(j)
        }, j*1000)
    })();
}

ok!试试现在他能正常工作吗?对这段代码再进行一点改进

for(var i=1; i<=5; i++){
    (function(j){
        setTImeout(function time(){
            console.log(j)
        }, j*1000)
    })(i);
}

总的来说,就是在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数可以将新的作用域封闭在每个迭代内部,我们同时在迭代的过程中将每次迭代的i值作为参数传入进新的作用域,这样在迭代中创建的封闭作用域就都会含有一个具有正确值的变量供我们访问。ok,it’s work!

###块作用域

仔细思考我们前面的解决方案。我们使用IIFE在每次迭代时都创建一个新的作用域。也就是说,每次迭代我们都需要一个块作用域。前面我们提到,你需要对ES6中的let关键字进行了解,它可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
本质上来讲它是将一个块转换成可以被关闭的作用域

for(var i=1; i<=5; i++){
        let j = i; //闭包的块作用域
        setTImeout(function time(){
            console.log(j)
        }, j*1000)
}

如果将let声明在for循环的头部那么将会有一些特殊的行为,有多特殊呢?它会指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。不管这句话有多拗口,看看代码吧!

    for(let i=1; i<=5; i++){
        setTImeout(function time(){
            console.log(i)
        }, i*1000)
}

闭包的运用————模块

模块的代码模式就是利用了闭包的强大威力,但从表面上看,它们似乎与回调无关。下面一起来研究:模块。

function foo () {                            
   var something = 'cool';                    
   var another = [1,2,3];                     
    function doSomething () {                 
         console.log(something);              
    }                                         
    function doAnother() {                    
         console.log(another.join("!"));      
    }                                         
}                                             

这段代码里并没有明显的闭包,只有两个私有数据变量something和another,以及doSomething()和doAnoter()两个内部函数,它们的词法作用域(而这就是闭包)也就是foo()的内部作用域。
接下来考虑以下代码:

function coolModule () {                            
   var something = 'cool';                    
   var another = [1,2,3];                     
    function doSomething () {                 
         console.log(something);              
    }                                         
    function doAnother() {                    
         console.log(another.join("!"));      
    }
    return {
        doSomething:doSomething,
        doAnother:doAnother
    };                                      
}   
var  foo = coolModule();
foo.doSomething();//cool
foo.doAnother(); //1!2!3 

这个模式在javascript中被称为模块。最常见的实现模块模式的方法就是模块暴露,这里展示的是它的演变形式。
接下来我么分析一下代码:
首先,coolModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
其次,coolModule()返回一个用对象字面量语法{key:value,…}来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API

这个对象类型的返回值最终被赋值给外部的变量foo,然后就可以通过它来访问API的属性方法了。

doSomething()和doAnother()函数具有涵盖模块实例内部作用域的闭包(用过调用CoolModule()实现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创造了可以观察实践闭包的条件。

如果要简单的描述,模块模式需要具备2个必要条件:
1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

注意:一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。

闭包作用之一;比如扩展该对象,你可以添加自己的私有成员和方法,然后使用闭包在其内部封装这些变量和函数声明。只暴露你想暴露的public成员和方法

上面的代码很不错了,但如果我们想做到只有在使用的时候才初始化,那该如何做呢?为了节约资源的目的,我们可以另外一个构造函数里来初始化这些代码。

下面我来实现一个简单单例模式:

var foo=(function coolModule () {
     // 私有变量和方法
    var something = 'cool';
    var another = [1, 2, 3];
    function doSomething() {
        console.log(something);
    }
    function doAnother() {
        console.log(another.join("!"));
    }
     //公有属性与方法(可以访问私有变量和方法)
    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
})(); //利用了IIFE
foo.doSomething(); //cool
foo.doAnother(); // 1!2!3

我们将模块函数转换成了IIFE,立即调用这个函数并将返回值直接赋值给单例的模块实例标识符foo。

知道了单例如何实现了,但单例用在什么样的场景比较好呢?其实单例一般是用在系统间各种模式的通信协调上,下面的代码是一个单例的最佳实践:

var SingletonTester = (function () {
   //参数:传递给单例的一个集合
   function Singleton (args) {
       //设置args变量为接收的参数或者为空(如果没有提供的话)
       var args = args||{};
       //设置name参数
       this.name="SingletonTester";
       this.pointX = args.pointX || 6 ; //从接收的参数里获取,或者设置为默认值
       this.pointY = args.pointY || 8;
   }
   //实例容器
   var instance;
   var _static = {
       name:'SingletonTester',
       //获取实例的方法
       //返回Singleton的实例
       getInstance:function (args) {
           if(instance === undefined){
               instance = new Singleton(args);
           }
           return instance;
       }
   };
   return _static;
})();
var st = SingletonTester.getInstance({pointX:23,pointY:89});
console.log(st.pointX); //23
文章目录
  1. 1. 什么是闭包
  2. 2. 闭包在哪些函数中应用?
  3. 3. 循环和闭包
  4. 4. 闭包的运用————模块