这部分内容是《javaScript设计模式》的笔记,这本书成书在2010年左右,内容有点老。所讲的模式是JS流行初期,工程师对其封装方式的一些尝试和总结。这部分内容是为了了解封装的概念和从无到有的过程。后面笔记中的ES6模块和CommonJS模块是根据这些模块特点对模块的标准实现方式。
Module模式主要解决javascript的封装问题;在module模式产生的时期,javascript还没有块作用域的概念。所以Module模式的封装依靠javascript函数的闭包来实现。所有的内容都定义在一个立即执行的函数中。返回一个对象,包含所有的公用API。特点如下:
/**经典Module模式
* 通过闭包来实现对私有函数和变量的封装。基本结构就是一个立即执行的匿名函数,
* 返回一个对象,包含所以的公共API。
*/
var basketModule = (function() {
//函数创造一个私有作用域
var basket = [];
function doSomethingPrivate() {
//还没想好要实现啥
}
function doSomethingElsePrivate() {}
//返回一个暴露公有API的对象
return {
addItem: function(values) {
basket.push(values);
},
getItemCount: function() {
return basket.length;
},
doSomething: doSomethingPrivate,
getTotal: function() {
var itemCount = this.getItemCount(),
total = 0;
while (itemCount--) {
total += basket[itemCount].price;
}
return total;
}
}
}());
basketModule.addItem({
item: 'bread',
price: 0.5
});
basketModule.addItem({
item: 'butter',
price: 0.3
});
console.log(basketModule.getItemCount());//2
console.log(basketModule.getTotal()); //0.8
console.log(basketModule.basket);//undefined
Revealing Module模式,是Module模式的一种改进;在闭包内定义所有的函数和变量(这样我们在定义变量名时,就更符合模块本身的含义,而不需要考虑作为API的命名),返回一个匿名对象,它拥有指向私有函数或变量的指针。匿名对象的属性是希望展示为共有的API。后面讲的CommonJS就是一种揭示模块的模式。它的特点如下:
下面是书上的简单例子,可以看出它的结构比上面Module模式更清爽。
/**Revealing Module模式
* 在前面module模式的基础上使输出的API命名更加一致,方便维护
* ES6中的export{}跟它有点像
*/
var myRevealingModule = (function() {
var privateCounter = 0;
function privateFunction() {
privateCounter++;
}
function publicFuntion() {
publiceIncrement(); //开始时,先自增一次,这样就不会跟之前的相同了
}
function publiceIncrement() {
privateFunction();
}
function publicGetCount() {
return privateCounter;
}
//暴露公有指针指向私有函数和属性上
return {
start: publicFuntion,
increment: publiceIncrement,
count: publicGetCount
}
}());
myRevealingModule.start();
console.log(myRevealingModule.count());//1
myRevealingModule.increment();
console.log(myRevealingModule.count());//2
Singleton模式限制了类的实例化只有一次;即:在实例不存在的情况下,创建类的新实例;如果实例已经存在,它简单的返回该对象的引用。它适用的场景:
/**Singleton模式
* 单例模式的特点是它限制了类的实例化次数只能一次,使用场景:
* 1、当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时;
* 2、该唯一的实例应该是通过子类化可拓展的,而且客户应该无需更改代码就能使用一个拓展的实例时。
*/
var mySingleton = (function() {
var instance;
function init() {
//Singleton 私有方法和变量
function privateMethod() {
console.log('I am private');
}
var privateVariable = 'I\'m also private';
var privateRandomNumber = Math.random();
return {
//public methods and variable
publicMethod: function() {
console.log('The public can see me!');
},
publicProperty: 'I am also public',
getRandomNumber: function() {
return privateRandomNumber;
}
};
};
return {
//获取Singleton的实例,如果存在直接返回;不存在,创建新实例然后返回
//这个地方返回一个函数,可以延时加载,先记载mySingleton,使用时再调用getInstance
getInstance: function() {
if (!instance) {
instance = init();
}
return instance;
}
}
}());
var singleA = mySingleton.getInstance();
var singleB = mySingleton.getInstance();
console.log(singleA === singleB); //true
/**
* 下面的代码实现了拓展,
* FooSingleton是BaseSingleton的子类,我们使用一个类似工厂函数来
* 选择不同instance对象。
* */
mySingleton.getInstance = function() {
if (this._instance == null) {
if (isFoo()) {
this._instance = new FooSingleton();
} else {
this._instance = new BasicSingleton();
}
}
return this._instance;
}
CommonJS是模块的一种实现理论。NodeJS中的模块是CommonJS的一种实现方式。Webpack的等工具也支持CommonJS。此处讨论的CommonJS是以NodeJS中的实现为准的。解释CommonJS的基本特征。
NodeJS是一种服务器端javaScript环境。它是基于Chrome的V8引擎(这个不影响我们使用,只是底层处理快慢的问题)。它内置了操作系统的API模块,通过加载这些模块,我们可以向使用C/C++一样使用javaScript。它与客户端javaScript有下面的不同:
上面我们介绍了NodeJS中,所有的JS文件都是一个module对象,它是NodeJS自动调用的,只要我们用node运行一个js文件。或者是在js文件中require其他模块,都会调用这个函数。Module构造函数的作用是将我们的js文件,统一包装成格式一致的CommonJS模块。通过module.exports输出公有API,通过id/parent/children来标识模块及其之间的联系。
从下面摘抄的源码中可以看出来:
/**NodeJS早期的代码中Module函数*/
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if(parent && parent.children){
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
module.exports = Module;
var module = new Module(filename, parent);
/************************************************************
* 最新的node源码中Module函数
************************************************************/
function updateChildren(parent, child, scan) {
const children = parent && parent.children;
if (children && !(scan && children.includes(child)))
children.push(child);
}
const moduleParentCache = new SafeWeakMap();
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
this.exports = {};
moduleParentCache.set(this, parent);
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
上面我们已经学习了一个Module对象包含那些内容;下面的部分,我们介绍如何加载一个模块;每一个使用Module对象封装的模块,都有一个require函数,我们使用全局require会调用Module对象中的相应函数。下面代码是对require()函数的简单模拟,它没有实现require的全部功能,但展现了require的主要功能。我们以下面的代码为例,解析require()函数的主要功能:
下面的代码摘自《Node.js设计模式》
/**require()的简单实现 */
const require = Module.prototype.require = (moduleName) => {
console.log(`Require invoked for module: ${moduleName}`);
const id = require.resolve(moduleName); //[1]
if (require.cache[id]) { //[2]
return require.cache[id].exports;
}
//module metadata
const module = new Module(id, this);//[3]
//Update the cache
require.cache[id] = module; //[4]
//load the module
loadModule(id, module, require); //[5]
// return exported variable
return module.exports; //[6]
}
require.cache = {};
//用于解析module的完整路径
require.resolve = (moduleName) => {
/* reslove a full module id from the moduleName */
};
/**Module加载器 */
function loadModule(filename, module, require) {
const wrappedSrc = `(function(module,exports,require){
${fs.readFileSync(filename,'utf8')}
})(module,module.exports,require);`;
eval(wrappedSrc);
}
上面我们已经对require函数的实现原理,做了解析;我们现在通过循环加载的例子来对这个过程进行进一步的分析:
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
shell运行结果:
$ node main.js
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
下面是各种使用CommonJS编写模块的例子;注意:输出API为module.exports,它有一个引用为exports;当我们使用命名导出的方式时(也就是以exports属性的形式导出),可以使用exports.prop = val的方式;一旦需要赋值,则要使用module.exports= obj的方式,并且在此模块中都应以module.exports的方式添加输出API。这个具体就是JS中命名的原理。下面是输出API的几种形式:
/**命名导出Named exports */
module.exports.info = (message) => {
console.log("info:", message);
};
module.exports.verbose = (message) => {
console.log("verbose:", message);
};
//导出函数
/**
* 只要使用这种直接赋值的方式,就必须用module.exports;为保证不覆盖其他输出,
* 这一句必须在其他给module.exports添加属性的语句之前。
* 而且这就切断了module.exports与exports直接的关系。
* 后面都要用module.exports
* */
module.exports = (message) => {
console.log("logger: ", message);
};
//logger2.js
//导出构造函数
class Logger {
constructor(name) {
this.name = name;
}
log(message) {
console.log(`[${this.name}]: ${message}`);
}
info(message) {
console.log(`info: ${message}`);
}
verbose(message) {
console.log(`verbose: ${message}`);
}
}
module.exports = Logger;
//logger3.js
//导出实例
function Logger(name) {
this.count = 0;
this.name = name;
}
Logger.prototype.log = function(message) {
this.count++;
console.log(`[${this.name}][${this.count}] ${message}`);
}
module.exports = new Logger('DEFAULT');
module.exports.Logger = Logger;
//patcher.js
//修改其他模块或者全局变量
require('./logger3').customMessage = () => {
console.log("This is a new function added by patcher.js");
}
对上面模块的调用如下:
const logger = require("./logger.js");
logger("This is an logger message");
logger.info("This is an information message");
logger.verbose("This is a verbose message");
const Logger = require("./logger2.js");
const dbLogger = new Logger("DB");
dbLogger.log("class output message");
require('./patcher');
const logger3 = require("./logger3");
logger3.log("instance message");
const costomLogger = new logger3.Logger('CUSTOM');
costomLogger.log("instance message");
logger3.customMessage();
ES6通过export和import来实现模块的导出和导入:
下面是基本语法的例子:
export var a = 5; //导出变量
export function add(a,b){return a+b;} //导出函数
var c =6;
function substract(a,b){return a-b;}
export {c as aliasC ,substract}; //把要导出的放在一起
improt * as moduleName from "moduleName";
// 上面的这些语句可以在一个文件中相当于exprot{a,add,c,substract};但不提倡
//另外一种只导出一个变量的办法(这个不提倡)
var b = 4;
export default b;
ES6的模块引入有下面的特点:
下面是模块的特点的例子,我们定义了三个模块,由main来引用;之所以文件后缀为mjs,是因为这是nodeJS使用ES6module的约定。
//lib.mjs
var foo = 'bar';
setTimeout(() => foo = 'baz', 100);
var a = 10;
export { foo, a };
//lib2.mjs
export { foo, a }
from './lib.mjs';
//console.log(a); 报错,它是直接导出,没有导入此文件。
//lib3.mjs
var foo2 = 'true';
setTimeout(() => foo2 = 'false', 1000);
export default foo2;
//main.mjs
import * as obj from './lib.mjs';
import foo2 from './lib3.mjs';
import { a, foo } from './lib2.mjs';
//obj.b = 'false';//报错,引入的变量是只读的。
//main: baz baz foo2: true;foo的值改变了,说明引入的是整个脚本,lib.mjs中异步函数对foo的改变
//在main中有体现;而foo2没有改变说明传递的是对象的引用,直接改变对象,不能被传递。
setTimeout(() => console.log("main:", foo, obj.foo, 'foo2:', foo2), 3000);
console.log('the same module? ', a === obj.a); //true,说明两次引入的是同一个模块,且实例化了一次
关于ES6静态加载的解释:
下面,我们以一个例子来对ES6的循环加载做个分析:
//a.mjs
import { bar } from "./b.mjs";
console.log("a.mjs", bar);
//换作let foo='foo',会报错
export var foo = 'foo';
//换作 var f= function(){ return 'function improved';},会报错
export function f() { return "function improved"; };
//b.mjs
import { foo, f } from './a.mjs';
console.log("b.mjs", foo, f());
export var bar = 'bar';
//main.mjs
import { foo } from "./a.mjs";
import { bar } from "./b.mjs";
console.log("main.mjs");
运行结果:
$ node main.mjs
b.mjs undefined function improved
a.mjs bar
main.mjs
import()函数是ES2020引入的动态加载模块的方法;与上面介绍的ES6模块有以下区别:
下面是一个简单的例子,异步加载一个脚本来作为click的事件处理函数。
button.addEventListener('click', event => {
import ('./dialog.js').then(dialog => { dialog.open(); }).catch(err => console.err(err));
});
主要差异有:
在模块管理中,我们组好保证使用引用模块的方式是相同的,要么全部是ES6模块,要么是CommonJS模块。但有时我们会混合使用或者说定义兼容的模块。下面我们来讨论这个问题。(nodeJS要高于v13.2版本)
NodeJS最早只支持CommonJS模块格式,现在又支持ES6模块格式,那么就必须有所区分。
package.json是模块的说明文件(nodeJS的包管理程序npm使用);我们可以使用npm init来初始化这样一个文件。为了在nodeJS中兼容ES6模块,它增加了type和export字段。
//导入自定义CommonJS模块
import cjsModule from './index.cjs'; //后面这个文件一定要是cjs后缀
//导入内置模块
import { createRequire } from 'module';
//定义require函数(跟CommonJS中require一样),但次函数可以在ES6中使用
const require = createRequire(import.meta.url);
const cjsModule = require('./index.cjs');
下面的例子中我们定义了一个兼容的模块myModule,这个模块文件夹中有三个文件:package.json,index.cjs和wrapper.js;具体内容如下:
//package.json
{
"name": "mymodule",
"version": "1.0.0",
"description": "an example",
"main": "index.cjs",
"exports": {
"1require": "./index.cjs",
"default": "./wrapper.js"
},
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "afc",
"license": "MIT"
}
//index.cjs
module.exports.name = "value";
//wrapper.js
import cjsModule from "myModule";
export const name = cjsModule.name;
//main.js加载CommonJS模块
console.log(require("myModule").name);
//main.mjs加载ES6模块
import { name } from 'myModule';
console.log(name);
参考数目: