codecamp

深入理解JavaScript系列(3)

大纲
  1. 1. 内容简要
  2. 2. BACKBONE
    1. 2.1. 基本用法
    2. 2.2. 匿名闭包和全局变量
    3. 2.3. 高级用法
      1. 2.3.1. 扩展
      2. 2.3.2. 克隆和继承
      3. 2.3.3. 跨文件共享私有对象
      4. 2.3.4. 子模块
  3. 3. 总结

本文是深入理解JavaScript系列的第篇读文笔记,博客原文在这里

内容简要

本文是整个深入理解JavaScript系列中第一篇稍微有点难啃的文章,特别是对新手来说。大叔这篇文章的写作借鉴了下面两篇文章,

主要参考的是前者。阐述的内容就是模块化模式

首先,什么叫模块化模式?它具有哪些特点?

模块化模式(或者说模块化编程)在JavaScript中是一个非常常见的编程技巧,它按照功能(或者业务)将JavaScript代码封装在一起组成一个模块,对外暴露特定的额方法和属性。一般来说,模块化模式具有如下几个特点,

  • 模块化,可重用
  • 将一系列的方法及属性封装在一起,与其他模块彼此独立,不污染全局作用域,松耦合。(大叔这里说的松耦合的意思,我猜测应该就是指模块的独立性)
  • 暴露特定的接口(方法或者属性),模块的其他方法外部不可访问

BACKBONE

文章的主体将分别介绍模块化模式的基本用法和高级用法。

基本用法

话不多说,老夫先把大叔的一段代码拉出来溜溜,

var Calculator = function (eq) {
    /*
    这里可以声明私有成员
    */
    var eqCtl = document.getElementById(eq);
    // 暴露公开的成员
    return {
        add: function (x, y) {
            var val = x + y;
            eqCtl.innerHTML = val;
        }
    };
};

我们可以通过如下的方式来调用:

var calculator = new Calculator('eq');
calculator.add(2, 2);

这里使用了new Calculator()把函数Calculator当作一个构造函数调用。有的同学可能会问,这里不使用new Calculator(),直接调用Calculator()函数好像也是可以的啊,那new Calculator()和不使用new到底啥区别啊?

首先我在之前的文章中有阐述过new xxx()这个操作的本质,不太清楚的可以移步看看。

在大叔举的这个例子中,使用new与不使用new都是没有错的,下面的calculator.add方法都是可以调用的。

不过这两者有自己适合的场景,

  • 使用new一定会返回一个object,其内部的this指向object自身,且object中方法或者属性可以通过this相互访问。这种是使用JavaScript构建OOP编程的基础。
  • 不使用new的含义就是简单的调用函数,其返回值根据函数的return语句来确定,且内部的this都指向全局作用域。这种一般用于命名空间的管理,独立模块的封装。

匿名闭包和全局变量

闭包是函数式编程语言具有的一种语法糖(暂且这么说吧,虽然不太准备)。JavaScript中对闭包的应用随处可见,匿名闭包更是让一切成为可能的基础,而这也是JavaScript最灵活的特性。

下面我们来创建一个最简单的闭包函数,函数内部的代码一直存在于闭包内,在整个运行周期内,该闭包都保证了内部的代码处于私有状态。

(function () {
    // ...所有的变量和function都在这里声明,并且作用域也只能在这个匿名闭包里
    // ...但是这里的代码依然可以访问外部全局的对象
})();

可以看出匿名闭包其实就是一个自执行函数。关于自执行函数在后面的文章中还会更加深入的讨论。现在只需要知道自执行函数的本质是一个函数表达式,在运行时将会产生一个封闭作用域(或者说私有作用域),这个封闭作用域可以访问外部的全局变量,但是不能被外部访问。

这里可能会遇到一个问题,比如下面的代码,

var name = 'a';
(function() {
    console.log(name); // a
    name = 'b'; // 这里不带var的赋值,将会将name隐式的提升为全局变量,且覆盖了闭包外部的name
    console.log(name); // b
})();
console.log(name); // b

可见,由于JavaScript中存在隐式全局变量这样一种东西,当在闭包中需要访问外部的全局变量时,万一操作不当,就会隐式的声明一个你不知道的全局变量。

现在流行的JavaScript库,比如JQuery,都采用这样一种方式来达到从闭包内部访问外部的全局变量的目的,

(function (window, undefined) {
    // JQuery的源码其实就是一个大闭包!!
})(window);

可见,我们可以将全局变量当成一个参数传入到匿名函数然后使用,相比隐式全局变量,它又清晰又快。

有时候可能不仅仅要使用全局变量,而是也想声明全局变量,如何做呢?我们可以通过将匿名函数的返回值赋值给这个全局变量,代码如下,


上面的代码声明了一个全局变量blogModule,并且带有2个可访问的属性:blogModule.AddTopicblogModule.Name,除此之外,其它代码都在匿名函数的闭包里保持着私有状态。

高级用法

