JavaScript Modules: A Beginner’s Guide 笔记

date
Feb 17, 2018
slug
beginner-guide
status
Published
tags
JavaScript
summary
type
Post
 
原文地址:

前言

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. 避免全局命名空间污染
  1. 依赖关系更加清晰
注意: CommonJS 采用同步加载模块的策略,而从硬盘读取一个模块要比浏览器加载一个模块快得多,所以它主要用于服务端编程(NodeJS),而不适用于浏览器端。

CommonJS 的循环依赖解法

假设有a、b两个模块互相依赖对方
//------ a.js ------
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

//------ b.js ------
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

//------ main.js ------
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
在 CommonJS 规范中,当遇到require() 语句时,会执行同步地执行 require 模块中的代码,并缓存执行的结果,当下次再次加载时不会重复执行,而是直接取缓存的结果。正因为此,出现循环依赖时才不会出现无限循环调用的情况。
但是 CommonJS 的循环依赖解法有1个限制
  • b 引用 a, 需要用到 a 中的 done 变量,那么 done 变量在 a 中需要先于 require('b')进行导出,这样才能保证 b 模块能拿到 done 变量

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 Module

ES Module 的特点,也就是它的设计目标:
  • 推崇模块的默认导出
  • 静态模块模型
  • 同时支持同步和异步加载
  • 支持循环依赖
 

© Sytone 2021