设计模式、懒加载、数组扁平化、回调函数(Promise)......

457 阅读9分钟

设计模式

  • 定义:为了实现某一类功能给出的简洁而优化的解决方案

沙箱模式

  • 利用了函数内间接返回了一个函数
  • 外部函数返回一个对象,对象内书写多个函数
function outer () {
    let a = 100
    let b = 200

    // 创建一个 沙箱, "间接的返回一个函数"
    const obj = {
        getA: function () {
            return a
        },
        getB: function () {
            return b
        },
        setA: function (val) {
            a = val
        }
    }
    return obj
}

// 得到一个沙箱
const res1 = outer()

console.log(res1.getA())    // 100
console.log(res1.getB())    // 200

res1.setA(999)
console.log(res1.getA())    // 999

// 重新得到一个沙箱
const res2 = outer()
console.log(res2.getA())    // 100

沙箱小案例

<button class="sub">-</button>
<input class="inp" type="text" value="1">
<button class="add">+</button>
<br>
<button class="sub1">-</button>
<input class="inp1" type="text" value="1">
<button class="add1">+</button>
// 准备一个沙箱
function outer() {
    let a = 1
    return {
        getA() {
            return a
        },
        setA(val) {
            a = val
        }
    }
}

// 0. 获取元素
const subBtn = document.querySelector('.sub')
const addBtn = document.querySelector('.add')
const inp = document.querySelector('.inp')

// 0. 准备变量
// let count = 1
let res = outer()
subBtn.onclick = function () {
    let count = res.getA()
    res.setA(count - 1)
    inp.value = res.getA()
}
addBtn.onclick = function () {
    // count++
    let count = res.getA()
    res.setA(count + 1)
    inp.value = res.getA()
}

// 0. 获取元素
const subBtn1 = document.querySelector('.sub1')
const addBtn1 = document.querySelector('.add1')
const inp1 = document.querySelector('.inp1')

// 0. 准备变量
let res1 = outer()
subBtn1.onclick = function () {
    let count = res1.getA()
    res1.setA(count - 1)
    inp1.value = res1.getA()
}
addBtn1.onclick = function () {
    let count = res1.getA()
    res1.setA(count + 1)
    inp1.value = res1.getA()
}

语法糖

  • 尽可能的简化沙箱模式的语法
  • 利用 get 和 set 进行操作数据
  • 语法糖:
    • 在不影响功能的情况下提供一点更适合操作的语法
function outer() {
    let a = 100
    let b = 200

    return {
        get a() { return a },
        get b() { return b },
        set a(val) { a = val }
    }
}

let res = outer()
console.log(res.a)
console.log(res.b)
res.a = 999
console.log(res.a)   // 999 

单例模式

  • 核心:一个构造函数一生只有一个实例化对象
  • 思路:
    • 实例化一个类的时候,先判断这个类是否被实例化过了
    • 如果被实例化过,直接拿到实例化对象;如果没有,则正常进行实例化
  • 实现:单独找一个变量, 初始化为 null
  • 每一次创建实例化的时候, 都判断一下该变量的值;如果为 null, 那么创建一个实例, 赋值给该变量;如果不为 null, 那么我们不再创建, 直接给出该变量的值
        const Fun = (() => {
            let obj = null
            class Fun {
                constructor() {
                    this.text = '默认文本'
                    console.log(123);
                }
                setText(text) {
                    this.text = text
                }
            }
            return function (text) {
                if (obj === null) {
                    obj = new Fun()
                }
                obj.setText(text)
                return obj
            }
        })()
        const f1 = new Fun('普通文本')
        console.log(f1);
        const f2 = new Fun('警告文本')
        console.log(f2);
        const f3 = new Fun('成功文本')
        console.log(f3);

