js的模块化编程有哪些方式

JavaScript08

js的模块化编程有哪些方式,第1张

基础

我们首先简单地概述一下,自从三年前Eric Miraglia(YUI的开发者)第一次发表博客描述模块化模式以来的一些模块化模式。如果你已经对于这些模块化模式非常熟悉了,大可以直接跳过本节,从“进阶模式”开始阅读。

匿名闭包

这是一种让一切变为可能的基本结构,同时它也是Javascript最棒的特性。我们将简单地创建一个匿名函数并立即执行它。所有的代码将跑在这个函数内,生存在一个提供私有化的闭包中,它足以使得这些闭包中的变量能够贯穿我们的应用的整个生命周期。

复制代码 代码如下:

(function () {

// ... all vars and functions are in this scope only

// still maintains access to all globals

}())

注意这对包裹匿名函数的最外层括号。因为Javascript的语言特性,这对括号是必须的。在js中由关键词function开头的语句总是会被认为是函数声明式。把这段代码包裹在括号中就可以让解释器知道这是个函数表达式。

全局变量导入

Javascript有一个特性叫做隐式全局变量。无论一个变量名在哪儿被用到了,解释器会根据作用域链来反向找到这个变量的var声明语句。如果没有找到var声明语句,那么这个变量就会被视为全局变量。如果这个变量用在一句赋值语句中,同时这个变量又不存在时,就会创建出一个全局变量。这意味着在匿名闭包中使用或创建全局变量是很容易的。不幸的是,这会导致写出的代码极难维护,因为对于人的直观感受来说,一眼根本分不清那些是全局的变量。

幸运的是,我们的匿名函数提供了简单的变通方法。只要将全局变量作为参数传递到我们的匿名函数中,就可以得到比隐式全局变量更清晰又快速的代码了。下面是示例:

复制代码 代码如下:

(function ($, YAHOO) {

// now have access to globals jQuery (as $) and YAHOO in this code

}(jQuery, YAHOO))

模块导出

有时你不仅想要使用全局变量,你还想要声明它们,以供反复使用。我们可以很容易地通过导出它们来做到这一点——通过匿名函数的返回值。这样做将会完成一个基本的模块化模式雏形,接下来会是一个完整的例子:

复制代码 代码如下:

var MODULE = (function () {

var my = {},

privateVariable = 1

function privateMethod() {

// ...

}

my.moduleProperty = 1

my.moduleMethod = function () {

// ...

}

return my

}())

注意我们已经声明了一个叫做MODULE的全局模块,它拥有2个公有的属性:一个叫做MODULE.moduleMethod的方法和一个叫做MODULE.moduleProperty的变量。另外,它还维护了一个利用匿名函数闭包的、私有的内置状态。同时,我们可以很容易地导入需要的全局变量,并像之前我们所学到的那样来使用这个模块化模式。

进阶模式

上面一节所描述的基础已经足以应对许多情况,现在我们可以将这个模块化模式进一步的发展,创建更多强大的、可扩展的结构。让我们从MODULE模块开始,一一介绍这些进阶模式。

放大模式

整个模块必须在一个文件中是模块化模式的一个限制。任何一个参与大型项目的人都会明白将js拆分多个文件的价值。幸运的是,我们拥有一个很棒的实现来放大模块。首先,我们导入一个模块,并为它添加属性,最后再导出它。下面是一个例子——从原本的MODULE中放大它:

复制代码 代码如下:

var MODULE = (function (my) {

my.anotherMethod = function () {

// added method...

}

return my

}(MODULE))

我们用var关键词来保证一致性,虽然它在此处不是必须的。在这段代码执行完之后,我们的模块就已经拥有了一个新的、叫做MODULE.anotherMethod的公有方法。这个放大文件也会维护它自己的私有内置状态和导入的对象。

宽放大模式

