策略模式 在JavaScript中的实现

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

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

需求

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

原始代码

html 复制代码
<!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中,策略模式可以通过以下方式理解:

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

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

解决方案 1 普通对象

在JavaScript中,对象 object 天然具备 判断哪种策略 - 使用策略能力

javascript 复制代码
对象[策略]();
obj[key]();
javascript 复制代码
// 定义策略对象
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基于构造函数的面向对象的思想来实现

定义策略对象

js 复制代码
// 定义策略对象
const StrategyBlue = function () { }
const StrategyRed = function () { }
const StrategyGreen = function () { }
const StrategyPurple = function () { }
const StrategyYellow = function () { }

定义策略对应的行为

js 复制代码
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()
  },
}

定义负责消费策略的对象

js 复制代码
// 负责使用策略的对象
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);
}

完整代码

js 复制代码
<!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来替换面向对象的语法

html 复制代码
<!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 实现魔法摄像头的代码

传送门

可以看到,而已根据自身项目情况来考虑使用哪个版本的策略模式 以下提供优化后的代码

html 复制代码
<!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>
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax