数据劫持、数据代理与回调函数

106 阅读4分钟

一、数据劫持

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

完成数据驱动视图需要介入数据劫持帮助我们完成

数据劫持:以原始数据为基础,对数据进行一份复刻

复刻出来的数据是不允许修改的,值从原始数据里面获取

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

配置项对象的形式书写

配置项:

value:这个属性对应的值

writable:该属性是否可以被重写,默认是false,不允许被修改

enumerable:该属性是否可以被枚举,默认是false不能被枚举到(枚举——遍历)

get:是一个函数,叫做 getter 获取器,可以决定当前属性的值,不能与 value writable 同时出现

set:是一个函数,叫做setter设置器,当你需要修改这个属性的时候,会触发该函数

const obj = {}
obj.name = '张三'

Object.defineProperty(obj, 'age', {
    enumerable: true,
    get(){
        console.log('你现在访问了obj的age属性,然后这个函数内可以做很多事')
        return 300
    },
    set(val){
    console.log('你现在要修改obj的age属性,所以触发了setter函数,你想要设置的值为:', val)
    }
})
obj.age = 99
console.log(obj.age)

二、数据劫持与渲染页面

HTML

<h1 class = "box"></h1>
<h1 class = "box2"></h1>

JS

    const box = document.querySelector('.box')
    const box2 = document.querySelector('.box2')
    // 原始对象
    const obj = {}
    obj.name = '张三'
    obj.age = 18

    // 将obj的属性,劫持到这个对象内
    const res = {}
    Object.defineProperty(res, 'age', {
      enumerable: true,
      get() {
        // console.log('你现在访问了obj的age属性,然后这个函数内可以做很多事')
        return obj.age
      },
      set(val) {
        // console.log('你现在要修改obj的age属性,所以触发了setter函数,你想要设置的值为:', val)
        box.innerHTML = `res年龄:${val}`
        obj.age = val
      }
    })
    res.age = 88
    console.log(res)

    box.innerHTML = `res年龄:${res.age}`

    box2.innerHTML = `obj年龄:${obj.age}`

三、封装函数劫持

    // 这个函数由框架提供,我们直接使用即可
    //如果劫持的属性多了,原本的写法不太方便,代码量比较多,所以封装数据劫持
    function observer(origin, callback) {
      // 1.创建一个对象,将origin内部的属性劫持到这个对象内
      const target = {}

      // 2.劫持origin上的属性到target中
      for (let key in origin) {
        Object.defineProperty(target, key, {
          enumerable: true,
          get() {
            // console.log('你现在访问了obj的age属性,然后这个函数内可以做很多事')
            return origin[key]
          },
          set(val) {
            // console.log('你现在要修改obj的age属性,所以触发了setter函数,你想要设置的值为:', val)
            origin[key] = val
            callback(target)
          }
        })
      }

      // 3.将劫持后的target返回出去
      return target
    }

    const box = document.querySelector('.box')
    const box2 = document.querySelector('.box2')
    
    // 原始对象
    const obj = {}
    obj.name = '张三'
    obj.age = 18

    const newObj = observer(obj, fn)
    console.log(newObj)

    function fn(res) {
      box.innerHTML = `obj 年龄:${res.age};名字:${res.name}`
    }
    newObj.age = 999
    newObj.name = '李四'

四、数据劫持升级

础版数据劫持语法

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

升级版数据劫持语法

Object.defineProperties('哪个对象', '配置项')

    const box1 = document.querySelector('.box1')
    const box2 = document.querySelector('.box2')

    // 原始对象
    const obj = {}
    obj.age = 18
    obj.name = '张三'

    // 将数据劫持后的对象属性存放在res对象中
    const res = {}

    // 基础版
    // Object.defineProperties(res, {
    //   age: {
    // get() {
    //   return obj.age
    // },
    // set(val) {
    //   box2.innerHTML = `res对象的age属性:${val}, res对象的name属性:${res.name} `
    //   obj.age = val
    // }
    //   },
    //   name: {
    //     get() {
    //       return obj.name
    //     },
    //     set(val) {
    //       box2.innerHTML = `res对象的age属性:${res.age}, res对象的name属性:${val} `
    //       obj.name = val
    //     }
    //   }
    // })

    // 利用循环优化代码量
    for (let key in obj) {
      Object.defineProperties(res, {
        //此时的key我们的需求是当一个变量使用,如果直接写,那么会当成一个字符串,解决方案:在key加一个[]包裹起来,当成变量
        [key]: {
          get() {
            return obj[key]
          },
          set(val) {
            obj[key] = val
            box2.innerHTML = `res对象的age属性:${res.age}, res对象的name属性:${res.name} `
          }
        }
      })
    }

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

    // 首次渲染完毕页面后,更改两个对象的属性值
    obj.age = 666 //obj的修改不会影响box1
    obj.name = '李四'

    // console.log(res)

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