我们的上面例子需要我们的初始化模块最先被执行,然后放大模块才能执行,当然有时这可能也不一定是必需的。Javascript应用可以做到的、用来提升性能的、最棒的事之一就是异步执行脚本。我们可以创建灵活的多部分模块并通过宽放大模式使它们可以以任意顺序加载。每一个文件都需要按下面的结构组织:

复制代码 代码如下:

var MODULE = (function (my) {

// add capabilities...

return my

}(MODULE || {}))

在这个模式中,var表达式使必需的。注意如果MODULE还未初始化过,这句导入语句会创建MODULE。这意味着你可以用一个像LABjs的工具来并行加载你所有的模块文件,而不会被阻塞。

紧放大模式

宽放大模式非常不错,但它也会给你的模块带来一些限制。最重要的是,你不能安全地覆盖模块的属性。你也无法在初始化的时候,使用其他文件中的属性(但你可以在运行的时候用)。紧放大模式包含了一个加载的顺序序列,并且允许覆盖属性。这儿是一个简单的例子(放大我们的原始MODULE):

复制代码 代码如下:

var MODULE = (function (my) {

var old_moduleMethod = my.moduleMethod

my.moduleMethod = function () {

// method override, has access to old through old_moduleMethod...

}

return my

}(MODULE))

我们在上面的例子中覆盖了MODULE.moduleMethod的实现,但在需要的时候,可以维护一个对原来方法的引用。

克隆与继承

复制代码 代码如下:

var MODULE_TWO = (function (old) {

var my = {},

key

for (key in old) {

if (old.hasOwnProperty(key)) {

my[key] = old[key]

}

}

var super_moduleMethod = old.moduleMethod

my.moduleMethod = function () {

// override method on the clone, access to super through super_moduleMethod

}

return my

}(MODULE))

这个模式可能是最缺乏灵活性的一种选择了。它确实使得代码显得很整洁,但那是用灵活性的代价换来的。正如我上面写的这段代码,如果某个属性是对象或者函数,它将不会被复制,而是会成为这个对象或函数的第二个引用。修改了其中的某一个就会同时修改另一个(译者注:因为它们根本就是一个啊!)。这可以通过递归克隆过程来解决这个对象克隆问题,但函数克隆可能无法解决,也许用eval可以解决吧。因此,我在这篇文章中讲述这个方法仅仅是考虑到文章的完整性。

跨文件私有变量

把一个模块分到多个文件中有一个重大的限制:每一个文件都维护了各自的私有变量,并且无法访问到其他文件的私有变量。但这个问题是可以解决的。这里有一个维护跨文件私有变量的、宽放大模块的例子:

复制代码 代码如下:

var MODULE = (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

}

// permanent access to _private, _seal, and _unseal

return my

}(MODULE || {}))

所有文件可以在它们各自的_private变量上设置属性,并且它理解可以被其他文件访问。一旦这个模块加载完成,应用程序可以调用MODULE._seal()来防止外部对内部_private的调用。如果这个模块需要被重新放大,在任何一个文件中的内部方法可以在加载新的文件前调用_unseal(),并在新文件执行好以后再次调用_seal()。我如今在工作中使用这种模式,而且我在其他地方还没有见过这种方法。我觉得这是一种非常有用的模式,很值得就这个模式本身写一篇文章。

子模块

我们的最后一种进阶模式是显而易见最简单的。创建子模块有许多优秀的实例。这就像是创建一般的模块一样:

复制代码 代码如下:

MODULE.sub = (function () {

var my = {}

// ...

return my

}())

虽然这看上去很简单,但我觉得还是值得在这里提一提。子模块拥有一切一般模块的进阶优势,包括了放大模式和私有化状态。

我们在开发及接触新技术的时候,总会接触到该技术以何种规范编写,为了更加系统了解这些规范,本文总结了AMD,CMD,CommonJs,UMD,ESM几种规范,供大家学习了解。

AMD 是 RequireJS 在推⼴过程中对 模块定义的规范化 产出,它是⼀个概念,RequireJS是对这个概念的实现,就好⽐JavaScript语⾔是对ECMAScript规范的实现。AMD是⼀个组织,RequireJS是在这个组织下⾃定义的⼀套脚本语⾔

