一、函数柯里化
- 把一次传递两个参数, 变成需要调用两次但每次只传递一个参数
- 利用了闭包, 把第一次传递的参数保存下来(延长生命周期)
- 正则验证密码
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(哪一个对象, 属性名, { 配置项 }) - 配置项:
- value: 该属性对应的值
- writable: 该属性是否可以被重写, 默认是 false,代表不允许被修改,改成true则允许被修改
- enumerable: 该属性是否可被枚举, 默认是 false,代表不能被遍历
- get: 是一个函数, 叫做 getter 获取器, 可以来决定该属性的值
- get 函数的返回值, 就是当前这个属性的值
- 注意: 不能和 value 与 writable 一起使用, 会报错
- 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 = "男";