函数柯里化+数据劫持--js(二十二)

115 阅读4分钟

一、函数柯里化

  • 把一次传递两个参数, 变成需要调用两次但每次只传递一个参数
  • 利用了闭包, 把第一次传递的参数保存下来(延长生命周期)
  • 正则验证密码
const reg = /^\w{6,12}$/;
const res = reg.test("asdvkashas");
console.log(res);
  • 封装为函数
function fn1(reg, str) {
    return reg.test(str);
}
console.log(fn1(/^\w{6,12}$/, "abcdefg"));
console.log(fn1(/^\w{6,12}$/, "abcdefghijkl"));
console.log(fn1(/^\w{6,12}$/, "abcdefghijklmnopq"));
  • 封装为柯里化函数
 function test(reg) {
            return function (str) {
                return reg.test(str)
            }
        }

        const res_1 = test(/^\w{4,6}$/)
        console.log(res_1('QF001'))//true
        console.log(res_1('qwe123'))//true
        console.log(res_1('!@#$%^&*(*&^%$#@!#$%^&)'))//false

        const res_2 = test(/^\d{4,6}$/)
        console.log(res_2('QF001'))//false
        console.log(res_2('12345'))//true

二、函数柯里化封装

/**
 *  函数柯里化
 *      内层函数负责处理功能
 *      外层函数负责处理参数 (当参数满足指定数量时在执行)
 *
 *  功能: 拼接地址栏字符串
 *      http://localhost:8080/index.html
 *          传输协议: http  https
 *          域名:   localhost   127.0.0.1
 *          端口号: 0~65535
 *          地址:   /a  /a/b    /a/b/index
 *
 *  http://localhost:8080/地址1
 *  http://localhost:8080/地址2
 *  http://localhost:8080/地址3
 *  http://localhost:443/地址1
 *  http://localhost:445/地址2
 *  http://localhost:7777/地址3
 *  http://127.0.0.1:7777/地址1
 *  http://127.0.0.2:7778/地址2
 *  http://127.0.0.3:7779/地址3
 *
 */
function fn(a, b, c, d) {
    return a + "://" + b + ":" + c + d;
}
// let res = fn('http', 'localhost', '8080', '/a/b/d')
// console.log(res)

// 外层函数: 负责接受功能参数, 与基础参数
function currying(callback, ...arg) {
    // 利用扩展运算符以数组的形式拿到所有的 基础参数, 没有的话为 空数组

    // 内层函数负责判断参数是否满足指定数量, 满足调用函数, 不满足继续接收参数
    return function (..._arg) {
        // 将来内层函数也有可能接收参数
        _arg = [...arg, ..._arg];
        if (_arg.length >= callback.length) {
            // callback.length 拿到函数形参的数量
            return callback(..._arg);
        } else {
            return currying(callback, ..._arg);
        }
    };
}
 
let r1 = currying(fn, "https", "localhost");
let r2 = currying(fn, "https");
let r3 = currying(fn);
let res1 = r1("8080", "/a/b/c");
let res2 = r1("7777", "/index");

console.log(res1);
console.log(res2);
    function keli(callback, ...args) {
            return function (..._args) {
                _args = [...args, ..._args]
                if (_args.length >= callback.length) {
                    return callback(..._args)
                } else {
                    return keli(callback, ..._args)
                }
            }
        }
        
         const res_1 = keli(fn, 'http', '127.0.0.1')

        /**
         *  调用了 内层函数, 并且又重新传递了一个字符串, 现在结合之前的两个字符串, 一共收集到了 3 个 字符串
         *
         *  所以根据内层函数里边的逻辑, 我们此时运行的 是 else 分支
         *
         *  内层函数中 返回了一个 keli 函数的调用结果
         *
         *  所以实际上我们相当于是拿到了 内层函数
        */
        const res_2 = res_1('8081')
        console.log(res_2)    // 内层函数

        /**
         *  刚才分析得出 res_2 其实就是我们的内层函数, 目前结合我现在传递的一个参数, 一共有 4 个参数了
         *
         *  所以此时就回调用函数, 而不是继续返回一个内层函数收集参数
        */
        const str = res_2('/a.html', 10086)
        console.log(str)

