数据代理(数据劫持),回调函数(回调地狱),promise)

253 阅读15分钟

数据劫持

用处 :

  • 将来使用框架的时候(vue), 框架目前都支持一个数据驱动视图

如何操作 :

  • 完成驱动视图, 需要借助数据劫持 帮助我们完成以原始数据为 基础, 对数据进行一份复制
  • 复制出来的数据是不会被修改的, 值从原始数据里面获取

语法 : Object.defineProperty( 哪一个对象, 属性, {配置项} )

配置项 : 是一个对象的格式

  • value : 这个属性对应的值
  • writable : 该属性是否可以被重写, 默认是 false (不允许修改)
  • Enumerator : 该属性是否可以被枚举(遍历), 默认是false(不能被枚举到)
  • get(){} : 是一个函数, 叫做 getter 获取器, 可以决定当前属性的值
    • 写了get 就不能和 value, writable 同时使用
  • set(){} : 是一个函数, 叫做 setter 获取器, 可以决定当前属性的值

- 代码实现思路 :

  1. 创建出一个对象, 给对象设置属性及属性值
  2. 通过数据劫持方法, 进行传参(传对象,属性名,{配置项})
  3. 通过配置项里的 Enumerator: true 方法设置可以修改属性值
  4. 通过配置项中的 get 方法返回属性值, set 方法改变属性值 */
        // 1. 创建出一个对象, 给对象设置属性及属性值
        const obj = {}
        obj.name = '张三' 
        obj.age = 99
        
        // 2. 通过数据劫持方法, 进行传参(传对象,属性名,{配置项}) 
        Object.defineProperty(obj, 'age', {
            // value: 18,
            // writable : true,

            // 3. 通过配置项里的 Enumerator: true 方法设置可以修改属性值
            Enumerator: true,

            // 4. 通过配置项中的 get 方法返回属性值, set 方法改变属性值
            get() {
                console.log('你现在访问了obj的 age属性, 然后还可以在函数内部做很多事情');
                return 300
            },
            set(val) {
                console.log('你现在想要修改obj的age 属性, 所以触发了 set 函数, 你想要设置的值为' , val);
            }
        })
        console.log(obj); // age : 300

封装数据劫持

为什么要封装数据劫持 ?

  • 如果要劫持的数据多了, 援鄂版的写法不方便, 只能一个一个属性劫持, 代码较多

- 代码实现思路 :

  1. 封装一个数据劫持函数, 传参(旧对象, 回调函数)
  2. 创建一个新对象
  3. 通过遍历对象, 将劫持旧对象的属性 放到新对象中
  4. 通过配置数据劫持的方法, 进行传参(传新对象, 属性名,{配置项})
  5. 通过配置项中的 get 方法返回属性值, set 方法修改属性值
  6. 将劫持后的新对象 返回出去 验证 :
  7. 获取页面元素
  8. 创建一个对象(原始对象)
  9. 创建一个劫持后的对象, 调用封装数据劫持对象
  10. 封装一个回调函数, 起渲染页面作用
  11. 修改属性值
        // 0. 封装数据劫持, 如果劫持的数据多了, 原本的写法不方便, 代码较多
        function observer(origin, callback) {
            // 1. 创建一个新对象
            const target = {}
            // 2.  通过遍历对象, 将劫持旧对象的属性 放到新对象中
            for (let key in origin) {
                // 3. 通过数据劫持方法, 进行传参(传新对象,属性名,{配置项}) 
                Object.defineProperty(target, key, {
                    // 4. 通过配置项里的 Enumerator: true 方法设置可以修改属性值
                    Enumerator: true,
                    // 5. 通过配置项中的 get 方法返回属性值, set 方法修改属性值
                    get() {
                        return origin[key]
                    },
                    set(val) {
                        origin[key] = val
                        callback(target)
                    }
                })
            }
            // 99. 将劫持后的target 返回出去
            return target
        }
        // 验证
        // 0. 获取页面元素
        const h1 = document.querySelector('h1')
        const h2 = document.querySelector('h2')

        // 1. 创建一个对象(原始对象)
        const obj = {}
        obj.name = '战三'
        obj.age = 18

        // 2. 创建一个劫持后的对象, 调用封装数据劫持对象
        const newobj = observer(obj, fn)
        // 3. 封装一个回调函数, 起渲染页面作用
        function fn(res) {
            h1.innerHTML = `年龄 : ${res.age}; 名字 : ${res.name}`
        }
        // 4. 修改属性值
        newobj.age = 666
        newobj.name = '李四'

