数据劫持
用处 :
- 将来使用框架的时候(vue), 框架目前都支持一个数据驱动视图
如何操作 :
- 完成驱动视图, 需要借助数据劫持 帮助我们完成以原始数据为 基础, 对数据进行一份复制
- 复制出来的数据是不会被修改的, 值从原始数据里面获取
语法 : Object.defineProperty( 哪一个对象, 属性, {配置项} )
配置项 : 是一个对象的格式
- value : 这个属性对应的值
- writable : 该属性是否可以被重写, 默认是 false (不允许修改)
- Enumerator : 该属性是否可以被枚举(遍历), 默认是false(不能被枚举到)
- get(){} : 是一个函数, 叫做 getter 获取器, 可以决定当前属性的值
- 写了get 就不能和 value, writable 同时使用
- set(){} : 是一个函数, 叫做 setter 获取器, 可以决定当前属性的值
- 代码实现思路 :
- 创建出一个对象, 给对象设置属性及属性值
- 通过数据劫持方法, 进行传参(传对象,属性名,{配置项})
- 通过配置项里的 Enumerator: true 方法设置可以修改属性值
- 通过配置项中的 get 方法返回属性值, set 方法改变属性值 */
// 1. 创建出一个对象, 给对象设置属性及属性值
const obj = {}
obj.name = '张三'
obj.age = 99
// 2. 通过数据劫持方法, 进行传参(传对象,属性名,{配置项})
Object.defineProperty(obj, 'age', {
// value: 18,
// writable : true,
// 3. 通过配置项里的 Enumerator: true 方法设置可以修改属性值
Enumerator: true,
// 4. 通过配置项中的 get 方法返回属性值, set 方法改变属性值
get() {
console.log('你现在访问了obj的 age属性, 然后还可以在函数内部做很多事情');
return 300
},
set(val) {
console.log('你现在想要修改obj的age 属性, 所以触发了 set 函数, 你想要设置的值为' , val);
}
})
console.log(obj); // age : 300
封装数据劫持
为什么要封装数据劫持 ?
- 如果要劫持的数据多了, 援鄂版的写法不方便, 只能一个一个属性劫持, 代码较多
- 代码实现思路 :
- 封装一个数据劫持函数, 传参(旧对象, 回调函数)
- 创建一个新对象
- 通过遍历对象, 将劫持旧对象的属性 放到新对象中
- 通过配置数据劫持的方法, 进行传参(传新对象, 属性名,{配置项})
- 通过配置项中的 get 方法返回属性值, set 方法修改属性值
- 将劫持后的新对象 返回出去 验证 :
- 获取页面元素
- 创建一个对象(原始对象)
- 创建一个劫持后的对象, 调用封装数据劫持对象
- 封装一个回调函数, 起渲染页面作用
- 修改属性值
// 0. 封装数据劫持, 如果劫持的数据多了, 原本的写法不方便, 代码较多
function observer(origin, callback) {
// 1. 创建一个新对象
const target = {}
// 2. 通过遍历对象, 将劫持旧对象的属性 放到新对象中
for (let key in origin) {
// 3. 通过数据劫持方法, 进行传参(传新对象,属性名,{配置项})
Object.defineProperty(target, key, {
// 4. 通过配置项里的 Enumerator: true 方法设置可以修改属性值
Enumerator: true,
// 5. 通过配置项中的 get 方法返回属性值, set 方法修改属性值
get() {
return origin[key]
},
set(val) {
origin[key] = val
callback(target)
}
})
}
// 99. 将劫持后的target 返回出去
return target
}
// 验证
// 0. 获取页面元素
const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')
// 1. 创建一个对象(原始对象)
const obj = {}
obj.name = '战三'
obj.age = 18
// 2. 创建一个劫持后的对象, 调用封装数据劫持对象
const newobj = observer(obj, fn)
// 3. 封装一个回调函数, 起渲染页面作用
function fn(res) {
h1.innerHTML = `年龄 : ${res.age}; 名字 : ${res.name}`
}
// 4. 修改属性值
newobj.age = 666
newobj.name = '李四'
基础版 :
语法 : Object.defineProperty( 哪一个对象, 属性, {配置项} )
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
}
}
})
升级版 :
语法 : Object.defineProperties('哪个对象', {配置项})
const box1 = document.querySelector('.box1')
const box2 = document.querySelector('.box2')
// 原始对象
const obj = {}
obj.name = '张三'
obj.age = 18
// 将数据劫持后的属性放在res 对象中
const res = {}
// 利用循环优化代码量 :
// 1. 通过遍历对象, 将劫持旧对象的属性 放到新对象中
for (let key in obj) {
// 2. 通过配置数据劫持的方法, 进行传参(传新对象, 属性名,{配置项})
Object.defineProperties(res, {
// 此处的key 我们的需求是当一个变量使用, 如果直接写 那么会当成一个字符串, 解决方案在key 加一个[], 包裹起来, 当一个变量
[key]: {
// 3.通过配置项中的 get 方法返回属性值, set 方法修改属性值
get() {
return obj[key]
},
set(val) {
obj[key] = val
box2.innerHTML = `res对象的 age属性:${res.age},name属性 : ${res.name}`
}
},
})
}
// console.log(res);
box1.innerHTML = `obj对象的 age属性:${obj.age},name属性 : ${obj.name}`
box2.innerHTML = `res对象的 age属性:${res.age},name属性 : ${res.name}`
obj.age = 666 // obj的修改不会影响视图
obj.name = '李四'
res.age = 999 // res 的修改会触发 set 函数, set 函数内有一行代码会让box2 更新, 所以res 修改会让页面重新修改
res.name = '王五'
自己劫持自己版本 :
- 全部都在 原始属性对象上进行操作
// 原始对象
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 = '李四'
数据代理 (ES6以后官方推出的)
- 数据代理 是官方名字, 也叫做 数据劫持
- proxy : 是ES6 以后官方退出的, 是一个内置构造函数, 需要 new
语法 : new Proxy('要代理的对象', {配置项}) : 返回实例对象, 就是代理结果数据
- new Proxy()表示生成一个Proxy实例
- 代理原始对象: 要被代理的对象,可以是一个object或者function
- 配置项:也是一个对象,对该代理对象的各种操作行为处理。
- 代码实现思路 :
- 创建一个对象, 添加属性及水性指
- 通过ES6推出的 proxy(), 来进行数据劫持
- 2-1 因为proxy , 最后会返回一个代理后的对象, 所以我们需要声明一个变量去接收
- 2-2 通过proxy(), 进行传参(要代理的对象(原来对象), {配置项})
- 2-3 在配置项里, 通过get(){} 方法, 传参(新对象, 该对象内部的属性(自动分配)), 返回属性值
- 2-3 在配置项里, 通过set(){} 方法, 传参(新对象, 该对象内部的属性(自动分配), 修改后的属性值)
- n. 在代理完成后 给原始对象 新加一个属性, 此时代理对象依然能够访问到 (proxy 独有的功能)
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 = 'qwe'
console.log(res.age);
console.log(res.name);
console.log(res.abc);
res.age = 99
res.name = '李四'
回调函数
- 本质上就是一个函数
- 例 :
- 一个函数A 以实参的形参传递到函数B 中,
- 再函数B 中, 以形参的方式调用函数, 此时, 函数A 就可以称为 函数B 的回调函数
使用场景 : 为异步代码的解决方案
回调地狱
回调地狱是什么 ?
- 这不是我们写代码的时候出现的某个漏洞
- 只是我们再利用回调函数解决问题的时候, 代码量多了之后的一个视觉体验
- 回调地狱的代码不利于我们去维护或者管理
- 所以后续再处理 异步任务的时候, 我们就需要一些更加简洁的方法
- 这时就出现了一个东西叫做 promise, 他也是一个异步的解决方案
fn(
() => {
fn(
() => { console.log('班长买完水后, 又买了一箱饮料') },
() => { console.log('班长就买了一瓶水, 他不愿意给你们买饮料') }
)
},
() => {
fn(
() => { console.log('班长第二次买水 成功了') },
() => {
fn(
() => { console.log('班长第三次买水 成功了') },
() => { console.log('班长第三次买水 又失败了, 确实不争气') }
)
}
)
}
)
promise : 是 JS内置的一个构造函数
作用 : 一种新的异步代码封装方案, 用来代替 回调函数的, 解决回调地狱问题
promise 三个状态 :
- 持续 : pending
- 成功 : filfilled
- 失败 : rejected
- 只会在 持续状态转换为 成功
- 或者 失败状态转换为 失败
语法 : const p = new Promise()
// 检测机构
//resolve : 处理异步时成功的状态
//reject : 处理异步时失败的状态
new Promise((resolve,reject) => {
if(处理异步){
resolve([参数]);
}else{
reject([参数]);
}
})
Promise 对象上的方法 :
- .then(( ) => { }) 方法 ( 会在 promise 状态成功(resolve)的时候 执行)
- .catch(( ) => { }) 方法 ( 会在 promise 状态失败(reject)的时候 执行)
- .then( ).catch(error){ }.finally{ } => 每一次都会执行, 不考虑成功或 失败
function fn() {
const p = new Promise(function (resolve, reject) {
const timer = Math.ceil(Math.random() * 3000)
console.log('买瓶水去');
setTimeout(() => {
if (timer > 1500) {
console.log('买水失败,用时', timer);
reject('超时,所以买水失败') // shibai()
} else {
console.log('买水成功,用时', timer);
resolve('没有超时, 买水成功')// chenggong()
}
}, timer)
})
return p
}
- 普通调用 :
//普通调用
res.then(() => {
console.log('如果我执行了, 说明promise状态为成功');
})
res.catch(() => {
console.log('如果我执行了, 说明promise状态失败了');
})
- 链式调用 : 当在第一个 then 里面 返回一个 新的 promise 对象 然后你可以在 第一个 then 的后面 再次书写一个 then
// 链式调用
res.then(() => {
console.log(`因为${str},多以奖励`);
return fn() // 中断函数
}).then((str) => {
console.log('如果我输出了, 表示第二次买水成功');
return fn()
}).then((str) => {
console.log('如果我输出了, 表示第三次买水成功');
}).catch(() => {
console.log('如果我输出了, 说明之前某一次买水失败了');
})
// 1. promise 对象上的方法
/*
1. 调用封装的函数 (可以声明一个变量去接收)
2. 如果函数执行成功, 则执行 .then()
如果函数执行失败, 则执行 .catch()
不管函数执行成功或失败, 都会执行 .finally() 里的代码块
*/
const res = fn()
res.then(() => {
console.log('成功时执行');
}).catch(() => {
console.log('失败时执行');
}).finally(() => {
/*
正常业务场景中, 我们在发起一个请求的时候, 会将页面弹出一个遮罩层
然后再请求结束的时候, 需要将这个遮罩层关闭
这个时候如果放在 then 中, 那么会有一个问题, 就是请求失败的时候
所以我们一般不会放在 then 关闭遮罩层, 而是放在 finally 中
*/
console.log('每一次都会执行,(不会考虑成功还是失败)');
})
async 与 await : 可以把异步代码写的看起来像同步代码
- 是 promise 的一种调用方案 (也就是说, 必须结合着 peomise一起使用)
- 能够将异步代码 写的像 '同步代码一样'
- async 关键字 : 书写在一个函数的 开头, 表明当前函数是一个异步函数
- 内部可以书写一个异步函数, 内部可以写 await
- await 关键字 : 具有等待的含义, 书写在异步任务前
- 代码运行到这个位置的时候, 会有一个等待效果
- 一直等到这个异步任务结束, 并且将异步任务的反馈结果 当一个值返回
function fn() {
const p = new Promise(function (resolve, reject) {
const timer = Math.ceil(Math.random() * 3000)
console.log('帮买瓶水去');
setTimeout(() => {
if (timer > 3000) {
console.log('买水失败,用时', timer);
reject('超时,所以买水失败') // shibai()
} else {
console.log('买水成功,用时', timer);
resolve('没有超时, 买水成功')// chenggong()
}
}, timer)
})
return p
}
async function newFn() {
// 因为函数开头写了 async, 所以这个函数就是一个独特的异步函数, 内部可以书写 await
const r1 = await fn()
console.log('第一次买水', r1);
const r2 = await fn()
console.log('第二次买水', r2);
const r3 = await fn()
console.log('第三次买水', r3);
}
newFn()
async 与 await 缺点 :
- 不能正常拿到(捕获到)promise 失败状态
- 如果失败,会报错,终止程序继续执行
解决方案 :
- try{ } catch {}
- 封装一个永远不会失败的 promise
- 也就是说, 不管请求状态如何, 永远调用 resolve, 让 promise的状态一定是成功
- 为了区分这次请求是成功 还是失败, 我们不在单纯的返回一个字符串了
- 而是返回一个对象
- 注意 : 这个对象 我们的约定, 里边有一个 code属性, 成功时赋值为1, 失败时赋值为0
function fn() {
const p = new Promise(function (resolve, reject) {
const timer = Math.ceil(Math.random() * 3000)
console.log('买瓶水去');
setTimeout(() => {
if (timer > 1500) {
console.log('买水失败,用时', timer);
resolve({
code : 0, asg : '超时,买水失败'
})
} else {
console.log('买水成功,用时', timer);
resolve({
code : 1, asg : '没有超时,买水成功'
})
}
}, timer)
})
return p
}
// 解决缺点方案 1 :
newFn()
async function newFn() {
try {
const r1 = await fn()
console.log('第一次买水', r1);
} catch (error){
console.log(error); //把错误信息给到 error 参数
}
// 如果失败, 也会继续输出
console.log('我的执行并不会被打断');
}
// 解决方案 2 :
newFn()
async function newFn() {
const r1 = await fn()
console.log('第一次买水', r1);
if(r1.code === 0){
console.log('请求失败');
}else{
console.log('请求成功, 正常执行');
}
}
promise 构造函数方法 :
- Promise.all([ ]) => 调用所有的Promise对象, .then返回全部成功, 只要有一个失败, 就会返回reject(.catch)
- Promise.race([ ]) => 调用所有Promise对象, .then返回最快的那一个成功, .catch返回最快的哪一个失败状态
- Promise.allSettled([ ]).then((res) => { console.log(res) }
// 2. peomise 构造函数的一些方法
Promise.all([fn(), fn(), fn()]).then(() => {
console.log('所有的参数 全部返回一个成功状态, 会执行');
}).catch(() => {
console.log('所有的参数 中有一个为失败状态, 会执行catch');
})
Promise.race([fn(),fn(),fn()]).then(()=>{
console.log('这些参数中, 结束最快的哪一个状态为 成功的时候执行');
}).catch(()=>{
console.log('这些参数中, 结束最快的哪一个状态为 失败的时候执行');
})
/*
-每一个异步任务都想得到结果就使用Promise.allSettled()
-异步任务要求每个都成功才能往下执行就使用Promise.all()
*/
Promise.allSettled([fn(),fn(),fn()]).then((res)=>{
// 在书序内传递的 promise 全部执行完毕后, 返回一个数组给到 then 函数
// 数组内的对象就是我们的传递进来的 promise 对象的执行结果
console.log(res);
})
Promise.resolve().then(()=>{
console.log('强制返回一个状态成功的promise ');
})
Promise.reject().then(()=>{
console.log('如果wo打印了,说明当前的 promise 状态为成功');
}).catch(()=>{
console.log('强制返回一个 状态为 失败的 promise 对象');
})
设计模式
作用 :
- 为了解决 某一类问题的 一个优化过的代码解决方案
单例模式
- 一个构造函数, 一生只能创建一个实例化对象
- 准备一个变量默认赋值为 null, 然后再第一次实例化的时候 给这变量赋值为实例化对象
- 后续在调用实例化的时候, 就不再创建实例化对象了, 而是拿到提前准备好的变量
class Dialog {
constructor() {
console.log('在页面中, 创建了一个 弹出层');
}
}
let instance = null
function newlDialog() {
if (instance === null) {
instance = new Dialog()
}
return instance
}
let d1 = newlDialog()
console.log(d1); // 在页面中, 创建了一个 弹出层; Dialog {}
let d2 = newlDialog()
console.log(d2); // Dialog {}
升级版 :
- 原本的 instance 变量为一个全局变量, 本次利用自行执行函数与 闭包 将其他修改 为了 局部变量
const newlDialog = function () {
// 在 自执行函数内 创建一个变量(局部变量)
let instance = null
return function () {
if (instance === null) {
instance = new Dialog()
}
return instance
}
}()
const d1 = newlDialog()
console.log(d1); // 在页面中, 创建了一个 弹出层; Dialog {}
const d2 = newlDialog()
console.log(d2); // Dialog {}
升级版 :
const newlDialog = (function () {
// 在 自执行函数内 创建一个变量(局部变量)
let instance = null
class Dialog {
constructor() {
this.title = ''
console.log('在页面中, 创建了一个 弹出层');
}
setTitle(newTitle) {
this.title = newTitle
// console.log(this.title); // 第一次: 警告 第二次 : 通用
}
}
return function (type) {
if (instance === null) {
// 当前语句的代码只会在第一次调用时 执行
instance = new Dialog()
}
// 此时的位置, 每一次调用 newDialog 都会执行一次
instance.setTitle(type)
return instance
}
})()
const d1 = newlDialog('警告')
console.log(d1); // Dialog{title: "警告"}
// 过了很久之后调用, 第二次调用
const d2 = newlDialog('通用')
// console.log(d2); // Dialog{title: "通用" }
策略模式 : 为了解决过多的 if..else 的嵌套问题
- 核心 (当前案例 ):
- 创建一个数据结构, 这个结构内存存储着各种那个折扣记录, 对应的值, 是这个折扣的计算方式
- {
- '8折' : 商品总价的 * 80%
- '9折' : 商品价格的 * 85%
- ....
- }
基础版
// 基础版 :
if ('8折') {
console.log('商品总价的 * 80%');
} else if ('85折') {
console.log('商品价格的 * 85%');
} else if ('9折') {
console.log('商品价格的 * 90%');
} else if ('95折') {
console.log('商品价格的 * 95%');
}
升级版 :
const calcPrice = (function () {
// 当前对象内 存储着 各种折扣 及 对应的 打折商品总价的计算方式
const calcList = {
'80%': (total) => { return (total * 0.8).toFixed(2) },
'70%': (total) => { return (total * 0.7).toFixed(2) },
'60%': total => (total * 0.6).toFixed(2),
'50%': total => (total * 0.5).toFixed(2),
}
/*
所有的引用数据类型, 都可以当做一个对象来用
*/
function inner(type, total) {
return calcList[type](total)
}
inner.add = function (type, fn) {
calcList[type] = fn
console.log(calcList);
}
inner.sub = function (type, fn) {
delete calcList[type]
}
inner.getList = function () {
return calcList
}
return inner
})()
console.log(calcPrice('80%', 500));
console.log(calcPrice('70%', 500));
calcPrice.add('40%', total => (total * 0.4).toFixed(2));
calcPrice.add('30%', total => (total * 0.3).toFixed(2));
console.log(calcPrice('40%', 500));
console.log(calcPrice.getList())