JavaScript全解析-数据操作

本文介绍了JavaScript中的数据劫持技术,包括使用`Object.defineProperty`实现数据监听,以及数据劫持的升级版和数据代理。还探讨了数组扁平化和递归操作数组的方法。文章通过示例代码详细阐述了这些概念和技术的应用。
摘要由CSDN通过智能技术生成

数据劫持

●在框架中很少使用原生js
●在框架中更多是关注数据
●有一句话叫做: 数据驱动视图
○也就是说数据发生变化 , 视图也跟随这发生变化
什么是数据劫持
●数据劫持就是以原数据为基础 , 对数据进行一份复刻
●复刻出来的数据不允许修改 , 值是从原始数据中获取的
如何数据劫持
●语法:
○Object.defineProperty(给那一个对象,属性名,{配置项})
○在配置项中包含着属性名的值
○配置项中的内容:
=> value:该属性名对应的值
=> writable:该属性是否可以被重写(就是重新设置值)
-> 默认是 false 不允许被重写 , 也就是只读
-> 选填是 true 表示允许被重写 , 也就是可以被修改
=> enumerable: 表示该属性是否可以不枚举(遍历)
-> 默认是 false 不可以枚举
-> 选填是 true 表示可以遍历
=> get: 是一个函数 , 叫做getter获取器
-> 可以用来获取该属性的值
-> get属性的返回值就是这个属性的值
-> 注意: 不能和 value 和 writable 一起使用 , 会报错
=> set: 是一个函数 , 叫做setter设置器
-> 当你需要修改该属性值的时候 , 会触发该函数

// 设置
Object.defineProperty(obj,'age',{
    value:18,
    writable:true,
    enumerable:true,
    get () {
        // 该函数的返回值就是这个属性的值
        // 上面设置了value 和 writable 这里就不能返回了
        return 20
    },
    set (val) {
        console.log('你想要修改age的值,修改为:',val);
    }
})
console.log(obj); // 打印结果: {age: 18}
// 之后我们尝试修改
// 修改我们之前的普通设置
obj.name = 'Rose'
console.log(obj); // 没有问题可以修改
// 修改通过Object.defineProperty设置的值
obj.age = 30
console.log(obj); // 打印出来的结果还是18 , 这个时候是不允许被修改的
// 在没有书写 enumerable 之前是不可以遍历出age的 , 书写了enumerable:true 以后是可以的
for (let k in obj) {
    console.log(k);
}
// 测试get函数的返回值不能和 value 和 writable一起使用
console.log(obj)


复制代码

实现数据劫持

//  实现数据劫持
// 准备一个对象
const obj = {name:'Rose',age:25}
// obj是我的原始数据 , 我需要将 obj 劫持一份出来
// 因为原始数据是对象 , 我们也要准备一个对象
const result = {}
//开始劫持
Object.defineProperty(result, 'name', {
    get () {
        return obj.name
    },
    set (val) {
        // 我去修改原始数据obj中的name属性
        // 这个时候就会导致 result 内的name属性因为obj内的name属性的修改而修改
        obj.name = val
        box.innerHTML =`我是${result.name},今年${result.age}岁了`
    }
})
// 继续劫持一个数据age
Object.defineProperty(result, 'age', {
    get () {
        return obj.age
    },
    set (val) {
        obj.age = val
        box.innerHTML =`我是${result.name},今年${result.age}岁了`
    }
})
// 利用我们劫持出来的数据渲染页面
//获取元素
const box = document.querySelector('.box')
box.innerHTML =`我是${result.name},今年${result.age}岁了`
// 因为result是利用obj复刻出来的 , 渲染出来的页面是一样的
        
// 打印我们的劫持结果
console.log(obj);
console.log('result:',result);
// 劫持完毕以后我们在修改我的原始数据
obj.name = '小红'
console.log('obj:',obj);
console.log('result:',result);// 劫持出来的数据也会修改
// 但是这样的修改方式我们是不推荐的 , 我们要利用setter设置器来设置
result.name = '小明'


复制代码

数据劫持简单封装
●因为以上的操作还是比较麻烦的
●我们自己做一个简单的封装