基础版 :

语法 : Object.defineProperty( 哪一个对象, 属性, {配置项} )
      Object.defineProperties(res, {
            age: {
                get() {
                    return obj.age
                },
                set(val) {
                    box2.innerHTML = `res对象的 age属性:${val},name属性 : ${res.name}`
                    obj.age = val
                }
            },
            name: {
                get() {
                    return obj.name
                },
                set(val) {
                    box2.innerHTML = `res对象的 age属性:${res.age},name属性 : ${val}`
                    obj.name = val
                }

            }
        })

升级版 :

语法 : Object.defineProperties('哪个对象', {配置项})
        const box1 = document.querySelector('.box1')
        const box2 = document.querySelector('.box2')

        // 原始对象
        const obj = {}
        obj.name = '张三'
        obj.age = 18
        // 将数据劫持后的属性放在res 对象中
        const res = {}
       // 利用循环优化代码量 : 
        // 1. 通过遍历对象, 将劫持旧对象的属性 放到新对象中
        for (let key in obj) {
           // 2. 通过配置数据劫持的方法, 进行传参(传新对象, 属性名,{配置项})
            Object.defineProperties(res, {
                // 此处的key 我们的需求是当一个变量使用, 如果直接写 那么会当成一个字符串, 解决方案在key 加一个[], 包裹起来, 当一个变量
                [key]: {
                    // 3.通过配置项中的 get 方法返回属性值, set 方法修改属性值
                    get() {
                        return obj[key]
                    },
                    set(val) {
                        obj[key] = val
                        box2.innerHTML = `res对象的 age属性:${res.age},name属性 : ${res.name}`
                    }
                },

            })
        }
        // console.log(res);
        box1.innerHTML = `obj对象的 age属性:${obj.age},name属性 : ${obj.name}`
        box2.innerHTML = `res对象的 age属性:${res.age},name属性 : ${res.name}`

        obj.age = 666  // obj的修改不会影响视图
        obj.name = '李四'

        res.age = 999  // res 的修改会触发 set 函数, set 函数内有一行代码会让box2 更新, 所以res 修改会让页面重新修改
        res.name = '王五'

自己劫持自己版本 :

  • 全部都在 原始属性对象上进行操作
        // 原始对象
        const obj = {}
        obj.age = 18
        obj.name = '张三'

        // 升级版: 自己劫持自己 (全都在 原始对象上进行操作)
        for (let key in obj) {
            Object.defineProperties(obj, {
                /**
                 *  通常我们在处理 "自己劫持自己" 的时候, 不会再对象的原属性上操作, 而是复制出来一份一摸一样的数据操作
                 * 
                 *  为了和原属姓名相同, 所以会在 原本的属性名前 加一个下划线, 用来区分
                */
                ['_' + key]: {
                    value: obj[key],
                    writable: true
                },
                [key]: {
                    get() {
                        return obj['_' + key]
                    },
                    set(val) {
                        obj['_' + key] = val
                        box.innerHTML = `obj 对象的 age 属性: ${obj.age}, name 属性: ${obj.name}`
                    }
                }
            })
        }

        // 首次打开页面的时候, 给页面做一个赋值
        box.innerHTML = `obj 对象的 age 属性: ${obj.age}, name 属性: ${obj.name}`

        // 首次渲染完毕页面后 更改对象的属性值
        obj.age = 666
        obj.name = '李四'

