JavaScript Modules: A Beginner’s Guide 笔记

原文地址:

前言

JavaScript 这门语言刚被设计出来的时候,它的开发者根本没有想到它在今天会如此流行,也没有将其设计成一门模块化的语言。如今,用户体验越来越重要,前端工程也越来重,若没有模块化的组织,使用 JavaScript 开发项目必定十分痛苦。现在,通过学习这篇文章来了解:

  • 模块化解决了前端开发中的哪些痛点?
  • JavaScript 模块化是怎么实现的?
  • 现如今的 JavaScript 模块化方案有哪些?

笔记

我们为什么需要模块化

1)可维护性: 模块是自包含的。一个设计良好的模块旨在尽可能地减少对其他代码库的依赖,以便能够独立地维护与扩展,当一个模块与其他依赖分离时,维护起来要容易很多。
2)命名空间: 在 JavaScript 中,top-level函数的作用域以外的变量所处的作用域是全局的。每个人都可以访问全局变量,这样很容易造成命名空间的污染。而模块化能够创造为一个模块所需要的变量创造私有空间,避免了命名空间的污染。
3)可复用: 例如,我们将自己写的某个工具函数封装成一个设计良好的模块,那么我们就可以在多个项目中引用它从而实现了模块的复用,如果要维护更新这个模块,也不会引起side-effect

如何实现模块化(模块设计模式)

创建一个闭包(IIFE模式)

var global = 'Hello, I am a global variable :)';

(function () {
  // We keep these variables private inside this closure scope
  
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item}, 0);
    
    return 'Your average grade is ' + total / myGrades.length + '.';
  }

  var failing = function(){
    var failingGrades = myGrades.filter(function(item) {
      return item < 70;});
      
    return 'You failed ' + failingGrades.length + ' times.';
  }

  console.log(failing());
  console.log(global);
}());

// 'You failed 2 times.'
// 'Hello, I am a global variable :)'

闭包很好的解决了命名空间的问题,上述代码中的myGrades成为了私有变量,在闭包外无法访问;而全局变量则仍可以在闭包内访问。

Global Import

闭包虽然能直接访问全局变量,但是这样的写法并不优雅,依赖关系并不清晰。优化一下,我们可以将全局变量注入到闭包中:

(function (globalVariable) {

  // Keep this variables private inside this closure scope
  var privateFunction = function() {
    console.log('Shhhh, this is private!');
  }

  // Expose the below methods via the globalVariable interface while
  // hiding the implementation of the method within the 
  // function() block

  globalVariable.each = function(collection, iterator) {
    if (Array.isArray(collection)) {
      for (var i = 0; i < collection.length; i++) {
        iterator(collection[i], i, collection);
      }
    } else {
      for (var key in collection) {
        iterator(collection[key], key, collection);
      }
    }
  };

  globalVariable.filter = function(collection, test) {
    var filtered = [];
    globalVariable.each(collection, function(item) {
      if (test(item)) {
        filtered.push(item);
      }
    });
    return filtered;
  };

  globalVariable.map = function(collection, iterator) {
    var mapped = [];
    globalUtils.each(collection, function(value, key, collection) {
      mapped.push(iterator(value));
    });
    return mapped;
  };

  globalVariable.reduce = function(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    globalVariable.each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

    return accumulator;

  };

 }(globalVariable));

这样写除了能使代码结构更加清晰,而且性能也更好。因为在闭包内部调用gloablVarible的时候,解释器能够直接找到局部的gloablVarible,而不用在整个全局作用域寻找。

创建一个对象作为接口

闭包能够实现变量的私有化,但是如果如要像 Java 的类一样兼具私有与公共属性,则需要创建一个自包含(self-contained)的对象接口:

var myGradesCalculate = (function () {
    
  // Keep this variable private inside this closure scope
  var myGrades = [93, 95, 88, 0, 55, 91];

  // Expose these functions via an interface while hiding
  // the implementation of the module within the function() block

  return {
    average: function() {
      var total = myGrades.reduce(function(accumulator, item) {
        return accumulator + item;
        }, 0);
        
      return'Your average grade is ' + total / myGrades.length + '.';
    },

    failing: function() {
      var failingGrades = myGrades.filter(function(item) {
          return item < 70;
        });

      return 'You failed ' + failingGrades.length + ' times.';
    }
  }
})();

myGradesCalculate.failing(); // 'You failed 2 times.' 
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

如要创建一个公共变量/方法,可以通过把它们放在return语句中返回来暴露给外部作用域,

Revealing module pattern

上面代码的return语句中,两个函数的声明与实现耦合在一起,需要再优化一下:首先默认所有的变量和方法都是私有的,然后在return时,才选择性暴露对外的属性。


