深入理解javascript系列(九):应用闭包

1,727 阅读6分钟

理论是自信的基础,结合理论的实践才能让我们走的更远。

前两个系列,我记录了闭包的学习,如何利用闭包解决实际问题了?其实,很多东西你我都知道,不是一蹴而就的,不是你今天学了就会了,还需要多次练习,反复练习。相信终究一天你我会运用自如。

下面就通过3个小例子,来运用闭包解决实际问题吧。

9.1  循环、setTimeout与闭包

在面试题中常常会遇到一个与循环、闭包有关的问题,如下:

//利用闭包的知识修改这段代码,让代码的执行结果为隔秒输出1,2,3,4,5
for (var i = 1; i<=5; i++) {
    setTimeout(timer=()=>{
        console.log(i)
    },i*1000)
}

首先来分析一下如果直接运行这个例子会输出什么结果。(涉及到定时器线程的相关知识还需要自行学习)

前面我们已经知道,for循环的大括号并不会形成自己的作用域,因此这个时候肯定是没有闭包产生的,而i值作为全局的一个变量,会随着循环的过程递增。因此循环结束之后,i变成了6。

而每一个循环中,setTimerout的第二个参数访问的都是当前的i值,因此第二个i值分别是1,2,3,4,5。而第一个参数timer函数中虽然访问的是同一个i值,但是由于延迟的原因,当timer函数被setTimerout运行时,循环已经结束,即i已经变为6了。

因此这段代码执行结果是隔秒输出6.

而我们想要的是隔秒输出1,2,3,4,5,因此需要借助闭包的特性,将每一个i值都用一个闭包保护起来。每一轮循环,都把当前的i值保存在一个闭包中,当setTimerout中定义的操作执行时,访问对应闭包即可。

这个时候我们回想一下闭包形成的条件,简单来说,就是一个函数中定义了一个子函数,子函数内部访问了函数的变量对象。因此我们只需要创建一个这样的环境即可。

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

定义一个匿名函数,称作A,并将其当作闭包环境。而timer函数则作为A的内部函数,当A执行时,只需访问A的变量对象即可。因此将i值作为参数传入,这样也就满足了闭包的条件,并将i值保存在了A中。

同样的道理也可以在timer函数里做文章。还有更多方法,这里就不一一赘述。

9.2  单例模式与闭包

好,我继续我的笔记。

在javascript中有许多解决特定问题的编码思维(设计模式,Alloy Team出的那本个人觉得很棒),例如工厂模式、发布订阅模式、装饰者模式、单例模式等。其中,单例模式是早期开发最常用的模式之一,而它的实现,与闭包戚戚相关。

所谓单例模式,就是只有一个实例。

1.  最简单的单例模式

对象字面量的方法就是最简单的单例模式,我们可以将属性与方法依次放在字面量里。

var person = {
    name: 'pan',
    age: '18',
    getName: function() {
        return this.name
    },
    getAge: function() {
        return this.age
    }
}

但是这样的单例模式有一个严重的问题,即它的属性可以被外不修改。因此在许多场景中,这样的写法并不符合我们的需求,我们更期望对象能够有自己的私有方法与属性。

PS:个人觉得不论学习什么,都应该回顾它的历史,以便更利于当下的学习。理论性知识发展到目前,不是偶然,总有那么一个阶段性的过渡。司徒正美在他的作品《javascript框架设计》前言中就说到了这么一个事“当初,我阅读jQuery源码,最初看的是1.4.3版本,看得一头雾水,一气之下,从最初的1.0版本开始看。看完所有版本,了解其迭代过程,才明白”。

2.  有私有方法/属性的单例模式

通过前面所学的知识我们很容易就能想到,想要一个对象拥有自己私有的方法属性,那么只需要创建一个单独的作用域将对象与外界隔离起来就行了。这里我们借助匿名函数自执行的方式即可。

var person = (function() {
     var name = 'pan';
     var age = 18;
 
     return{
        function getName() {        return name
     };
     function getAge() {
         return age
     }    }   
})();
person.getName();

私有变量的好处在于,外界对于私有变量能够进行什么样的操作是可以控制的。我们可以提供一个getName方法让外界可以访问名字,也可以额外提供一个setName方法,来修改它的名字。对外提供什么样的能力,完全由我们自己决定。

现在我们正走在模块化的道路上....

3.  调用时才初始化的单例模式

有的时候(使用频次较少)我们希望自己的实例仅仅只是在调用的时候才被初始化,而不像上面两个例子那样,即使没有调用person,person的实例在函数自执行的时候就返回了。

那么我们就需要在上面例子的基础上做一点小小的改动了。

var person = (function(){
    //定义一个变量,用来保存实例
    var instance = null;
    var name = 'pan';
    var age = 18;

    //初始化方法
    function init() {
        return{
            getName: function() { return name;},
            getAge: function() { return age;}
        }
    }

    return {
        getInstance: function() {
                if(!instance){
                    instance = init()
                }
                return instance
            }
    }
})();

//只在使用时获取实例
var p1 = person.getInstance();

在这个例子中,我们对匿名函数中定义了一个instance变量用来保存实例。在getInstance方法中判断了是否对他进行重新赋值。由于这个判断的存在,因此变量instance仅仅只在第一次调用getInstance方法时赋值了。

在写一个系列,我将持续更新我闭包相关应用。感谢阳波大神。

这些都是我以往的学习笔记。如果您看到此笔记,希望您能指出我的错误。有这么一个群,里面的小伙伴互相监督,坚持每天输出自己的学习心得,不输出就出局。希望您能加入,我们一起终身学习。欢迎添加我的个人微信号:Pan1005919589