数据代理 (ES6以后官方推出的)

  • 数据代理 是官方名字, 也叫做 数据劫持
  • proxy : 是ES6 以后官方退出的, 是一个内置构造函数, 需要 new

语法 : new Proxy('要代理的对象', {配置项}) : 返回实例对象, 就是代理结果数据

  • new Proxy()表示生成一个Proxy实例
  • 代理原始对象: 要被代理的对象,可以是一个object或者function
  • 配置项:也是一个对象,对该代理对象的各种操作行为处理。

- 代码实现思路 :

  1. 创建一个对象, 添加属性及水性指
  2. 通过ES6推出的 proxy(), 来进行数据劫持
    • 2-1 因为proxy , 最后会返回一个代理后的对象, 所以我们需要声明一个变量去接收
    • 2-2 通过proxy(), 进行传参(要代理的对象(原来对象), {配置项})
    • 2-3 在配置项里, 通过get(){} 方法, 传参(新对象, 该对象内部的属性(自动分配)), 返回属性值
    • 2-3 在配置项里, 通过set(){} 方法, 传参(新对象, 该对象内部的属性(自动分配), 修改后的属性值)
  • n. 在代理完成后 给原始对象 新加一个属性, 此时代理对象依然能够访问到 (proxy 独有的功能)
        const obj = {
            name: '张三',
            age: 18
        }
        // 这里new Proxy 
        // 第一个参数 要代理的对象, 
        // 第二个参数 一些配置项, 最后会返回一个代理后的对象, 我们需要使用一个变量去接收
        const res = new Proxy(obj, {
            get(target, property) {
                // 第一个形参 : 就是你要代理的这个对象, 在当前案例中指的是obj
                // 第二个形参 : 就是该对象内部的属性, 自动分配
                return target[property]  // 返回属性值
            },
            set(target, property, val) {
                target[property] = val   // 修改属性值
                console.log(`你现在想要修改 形参target的${property}属性, 修改的值为${val},除此之外你还可以做很多事`);
            }
        })
        // 在代理完成后 给原始对象 新加一个属性, 此时代理对象依然能够访问到 (proxy 独有的功能)
        obj.abc = 'qwe'
        console.log(res.age);
        console.log(res.name);
        console.log(res.abc);

        res.age = 99
        res.name = '李四'

回调函数

  • 本质上就是一个函数
  • 例 :
  • 一个函数A 以实参的形参传递到函数B 中,
  • 再函数B 中, 以形参的方式调用函数, 此时, 函数A 就可以称为 函数B 的回调函数

使用场景 : 为异步代码的解决方案

回调地狱

回调地狱是什么 ?

  • 这不是我们写代码的时候出现的某个漏洞
  • 只是我们再利用回调函数解决问题的时候, 代码量多了之后的一个视觉体验
  • 回调地狱的代码不利于我们去维护或者管理
  • 所以后续再处理 异步任务的时候, 我们就需要一些更加简洁的方法
  • 这时就出现了一个东西叫做 promise, 他也是一个异步的解决方案
        fn(
            () => {
                fn(
                    () => { console.log('班长买完水后, 又买了一箱饮料') },
                    () => { console.log('班长就买了一瓶水, 他不愿意给你们买饮料') }
                )
            },
            () => {
                fn(
                    () => { console.log('班长第二次买水 成功了') },
                    () => {
                        fn(
                            () => { console.log('班长第三次买水 成功了') },
                            () => { console.log('班长第三次买水 又失败了, 确实不争气') }
                        )
                    }
                )
            }
        )

promise : 是 JS内置的一个构造函数

作用 : 一种新的异步代码封装方案, 用来代替 回调函数的, 解决回调地狱问题

promise 三个状态 :

  • 持续 : pending
  • 成功 : filfilled
  • 失败 : rejected
  • 只会在 持续状态转换为 成功
  • 或者 失败状态转换为 失败

语法 : const p = new Promise()