三、函数的防抖与节流

1、防抖:

  • 在短时间内快速的触发一件事, 每次都用新的事件替代上一次, 也就是说那么我们只会执行最后一次触发的事件
  • 短时间内快速触发一件事, 只会执行最后一次

1)方法一

 let timerID = 0 // 用于存储倒计时器的ID, 用于将来的清除
        box.oninput = function (e) {
            clearInterval(timerID)
            timerID = setTimeout(() => {
                console.log('搜索了: ', e.target.value)
            }, 300)
        }

2)方法二

  box.oninput = (() => {
            let timerID = 0
            return function (e) {
                clearInterval(timerID)
                timerID = setTimeout(() => {
                    console.log('搜索了: ', e.target.value)
                }, 300)
            }
        })()

3)方法三

 box.oninput = ((timerID) => {
            return function (e) {
                clearInterval(timerID)
                timerID = setTimeout(() => {
                    console.log('搜索了: ', e.target.value)
                }, 300)
            }
        })(0)

2、节流:

  • 在短时间内快速的触发一件事, 当一事件处理函数开始执行的时候, 在此期间, 不允许重复触发
  • 以前的瀑布流/轮播图 都有使用节流处理

1)方法一

  let flag = true // 当前变量是开关变量, 默认值为 true 表示能够处理事件
        box.oninput = function (e) {
            if (!flag) return
            flag = false
            setTimeout(() => {
                console.log('搜索了: ', e.target.value)
                flag = true
            }, 300)
        }

2)方法二

    box.oninput = (() => {
            let flag = true
            return function (e) {
                if (!flag) return
                flag = false
                setTimeout(() => {
                    console.log('搜索了: ', e.target.value)
                    flag = true
                }, 300)
            }
        })()

3)方法三

 box.oninput = ((flag) => {
            return function (e) {
                if (!flag) return
                flag = false
                setTimeout(() => {
                    console.log('搜索了: ', e.target.value)
                    flag = true
                }, 300)
            }
        })(true)

四、数据劫持

  • 将来在框架中我们通常都是 数据驱动视图
  • 也就是说: 修改完毕数据, 视图自动更新

1. 数据劫持

  • 基于一个原本数据,劫持出来一个数据
  • 当前方法添加的属性,不允许被修改, 也不允许被遍历到
  • 语法: Object.defineProperty(哪一个对象, 属性名, { 配置项 })
  • 配置项:
    1. value: 该属性对应的值
    2. writable: 该属性是否可以被重写, 默认是 false,代表不允许被修改,改成true则允许被修改
    3. enumerable: 该属性是否可被枚举, 默认是 false,代表不能被遍历
    4. get: 是一个函数, 叫做 getter 获取器, 可以来决定该属性的值
      • get 函数的返回值, 就是当前这个属性的值
      • 注意: 不能和 value 与 writable 一起使用, 会报错
    5. set: 是一个函数, 叫做 setter 设置器, 当你需要修改该属性的值的时候会触发该函数
const obj = {};
obj.name = "Jack";

Object.defineProperty(obj, "age", {
    // value: 20,
    // writable: true,
    enumerable: true,
    get() {
        return 100;
    },
    set(val) {
        console.log("你想修改 age 的值, 你想修改为", val);
    },
});
// // console.log(obj)
// obj.name = 'Jack001'
obj.age = 50;
console.log(obj);

// for (let k in obj) {
//     console.log(k)
// }

2. 实现数据劫持

const obj = {
    name: "QF001",
    age: 18,
};
const res = {};
Object.defineProperty(res, "name", {
    get() {
        return obj.name;
    },
    set(val) {
        // 设置的时候, 直接去修改原对象 obj 中对应属性的值, 就能够影响到 res对象中对应的值
        obj.name = val;
    },
});
Object.defineProperty(res, "age", {
    get() {
        return obj.age;
    },
    set(val) {
        // 设置的时候, 直接去修改原对象 obj 中对应属性的值, 就能够影响到 res对象中对应的值
        obj.age = val;
    },
});
console.log(res)
console.log(res.age)
console.log(res.name)
        const obj = {
            name: 'QF001',
            age: 18
        }

        const newObj = {}
        for (let key in obj) {
            Object.defineProperty(newObj, key, {
                get() {
                    return obj[key]
                },
                set(val) {
                    obj[key] = val
                }
            })
        }

        console.log(newObj)

