JavaScript——数据劫持、回调函数

113 阅读5分钟

数据劫持

1. 数据劫持

  • 将来我们在使用框架的时候(vue), 框架目前都支持一个 "数据驱动视图",完成数据驱动视图 需要借助 数据劫持帮助我们完成
  • 以原始数据为基础, 对数据进行一份复刻, 复刻出来数据是不允许修改的, 值从原始数据里面获取
  • 语法:Object.defineProperty('哪一个对象', '属性', '配置项') 配置项:
  • value: 这个属性对应的值
  • writable: 该属性是否可以被重写, 默认是 false 不允许被修改
  • enumerable: 该属性是否可以被枚举, 默认是 false 不能被枚举到
  • get: 是一个函数, 叫做 getter 获取器, 可以决定当前属性的值, 不能与 value writable 同时出现
  • set: 是一个函数, 叫做 setter 设置器, 当你需要修改这个属性的时候, 会触发该函数
const obj = {}
obj.name = '张三'

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

        obj.age = 99

2. 数据劫持与渲染页面

<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() {
            // console.log('你现在访问了 obj 的 age 属性, 然后这函数内可以做很多事')
            return obj.age
        },
        set(val) {
            // console.log('你现在想要修改 obj 的 age 属性, 所以触发了 setter 函数, 你要想要设置的值为: ', val)
            box.innerHTML = `res 年龄: ${val}`
            obj.age = val
        }
    })

    res.age = 999

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

    // obj.age = 666
    // res.age = 777
    // box2.innerHTML = `obj 年龄: ${obj.age}`

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

</script>

3. 封装数据劫持

// 将来工作中: 这个函数由框架提供, 我们直接使用即可

// 为什么要封装函数: 如果劫持的属性多了, 原本的写法不太方便, 代码量比较多, 所以封装数据劫持

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)
function fn(res) {
    box.innerHTML = `年龄: ${res.age}; 名字: ${res.name}`
}

newObj.age = 666
newObj.name = '李四'

4. 数据劫持升级

数据劫持升级

  • 基础版数据劫持语法: Object.defineProperty(那个对象, 属性, {配置项})
  • 升级版数据劫持语法: Object.defineProperties('那个对象', '配置项')
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
//         }
//     },
// })

// 利用循环 优化代码量
const res = {}
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}, 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 = '王五'

5.数据劫持升级2

<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
            },
            [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 = '李四'

</script>

6. 数据代理

  • 是 官方给的一个名字, 有部分程序员还是习惯性的叫做 数据劫持
  • 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.abc)

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

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(chenggong, shibai) {
    const timer = Math.ceil(Math.random() * 3000)
    console.log('班长, 去帮我买瓶水')

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

fn(
    () => { console.log('谢谢班长, 我和你开玩笑的, 退了吧!') },
    () => { console.log('买不到就别回来了') }
)

回调地狱

这不是我们写代码的时候出现的某个漏洞,只是我们在利用回调函数解决问题的时候, 代码量多了之后的一个视觉体验

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

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

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

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

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

promise

  1. 作用: 一种新的异步代码封装方案, 用来代替 回调函数的

  2. promise 的三个状态

  • => 持续: pending
  • => 成功: fulfilled
  • => 失败: rejected
  1. promise 只会在持续状态转换为 成功,或者 从持续状态转换为 失败

  2. promise 的基本语法: promise 是 JS 内置的一个构造函数 const p = new Promise()

  3. new Promise 得到的对象 我们叫做 promise 对象

  4. promise 对象上 有一些方法

  • => then 方法 (会在 promise 状态成功的时候 执行)
  • => catch 方法 (会在 promise 状态失败的时候 执行)
const p = new Promise(function (resolve, reject) {
    /**
     *  形参名无所谓
     *      第一个形参: 内部的值是一个函数, 调用之后可以将当前这个 promise 的状态设置为 成功
     *      第二个形参: 内部的值是一个函数, 调用之后可以将当前这个 promise 的状态设置为 失败
    */

    // 书写我们的异步代码
    const timer = Math.ceil(Math.random() * 3000)
    console.log('班长, 去帮我买瓶水')

    setTimeout(() => {
        if (timer > 1500) {
            console.log('买水失败, 用时: ', timer)
            // shibai()
            reject()
        } else {
            console.log('买水成功, 用时: ', timer)
            // chenggong()
            resolve()
        }
    }, timer)
})

// console.log('打印 变量 p: ', p)

p.then(() => {
    console.log('如果我这行内容打印, 说明 promise 的状态为 成功')
})
p.catch(() => {
    console.log('如果我这行内容打印, 说明 promise 的状态为 失败')
})