// 检测机构
//resolve : 处理异步时成功的状态
//reject : 处理异步时失败的状态
new Promise((resolve,reject) => {
    if(处理异步){
        resolve([参数]);
    }else{
        reject([参数]);
    }
})

Promise 对象上的方法 :

  • .then(( ) => { }) 方法 ( 会在 promise 状态成功(resolve)的时候 执行)
  • .catch(( ) => { }) 方法 ( 会在 promise 状态失败(reject)的时候 执行)
  • .then( ).catch(error){ }.finally{ } => 每一次都会执行, 不考虑成功或 失败
      function fn() {
            const p = new Promise(function (resolve, reject) {
                const timer = Math.ceil(Math.random() * 3000)
                console.log('买瓶水去');
                setTimeout(() => {
                    if (timer > 1500) {
                        console.log('买水失败,用时', timer);
                        reject('超时,所以买水失败') // shibai()
                    } else {
                        console.log('买水成功,用时', timer);
                        resolve('没有超时, 买水成功')// chenggong()
                    }
                }, timer)
            })
            return p
        }
  • 普通调用 :
        //普通调用
        res.then(() => {
            console.log('如果我执行了, 说明promise状态为成功');
        })
        res.catch(() => {
            console.log('如果我执行了, 说明promise状态失败了');
        })
  • 链式调用 : 当在第一个 then 里面 返回一个 新的 promise 对象 然后你可以在 第一个 then 的后面 再次书写一个 then
        // 链式调用 
        res.then(() => {
            console.log(`因为${str},多以奖励`);            
            return fn()  // 中断函数
        }).then((str) => {
            console.log('如果我输出了, 表示第二次买水成功');
            return fn()
        }).then((str) => {
            console.log('如果我输出了, 表示第三次买水成功');
        }).catch(() => {
            console.log('如果我输出了, 说明之前某一次买水失败了');
        })
        // 1. promise 对象上的方法
        /*
               1. 调用封装的函数 (可以声明一个变量去接收)
               2. 如果函数执行成功, 则执行 .then()
                  如果函数执行失败, 则执行 .catch()
                  不管函数执行成功或失败, 都会执行 .finally() 里的代码块
        */
        const res = fn()
        res.then(() => {
            console.log('成功时执行');
        }).catch(() => {
            console.log('失败时执行');
        }).finally(() => {
            /*
                正常业务场景中, 我们在发起一个请求的时候, 会将页面弹出一个遮罩层
                然后再请求结束的时候, 需要将这个遮罩层关闭
                这个时候如果放在 then 中, 那么会有一个问题, 就是请求失败的时候
                所以我们一般不会放在 then 关闭遮罩层, 而是放在 finally 中
            */
            console.log('每一次都会执行,(不会考虑成功还是失败)');
        })

async 与 await : 可以把异步代码写的看起来像同步代码

  • 是 promise 的一种调用方案 (也就是说, 必须结合着 peomise一起使用)
  • 能够将异步代码 写的像 '同步代码一样'
  • async 关键字 : 书写在一个函数的 开头, 表明当前函数是一个异步函数
    • 内部可以书写一个异步函数, 内部可以写 await
  • await 关键字 : 具有等待的含义, 书写在异步任务前
    • 代码运行到这个位置的时候, 会有一个等待效果
    • 一直等到这个异步任务结束, 并且将异步任务的反馈结果 当一个值返回
        function fn() {
            const p = new Promise(function (resolve, reject) {
                const timer = Math.ceil(Math.random() * 3000)
                console.log('帮买瓶水去');
                setTimeout(() => {
                    if (timer > 3000) {
                        console.log('买水失败,用时', timer);
                        reject('超时,所以买水失败') // shibai()
                    } else {
                        console.log('买水成功,用时', timer);
                        resolve('没有超时, 买水成功')// chenggong()
                    }
                }, timer)
            })
            return p
        }

        async function newFn() {
            // 因为函数开头写了 async, 所以这个函数就是一个独特的异步函数, 内部可以书写 await
            const r1 = await fn()
            console.log('第一次买水', r1);

            const r2 = await fn()
            console.log('第二次买水', r2);

            const r3 = await fn()
            console.log('第三次买水', r3);
        }

        newFn()