3.数据劫持实战

    <h1></h1>
    <input type="text">
     // 0. 获取标签
        const h = document.querySelector('h1')
        const inp = document.querySelector('input')

        // 0. 准备全局变量
        const info = {
            text: '一个默认的字符串'
        }
        // 1.5 添加一个数据劫持
        Object.defineProperty(info, '_text', {
            get() {
                console.log('访问了 _text 属性')
                return info.text
            },
            set(val) {
                console.log('修改了 _text 属性, 修改的值: ', val)
                info.text = val
                h.innerHTML = info._text
            }
        })
        h.innerHTML = info._text
        // 2. 输入事件触发时, 修改数据
        inp.oninput = function () {
            info._text = this.value
        }

4. 封装数据劫持

function observer(origin, fn) {
    // origin 劫持的原数据, fn 你想做的事

    // 劫持目标
    const target = {};

    for (let k in origin) {
        Object.defineProperty(target, k, {
            get() {
                return origin[k];
            },
            set(val) {
                // 设置的时候, 直接去修改原对象 obj 中对应属性的值, 就能够影响到 res对象中对应的值
                origin[k] = val;
                fn(target);
            },
        });
    }

    // 首次调用一下, 让页面有数据
    fn(target);

    // 返回一个被劫持后的数据
    return target;
}
const obj = {
    name: "QF001",
    age: 18,
};
function fn(res) {
    box.innerHTML = `名字: ${res.name}; 年龄: ${res.age}`;
}
const app = observer(obj, fn);
console.log(app);

inp.oninput = function (e) {
    app.age = e.target.value;
};

五、模拟双花括号语法

1、封装操作

/**
 *  为了将来使用者考虑, 需要制定一些规则
 *      + 把 渲染页面这件事, 直接书写在 html 结构内
 *      + 让我来捕获, 我来替你进行渲染
 *
 *  约定一些渲染规则
 *      + 因为我们已经确定了根元素
 *      + 把我的约定放在了根元素内就可以了
 *      + 当你需要渲染一个基本数据的时候
 *          => 约定一个自己认识的符号
 *          => {{}}
 */
/**
 * options 是一个配置项
 *  所有参数都以配置型的方式传递给我
 *      el: 参数, 需要一个选择器 用来锁定一个 页面结构
 *      data: 你需要在页面上渲染的数据
 */
function fn(options = {}) {
    // 1. 确保传递了 el 参数
    if (options.el === undefined) {
        throw new Error("您的 el 选项没有传递, 请您传递");
    }

    // 2. 确保传递了 data 对象
    if (options.data == undefined || options.data.constructor !== Object) {
        throw new Error("您没有按照规定传递一个 对象形式的 data");
    }

    // 3. 确保能正确拿到根节点
    const root = document.querySelector(options.el);
    if (root === null) {
        throw new Error("您传递的 el 参数有误, 我没有获取到对应节点");
    }

    // 4. 劫持 options.data 内的数据
    const _data = {};
    // 4.1 留存一份 data 的数据 (可选, 有了更好)
    _data.origin = options.data;
    // 4.2 保存原始基础结构 (不存的话, 首次渲染完毕页面结构将没有 {{}}, 那么render函数就没办法找到对应的位置赋值, 先写后续演示问题)
    const rootStr = root.innerHTML;
    // 4.3 开始劫持
    for (let k in options.data) {
        Object.defineProperty(_data, k, {
            get() {
                return options.data[k];
            },
            set(val) {
                options.data[k] = val;
                // 重新修改数据时需要重新渲染页面, 封装一个 rander 函数
                rander(root, _data, rootStr);
            },
        });
    }

    // 5. 一开始的时候先渲染一些页面
    rander(root, _data, rootStr);

    // 6. 返回劫持后的对象
    return _data;
}
function rander(root, _data, str) {
    // 正则
    const reg = /{{ *(\w+) *}}/g;

    // 从 str 内捕获到所有的内容
    const res = str.match(reg);
    res.forEach((item) => {
        const key = reg.exec(str)[1];
        console.log(key);

        str = str.replace(/{{ *(\w+) *}}/, _data[key]);
    });

    // 替换完毕以后, 去更新 root 内的数据
    root.innerHTML = str;
}