function observer (origin,fn) {
    // origin : 原始数据 , 也就的你给我的数据
    // fn : 我想要做的事情
    // 定义一个劫持目标
    const target = {}
    // 我们循环拿到传递进来的数据
    for (let k in origin) {
        // 开始劫持
        Object.defineProperty(target, k,{
            // 返回的数据
            get () {
                return origin[k];
            },
            set (val) {
                // 修改的是origin内的数据
                origin[k] = val
                // 修改完毕要执行一下我的功能函数
                fn(target)
            }
        })
    }
    // 初始的时候我们也要执行一下我们的功能函数
    fn(target)
    // 返回一个被劫持后的数据
    return target
}
// 获取元素
const box = document.querySelector('.box')
// 将来我们使用的时候
const app = observer({name:'Rose',age:30},result => {
    // 箭头函数中是我想要做的事情
    box.innerHTML =`我是${result.name},今年${result.age}岁了`
})
console.log(app);
// 获取元素
const inp = document.querySelector('input')
inp.oninput = () => {
    app.age = inp.value


复制代码

数据劫持 + 渲染
结构内容

    <!-- 需要渲染的结构 -->
    <div id="app">
        <!-- 根据我们的约定使用 -->
        <p>你好 想使用我的name属性 : {{ name }}</p>
        <p>你好 想使用我的name属性 : {{ age }}</p>
    </div>


复制代码

封装操作
封装约定


为了将来使用者考虑 , 需要指定一些规则
 => 把渲染页面这个事情 , 直接书写在 html 结构中
 => 让我来捕获 , 我来替你进渲染
约定一下我们的渲染规则
 => 因为我们已经确定了最终的根元素
 => 我们只要把我们的约定放到根元素中就可以了
 => 如何放进去
     -> 当你需要渲染一个基本数据的时候
     -> 约定一个自己认识的标识符
     -> 我们就约定 {{}}
     -> vue中使用的就是{{}}


复制代码

封装


 <script>

            // options是我们的配置项
            // 所有的参数都已配置项的方式传递给我
            /* 
                参数:
                    => el : 需要一个选择器 , 用来锁定一个页面结构,将来所有需要渲染的东西都在这个结构里面
                    => data : 表示的是需要在页面上渲染的数据
            */

        function hijack(options = {}) {
            // 为了防止没有传递 设置一个默认值 , 是一个对象
            console.log(options);
            // 首先判断元素是否存在
            // 如果没有传递进来 , 后续就什么都不做了
            // if (!options.el) return // 这样的方式可以 ,但是不够友好 , 最好是能给一个提示(报错)

            // 我们换一个写法: 手动抛出异常:
            // 语法: throw new Error(错误信息)
            if (!options.el) throw new Error('这里需要一个范围元素,但是你没有传递')
            // 代码能来到这里说明有传递参数进来
            // console.log('你传递了参数:',options.el);

            // 之后根据你传递进来的 el 选项去获取元素
            const app = document.querySelector(options.el)
            // console.log(app);
            // 这里判断一下你传递的参数(#app)有没有写错
            if (!app) throw new Error('传递的el有错误我没有获取到')

            // 从这之后我们就正常的开始渲染就好了
            // 首先需要劫持 options.data 中的数据
            // 再进行劫持之前需要判断有传递data
            if (!options.data) return
            // 之后要确定传递的data一定是一个对象才可以
            if (Object.prototype.toString.call(options.data) !== '[object Object]') throw new Error('这里必须传递一个对象,你传递的不是')
            // 定义一个对象
            const _data = {}
            // 我们最好是把传递进来的数据留存一份
            _data.origin = options.data
            // 我们需要保存原始的基础结构
            const appStr = app.innerHTML
            // 开始劫持
            for (let k in options.data) {
                Object.defineProperty(_data,k,{
                    get () {
                        return options.data[k]
                    },
                    set (val) {
                        options.data[k] = val
                        // 将来我们任何一个属性需要更改的时候都需要执行一遍
                        rander(app,_data,appStr)
                    }
                })
            }
            // 一开始的时候需要初始执行
            rander(app,_data,appStr)
            // 最后我们一定是要返回我们劫持后的数据的
            return _data
        }
        // 准备一个渲染函数
        function rander (app,_data,appStr) {
            // 这里需要获取到元素内的所有内容
            let str = appStr
            // console.log(str);
            // 准备一个正则,用来截取{{}}
            let reg = /{{ *(\w+) *}}/g

            // 之后从str中捕获出来我们需要的内容
            let res = str.match(reg)
            // console.log(res);

            // 再之后我们用劫持内的数据 ,替换掉str内的内容
            // 既然需要劫持内的数据我们就要传递进来 , 我们使用_data
            res.forEach( () => {
                // 需要从str中拿到 key
                // let key = reg.exec(str)
                // console.log(key);
                let key = reg.exec(str)[1]
                // console.log(key);

                // 开始替换
                str = str.replace(/{{ *(\w+) *}}/,_data[key])
                console.log(str);
            })
            // 替换完毕以后我们替换app中的数据
            app.innerHTML = str

        }
    </script>


复制代码

使用


  <script>
        // 使用
        const app = hijack({
            // 传递范围元素
            el:"#app",
            // 准备我们需要渲染的数据
            data: {
                name:'Jack',
                age:25,
            }
        })

        console.log(app);
    </script>

复制代码

数据劫持升级
●刚刚我们的数据劫持是劫持出一个新的来
●现在就是在自己身上劫持
●操作的就是原始数据
劫持升级操作
语法:

Object.defineProperties(对象,{

​            哪一个属性:{配置项},

​            哪一个属性:{配置项},

​            ....

​          })


复制代码

配置项中的内容


value:该属性名对应的值
writable:该属性是否可以被重写(就是重新设置值)
  - 默认是 false 不允许被重写 , 也就是只读
  - 选填是 true 表示允许被重写 , 也就是可以被修改
    enumerable: 表示该属性是否可以不枚举(遍历) 
  - 默认是 false 不可以枚举
  - 选填是 true 表示可以遍历
    get: 是一个函数 , 叫做getter获取器
  - 可以用来获取该属性的值
  - get属性的返回值就是这个属性的值
  - 注意: 不能和 value 和 writable 一起使用 , 会报错
    set: 是一个函数 , 叫做setter设置器
  - 当你需要修改该属性值的时候 , 会触发该函数


复制代码

// 数据劫持
const obj = {name:'Jack',age:30}
console.log('原始obj:',obj);
// 开始劫持 把obj劫持到obj身上
// 拿到obj中的所有的key
for (let k in obj) {
    // 开始劫持
    Object.defineProperties(obj,{
        // 这样操作会报错
        // 一旦 obj.name 被修改
        // 不停的获取不停的该 , 不停的该不停的获取
        // 就会造成栈溢出
        // name:{
        //     get () {
        //         return obj.name
        //     }
        // }
        // 步骤1: 我们需要复制一份属性出来(也就是备份一份)
        // 这里需要拼接一个变量出来
        // 假设:之前的属性是name , 我们复制出来的属性叫做:_name
        // 语法: ['_' + 属性名] : {}
        ['_' + k] : {
            value: obj[k],
            writable: true 
        },
        // 步骤2: 正式开始劫持(劫持的是我们备份的)
        [k] : {
            get () {
                return this['_' + k]
            },
            set (val) {
                this['_' + k] = val
                // 渲染页面操作
                console.log('你尝试修改 ' + k + ' 属性, 你要修改为 : ', val)
            }
        }
    })
}
console.log('劫持后obj',obj);
// 尝试修改
obj.age = 66


复制代码

劫持升级的缺点
●就是只能劫持当前以前的数据
●如果劫持过后 , 后期动态插入的数据不好使(也就的不能再被劫持到)

obj.gender = '男'
console.log(obj); // {gender: '男', _name: 'Jack', _age: 66}

复制代码

数据代理
●这个是官方给的说法(名字) , 但是在社区里大家还是都加数据劫持
●因为大家都习惯了这个叫法
●而且这个行为也是一个劫持行为
●是 ES6 提供的语法 , 是一个内置构造函数
●语法: const 变量名 = new Proxy(要代理的原始对象,{ 配置项 })
●返回值: 一个实例对象 , 就是代理结果数据

// 定义一个对象
const obj = {name: 'Jack', age:25} 
// 开始代理
const result = new Proxy(obj, {
    // 配置get来进行代理设置
    get (target,prototype) {
        // target:就是你要代理的目标对象 , 我们这个案例中就是obj
        // prototype: 是该对象内的每一个属性 , 自动遍历
        return target[prototype]
    },
    // 配置set来进行修改
    set (target,prototype,val) {
        // target: 就是你要代理的目标对象
        // prototype: 就是对象内你要修改的那个属性
        // val: 就是你要修改的那个属性的值
        target[prototype] = val
        console.log('你尝试修改'+ prototype + '属性,想要修改为:'+ val + ',我要根据你修改的内容渲染页面');
        // 注意: 简单代理需要返回 true 让程序正常执行
        return true 
    }
})
console.log('原始数据:',obj);
console.log('代理数据:',result);
console.log('代理结果 name:',result.name);
        
// 尝试修改
result.name = 'Rose'
console.log('代理结果 name:',result.name);
        
// 动态插入数据
result.gender = '男'


复制代码

数组扁平化
●就是把多维的数组转成一位的数组格式


const arr = [1, 2, [3, 4, [5, 6, [7, 8, [9,]]]]]
// res = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
// 方案1:
// 准备一个新数组
// 遍历原始数组, 依次插入到新数组内
// 因为不确定深度有多少层, 需要递归实现
function flat(origin) {
    // 准备一个新数组
    const ary = []
    // 递归遍历原始数组
    function fn(origin) {
        origin.forEach(item => {
            // 判断 item 如果是一个数组 - 递归
            if (item.constructor === Array) {
                // 把 item 内的所有内容拆开插入到 ary 内
                fn(item)
                // 判断 item 如果不是一个数组 - 插入 ary
            } else {
                ary.push(item)
            }
        })
    }
    // 把 origin 内的所有内容拆开插入到 ary 内
    fn(origin)
    // 返回 ary
    return ary
}
// 使用的时候
// res 接受的就是一个扁平后的数组
const res = flat(arr)
console.log(res)
// 方案2: 利用 toString()
// 缺点: 尽量操作的全都是基本数据类型
//   数组.toString() 就是把数组的中括号全部去掉
// 注意: 这个方法的返回值是一个字符串, 需要再次使用 split() 拆开
const res1 = arr.toString().split(',')
console.log(res1)
// 方案3: 利用 flat 方法
// ES6 提供的数组常用方法
// 语法: 数组.flat(数字)
//   数字: 表示拆开多少层
const res2 = arr.flat(Infinity)
console.log(res2)


复制代码

递归操作数组
●就是利用递归的方式把一个多维数组变成一维数组

// 定义一个多维数组
var arr = [11, [22, 33, [44, [55,[66]]]]];
// 定义一个新数组
var newArr = [];
// 声明一个函数 , 传递需要我们操作的数组
function copyArr(arr) {
    // 获取到我们数组中的每一项
    arr.map(item => {
        // 判断里面的每一项是不是一个数组
        if (Array.isArray(item)) {
            // 代码能执行到这里说明是一个数组,继续调用我们的copyArr函数
            copyArr(item)
        } else {
            // 代码执行到这里说明不是一个数组,那就把数据添加到新数组中
            newArr.push(item);
        }
    })
}
// 调用函数,传递我们要操作的数组
copyArr(arr)
// 查看一下我们的新数组
console.log(newArr)


复制代码

拆解数组


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="./address.js"></script>
    <script>
        /*
            [ {} * 3300+ ]

            [
                {
                省份
                chidlren: [
                    {
                    市
                    children: [
                        { 区 }
                    ]
                    }
                ]
                }
            ]

            分析数据:
                { ID: 自己的编号, TopID: 0, AddName: 'xxx' }
                { ID: 自己的编号, TopID: 2, AddName: 'xxx' }
                { ID: 自己的编号, TopID: 3, AddName: 'xxx' }
                { ID: 自己的编号, TopID: 373, AddName: 'xxx' }
                + TopID 为 0 的数据, 表示我是 一级数据(省)
                + 如果某一个数据的 TopID 和 省的 id 一样, 表示我是 市
                + 如果某一个数据的 TopID 和 市的 id 一样, 表示我是 区
        */

        // 1. 准备一个新数组
        const info = []

        // 2. 遍历原始数组, 把所有省份信息拿出来
        address.forEach(item => {
            // 判断 TopID 为 0 的数据
            if (item.TopID === 0) {
                // 每一个省份信息, 需要包含一个 children 属性, 值是一个数组
                // 该数组内存储的是该省对应的 市 信息
                item.children = []
                info.push(item)
            }
        })
        // console.log(info)

        // 3. 遍历原始数组, 找到市的信息
        address.forEach(item => {
            // 某一个 item 的 TopID 需要和 info 内某一个 ID 一样
            const res = info.findIndex(t => t.ID === item.TopID)
            if (res === -1) return

            // res 得到的是 一维的位置
            // console.log(res)

            // 把 item 插入到 res 的 children 内
            item.children = []
            // info[res].children.length 就是二维的位置
            item.one = res
            item.two = info[res].children.length
            info[res].children.push(item)
        })

        // 4. 遍历原始数组, 找到区的信息, 存放在市的 children
        //    问题: 市是在二级数组内
        //    假设: 一个数据(a)的 ID 是 224
        //         遍历数组的时候, 找到 TopID 是 224 的应该放在数据a 的 chidlren 内
        //         数据a 是在一个 二级数组内
        address.forEach(item => {
            // 找到的有 二维数据 和 三维数据
            // 二维数据找到的没有 one 和 two 的
            // 三维数据找到的是有 one 和 two 的
            const res = address.find(t => t.ID === item.TopID)

            // 一维数据 是 undefined
            if (!res) return
            if (res.one === undefined) return

            // 把 res 插入到 info[one].chidlren[two].chidlren
            info[res.one].children[res.two].children.push(item)
        })

        console.log(info)
    </script>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值