下面将会阐述几种稍微高级一点的用法,其实都是对上面所说的基本用法的扩展。

扩展

前面的基本用法可能不太适合大型的项目。因为大型的项目往往会有多人协作开发,每个人负责的模块不尽相同,这时候就需要把一个模块分割到不同的文件中去。

看下面的代码,

var blogModule = (function(my) {
    my.AddPhoto = function () {
        //添加内部代码
    };
    return my;
})(blogModule);

发现了没有,我们将blogModule自身作为参数传递给自执行函数。这个自执行函数执行完毕后,blogModule上就会多处一个方法AddPhoto。试想一下,多人开发中每个人都采用这种模式,那么完全就可以彼此独立的给blogModule添加自己所需的方法和属性。

一般来说,我们还会做一些异常判断,因为多人协作的过程中,每个人都不知道自己拿到的blogModule究竟有哪些东西,甚至都不知道这个对象是否为undefined的,改进如下,

var blogModule = (function(my) {
    my.AddPhoto = function () {
        //添加内部代码
    };
    return my;
})(blogModule || {}); // 这里是重点

如上面的代码,我们在传递参数的时候,其实传入的是一个判断表达式,这样就保证了在闭包内部blogModule必定是不为undefined,是不是很巧妙? :)

然而,有的时候我们在扩展的时候需要对某一些方法进行重写,那么该怎么办呢?看如下的代码,

var blogModule = (function (my) {
    var oldAddPhotoMethod = my.AddPhoto;
    my.AddPhoto = function () {
        // 重写方法,依然可通过oldAddPhotoMethod调用旧的方法
    };
    return my;
})(blogModule);

代码中,我们使用私有变量oldAddPhotoMethod缓存了原先的AddPhoto方法,然后重写了AddPhoto方法。通过这种方式,我们达到了重写的目的,当然如果你想在继续在内部使用原有的属性,你可以调用oldAddPhotoMethod来用。

克隆和继承

var blogModule = (function (old) {
    var my = {},
        key;
    for (key in old) {
        if (old.hasOwnProperty(key)) {
            my[key] = old[key];
        }
    }
    var oldAddPhotoMethod = old.AddPhoto;
    my.AddPhoto = function () {
        // 克隆以后,进行了重写,当然也可以继续调用oldAddPhotoMethod
    };
    return my;
})(blogModule);

说实话,我有点没太看懂这部分的内容,代码中的for in循环其实是将old对象克隆了一份赋值给my对象。但是这个克隆过程中,old对象中的object以及function类型的数据其实并没有发生克隆,只是多了个my对象的相应引用而已。

我不太明白的是,这个跟JavaScript的模块化有什么关系呢?望大神解惑。

跨文件共享私有对象

说实话,这部分的我看了很久才弄明白,别看代码量就10来行,但是真的不是那么好理解的。(也可能是我比较菜。)

这里的需求是这样的,一个module分割到多个文件中,我们想要各个文件中的私有变量能够交叉访问,该怎么做呢?

var blogModule = (function (my) {
    var _private = my._private = my._private || {},
        _seal = my._seal = my._seal || function () {
            delete my._private;
            delete my._seal;
            delete my._unseal;
        },
        _unseal = my._unseal = my._unseal || function () {
            my._private = _private;
            my._seal = _seal;
            my._unseal = _unseal;
        };
    return my;
})(blogModule || {});

任何文件都可以对他们的局部变量_private设置,并且此设置对其他的文件也立即生效。一旦这个模块加载结束,应用会调用blogModule._seal()进行“上锁”,这会阻止外部接入内部的_private。如果这个模块需要再次增生,应用的生命周期内,任何文件都可以调用_unseal()进行“开锁”,然后再加载新文件,加载新文件后又会初始化_pravite_seal_unseal,然后再次调用_seal()“上锁”。

这个过程很巧妙。其实我觉得,各个文件中的私有变量能够交叉访问这个需求简直就是个奇葩,为什么各个文件中的私有变量要交叉访问呢?这不是破坏了模块的独立性么?有人说其实这里的多文件其实仍然描述的是同一个模块,那我想说,这种情况下使用子模块应该是一种更优的选择。

子模块

子模块是最简单也是最经常使用的设计思路,

blogModule.subModule = (function () {
    var my = {};
    // ...
    return my;
})();

其实我个人觉得,子模块+模块扩展就足够应付一般的模块化模式需求了。使用过多的高级用法,必定给代码增加复杂度,给日后的维护和升级肯定为增加难度。

总结

就我目前的经验来说,模块化模式经常用到的几种方法包括,松耦合扩展私有作用域以及子模块。这几种就是最常用的方式。

不过现在业界出现了CMD、AMD等规范后,JavaScript代码的模块化管理更趋向于代码级别而不再是设计级别。所以本文中所描述的模块化模式,我猜测日后将会越来越淡化,越来越轻量化,比如用在配置中,工具方法的管理上等等。

深入理解JavaScript系列(2)
深入理解JavaScript系列(4)
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }