js分享17-设计模式(小白必看)

98 阅读6分钟

设计模式-单例模式

  • 设计模式:
    • 为了实现某一类功能给出的简洁而优化的解决方案
  • 设计模式 - 单例模式
    • 核心意义: 一个构造函数(类) 一生只有一个实例
  • 思路:
    • 实例化一个类的时候
    • 需要先判断, 如果当前类以前被实例化过, 不需要再次实例化了
    • 而是直接拿到原先的实例化对象
    • 如果没有被实例化过, 那么新实例化一个实例对象
  • 解决:
    • 单独找一个变量, 初始化为 null
    • 每一次创建实例的时候, 都判断一下该变量的值
      • 如果为 null, 那么创建一个实例, 赋值给该变量
      • 如果不为 null, 那么我们不再创建实例, 直接给出该变量的值
class Dialog {
    constructor() {
        console.log("在 页面中 创建了一个 弹出层");
    }
}

let instance = null;
function newDialog() {
    if (instance == null) {
        instance = new Dialog();
    }
    return instance;
}

// 第一次调用, 创建一个 实例化对象然后给到 instance 中, 并返回 instance
let d1 = newDialog();

// 第二次调用, 直接返回 instance
let d2 = newDialog();

console.log(d1);
console.log(d2);
console.log(d1 === d2);

单例模式变形

  • 单例模式问题

    1. 向外部写了一个全局变量 instance
    2. 多次调用时, 无法传递不一样的参数
    3. 构造函数的类名与创建实例的函数名是两个东西
  • 解决:

    1. 利用闭包
    let newDialog = (function fn() {
        let instance = null;
        return function () {
            if (instance == null) {
                instance = new Dialog();
            }
            return instance;
        };
    })();
    
    1. 创建一个 init 方法
    class Dialog {
        constructor() {
            console.log("在 页面中 创建了一个 弹出层, 文本为: ");
            this.title = "";
        }
        setTitle(newTitle) {
            this.title = newTitle;
            console.log("将 title 属性 重新修改为了: ", this.title);
        }
    }
    let newDialog = (function outer() {
        let instance = null;
        return function inner(str) {
            // 没有接收到不同参数的原因在于, 这个 if 只有第一次才会执行, 后续永远不会执行
            if (instance == null) {
                instance = new Dialog();
            }
            // 但是, 这个位置却可以多次执行, 所以我们可以在这里, 给这个 类传递不同的参数
            instance.setTitle(str);
            return instance;
        };
    })();
    
    1. 将类放在闭包函数内, 然后将函数名修改为类名
    let Dialog = (function outer() {
        // 因为 Dialog 这个 类, 永远就使用一次, 所以函数局部 更合适
        class Dialog {
            constructor() {
                console.log("在 页面中 创建了一个 弹出层, 文本为: ");
                this.title = "";
            }
            setTitle(newTitle) {
                this.title = newTitle;
                console.log("将 title 属性 重新修改为了: ", this.title);
            }
        }
    
        let instance = null;
        return function inner(str) {
            if (instance == null) {
                instance = new Dialog();
            }
            instance.setTitle(str);
            return instance;
        };
    })();
    

设计模式-策略模式

  • 设计模式 - 策略模式
    • 核心: 减少 if...else...
    • 作用: 把多种形式的内容罗列出来
  • 案例:
    • 已知一个购物总价 1376
    • 需要根据折扣计算最终价格
    • 折扣种类: 8 折, 7 折, 300-30, 500-50
  • 当前案例核心:
    • 用一个数据结构, 把各种折扣记录下来
    • 值存储的就是该折扣和总价的计算方式
const calcPrice = (function () {
    const calcList = {
        "80%": (total) => (total * 0.8).toFixed(2),
        "70%": (total) => (total * 0.7).toFixed(2),
    };
    // 任何引用数据类型, 都可以当作 对象去使用
    function inner(type, total) {
        return calcList[type](total);
    }
    inner.add = function (type, fn) {
        calcList[type] = fn;
    };
    inner.sub = function (type) {
        delete calcList[type];
    };
    inner.getList = function () {
        return calcList;
    };
    return inner;
})();
const res = calcPrice("80%", 1000);
console.log(res);

const res1 = calcPrice("70%", 1000);
console.log(res1);

calcPrice.add("300-30", (total) => total - Math.floor(total / 300) * 30);

const res2 = calcPrice("300-30", 1000);
console.log(res2);

calcPrice.sub("70%");
console.log(calcPrice.getList());

