对 数据劫持、数据代理、回调函数 的概念分析

105 阅读2分钟

1. 数据劫持概念

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

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

  以原始数据为基础, 对数据进行一份复刻, 复刻出来数据是不允许修改的, 值从原始数据里面获取

     基础版数据劫持语法: Object.defineProperty(那个对象, 属性, {配置项})
     
     升级版数据劫持语法: Object.defineProperties('那个对象', '配置项')

      配置项:
          value: 这个属性对应的值
          writable: 该属性是否可以被重写, 默认是 false 不允许被修改
          enumerable: 该属性是否可以被枚举, 默认是 false 不能被枚举到
          get: 是一个函数, 叫做 getter 获取器, 可以决定当前属性的值, 不能与 value writable 同时出现
          set: 是一个函数, 叫做 setter 设置器, 当你需要修改这个属性的时候, 会触发该函数
          

1)初级案例 Object.defineProperty

    <body>
        <h1 class="box"></h1>
        <h1 class="box2"></h1>
        <script>
            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() {
                    return obj.age //从obj中读取age的值,作为res的age值
                },
                set(val) {
                    box.innerHTML = `res 年龄: ${val}`
                    obj.age = val //将值付给obj对象的age属性
                }
            })
            res.age = 999
            box.innerHTML = `res 年龄: ${res.age}` //res.age运行get()函数
            box2.innerHTML = `obj 年龄: ${obj.age}`
            res.age = 777//调用了set(666)函数
        </script>
    </body>

2)封装数据劫持 Object.defineProperty

 将来工作中: observer这个函数由框架提供, 我们直接使用即可
 为什么要封装函数: 如果劫持的属性多了, 原本的写法不太方便, 代码量比较多, 所以封装数据劫持
<body>
    <h1 class="box"></h1>
    <h1 class="box2"></h1>
    <script>
        function observer(origin, callback) {
            // 1. 创建一个对象, 将 origin 内部的属性劫持到这个对象内
            const target = {}

            // 2. 劫持 origin 上的属性到 target 中
            for (let key in origin) {
                Object.defineProperty(target, key, {
                    enumerable: true,
                    get() {
                        return origin[key]
                    },
                    set(val) {
                        origin[key] = val
                        callback(target)
                    }
                })
            }

            // 3. 将劫持后的 target 返回出去
            return target
        }
        
        function fn(res) {
            box.innerHTML = `年龄: ${res.age}; 名字: ${res.name}` 
            //res.age会调用get()函数返回   res.name会调用get()函数
        }
        const box = document.querySelector('.box')
        const box2 = document.querySelector('.box2')

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

        // 创建一个数据劫持后的对象
        const newObj = observer(obj, fn)

        newObj.age = 666 // newObj.age = 666会调用 set(666)函数,将666赋值给obj的age
        newObj.name = '李四'// newObj.name ='李四' 会调用 set('李四')函数,将'李四'赋值给obj的age
    </script>
</body>

3) 数据劫持升级 Object.defineProperties

<body>
    <h1 class="box1"></h1>
    <h1 class="box2"></h1>
    <script>
        const box1 = document.querySelector('.box1')
        const box2 = document.querySelector('.box2')

        // 原始对象
        const obj = {}
        obj.age = 18
        obj.name = '张三'
        基础版
        const res = {}   // 将数据劫持后的对象属性存放在 res 对象中
        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
                }
            },
        })
// 利用循环 将上面的代码块 优化
// 此处的 key 我们的需求是当一个变量使用, 如果直接写 那么会当成一个字符串, 解决方案在 key 加一个 [] 包裹起来, 
当成一个变量
        const res = {}
        for (let key in obj) {
            Object.defineProperties(res, {
                [key]: {
                    get() {
                        return obj[key]
                    },
                    set(val) {
                        obj[key] = val
                        box2.innerHTML = `res 对象的 age 属性: ${res.age}, name 属性: ${res.name}`
                    }
                },
            })
        }

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

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

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

    </script>
</body>

4)数据劫持升级2 自己劫持自己

/**
* 通常我们在处理 "自己劫持自己" 的时候, 不会在对象的原属性上操作, 而是复制出来一份一模一样数据操作
* 为了和原属姓名相同, 所以会在 原本的属性名前 加一个下划线, 用来区分 给 obj添加_obj和_age属性,用来存储数据
*/
<body>
    <h1 class="box"></h1>
    <script>
        const box = document.querySelector('.box')

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

        // 升级版: 自己劫持自己 (全都在 原始对象上进行操作)
        for (let key in obj) {
            Object.defineProperties(obj, {
                ['_' + key]: {
                    value: obj[key],
                    writable: true
                }, //给 obj添加_obj和_age属性,用来存储数据
                [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会运行get() 返回 obj['_age']的值
        
        // 首次渲染完毕页面后 更改对象的属性值
        obj.age = 666 //会调用set(666)函数 将666赋给obj['_age']
    </script>
</body>

2. 数据代理

     *      是 官方给的一个名字, 有部分程序员还是习惯性的叫做 数据劫持 
     *      proxy   是 ES6以后官方推出的    是一个内置构造函数
    <script>
        const obj = {
            name: '张三',
            age: 18
        }

        // new Proxy(要代理的对象,{配置项}) 第一个参数 要代理的对象, 第二个参数 一些配置项,  最后
        //会返回一个代理后的对象, 我们需要使用一个变量去接收
        
        const res = new Proxy(obj, { //会返回一个代理后的对象, 我们需要使用一个res变量去接收
            get(target, property) {
                /**
                 *  第一个形参: 就是你要代理的这个对象, 在当前案例中指的就是 obj
                 *  第二个形参: 就是该对象内部的某一个属性, 自动分配
                */
                return target[property]
            },
            set(target, property, val) {
                target[property] = val

                console.log(`修改 形参target 的 ${property} 属性, 修改的值为 ${val}`)
            }
        })

        
        obj.abc = 'qwer'// 在代理完成后给原始对象新加一个属性, 此时代理对象res依然能够访问到 (Proxy 独有的功能)
        console.log(res.abc)//qwer
        console.log(res.age)//18  运行 get函数返回 返回参target[]数值
        console.log(res.name)//张三 运行 get函数返回

        console.log(obj.abc)//qwer
        console.log(obj.age)//18  运行 get函数返回 返回参target[]数值
        console.log(obj.name)//张三 运行 get函数返回

        res.age = 66 //修改值 会调用set函数
        // 形参target 的 age 属性, 修改的值为 66, 
        res.add //增加一个新的属性add
        console.log(res.add)
    </script>

3. 回调函数

 *   本质上就是一个普通函数
 *   一个函数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(A, B) {
            const timer = Math.ceil(Math.random() * 3000)
            console.log('班长, 去帮我买瓶水')

            setTimeout(() => {
                if (timer > 2500) {
                    console.log('买水失败, 用时: ', timer)
                    B()
                } else {
                    console.log('买水成功, 用时: ', timer)
                    A()
                }
            }, timer)
        }

        fn(
            () => {
                console.log('谢谢班长, 我和你开玩笑的, 退了吧!') //回调函数 箭头函数地址传递给形参A
            },
            () => {
                console.log('买不到就别回来了') //回调函数 箭头函数函数地址传递给形参B
            }
        )