策略模式

  • 核心:减少if...else的应用,把多种形式的内容罗列出来
  • 案例:
    • 已知一个购物总价 1376,需要根据折扣计算最终价格
    • 折扣种类: 8 折, 7 折, 300-30, 500-50
  • 思路:
    • 用一个数据结构, 把各种折扣记录下来
    • 值存储的就是该折扣和总价的计算方式
        const price = (() => {
            const list = {
                '80%': total => total * 0.8,
                '70%': total => total * 0.7,
            }
            // 将 inner 函数当作对象使用
            function inner (type,total) {
                return list[type](total)
            }
            inner.add = (key, value) => {
                list[key] = value
            }
            inner.del = key => {
                delete list[key]
            }
            // 返回inner函数,price的值就是inner函数
            return inner
        })()
        const res1 = price('80%', 1000)
        console.log(res1);
        price.add('300-30', (total) => total - Math.floor(total/300) * 30)
        const res2 = price('300-30', 1000)
        console.log(res2);

发布订阅模式

  • 这个模式和一个叫做观察者模式类似但不是一个东西
        /**
         *  案例: 买一本书
         *      1. 去到书店, 问店员有没有 JS 从入门到入土
         *      2. 没有, 给店员留下一个联系方式
         *      3. 直到店员给你打电话, 你触发技能(去买回来)
        */
        class Supermarket {
            constructor() {
                this.waiter = '张三'
                this.msg = {}
            }
            add(book, user) {
                if (this.msg[book] === undefined) {
                    this.msg[book] = []
                }
                this.msg[book].push(user)
            }
            del(book, user) {
                this.msg[book].forEach((item, index) => {
                    if (item === user) {
                        delete this.msg[book][index]
                    }
                });
            }
            call(book) {
                this.msg[book].forEach(item => {item()});
            }
        }
        const res = new Supermarket()
        const user1 = () => {console.log('快来买全球富婆联系方式');}
        const user2 = () => {console.log('快来买全球富婆联系方式');}
        const user3 = () => {console.log('快来买全球富婆联系方式');}
        const user4 = () => {console.log('快来买葵花宝典');}
        res.add('全球富婆联系方式', user1)
        res.add('全球富婆联系方式', user2)
        res.add('全球富婆联系方式', user3)
        res.add('葵花宝典', user4)
        res.del('全球富婆联系方式', user3)
        res.call('全球富婆联系方式')
        console.log(res);

懒加载(瀑布流类似)

  • 作用:将一些数据延缓加载,加快页面初始请求速度
    <div class="box" style="height: 2000px;"></div>
    <img data-src="./樱花.jpeg" alt="" width="500px">
        const imgEl = document.querySelector('img')
        window.onscroll = function () {
            // 加载成功后不在执行
            if (imgEl.src !== '') return
            // 浏览器可视窗口高度
            const wH = innerHeight
            // 页面卷曲高度
            const scrollH = document.documentElement.scrollTop
            // 图片顶部偏移量
            const imgT = imgEl.offsetTop
            if (wH + scrollH > imgT) {
                imgEl.src = imgEl.dataset.src
            }
        }

数组扁平化

  • 方法1:
        const data = [1, 2, { child: [1, 2, 3] }, [4, 5, [undefined, true, 8, 9]]]
        function setArr(arr) {
            const target = []
            function bian(origin) {
                origin.forEach(item => {
                    if (Object.prototype.toString.call(item) !== '[object Array]') {
                        // 不是数组直接添加到target中
                        target.push(item)
                    } else {
                        // 是数组则进行递归重新调用扁平化函数
                        bian(item)
                    }
                });
            }
            bian(arr)
            return target
        }
        const newArr = setArr(data)
        console.log(newArr);
  • 方法2:数组的flat方法
    • 当前方法如果不传参, 那么会帮我们执行 一层 扁平化
    • Infinity 在 JS 中表示一个 无穷大的 数字
        const data = [undefined,{ name: 'QF001' },function () { console.log(123) },null,true,false,1,2,3,[4,5,6,[7,8,9,[10,11,12]]]]
        const newArr = data.flat(Infinity)
        console.log(newArr);
  • 方法3:
    • 将数组中 所有的元素转成字符串
    • 只适用于纯字符串,undefined 和 null 不能出现在数组中
        const data = [undefined,{ name: 'QF001' },function () { console.log(123) },null,true,false,1,2,3,[4,5,6,[7,8,9,[10,11,12]]]]
        const newData = data.toString().split(',')
        console.log(newData)

