策略模式 在JavaScript中的实现

简介: 策略模式 在JavaScript中的实现

策略模式(Strategy Pattern)是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法或行为。该模式将算法封装成独立的

策略对象,使得这些策略对象可以互相替换,从而使得算法的变化独立于使用算法的客户端。 -- 来自查特著迪皮

需求

想要实现一个功能,点击不同按钮实现不同样式

G}}Z%])HM3]D{98P0~33ALR.png

原始代码

<!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)

LM11]3PMW~``F@LMP1OD]D6.png

分析

以上问题就很适合使用 策略模式

在JavaScript中,策略模式可以通过以下方式理解:

  1. 定义策略对象:首先,你需要定义一组策略对象,每个策略对象代表一种算法或行为。
  2. 使用策略对象:在需要使用算法或行为的地方,你可以通过选择合适的策略对象来实现不同的功能。这样可以在不修改客户端代码的情况下改变算法或行为。
  3. 切换策略:由于策略对象具有相同的接口,你可以根据不同的情况或条件来切换使用不同的策略对象。这使得你可以根据需要动态地选择合适的策略。

根据以上的分析,其实我们只需要换一个优雅的方式来替代高频率的 if-else即可。因为以上过程只需要表示为

%OB%R[R`Z)`KKA3P80CE3BY.png

解决方案 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>

目录
相关文章
|
3月前
|
设计模式 存储 JavaScript
|
设计模式 JavaScript 前端开发
JavaScript程序设计模式小技巧——策略模式,快看快用!!!(下)
JavaScript程序设计模式小技巧——策略模式,快看快用!!!(下)
|
设计模式 算法 JavaScript
JavaScript程序设计模式小技巧——策略模式,快看快用!!!(上)
JavaScript程序设计模式小技巧——策略模式,快看快用!!!(上)
|
设计模式 存储 JavaScript
常见 JavaScript 设计模式 — 原来这么简单(三)
常见 JavaScript 设计模式 — 原来这么简单
60 0
|
设计模式 JavaScript 前端开发
常见 JavaScript 设计模式 — 原来这么简单(一)
常见 JavaScript 设计模式 — 原来这么简单
114 0
|
设计模式 前端开发 JavaScript
常见 JavaScript 设计模式 — 原来这么简单(二)
常见 JavaScript 设计模式 — 原来这么简单
100 0
|
JavaScript 前端开发
JavaScript之观察者模式
JavaScript之观察者模式
163 0
JavaScript之观察者模式
|
设计模式 存储 JavaScript
23种JavaScript设计模式
23种JavaScript设计模式
89 0
|
设计模式 JavaScript 前端开发
JavaScript代码片段学设计模式
设计模式是任何优秀软件的基础,JavaScript 也不例外,学习设计模式,让你对代码组织多一些思路,通过代码片段来学习编码思路对于开发者来说是比较容易理解的,本文继续通过代码片段简单展示常见的设计模式,但不深入设计模式本身,在此推荐一本书《JavaScript设计模式》,通俗易懂,阅读完之后可以大幅提升编码水平。
97 0
|
前端开发 JavaScript API
JavaScript常用设计模式
设计模式是一种在长时间的经验与错误中总结出来可服用的解决方案。
5871 0