用javaScript原生的方式实现设计模式 应该在JavaScript中使用Class么
大部分场景下不鼓励使用JavaScript class
不使用 class 的情况下,JavaScript 开发中还能使用设计模式吗?—— 毕竟这是几十年来许许多多程序员先驱们总结出来的 精髓!
答案是 —— 当然可以!只不过是用 JavaScript 原生的方式(functional way)来实现。
单例模式
单例模式的目标是在整个程序中,某个类有且只有一个实例。
方案一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const Singleton = (function ( ) { let instance; function createInstance ( ) { console .log("call createInstance" ); const object = new Object ("I am the instance" ); return object; } return { getInstance: function ( ) { if (!instance) { instance = createInstance(); } return instance; } }; })(); const instance1 = Singleton.getInstance();const instance2 = Singleton.getInstance(); console .log("Same instance? " + (instance1 === instance2));
没有用 class 而是 函数和闭包
方案二:
ES6 module 的静态import中有以下规范,在一次程序运行中,一个 module 只会被初始化一次,无论被 import 几次, 拿到的都是同一个module实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 console .log("initialize singletonInstance module" );let counter = 0 ;export const singletonInstance = { increase: () => { counter++; }, getCounter: () => { return counter; } } export default singletonInstance;
编写几个内容相同的 js 文件,分别命名为 SingletonUser1.js 、SingletonUser2.js 、SingletonUser3.js
1 2 3 4 5 import singletonInstance from "./Singleton.js" singletonInstance.increase(); export default {}
然后编写一个测试文件 index.js
1 2 3 4 5 6 7 8 9 import SingletonUser1 from './SingletonUser1.js' ;import SingletonUser2 from './SingletonUser2.js' ;import SingletonUser3 from './SingletonUser3.js' ;import singletonInstance from "./Singleton.js" console .log("counter value: " + singletonInstance.getCounter());singletonInstance.increase(); console .log("counter value: " + singletonInstance.getCounter());
编写index.html
1 <script type="module" src="./index.js" ></script>
运行会得到以下结果
1 2 3 initialize singletonInstance module counter value: 3 counter value: 4
结果分析:
Singleton.mjs 就算被import了4次,也只会初始化一次
每个 SingletonUser 都会调用一次 increase 方法,所以第一次输出的 counter 值是 3;
index.js 又执行了一次 increase 方法,counter 值最后变成了 4 —— 可见它们是调用同一个 singletonInstance 实例
装饰者模式
装饰者模式用于 动态的给目标添加一些额外的属性或行为 —— 在JavaScript 里,目标既可以是对象,也可以是function,甚至可以是 Promise。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function takeNormalPicture (landscape ) { console .log("take a picture of the landsacpe - " + landscape); return { name: landscape } } const picture1 = takeNormalPicture("The Great Wall" );console .log(JSON .stringify(picture1, null , 4 ));function meituEnhance (takePicture ) { return (landscape )=> { const res = takePicture(landscape); console .log("enhance the picture with meitu" ); res.quality = 'high' ; return res; } } const takeBetterPicture = meituEnhance(takeNormalPicture); const picture2 = takeBetterPicture("The Great Wall" ); console .log(JSON .stringify(picture2, null , 4 ));
输出的结果
1 2 3 4 5 6 7 8 9 10 11 take a picture of the landsacpe - The Great Wall { "name" : "The Great Wall" } take a picture of the landsacpe - The Great Wall enhance the picture with meitu { "name" : "The Great Wall" , "quality" : "high" }
装饰者模式的精髓在于
动态地给目标添加一些额外的属性或行为 —— 装饰者模式可以对原目标(以function为例)的参数、过程、结果进行增强、修改、删除。
同时,调用者无感知 —— 装饰者的API跟原目标的API一模一样。
代理模式
代理模式跟装饰者模式实现上有几分相像,但是目的有些差异 —— 给目标(对象、function)创建一个代理,而代理内部通常有额外的逻辑(与原目标无关的逻辑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 function loadGifImage (path ) { console .log("loading GIF image from path : " + path); return { path: path, image: 'mock-image' } } function loadOtherImage (path ) { console .log("loading normal image from path : " + path); return { path: path, image: 'mock-image' } } function imageProxy ( ) { const map = {}; return function (path ) { if (path in map) { console .log("No need to load from fs for : " + path); return map[path]; } const image = path.endsWith('gif' ) ? loadGifImage(path) : loadOtherImage(path); map[path] = image; return image; } } const proxy = imageProxy(); proxy('img1.gif' ); proxy('img2.jpg' ); proxy('img3.png' ); proxy('img1.gif' ); proxy('img2.jpg' ); proxy('img4.gif' );
输出结果
1 2 3 4 5 6 loading GIF image from path : img1.gif loading normal image from path : img2.jpg loading normal image from path : img3.png No need to load from fs for : img1.gif No need to load from fs for : img2.jpg loading GIF image from path : img4.gif
实现上,imageProxy也是个高阶函数,同时内部用到了闭包的特性,放置了一个缓存 map。
同 装饰者模式一样,代理的 API 也尽量与原目标保持一致,让外部调用者无感知。
代理模式引入的额外逻辑通常有3类:
对外部调用者隐藏真实的执行者(如:上面的调用者可能根本不知道有 loadGifImage 这个函数)
优化执行过程 (如:上面加入了缓存,不必每次都去加载图片)
增加了额外的”内务工作”(house-keeping) ( 如:上面的图片缓存过多时,可能要释放掉一部分;清理过期资源等)
适配器模式
适配器模式通常用来适配新、老接口,让它们能和谐工作 —— 这里的接口不必是OOP中的接口,你可以理解为广义的接口 即 暴露给外部调用的 API 协议。
让我们看看鼎鼎大名的JavaScript http 客户端库 axios 的源代码里如何使用 适配器模式的。
axios 即可以在前端开发中使用,也可以在 Node 环境下使用 —— 它是怎么做到的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 module .exports = function httpAdapter (config ) { return new Promise (function dispatchHttpRequest (resolve, reject ) { }); } module .exports = function xhrAdapter (config ) { return new Promise (function dispatchXhrRequest (resolve, reject ) { }); } function getDefaultAdapter ( ) { var adapter; if (typeof XMLHttpRequest !== 'undefined' ) { adapter = require ('./adapters/xhr' ); } else if (typeof process !== 'undefined' ) { adapter = require ('./adapters/http' ); } return adapter; } adapter(config).then(function onAdapterResolution (response ) { });
适配器的核心在于
定义一个统一的接口;
写一层额外的代码调用、封装下层的 API ,让这层代码暴露出定义好的接口。
axios 源码中,写了两段代码分别调用且封装了 Node 下的 http.js 和 浏览器下的 XMLHttpRequest,这两段代码就是适配器,它们暴露出来的接口是一样的 —— 接收一个 config 对象,返回一个 Promise
命令模式 把命令(请求、操作)封装成对象发送给执行端执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const light = { turnOn: () => { console .log("turn on the light" ); }, turnOff: () => { console .log("turn off the light" ); } } const SwitchOnCommand = { name: 'SwitchOnCommand' , execute: light.turnOn } const SwitchOffCommand = { name: 'SwitchOffCommand' , execute: light.turnOff } function lightSwitchFactory ( ) { let lastCommand = null ; const receiveCommand = (command ) => { lastCommand = command; command.execute(); } return { receiveCommand: receiveCommand, undo: () => { if (!lastCommand) return ; console .log("undo the last command" ); if (lastCommand.name === 'SwitchOnCommand' ) { receiveCommand(SwitchOffCommand); }else { receiveCommand(SwitchOnCommand); } } } } const lightSwitch = lightSwitchFactory();lightSwitch.receiveCommand(SwitchOnCommand); lightSwitch.receiveCommand(SwitchOffCommand); lightSwitch.undo();
输出结果
1 2 3 4 turn on the light turn off the light undo the last command turn on the light
命令模式的精髓在于
把执行命令从一个动词变成名词 即 封装成对象,方便传递;
可以在命令对象里添加更多属性(如上面代码中的 name ),可以作为标志或其他功能;
命令可以被存储起来,方便实现撤销、重做等功能
其实 Redux 的 action 机制也有点像 命令模式,不过 redux 更进一步,把命令的执行函数拆分到了 actionCreator 、reducer 和 middleware 里。
责任链模式
责任链模式为请求创建一条接收者链,每当有请求发出,这条链上的接收者依次检查是否该由自己处理,如果是就(拦截)处理,否则就继续传递给下一个接收者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const LOGGER_LEVELS = { INFO: 0 , DEBUG: 1 , WARNING: 2 , ERROR: 3 } function createLogger (level, logFunc ) { return { accept: (paraLevel ) => paraLevel >= level, log: logFunc } } const emailLogger = createLogger(LOGGER_LEVELS.ERROR, (message ) => { console .log("send the log to admin email : " + message); }) const fileLogger = createLogger(LOGGER_LEVELS.WARNING, (message ) => { console .log("send the log to file : " + message); }) const consoleLogger = createLogger(LOGGER_LEVELS.INFO, (message ) => { console .log("send the log to console : " + message); }) const loggers = [emailLogger, fileLogger, consoleLogger];function log (messageObj ) { loggers.forEach(logger => { if (logger.accept(messageObj.level)) { logger.log(messageObj.message) } }) } log({level : LOGGER_LEVELS.INFO, message : "an info message" }) log({level : LOGGER_LEVELS.DEBUG, message : "a debug message" }) log({level : LOGGER_LEVELS.WARNING, message : "a warning message" }) log({level : LOGGER_LEVELS.ERROR, message : "an error message" })
输出结果
1 2 3 4 5 6 7 send the log to console : an info message send the log to console : a debug message send the log to file : a warning message send the log to console : a warning message send the log to admin email : an error message send the log to file : an error message send the log to console : an error message
责任链模式的精髓——提供了简洁的代码结构,省却了大量的if else (想象一下如果不使用责任链模式实现上面的需求,代码会变成什么样)
注意事项:
拦截请求是可选的,即一个接收者处理结束之后是否需要让后续的接收者继续处理;
如果决定拦截请求,就要格外小心责任链的顺序。