回调函数

  • 解释:
    • 把 函数 A 当作参数传递到 函数 B 内
    • 在 函数 B 内 以形参的方式调用函数 A
    • 函数 A 是 函数 B 的回调函数
        function fn (res,rej) {
            let time = Math.floor(Math.random() * 3000)
            if (time > 2000) {
                console.log('defeat',time);
                rej()
            } else {
                console.log('success',time);
                res()
            }
        }

        fn (
            () => {console.log('请求成功');},
            () => {console.log('请求失败');}
        )

回调地狱

按照回调函数的语法进行封装, 只能通过 传递一个函数作为参数来调用;当你使用回调函数过多的时候, 会出现回调地狱的代码结构(太过可怕不做演示)

  • 解决:
    • ES6推出了一种新的封装异步代码的方式, 叫做 Promise (承诺期约)

Promise

  • 定义:一种异步代码的封装方案;因为换了一种封装方案, 不需要按照回调函数的方式去调用, 需要按照 Peomise 的形式去调用
  • Promise三种状态:
    • 持续:pending
    • 成功:fulfilled
    • 失败:rejected
  • promise 的两种转换
    • 从持续转换为失败
    • 从持续转换为成功
  • 基本语法:
    • ·const p = new Promise(function () {书写异步代码})·
    • p是一个实例化对象
  • promise 对象可以触发两个方法
    • p.then(函数)
    • p.catch(函数)
    • 这两个方法只有Promise的状态为成功或者失败的时候执行
function fn() {
    const p = new Promise(function (resolve, reject) {
        const timer = Math.ceil(Math.random() * 3000) + 2000;
        setTimeout(() => {
            if (timer > 3500) {
                reject("班长买水失败");
            } else {
                resolve("班长买水成功");
            }
        }, timer);
    });
    return p;
}
// 将来在使用的时候 res 得到的是 promise 的实例对象 p
const res = fn();
res.then(function (type) {
    // 这个函数执行代码 promise 状态为成功状态!!!
    console.log("因为", type, "谢谢班长, 我准备了20个bug, 回馈给你");
});
res.catch(function (type) {
    // 这个函数执行代码
    console.log("因为", type, "谢谢班长, 我准备了800个bug, 开心死你");
});

上述代码Promise中的函数的两个参数都是函数,调用第一个改变Promise的状态为成功,调用第二个改变状态为失败;resolve 和 reject 调用时可以传递一个参数, 这个参数会被传递给对应的 then catch

链式调用

  • Promise的调用方式:
    • 当在第一个then里面返回一个封装的Promise函数调用结果时,可以在第一个then后面继续写then