var myGradesCalculate = (function () {
    
  // Keep this variable private inside this closure scope
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item;
      }, 0);
      
    return'Your average grade is ' + total / myGrades.length + '.';
  };

  var failing = function() {
    var failingGrades = myGrades.filter(function(item) {
        return item < 70;
      });

    return 'You failed ' + failingGrades.length + ' times.';
  };

  // Explicitly reveal public pointers to the private functions 
  // that we want to reveal publicly

  return {
    average: average,
    failing: failing
  }
})();

myGradesCalculate.failing(); // 'You failed 2 times.' 
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

这种写法称为:Revealing module pattern

JavaScript 模块化解决方案

如果要写一个体量较大的项目,不同模块之间的依赖关系就会变得复杂,而上面实现的模块方案无法管理模块之间的依赖关系。另外,不同模块之间仍然存在命名空间冲突的问题。在官方正式推出相关标准之前,CommonJS 和 AMD 给出了解决方案。

CommonJS

CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。Node 的 module 遵循的就是 CommonJS 规范。CommonJS 并不是属于 ECMAScript TC39 小组的工作,但 TC39 中的一些成员参与 CommonJS 的制定。更多关于 CommonJS 的介绍请查看 http://wiki.commonjs.org/wiki/CommonJS
定义一个 CommonJS module:

function myModule() {
  this.hello = function() {
    return 'hello!';
  }

  this.goodbye = function() {
    return 'goodbye!';
  }
}

module.exports = myModule;

module是 CommonJS 规范中预先定义好的对象。如果其他代码想使用 myModule 模块,便可以通过require引入:

var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

CommonJS 的优点:

  1. 避免全局命名空间污染
  2. 依赖关系更加清晰

注意: CommonJS 采用同步加载模块的策略,而从硬盘读取一个模块要比浏览器加载一个模块快得多,所以它主要用于服务端编程(NodeJS),而不适用于浏览器端。

AMD

显然,在浏览器端我们需要一个支持异步加载模块的模块化规范—— AMD(Asynchronous Module Definition)。RequireJS 是对 AMD 的实现。
通过 AMD 的方式来引入模块:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
  console.log(myModule.hello());
});

define的第一个参数是包含所需依赖的模块名称的数组。AMD 会异步地将模块加载,等模块加载完毕之后,回调函数才会执行。
define既可以引用模块,也可以定义模块:


define([], function() {

  return {
    hello: function() {
      console.log('hello');
    },
    goodbye: function() {
      console.log('goodbye');
    }
  };
});

除了支持异步加载,AMD 的另一个好处是你的模块可以是对象,函数,构造函数,字符串,JSON 和其他类型,而 CommonJS 只支持对象作为模块。

UMD

假如我们想写一个同时可以在浏览器端和服务端运行的模块,那么这个模块就需要同时支持 AMD 和 CommonJS。这时,你就需要 UMD(Universal Module Definition)。

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      // AMD
    define(['myModule', 'myOtherModule'], factory);
  } else if (typeof exports === 'object') {
      // CommonJS
    module.exports = factory(require('myModule'), require('myOtherModule'));
  } else {
    // Browser globals (Note: root is window)
    root.returnExports = factory(root.myModule, root.myOtherModule);
  }
}(this, function (myModule, myOtherModule) {
  // Methods
  function notHelloOrGoodbye(){}; // A private method
  function hello(){}; // A public method because it's returned (see below)
  function goodbye(){}; // A public method because it's returned (see below)

  // Exposed public methods
  return {
      hello: hello,
      goodbye: goodbye
  }
}));

UMD 会优先判断是当前环境是否支持 AMD,然后再检验是否支持 CommonJS,否则认为当前环境为浏览器环境。

ES6 Modules

终于,TC39 在 ECMAScript 6 中引入了原生模块化解决方案。
相对于 CommonJS 或 AMD 而言,ES6 Modules 的优点在于它:

  • 紧凑的声明式语法
  • 异步加载
  • 对循环依赖更好的支持http://exploringjs.com/es6/ch_modules.html#sec_cyclic-dependencies
  • ES6 Modules 中导入的是模块的“只读视图”,类似于指针,而 CommonJS 则是普通的值传递或者引用传递。例子:
    • CommonJS:
      // lib/counter.js
      
      var counter = 1;
      
      function increment() {
        counter++;
      }
      
      function decrement() {
        counter--;
      }
      
      module.exports = {
        counter: counter,
        increment: increment,
        decrement: decrement
      };
      
      
      // src/main.js
      
      var counter = require('../../lib/counter');
      
      counter.increment();
      console.log(counter.counter); // 1
      
    • ES Modules
      // lib/counter.js
      export var counter = 1;
      
      export function increment() {
        counter++;
      }
      
      // src/main.js
      import { counter, increment } from '../../lib/counter';
      
      console.log(counter); // 1
      increment();
      console.log(counter); // 2
      
Show Comments