我见过不少初学前端的朋友们认为,要成为前端的高手,就要学会那些主流的框架。比如前两年流行的AngularJS,今年的ReactJS。还有朋友学了ReactJS后,想学ReactNative成为iOS高手。当我问他们一些Javascript的基本概念,比如什么是闭包,什么是事件冒泡机制,什么是原型链,为什么要有立即执行函数时,却答不上来。因为用了框架后,在一些小应用中,你根本不需要接触上述概念。但是这些原理性的东西确是你通往高手殿堂的必经之路。

框架只是一种实现的方法,每个框架都是一种开发思想的产物,而这个思想会随着时代的变化完全被改变。Java的后端经历了从EJB,到Struts,到Struts2和SpringMVC的过程。而前端就更快了,短短几年Backbone,Ember,Angular,React,马上一个全新的Angular2就要正式发布了。了解框架是必须的,但是千万不要以为学了框架就是高手,真正的高手是真正懂得框架背后的编程思想,懂得框架之下的底层原理,懂得最适合当前你所做的项目的前端,后端,客户端的开发架构。扯那么多,主要是想抒发下对当下开发人员浮躁之风的感触。这篇文章要介绍的就是Javascript中原理性的东西:闭包和立即执行函数。

闭包 - Closure

先看个闭包的例子。我们想实现一个计数器,最简单的方法就是定义一个全局变量,计数的时候将其加1。但是全局变量有风险,哪里都有可能不小心改掉它。那局部变量呢,它只在函数内部有效,函数调用完后它就没了,全局没法使用。那我们又想让计数器全局能使用,又不想让这个变量可以随便修改怎么办。这就需要闭包了:

function count() {
    var i = 0;
    return function() {
        return ++i;
    }
}

这个例子实现了一个简单的计数器。函数count()定义了一个局部变量i,并返回一个内部匿名函数。因为是内部函数,所以它可以访问其外部函数的局部变量i,并且将其加1并返回。让我们看下怎么使用这个计数器。

c1 = count();
console.log(c1());    // print 1
console.log(c1());    // print 2
console.log(c1());    // print 3
c2 = count();
console.log(c2());    // print 1

每次调用count()函数后就会生成一个计数器,而且不同的计数器之间不干扰。因为两次调用同一个函数,创建的栈是不同的,因此栈内的局部变量是不同的。上例中,我们生成了全局的计数器c1c2,他们都是不带参数的函数,即count()中返回的匿名函数。此后每次调用计数器,比如c1(),计数就会自增1并返回。但是由于count()函数已经调用完毕,我们将无法通过任何其他办法去修改count()中变量i的值。这就是闭包最实用的功能,就是将你想操作的变量或对象隐藏起来,只允许特定的方法才能访问它。

立即执行函数 - Immediately Invoked Function

n年前看到jQuery的源码时,很好奇它的最外层代码结构是这个样子的(现在已经不一样了):

var jQuery = (function() {
    ...
})();

作为前端小白的我,实在想不通这是为什么,好好定义一个函数,为啥还要调用它。大家知道Javascript在ES6之前并不严格支持面向对象。JS的对象其实就是一个map,比如下面的例子:

var car = {
    speed: 0,
    start: function() { this.speed = 40;},
    getSpeed: function() {return this.speed;}
};

car.start();
console.log(car.getSpeed());    // print 40

这个对象有其成员变量speed及成员函数start()getSpeed(),但是它的成员变量没法私有化,同时它也没法被继承。要实现对象的继承,你可以使用构造函数和原型继承。但怎么才能将成员变量私有化来实现对象的封装呢(而且有时候我们也不想那么麻烦使用原型)?有心的读者看了上面闭包的介绍,肯定马上有了想法。对,使用闭包!

function car() {
    var speed = 0;
    return {
        start: function() { speed = 50;},
        getSpeed: function() {return speed;}
    }
}

var car1 = car();
car1.start();
console.log(car1.getSpeed());    // print 50

说了那么多,跟立即执行函数有什么关系呢。你再仔细看看上面的例子,你有了闭包函数来帮你创建car对象,这个函数就类似于工厂方法,它可以根据你的需要创建多个不同的对象。不过开发的时候经常会遇到这样的情况,就是我们希望对象只有一份,比如jQuery库的对象,它必须确保整个程序只有一份,多了也没用。在后端开发模式中,这叫单例模式,可以通过私有化构造函数来实现,那么在JS里呢?

既然函数没法私有话,那唯一的办法就是让这个工厂方法能且只能被调用一次。不能多次被调用,那这个函数一定要是匿名的;而且能被调用一次,那就必须在声明的时候立马就执行。这时候,我们就可以邀请立即执行函数出场了:

var car = (function() {
    var speed = 0;
    return {
        start: function() { speed = 60;},
        getSpeed: function() {return speed;}
    }
})();

car.start();
console.log(car.getSpeed());    // print 60

很多人一开始会看错,认为对象car是一个函数,其实它是这个匿名的工厂方法执行完返回的对象,该对象拥有start()getSpeed()两个成员函数,而这两个函数所需要访问的speed变量对外不可见。同时你无法再次调用这个匿名的工厂方法来创建一个相同的对象。是不是很神奇?一个单例的,有着私有成员的对象就这么创建好了。

立即执行函数还有种写法就是:

var car = (function() {
    ...
}());

我个人比较喜欢前一种。

一直很崇拜前端的大牛们因为他们能把灵活的脚本语言玩出各种花样来,而后端的开发模式相对固定。当然这样也有好处,就是可以限制初级程序员乱来。现在两端的开发模式都在互相学习中,前端可以用Coffee Script和Less,Sass等来提高代码的可读性,而后端也开始支持如Lambda,var变量等灵活的开发方式。作为一名开发人员,我个人觉得,不要把自己限制在前端或者后端,两端都要懂。你可以其中一块更强,但另一块也绝对拿得出手。