RequireJS:是⼀个AMD框架,可以异步加载JS⽂件,按照模块加载⽅法,通过define()函数定义。第⼀个参数是⼀个数组,⾥⾯定义⼀些需要依赖的包,第⼆个参数是⼀个回调函数,通过变量来引⽤模块⾥⾯的⽅法,最后通过return来输出()。

是⼀个 依赖前置 、 异步定义 的AMD框架(在参数⾥⾯引⼊js⽂件),在定义的同时如果需要⽤到别的模块,在最前⾯定义好即在参数数组⾥⾯进⾏引⼊,在回调⾥⾯加载

AMD 定义了一套 JavaScript 模块依赖异步加载标准,来解决同步加载的问题。模块通过 define 函数定义在闭包中,格式如下:

define(id?: String, dependencies?: String[], factory: Function|Object)

一些栗子:

注意:在 webpack 中,模块名只有局部作用域,在 Require.js 中模块名是全局作用域,可以在全局引用。

定义一个没有 id 值的匿名模块,通常作为应用的启动函数:

依赖多个模块的定义:

模块输出:

在模块定义内部引用依赖:

SeaJS 在推⼴过程中对模块定义的规范化产出,是⼀个同步模块定义,是SeaJS的⼀个标准,SeaJS是CMD概念的⼀个实现,SeaJS是淘宝团队提供的⼀个模块开发的js框架.

通过define()定义, 没有依赖前置 ,通过require加载模块,CMD是 依赖就近 ,在什么地⽅使⽤到模块就在什么地⽅require该模块,即⽤即返,这是⼀个 同步 的概念

在前端浏览器⾥⾯并不⽀持module.exports,CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。

Nodejs端是使⽤CommonJS规范的,前端浏览器⼀般使⽤AMD、CMD、ES6等定义模块化开发的。输出⽅式有2种:默认输出module export 和带有名字的输出exports.area

CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

兼容AMD和commonJS规范的同时,还兼容全局引⽤的⽅式

Function.prototype.method = function (name, func) {//在Function的原型上增加一个函数叫做“method”。该方法会在“类”的原型上增加指定函数名的函数。

    if (!this.prototype[name]) {

        this.prototype[name] = func

    }

}    

Object.method('superior', function (name) {//增加一个超级方法。该方法保存它从原型上继承来方法。用来模拟面向对象中的super关键字。

    var that = this,

        method = that[name]

    return function (  ) {

        return method.apply(that, arguments)

    }

})

var mammal = function (spec) {//定义一个哺乳动物类型。

    var that = {}

    that.get_name = function (  ) {//获取动物名字。

        return spec.name

    }

    that.says = function (  ) {//动物打招呼的方法。

        return spec.saying || ''

    }

    return that

}

//var myMammal = mammal({name: 'Herb'})//创建一个哺乳动物的实例。

var cat = function (spec) {//定义一个“猫”的类型。

    spec.saying = spec.saying || 'meow'//为这个类型指定一个“叫声”,这里是“喵”

    var that = mammal(spec)//创建一个哺乳动物实例。

    that.purr = function (n) {//为这个哺乳动物创建一个猫的咕噜咕噜声音的方法“purr”

        var i, s = ''

        for (i = 0 i < n i += 1) {

            if (s) {

                s += '-'

            }

            s += 'r'

        }

        return s

    }

    that.get_name = function (  ) {//获取“猫”的名字。猫的叫声(meow) + 猫的名字 + 猫的叫声(meow)

        return that.says(  ) + ' ' + spec.name +

                ' ' + that.says(  )

}

    return that

}

var myCat = cat({name: 'Henrietta'})//创建一只猫的实例。名字叫Henrietta

var coolcat = function (spec) {//创建一个“酷猫”的类型

    var that = cat(spec),//创建一只普通猫。

        super_get_name = that.superior('get_name')//保存普通猫的“获取名字”的方法。

    that.get_name = function (n) {//酷猫有自己的“获取名字”的方法,“like” + 普通猫的“获取名字方法” + “baby”

        return 'like ' + super_get_name(  ) + ' baby'

    }

    return that

}

