策略模式(Strategy Pattern)是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法或行为。该模式将算法封装成独立的
策略对象,使得这些策略对象可以互相替换,从而使得算法的变化独立于使用算法的客户端。 -- 来自查特著迪皮
需求
想要实现一个功能,点击不同按钮实现不同样式
原始代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } section { display: flex; padding: 10px; } button { margin: 0 10px; background-color: slateblue; outline: none; color: #fff; width: 100px; height: 100px; display: flex; align-items: center; justify-content: center; } div { width: 100px; height: 100px; margin: 50px auto; background-color: gray; } </style> </head> <body> <section> <button id="blue">蓝色 高度30</button> <button id="red">红色 高度40</button> <button id="green">绿色 高度50</button> <button id="purple">紫色 高度60</button> <button id="yellow">黄色 高度70</button> </section> <div>div</div> <script> const buttons = document.querySelectorAll("button"); const div = document.querySelector("div"); buttons.forEach(button => button.addEventListener('click', function (e) { const idType = button.id; // 重点代码======================= if (idType === "blue") { div.style.backgroundColor = "blue"; div.style.height = "30px"; } if (idType === "red") { div.style.backgroundColor = "red"; div.style.height = "40px"; } if (idType === "green") { div.style.backgroundColor = "green"; div.style.height = "50px"; } if (idType === "purple") { div.style.backgroundColor = "purple"; div.style.height = "60px"; } if (idType === "yellow") { div.style.backgroundColor = "yellow"; div.style.height = "70px"; } // 重点代码======================= })) </script> </body> </html>
问题
以上代码,明显存在冗余、不方便维护的问题。也就是违背了 开放-封闭原则 (Open-Close Principle,OCP)
分析
以上问题就很适合使用 策略模式
在JavaScript中,策略模式可以通过以下方式理解:
- 定义策略对象:首先,你需要定义一组策略对象,每个策略对象代表一种算法或行为。
- 使用策略对象:在需要使用算法或行为的地方,你可以通过选择合适的策略对象来实现不同的功能。这样可以在不修改客户端代码的情况下改变算法或行为。
- 切换策略:由于策略对象具有相同的接口,你可以根据不同的情况或条件来切换使用不同的策略对象。这使得你可以根据需要动态地选择合适的策略。
根据以上的分析,其实我们只需要换一个优雅的方式来替代高频率的 if-else即可。因为以上过程只需要表示为
解决方案 1 普通对象
在JavaScript中,对象 object 天然具备 判断哪种策略 - 使用策略能力
对象[策略](); obj[key]();
// 定义策略对象 const strategy = { blue(dom) { dom.style.backgroundColor = "blue"; dom.style.height = "30px"; }, red(dom) { dom.style.backgroundColor = "red"; dom.style.height = "40px"; }, green(dom) { dom.style.backgroundColor = "green"; dom.style.height = "50px"; }, purple(dom) { dom.style.backgroundColor = "purple"; dom.style.height = "60px"; }, yellow(dom) { dom.style.backgroundColor = "yellow"; dom.style.height = "70px"; }, } buttons.forEach(button => button.addEventListener('click', function (e) { const idType = button.id; // 重点代码======================= // 判断和使用策略 strategy[idType](div); // 重点代码======================= }))
解决方案 2 prototype
以上代码,可以实现 es5基于构造函数的面向对象的思想来实现
定义策略对象
// 定义策略对象 const StrategyBlue = function () { } const StrategyRed = function () { } const StrategyGreen = function () { } const StrategyPurple = function () { } const StrategyYellow = function () { }
定义策略对应的行为
StrategyBlue.prototype.setStyle = function (dom) { dom.style.backgroundColor = "blue"; dom.style.height = "30px"; } StrategyRed.prototype.setStyle = function (dom) { dom.style.backgroundColor = "red"; dom.style.height = "40px"; } StrategyGreen.prototype.setStyle = function (dom) { dom.style.backgroundColor = "green"; dom.style.height = "50px"; } StrategyPurple.prototype.setStyle = function (dom) { dom.style.backgroundColor = "purple"; dom.style.height = "60px"; } StrategyYellow.prototype.setStyle = function (dom) { dom.style.backgroundColor = "yellow"; dom.style.height = "70px"; }
定义不同的按钮和策略的映射关系
定义负责消费策略的对象
js
const mapStrategyType = { blue() { return new StrategyBlue() }, red() { return new StrategyRed() }, green() { return new StrategyGreen() }, purple() { return new StrategyPurple() }, yellow() { return new StrategyYellow() }, }
// 负责使用策略的对象 function DomElement() { this.dom = ""; this.strategy = ""; } DomElement.prototype.setDom = function (dom) { this.dom = dom; } DomElement.prototype.setStrategy = function (strategy) { this.strategy = strategy; } DomElement.prototype.executeStrategy = function (strategy) { this.strategy.setStyle(this.dom); }
完整代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } section { display: flex; padding: 10px; } button { margin: 0 10px; background-color: slateblue; outline: none; color: #fff; width: 100px; height: 100px; display: flex; align-items: center; justify-content: center; } div { width: 100px; height: 100px; margin: 50px auto; background-color: gray; } </style> </head> <body> <section> <button id="blue">蓝色 高度30</button> <button id="red">红色 高度40</button> <button id="green">绿色 高度50</button> <button id="purple">紫色 高度60</button> <button id="yellow">黄色 高度70</button> </section> <div>div</div> <script> const buttons = document.querySelectorAll("button"); const div = document.querySelector("div"); // 定义策略对象 const StrategyBlue = function () { } const StrategyRed = function () { } const StrategyGreen = function () { } const StrategyPurple = function () { } const StrategyYellow = function () { } // 定义策略映射关系 const mapStrategyType = { blue() { return new StrategyBlue() }, red() { return new StrategyRed() }, green() { return new StrategyGreen() }, purple() { return new StrategyPurple() }, yellow() { return new StrategyYellow() }, } StrategyBlue.prototype.setStyle = function (dom) { dom.style.backgroundColor = "blue"; dom.style.height = "30px"; } StrategyRed.prototype.setStyle = function (dom) { dom.style.backgroundColor = "red"; dom.style.height = "40px"; } StrategyGreen.prototype.setStyle = function (dom) { dom.style.backgroundColor = "green"; dom.style.height = "50px"; } StrategyPurple.prototype.setStyle = function (dom) { dom.style.backgroundColor = "purple"; dom.style.height = "60px"; } StrategyYellow.prototype.setStyle = function (dom) { dom.style.backgroundColor = "yellow"; dom.style.height = "70px"; } // 负责使用策略的对象 function DomElement() { this.dom = ""; this.strategy = ""; } DomElement.prototype.setDom = function (dom) { this.dom = dom; } DomElement.prototype.setStrategy = function (strategy) { this.strategy = strategy; } DomElement.prototype.executeStrategy = function (strategy) { this.strategy.setStyle(this.dom); } // 负责消费策略的实例 const domelement = new DomElement(); buttons.forEach(button => button.addEventListener('click', function (e) { const idType = button.id; const strategy = mapStrategyType[idType]();// 根据type返回对应策略实例 // 重点代码======================= domelement.setDom(div);// 设置要操作的dom domelement.setStrategy(strategy);// 设置策略 domelement.executeStrategy();// 调用策略 // 重点代码======================= })) </script> </body> </html>
解决方案 3 class
该版本使用 es6的class来替换面向对象的语法
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } section { display: flex; padding: 10px; } button { margin: 0 10px; background-color: slateblue; outline: none; color: #fff; width: 100px; height: 100px; display: flex; align-items: center; justify-content: center; } div { width: 100px; height: 100px; margin: 50px auto; background-color: gray; } </style> </head> <body> <section> <button id="blue">蓝色 高度30</button> <button id="red">红色 高度40</button> <button id="green">绿色 高度50</button> <button id="purple">紫色 高度60</button> <button id="yellow">黄色 高度70</button> </section> <div>div</div> <script> const buttons = document.querySelectorAll("button"); const div = document.querySelector("div"); // 定义策略对象 class StrategyBlue { setStyle(dom) { dom.style.backgroundColor = "blue"; dom.style.height = "30px"; } } class StrategyRed { setStyle(dom) { dom.style.backgroundColor = "red"; dom.style.height = "40px"; } } class StrategyGreen { setStyle(dom) { dom.style.backgroundColor = "green"; dom.style.height = "50px"; } } class StrategyPurple { setStyle(dom) { dom.style.backgroundColor = "purple"; dom.style.height = "60px"; } } class StrategyYellow { setStyle(dom) { dom.style.backgroundColor = "yellow"; dom.style.height = "70px"; } } // 定义策略映射关系 const mapStrategyType = { blue() { return new StrategyBlue() }, red() { return new StrategyRed() }, green() { return new StrategyGreen() }, purple() { return new StrategyPurple() }, yellow() { return new StrategyYellow() }, } // 负责使用策略的对象 class DomElement { constructor() { this.dom = ""; this.strategy = ""; } setDom(dom) { this.dom = dom; } setStrategy(strategy) { this.strategy = strategy; } executeStrategy = function (strategy) { this.strategy.setStyle(this.dom); } } // 负责消费策略的实例 const domelement = new DomElement(); buttons.forEach(button => button.addEventListener('click', function (e) { const idType = button.id; const strategy = mapStrategyType[idType]();// 根据type返回对应策略实例 // 重点代码======================= domelement.setDom(div);// 设置要操作的dom domelement.setStrategy(strategy);// 设置策略 domelement.executeStrategy();// 调用策略 // 重点代码======================= })) </script> </body> </html>
优化 神奇canvas 实现魔法摄像头的代码
可以看到,而已根据自身项目情况来考虑使用哪个版本的策略模式 以下提供优化后的代码
<!DOCTYPE html> <html> <head> <title>Canvas Demo</title> <style> button { border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden; user-select: none; outline: none; border: none; padding: 16px; background-color: #1d93ab; color: #fff; } button:focus { background-color: #e88f21 } </style> </head> <body> <div> <button data-type="gray">反转</button> <button data-type="blackwhite">黑白</button> <button data-type="brightness">亮度</button> <button data-type="sepia">复古</button> <button data-type="redMask">红色</button> <button data-type="greenMask">绿色</button> <button data-type="blueMask">蓝色</button> <button data-type="opacity">透明</button> <button data-type="mosaic">马赛克</button> <button data-type="linearGradient">渐变</button> <button id="takePhoto">拍摄</button> </div> <video id="videoElement" autoplay></video> <canvas id="canvasElement"></canvas> <script> // 获取视频元素和画布元素 const video = document.getElementById('videoElement'); const canvas = document.getElementById('canvasElement'); const ctx = canvas.getContext('2d'); const buttons = document.querySelectorAll("button[data-type]"); const takePhoto = document.querySelector("#takePhoto")// 截图 按钮 let drawType = "" // 当视频元素加载完成后执行 video.addEventListener('loadedmetadata', function () { // 设置画布大小与视频尺寸相同 canvas.width = video.videoWidth; canvas.height = video.videoHeight; }); // 操作类型 const editType = { dataTypeList: ["gray", "blackwhite", "brightness", "sepia", "redMask", "greenMask", "blueMask", "opacity", "linearGradient"], // 后续继续补充 } const handleData = { gray(data) { // 反转 for (let i = 0; i < data.length; i += 4) { data[i + 0] = 255 - data[i + 0]; data[i + 1] = 255 - data[i + 1]; data[i + 2] = 255 - data[i + 2]; } return data }, blackwhite(data) { for (let i = 0; i < data.length; i += 4) { const average = (data[i + 0] + data[i + 1] + data[i + 2] + data[i + 3]) / 3; data[i + 0] = average;//红 data[i + 1] = average; //绿 data[i + 2] = average; //蓝 } return data }, brightness(data) { for (let i = 0; i < data.length; i += 4) { const a = 50; data[i + 0] += a; data[i + 1] += a; data[i + 2] += a; } return data }, sepia(data) { for (let i = 0; i < data.length; i += 4) { const r = data[i + 0]; const g = data[i + 1]; const b = data[i + 2]; data[i + 0] = r * 0.39 + g * 0.76 + b * 0.18; data[i + 1] = r * 0.35 + g * 0.68 + b * 0.16; data[i + 2] = r * 0.27 + g * 0.53 + b * 0.13; } return data }, redMask(data) { for (let i = 0; i < data.length; i += 4) { const r = data[i + 0] const g = data[i + 1] const b = data[i + 2] const average = (r + g + b) / 3 data[i + 0] = average data[i + 1] = 0 data[i + 2] = 0 } return data }, greenMask(data) { for (let i = 0; i < data.length; i += 4) { const r = data[i + 0] const g = data[i + 1] const b = data[i + 2] const average = (r + g + b) / 3 data[i + 0] = 0 data[i + 1] = average data[i + 2] = 0 } return data }, blueMask(data) { for (let i = 0; i < data.length; i += 4) { const r = data[i + 0] const g = data[i + 1] const b = data[i + 2] const average = (r + g + b) / 3 data[i + 0] = 0 data[i + 1] = 0 data[i + 2] = average } return data }, opacity(data) { for (let i = 0; i < data.length; i += 4) { data[i + 3] = data[i + 3] * 0.3; } return data }, linearGradient(data) { for (let i = 0; i < data.length; i += 4) { const x = (i / 4) % canvas.width; // 当前像素的 x 坐标 const y = Math.floor(i / (4 * canvas.width)); // 当前像素的 y 坐标 // 计算当前像素的颜色值 const r = x / canvas.width * 255; // 红色分量 const g = y / canvas.height * 255; // 绿色分量 const b = 128; // 蓝色分量 const a = 100; // 不透明度 // 设置当前像素的颜色值 data[i] = r; // 红色分量 data[i + 1] = g; // 绿色分量 data[i + 2] = b; // 蓝色分量 data[i + 3] = a; // 不透明度 } return data }, mosaic(ctx, canvas) { ctx.imageSmoothingEnabled = false; // 禁用图像平滑处理 const tileSize = 10; // 马赛克块的大小 // 缩小马赛克块 ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width / tileSize, canvas.height / tileSize); // 放大回原来的大小 ctx.drawImage(canvas, 0, 0, canvas.width / tileSize, canvas.height / tileSize, 0, 0, canvas.width, canvas.height); }, } // 在每一帧绘制视频画面到画布上 function drawFrame() { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const imageObj = ctx.getImageData(0, 0, canvas.width, canvas.height); if (editType.dataTypeList.includes(drawType)) { imageObj.data = handleData[drawType](imageObj.data); ctx.putImageData(imageObj, 0, 0); } else if (drawType === "mosaic") { // 马赛克 handleData[drawType](ctx, canvas); } requestAnimationFrame(drawFrame); // setTimeout(drawFrame, 1000); } // 检查浏览器是否支持 getUserMedia API if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { // 请求访问摄像头 navigator.mediaDevices.getUserMedia({ video: true }) .then(function (stream) { // 将视频流绑定到视频元素上 video.srcObject = stream; // 开始绘制视频画面到画布上 requestAnimationFrame(drawFrame); }) .catch(function (error) { console.error('无法访问摄像头:', error); }); } else { console.error('浏览器不支持 getUserMedia API'); } buttons.forEach(button => { button.addEventListener("click", function (e) { drawType = e.target.dataset.type; }) }) takePhoto.addEventListener('click', function (e) { // 绘制原始 Canvas 的内容到新的 Canvas 上 ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height); // 将内容转换为数据 URL const dataURL = canvas.toDataURL(); // 创建一个 <a> 元素并设置属性 const link = document.createElement('a'); link.href = dataURL; link.download = 'screenshot.png'; // 设置要保存的文件名 // 模拟点击 <a> 元素来触发下载 link.click(); }) </script> </body> </html>