2、实际使用

const app = fn({
    el: "#root",
    data: {
        name: "QF001",
        age: 18,
        message: "醒醒, 别睡了, 就说你呢!",
    },
});

3、DOM 结构

<div id="root">
    <p>你好 想使用我的name属性 : {{ name }}</p>
    <p>你好 想使用我的age属性 : {{ age }}</p>
    <p>你好 想使用我的message属性 : {{ message }}</p>
</div>

六、数据劫持升级

  • 语法: Object.defineProperties(对象, {配置项})

  • 弊端: 数据劫持后新增的数据无法被劫持


        /**
         *  Object.defineProperties('劫持到那个对象', {
         *      属性1: {属性1的配置项},
         *      属性2: {属性2的配置项},
         *      属性3: {属性3的配置项},
         *  })
        */
// 原先: Object.defineProperty(对象, 属性, {配置项})

1.基本演示

const obj = {
            name: 'QF001',
            age: 18
        }
        const newObj = {}
        newObj.a = 100
        Object.defineProperties(newObj, {
            name: {
                // value: 10086
                value: obj.name,
                writable: true,
                enumerable: true
            },
            age: {
                get () {
                    return obj.age
                },
                set (val) {
                    obj.age = val
                }
            },
        })
        console.log(newObj)
        

2.真正升级


        const obj = {
            name: 'QF001',
            info: 666
        }

        // 如果是在 数据劫持前 给对象添加的了一些属性, 那么也会被数据劫持
        obj.qwe = 100

        // 真正的自己劫持自己
        for (let key in obj) {
            Object.defineProperties(obj,
                {
                    /**
                     * 目前我们是在一个对象中书写代码, 对象中 冒号左边的我们称之为属性名, 
                     *  这里不管些什么我们都当成一个字符串处理
                     * 
                     *  如果我们需要将冒号左边的内容当一个变量处理, 那么我们需要借助中括号语法
                    */
                    // key: {}      // 如果这样写 就是将 key 当成一个字符串使用
                    ['_' + key]: {    // 现在的写法是将 key 当成一个变量使用
                        value: obj[key],
                        writable: true
                    },
                    [key]: {
                        get() {
                            console.log('你访问了 obj 中的一个属性')
                            return obj['_' + key]
                        },
                        set(val) {
                            console.log('你想要修改 obj 中的一个属性')
                            obj['_' + key] = val
                        }
                    }
                }
            )
        }


        // 目前我们所接触到的 数据劫持, 如果在劫持后 添加一个新的属性, 那么这个属性不会被数据劫持
        obj.age = 18
        console.log(obj)

七、数据代理

  • proxy (数据代理是官方给的, 但是大家还是习惯叫 数据劫持)
  • ES6 提供的语法
  • 内置构造函数
    • Proxy(代理原始对象, {配置项})
    • get(target, property)
    • set(target, property, val)
const obj = {
    name: "QF001",
    age: 18,
};
console.log("原始的对象: ", obj);

// 开始代理
const res = new Proxy(obj, {
    // 配置 get 来进行代理设置
    get(target, property) {
        // target, 就是你要代理的目标对象, 当前案例中 为 obj
        // property, 就是该对象内的每一个属性, 自动遍历
        return target[property];
    },
    // 配置 set 来进行修改
    set(target, property, val) {
        // target, 就是你要代理的目标对象, 我们当前案例就是 obj
        // property, 就是你要修改的 对象属性
        // val, 就是你要修改那个属性的值
        target[property] = val;

        console.log(
            "你想要修改",
            property,
            "属性, 你想修改为: ",
            val,
            "我需要根据你修改的内容重新渲染页面"
        );

        // 注意!!! 简单代理需要返回 true (返回 true 代表属性设置成功; 后续详细讲解)
        return true;
    },
});

console.log("代理后的对象: ", res);

res.name = "100";

res.gender = "男";