fn()
    .then(function (type) {
        console.log(
            "第一次: 因为",
            type,
            "谢谢班长, 我准备了20个bug, 回馈给你"
        );
        return fn();
    })
    .then(function (type) {
        console.log(
            "第二次: 因为",
            type,
            "谢谢班长, 我准备了20个bug, 回馈给你"
        );
        return fn();
    })
    .then(function (type) {
        console.log(
            "第三次: 因为",
            type,
            "谢谢班长, 我准备了20个bug, 回馈给你"
        );
        return fn();
    })
    .catch(function (type) {
        console.log("因为", type, "谢谢班长, 我准备了800个bug, 开心死你");
    });
  • Promise的其他方法(实例化对象上)
    • then和catch都是
    • 语法:p.finally(函数)
    • 不管promise是成功还是失败, 只要 promise 执行结束, 我都会执行
  • promise构造函数上的方法
    • 语法Promise.all(多个Promise)
    • 作用: 可以同时触发多个 Promise 行为
    • 只有所有的Promise都成功了才算成功
      Promise.all([fn(), fn(), fn()])
          .then(function () {
              console.log("所有的 参数 都返回 成功状态");
          })
          .catch(function () {
              console.log("这些参数中, 有一个 为 失败状态");
          });
      
    • 语法Promise.race(多个Promise)
    • 作用: 可以同时触发多个 Promise 行为
    • 按照速度计算, 当第一个结束的时候就结束了, 成功或失败取决于第一个执行结束的 promise
      Promise.race([fn(), fn(), fn()])
      .then(function () {
          console.log("速度最快的那个执行完毕, 并且是成功状态时 执行");
      })
      .catch(function () {
          console.log("速度最快的那个执行完毕, 并且是失败状态时 执行");
      });
      
    • 语法Promise.allSettled([多个Promise])
    • 作用: 可以同时触发多个 Promise 行为
    • 在结果内以数组的形式给你返回 每一个 Promise 行为的成功还是失败
          Promise.allSettled([fn(), fn(), fn()]).then((res) => {
              console.log('执行结束', res)
          }).catch(() => {
              console.log('你看我会不会执行')
          })
      
    • 语法Promise.resolve()
    • 作用:强行返回一个成功状态的Promise对象
    • 语法Promise.reject()
    • 作用:强行返回一个失败状态的Promise对象

async 和 await

  • 注意: 需要配合的必须是 Promise 对象
  • 注意: Promise 的调用方案
  • 意义: 把 异步代码 写的看起来 "像" 同步代码
  • async 关键字的用法:
    • 直接写在函数前面,表明是异步函数
    • 意义: 表示在该函数内部可以使用 await 关键字
  • await关键字的用法:
    • 必须书写在一个有 async 关键字的函数内
    • await的后面必须是一个Promise的实例化对象
    • 原本用then接收的结果,可以使用变量接收
async function newFn() {
    /**
     * await 是等待的意思
     *  在当前 fn 函数内, await 必须要等到后面的 Promise 结束以后, 才会继续执行后续代码
     */
    const r1 = await fn();
    console.log("第一次: ", r1);
    const r2 = await fn();
    console.log("第二次: ", r1);
    const r3 = await fn();
    console.log("第三次: ", r1);
}
newFn();

async 和 await 语法的缺点

await只能捕获到Promise成功的状态,否则会报错

  1. 解决方法1 :
    • 使用 try...catch...
    • 语法:try{ 执行代码 } catch(err) { 执行代码 }
      • 首先执行 try 里面的代码, 如果不报错, catch 的代码不执行了
      • 如果报错, 不会爆出错误, 不会终止程序, 而是执行 catch 的代码
async function newFu() {
    try {
        const r1 = await fn();
        console.log(r1);
    } catch (error) {
        console.log("网络错误, 请检查网络并重新请求");
    }
}
newFu();
  1. 解决方法2:
    • 无论成功失败都只调用成功的函数resolve()
    • 传参的时候通过传入的数据辨认成功或者失败
function fn() {
    const p = new Promise(function (resolve, reject) {
        const timer = Math.ceil(Math.random() * 3000) + 2000;
        setTimeout(() => {
            if (timer > 3500) {
                resolve({ code: 0, msg: "班长买水失败" });
            } else {
                resolve({ code: 1, msg: "班长买水成功" });
            }
        }, timer);
    });
    return p;
}

async function newFn() {
    const r1 = await fn();
    if (r1.code === 0) {
        console.log("第一次请求失败, 请检查您的网络信息");
    } else {
        console.log("第一次请求成功", r1.msg);
    }

    const r2 = await fn();
    if (r2.code == 0) {
        console.log("第二次请求失败, 请检查您的网络信息");
    } else {
        console.log("第二次请求成功", r2.msg);
    }
}
newFn();