五、数据劫持升级(自己劫持自己)

    const box = document.querySelector('.box')

    // 原始对象
    const obj = {}
    obj.age = 18
    obj.name = '张三'


    // const res = {} // 将数据劫持后的对象属性存放在res对象中
    // for (let key in obj) {
    //   Object.defineProperties(res, {
    //     [key]: {
    //       get() {
    //         return obj[key]
    //       },
    //       set(val) {
    //         obj[key] = val
    //         box.innerHTML = `res对象的age属性:${res.age}, res对象的name属性:${res.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}, obj对象的name属性:${obj.name} `
          }
        }
      })
    }

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

    // 首次渲染完毕页面后,更改两个对象的属性值
    obj.age = 666 //obj的修改不会影响box1
    obj.name = '李四'

六、数据代理

数据代理是官方给的一个名字,有不部分程序员还是习惯性的叫做数据劫持

proxy是 ES6 以后官方推出的 是一个内置构造函数

    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 = 'qwer'

    // console.log(res.age)
    // console.log(res.name)
    // console.log(res.abc)

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

七、回调函数

回调函数 本质上就是一个普通函数

  • 一个函数 A 以实参的形式传递到函数 B 中

  • 在函数 B 中,以形参的方式调用函数 A

  • 此时,函数 A 就可以称为函数 B 的回调函数

回调函数的使用场景异步代码解决方案

    // function A() {
    //   console.log('我是函数 A')
    // }
    // function B(callback) {
    //   callback()
    // }
    // B(A)

    // 基础版
    // function fn(callback = () => { }) {
    //   console.log('班长,帮我买瓶水')

    //   setTimeout(() => {
    //     console.log('班长买到水了')
    //     callback()
    //   }, 3000)
    // }

    // function jinnang() {
    //   console.log('再帮我买一箱水');
    // }
    // fn(jinnang)

    // 网络请求模拟
    function fn(success, failure) {
      const timer = Math.ceil(Math.random() * 3000)
      console.log('班长,去帮我买瓶水')

      setTimeout(() => {
        if (timer > 2500) {
          console.log('买水失败,用时:超时', timer)
          failure()
        } else {
          console.log('买水成功,用时:', timer);
          success()
        }
      }, timer)
    }
    fn(
      ()=>{console.log('谢谢班长,我和你开玩笑的,退了吧!');},
      ()=>{console.log('买不到就别回来了');}
    )

八、回调地狱

  • 这不是我们写代码的时候出现的某个漏洞

  • 只是我们在利用回调函数解决问题的时候,代码量多了之后的一个视觉体验

  • 回调地狱的代码不利于我们去维护或者管理,所以后续再处理异步任务的时候,我们需要一些更简洁的方法

  • 此时出现了一个东西叫做 promise ,他也是一个异步代码的解决方案

    // 调用fn函数,班长就会去买水
    function fn(success, failure) {
      const timer = Math.ceil(Math.random() * 3000)
      console.log('班长,去帮我买瓶水')

      setTimeout(() => {
        if (timer > 2000) {
          console.log('买水失败,用时:超时', timer)
          failure()
        } else {
          console.log('买水成功,用时:', timer);
          success()
        }
      }, timer)
    }

    /**
     * 需求:
     *    在班长买水失败后,让他再次去买水(重新调用fn函数)
     * 
     * 新需求:如果班长第二次也失败了,让他继续去买水
     * 
     * 新需求:第一次买水成功的时候,让班长再去买一箱饮料
    */
    fn(
      () => {
        fn(
          () => { console.log('班长买完水后,又买了一箱饮料'); },
          () => { console.log('班长就买了一瓶水,他不愿意给你们买饮料'); }
        )
      },
      () => {
        fn(
          () => { console.log('班长第二次买水成功了') },
          () => {
            fn(
              () => { console.log('班长第三次买水成功了'); },
              () => { console.log('班长第三次买水又失败了,确实不争气'); }
            )
          }
        )
      }
    )