JavaScript Modules: A Beginner’s Guide 笔记
date
Feb 17, 2018
slug
beginner-guide
status
Published
tags
JavaScript
summary
type
Post
前言 我们为什么需要模块化如何实现模块化(模块设计模式)创建一个闭包(IIFE模式)Global Import创建一个对象作为接口Revealing module patternJavaScript 模块化解决方案CommonJSCommonJS 的循环依赖解法AMDUMDES6 Module
原文地址:
前言
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 的优点:
- 避免全局命名空间污染
- 依赖关系更加清晰
注意: 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 的特点,也就是它的设计目标:
- 推崇模块的默认导出
- 静态模块模型
- 同时支持同步和异步加载
- 支持循环依赖