async 与 await 缺点 :

  • 不能正常拿到(捕获到)promise 失败状态
  • 如果失败,会报错,终止程序继续执行

解决方案 :

  • try{ } catch {}
  • 封装一个永远不会失败的 promise
    • 也就是说, 不管请求状态如何, 永远调用 resolve, 让 promise的状态一定是成功
    • 为了区分这次请求是成功 还是失败, 我们不在单纯的返回一个字符串了
    • 而是返回一个对象
  • 注意 : 这个对象 我们的约定, 里边有一个 code属性, 成功时赋值为1, 失败时赋值为0
        function fn() {
            const p = new Promise(function (resolve, reject) {
                const timer = Math.ceil(Math.random() * 3000)
                console.log('买瓶水去');
                setTimeout(() => {
                    if (timer > 1500) {
                        console.log('买水失败,用时', timer);
                        resolve({
                            code : 0, asg : '超时,买水失败'
                        })
                    } else {
                        console.log('买水成功,用时', timer);
                        resolve({
                            code : 1, asg : '没有超时,买水成功'
                        })
                    }
                }, timer)
            })
            return p
        }

        // 解决缺点方案 1 : 
        newFn()
        async function newFn() {
            try {
                const r1 = await fn()
                console.log('第一次买水', r1);
            } catch (error){
                console.log(error); //把错误信息给到 error 参数
            }
            // 如果失败, 也会继续输出
            console.log('我的执行并不会被打断');
        }


        // 解决方案 2 :
         newFn()
         async function newFn() {
                 const r1 = await fn()
                 console.log('第一次买水', r1);
                 if(r1.code === 0){
                     console.log('请求失败');
                 }else{
                     console.log('请求成功, 正常执行');
                 }
         }