发布订阅模式

  • 设计模式-发布订阅
    • 首先明确 这个模式和一个叫做观察者模式类似但不是一个东西
    • 目前一批开发人员认为这两个是一个东西, 另外一批认为是两个东西
  • 案例: 买一本书
    • 以前
      1. 去到书店, 问店员有没有 JS 从入门到入土
      2. 没有, 回去
      3. 一会再回去问, 有没有
      4. 没有, 回去
      5. 一会再回去问, 有没有
      6. ...
      7. 一会再回去问, 有没有
      8. 有了, 购买
    • 现在
      1. 去到书店, 问店员有没有 JS 从入门到入土
      2. 没有, 给店员留下一个联系方式
      3. 直到店员给你打电话, 你触发技能(去买回来)
  • 在 JS 内 发布订阅 使用最多的地方 (addEventListener)
    • xxx.addEventListener('click', fn)
  • 思考: 以什么数据格式来记录
店员 ===
    {
        a: [fn1, fn2],
        b: [fn3, fn4],
    };
class Observer {
    constructor(name) {
        this.name = name; // 这步模拟店员名字, 可以不要
        // 店员的记录手册
        this.messages = {};
    }
    add(type, fn) {
        if (this.messages[type] === undefined) this.messages[type] = [];

        this.messages[type].push(fn);
    }
    remove(type, fn) {
        if (this.messages[type] === undefined) return;

        // if (fn === undefined) return this.messages[type] = []
        if (fn === undefined) return delete this.messages[type];

        this.messages[type] = this.messages[type].filter((item) => item !== fn);
    }
    trigger(type) {
        // console.log(type)
        // console.log(this.messages[type])
        this.messages[type].forEach((item) => item());
    }
}

const ob = new Observer("小李");

const A1 = () => {
    console.log("我是 张三, 我要买这本书");
};
const A2 = () => {
    console.log("我是 李四, 我要买这本书");
};
const B1 = () => {
    console.log("我是 王五, 我要买这本书");
};

ob.add("A1", A1);
ob.add("A1", A2);
ob.add("B1", B1);

// ob.remove('A1', A1)
// ob.remove('A1')
ob.remove("C1");

ob.trigger("A1");

console.log(ob);

vue花括号语法

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="root">
        <p>使用name属性: {{name}}</p>
        <p>使用age属性: {{ age}}</p>
        <p>使用width属性: {{width }}</p>
        <p>使用height属性: {{     height }}</p>
    </div>

    修改年龄: <input type="text" id="inp">

    <script src="./02_vue花括号语法.js"></script>

    <script>
        const app = createApp({
            el: '#root',
            data: {
                name: '张三',
                age: 18,
                width: 10086,
                height: 10010
            },
        })

        const inp = document.querySelector('#inp')

        inp.oninput = function () {
            app.age = this.value
        }

        // console.log(app)
    </script>
</body>

</html>
function createApp(options) {
    // 1. 安全判断

    // 1.1 options 的 el 属性必须传递
    if (options.el === undefined) {
        // return console.log('此时代码出现问题, 应该阻断程序运行, 并且通知开发者有问题')
        throw new Error("el 选项必须传递");
    }

    // 1.2 options 的 data 属性必须传递, 并且只能是 对象类型
    if (Object.prototype.toString.call(options.data) !== "[object Object]") {
        throw new Error("data 选项必须传递, 并且只能是对象类型");
    }

    // 1.3 el 属性的值, 必须能够获取到 DOM 节点
    const root = document.querySelector(options.el);
    if (root === null) {
        throw new Error(
            "el 选项必须为字符串, 并且需要能够获取到当前页面的根元素"
        );
    }

    // 2. 开始数据劫持 (核心代码)

    // 创建一个对象 存储我们劫持后的数据
    const _data = {};

    // 开始劫持
    for (let key in options.data) {
        Object.defineProperty(_data, key, {
            get() {
                return options.data[key];
            },
            set(val) {
                options.data[key] = val;

                // 每次修改数据后, 重新渲染页面
                bindHtml(root, _data, rootHtml);
            },
        });
    }

    // 存储一个最开始的标签结构
    const rootHtml = root.innerHTML;

    // 首次打开页面就回执行这个地方的代码, 调用渲染函数
    bindHtml(root, _data, rootHtml);

    // 返回劫持好的对象
    return _data;
}

/**
 *  三个形参的拼写不重要
 *
 *      第一个: 去哪个标签内修改
 *      第二个: 变量对应的数据是什么
 *      第三个: 原本默认的 html 结构, 防止初次选然后找不到 {{}}
 */
function bindHtml(root, _data, _str) {
    // 1. 创建正则, 用于捕获出字符串中所有的 {{XXX}}
    // const reg = /{{name}}/
    // const reg = /{{\w+}}/
    // const reg = /{{(\w+)}}/
    // const reg = /{{ *(\w+) *}}/
    const reg = /{{ *(\w+) *}}/g;

    // 2. 利用字符串的方法, 将所有符合规则的部分捕获到一个数组中
    const arr = _str.match(reg);

    // console.log(arr)
    // 3. 遍历数组, 拿到所有的字符串, 然后对字符串进行后续的操作
    arr.forEach((item) => {
        const key = /{{ *(\w+) *}}/.exec(item)[1];

        _str = _str.replace(/{{ *(\w+) *}}/, _data[key])
    })

    root.innerHTML = _str
}