原生 js 策略模式的理解
策略模式(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";
};
定义不同的按钮和策略的映射关系
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);// 设置要操作的domdomelement.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); // 设置要操作的domdomelement.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 APIif (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);// 将内容转换为数据 URLconst dataURL = canvas.toDataURL();// 创建一个 <a> 元素并设置属性const link = document.createElement("a");link.href = dataURL;link.download = "screenshot.png"; // 设置要保存的文件名// 模拟点击 <a> 元素来触发下载link.click();});</script></body>
</html>