promise 构造函数方法 :

  • Promise.all([ ]) => 调用所有的Promise对象, .then返回全部成功, 只要有一个失败, 就会返回reject(.catch)
  • Promise.race([ ]) => 调用所有Promise对象, .then返回最快的那一个成功, .catch返回最快的哪一个失败状态
  • Promise.allSettled([ ]).then((res) => { console.log(res) }
        // 2. peomise 构造函数的一些方法
        Promise.all([fn(), fn(), fn()]).then(() => { 
            console.log('所有的参数 全部返回一个成功状态, 会执行');
        }).catch(() => { 
            console.log('所有的参数 中有一个为失败状态, 会执行catch');

        })

        Promise.race([fn(),fn(),fn()]).then(()=>{
            console.log('这些参数中, 结束最快的哪一个状态为 成功的时候执行');
        }).catch(()=>{
            console.log('这些参数中, 结束最快的哪一个状态为 失败的时候执行');
        })
        
         /*  
           -每一个异步任务都想得到结果就使用Promise.allSettled()
           -异步任务要求每个都成功才能往下执行就使用Promise.all()
        */
        Promise.allSettled([fn(),fn(),fn()]).then((res)=>{
            // 在书序内传递的 promise 全部执行完毕后, 返回一个数组给到 then 函数
            // 数组内的对象就是我们的传递进来的 promise 对象的执行结果
            console.log(res);
        })
        
        Promise.resolve().then(()=>{
            console.log('强制返回一个状态成功的promise ');
        })
        Promise.reject().then(()=>{
            console.log('如果wo打印了,说明当前的 promise 状态为成功');
        }).catch(()=>{
            console.log('强制返回一个 状态为 失败的 promise 对象');
        })

设计模式

作用 :

  • 为了解决 某一类问题的 一个优化过的代码解决方案

单例模式

  • 一个构造函数, 一生只能创建一个实例化对象
  • 准备一个变量默认赋值为 null, 然后再第一次实例化的时候 给这变量赋值为实例化对象
  • 后续在调用实例化的时候, 就不再创建实例化对象了, 而是拿到提前准备好的变量
    class Dialog {
      constructor() {
        console.log('在页面中, 创建了一个 弹出层');
      }
    }

    let instance = null
    function newlDialog() {
      if (instance === null) {
        instance = new Dialog()
      }
      return instance
    }
    let d1 = newlDialog()
    console.log(d1);  // 在页面中, 创建了一个 弹出层; Dialog {}
    let d2 = newlDialog()
    console.log(d2);  // Dialog {}

升级版 :

  • 原本的 instance 变量为一个全局变量, 本次利用自行执行函数与 闭包 将其他修改 为了 局部变量
    const newlDialog = function () {
      // 在 自执行函数内 创建一个变量(局部变量)
      let instance = null
      return function () {
        if (instance === null) {
          instance = new Dialog()
        }
        return instance
      }
    }()
    const d1 = newlDialog()
    console.log(d1); // 在页面中, 创建了一个 弹出层; Dialog {}
    const d2 = newlDialog()
    console.log(d2);  //  Dialog {}

升级版 :

    const newlDialog = (function () {
      // 在 自执行函数内 创建一个变量(局部变量)
      let instance = null

      class Dialog {
        constructor() {
          this.title = ''
          console.log('在页面中, 创建了一个 弹出层');
        }
        setTitle(newTitle) {
          this.title = newTitle
          // console.log(this.title);  // 第一次: 警告  第二次 : 通用
        }
      }
      return function (type) {
        if (instance === null) {
          // 当前语句的代码只会在第一次调用时 执行
          instance = new Dialog()
        }
        // 此时的位置, 每一次调用 newDialog 都会执行一次
        instance.setTitle(type)
        return instance
      }
    })()

    const d1 = newlDialog('警告')
    console.log(d1);  //  Dialog{title: "警告"} 

    // 过了很久之后调用, 第二次调用
    const d2 = newlDialog('通用')
    // console.log(d2);  // Dialog{title: "通用" }

策略模式 : 为了解决过多的 if..else 的嵌套问题

  • 核心 (当前案例 ):
    • 创建一个数据结构, 这个结构内存存储着各种那个折扣记录, 对应的值, 是这个折扣的计算方式
    • {
    • '8折' : 商品总价的 * 80%
    • '9折' : 商品价格的 * 85%
    • ....
    • }

基础版

 // 基础版 : 
        if ('8折') {
            console.log('商品总价的 * 80%');
        } else if ('85折') {
            console.log('商品价格的 * 85%');
        } else if ('9折') {
            console.log('商品价格的 * 90%');
        } else if ('95折') {
            console.log('商品价格的 * 95%');
        }

升级版 :

        const calcPrice = (function () {
            // 当前对象内 存储着 各种折扣 及 对应的 打折商品总价的计算方式
            const calcList = {
                '80%': (total) => { return (total * 0.8).toFixed(2) },
                '70%': (total) => { return (total * 0.7).toFixed(2) },
                '60%': total => (total * 0.6).toFixed(2),
                '50%': total => (total * 0.5).toFixed(2),
            }
            /*  
               所有的引用数据类型, 都可以当做一个对象来用
            */
            function inner(type, total) {
                return calcList[type](total)
            }
            inner.add = function (type, fn) {
                calcList[type] = fn
                console.log(calcList);
            }
            inner.sub = function (type, fn) {
                delete calcList[type]

            }
            inner.getList = function () {
                return calcList
            }
            return inner
        })()

        console.log(calcPrice('80%', 500));
        console.log(calcPrice('70%', 500));

        calcPrice.add('40%', total => (total * 0.4).toFixed(2));
        calcPrice.add('30%', total => (total * 0.3).toFixed(2));
        console.log(calcPrice('40%', 500));

        console.log(calcPrice.getList())