var myCoolCat = coolcat({name: 'Bix'})//创建一只酷猫。

var name = myCoolCat.get_name(  )//酷猫的名字是like meow Bix meow baby

//        'like meow Bix meow baby'

上面采用prototype(原型扩展)的方式来实现“类”的继承,如,酷猫 >继承 >猫 >继承 >哺乳动物。

而下面采用了另外一种思路:使用工厂模式来创建对象,不是通过复用原型。而是拆分零部件。每个部件都是可拆卸,可组装的方式,来达到另一种代码复用的效果。

function createCar(numberOfDoors){//造车厂,指定门的数量,并制造一辆汽车。

  var numberOfWheels = 4//轮子的数量是4.

  function describe(){//描述一下车辆。

    return "I have " + numberOfWheels + " wheels and " + numberOfDoors + " doors."

  }

  return {//返回制造完成的汽车。

    describe: describe

  }

}

function createOdometer(){//加工车载里程计。

  var mileage = 0

  function increment(numberOfMiles){ mileage += numberOfMiles}//里程计跑表的方法

  function report(){ return mileage }//报告当前的里程数

  return {//返回制造完成的里程计

    increment: increment,

    report: report

  }

}

function createCarWithOdometer(numberOfDoors){//制造带有里程计的汽车

  var odometer = createOdometer()//制造一个里程计

  var car = createCar(numberOfDoors)//制造一辆车

  car.drive = function(numberOfMiles){//将里程计安装到汽车上,当汽车行驶的过程中,调用里程计的跑表方法。

    odometer.increment(numberOfMiles)

  }

  car.mileage = function(){//报告汽车的里程,通过已安装的里程计的报告方法,获取里程。

    return "car has driven " + odometer.report() + " miles"

  }

  return car//返回制造并组装完毕的汽车。

}

var twoFn=createCarWithOdometer(100)//创造一辆带有100个门的汽车(当然不可能。。Σ( ° △ °|||)︴)

console.log(twoFn)

上述两种方式,体现了javascript的精妙之处。

在面向对象语言中:

采用了类继承的方式,实现代码复用。

在javascript中,并没有真正的类。而是用“function”来代表一种类型,

比如:

function Person(){}

我们可以认为,Person就是所谓的javascript中的“类”的概念。我们可以这样

var p = new Person()

alert(p instanceof Person)//结果是true,因为p就是Person类型的一个实例。

alert(p instanceof Object)//结果也是true,因为所有实例终归都是Object。

而每个类型是如果扩展自己的新方法的呢?就是通过类型的prototype这个属性。

只有function才有prototype属性。

比如:

function Person(){}

Person.prototype.say = function(){alert('hello')}//人这种类型有打招呼的方法

var p1 = new Person()

var p2 = new Person()

p1.say()//hello

p2.say()//hello

这样,所有实例就都可以用打招呼的方法了。

当我们创建一个新的类型“男人”。想要继承Person的话。就像下面这样:

function Man(){}

var proto = Man.prototype = new Person()//男人也是人啊,男人的原型就是以一个普通人为标准来参考的,所以我们创建一个人类,赋值给男人,让男人来学习,参考人的特性。

proto.fight = function(){//男人比起人来,脾气大,因此有个打架的方法。

    alert('I\'ll kick your ass!!')

}

var m = new Man()

m.say()//hello

m.fight()//I'll kick your ass.这就是传说中的一句话说不上来就开打。。。

原型继承的核心就是这样的。

而另外一种思路就是工厂模式。

小汽车厂

轮子厂

里程计厂

卡车厂

小汽车厂需要轮子和里程计,分别向“轮子厂”和“里程计厂”要来了两个现成的产品,直接安装好,就完成了一辆小汽车。

卡车长也是一样的。所以这种方式也是一种很好的代码结构。