proxy详解
Proxy
在 ES6 标准中新增的一个非常强大的功能是 Proxy,它可以自定义一些常用行为如查找、赋值、枚举、函数调用等。通过 Proxy 这个名称也可以看出来它包含了“代理”的含义,只要有“代理”的诉求都可以考虑使用 Proxy 来实现。
Basic Syntax
语法
let p = new Proxy(target, handler)
解释
参数 | 含义 | 必选 |
---|---|---|
target | 用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理) | Y |
handler | 一个对象,其属性是当执行一个操作时定义代理的行为的函数 | Y |
MDN 给出的解释偏官方,通俗的讲第一个参数 target 就是用来代理的“对象”,被代理之后它是不能直接被访问的,而 handler 就是实现代理的过程。
场景
我们经常读取一个对象的 key-value:
let o = { name: 'xiaoming', age: 20 } console.log(o.name) // xiaoming console.log(o.age) // 20 console.log(o.from) // undefined
当我们读取 from 的时候返回的是 undefined,因为 o 这个对象中没有这个 key-value。想想看我们在读取数据的时候,这个数据经常是聚合的,当大家没有按照规范来的时候或者数据缺失的情况下,经常会出现这种“乌龙”现象。
如果我们不想在调用 key 的时候返回 undefined,之前的做法是这样的:
console.log(o.from || '')
如果我们对所有代码都是这种写法,那阅读性和观赏性就不得而知了。值得庆幸的是,ES6 的 Proxy 可以让我们轻松的解决这一问题:
let o = { name: 'xiaoming', age: 20 } let handler = { get(obj, key) { return Reflect.has(obj, key) ? obj[key] : '' } } let p = new Proxy(o, handler) console.log(p.from)
这个代码是想表达如果 o 对象有这个 key-value 则直接返回,如果没有一律返回 '',当然这里是自定义,大家可以根据自己的需要来写适合自己业务的规则。
Schema Validation
刚才对数据的“读操作”进行了拦截,接下来我们描述下“写操作”进行拦截。
场景 1
从服务端获取的数据希望是只读,不允许在任何一个环节被修改。
// response.data 是 JSON 格式的数据,来自服务端的响应 // 在 ES5 中只能通过遍历把所有的属性设置为只读 for (let [key] of Object.entries(response.data)) { Object.defineProperty(response.data, key, { writable: false }) }
如果我们使用 Proxy 就简单很多了:
let data = new Proxy(response.data, { set(obj, key, value) { return false } })
场景 2
对于数据交互而言,校验是不可或缺的一个环境,传统的做法是将校验写在了业务逻辑里,导致代码耦合度较高。如果大家使用 Proxy 就可以将代码设计的非常灵活。
// Validator.js export default (obj, key, value) => { if (Reflect.has(key) && value > 20) { obj[key] = value } } import Validator from './Validator' let data = new Proxy(response.data, { set: Validator })
场景 3
如果对读写进行监控,可以这样写:
let validator = { set(target, key, value) { if (key === 'age') { if (typeof value !== 'number' || Number.isNaN(value)) { throw new TypeError('Age must be a number') } if (value <= 0) { throw new TypeError('Age must be a positive number') } } return true } } const person = { age: 27 } const proxy = new Proxy(person, validator) proxy.age = 'foo' // <- TypeError: Age must be a number proxy.age = NaN // <- TypeError: Age must be a number proxy.age = 0 // <- TypeError: Age must be a positive number proxy.age = 28 console.log(person.age) // <- 28 // 添加监控 window.addEventListener( 'error', e => { console.log(e.message) // Uncaught TypeError: Age must be a number }, true )
场景 4
什么实例一个对象,每个对象都有一个自己的 id 而且只读。
class Component { constructor () { this.proxy = new Proxy({ id: Math.random().toString(36).slice(-8) }) } get id () { return this.proxy.id } }
练习
- 封装 ajax,让返回的数据是只读的
Revocable Proxies
除了使用常规的代理,还可以创建临时的代理,这个临时可以理解为:代理可以取消。
const dinosaur = { name: 'Fred', isDangerous: true } const dinoProxy = Proxy.revocable(dinosaur, { get: (target, property) => { return 'RAWRRRRRRRR' } }) // We need this assignment because the `revocable` method returns an object with the proxified object and the revoke method const proxifiedDinosaur = dinoProxy.proxy // Now everytime we access a property in our `proxifiedDinosaur` it gets intercepted by the `get` trap in our handler console.log(proxifiedDinosaur.name) // "RAWRRRRRRRR" console.log(proxifiedDinosaur.isDangerous) // "RAWRRRRRRRR" // Now, if we call `revoke` on our `dinoProxy` we get a TypeError when accessing properties on `proxifiedDinosaur` dinoProxy.revoke() console.log(proxifiedDinosaur.name) // TypeError: Cannot perform 'get' on a proxy that has been revoked console.log(proxifiedDinosaur.isDangerous) // TypeError: Cannot perform 'get' on a proxy that has been revoked
[!TIP]
一旦 revoke 被调用,proxy 就失效了,这也就起到了“临时”代理的作用
练习
- 希望在各个组件初始化的时候都赋值一个随机的 ID,但是这个组件调用这个 ID 是固定的,该怎么做?
- 临时代理有哪些应用场景呢?
- 如何把接口的数据用代理进行包装?
阅读
JavaScript中的Proxy可撤销
let obj = { name: '小明', age: 18 } let proxy = Proxy.revocable(obj, { get (target, key) { if (key == 'age') { return target[key] + 3 } return target[key] || '' } }) console.log(proxy.proxy.age, proxy) setTimeout(function () { proxy.revoke() setTimeout(function () { console.log(proxy.proxy.age) }, 1000) }, 1000)
JavaScript中的Proxy模块化过滤
let source = { name: '小李', price: 100, } function validate(target, key, value) { if (Reflect.has(target, key)) { if (key == 'price') { if (value < 300) { return target[key] = value } } } return false } let p = new Proxy(source, { get (target, key) { if (key == 'price') return target[key] || ''; return '您没有权限' }, set: validate }) p.price = 200 console.log(p.price, p.name)
JavaScript简单使用
let a = { x: '小花', price: 190 } let p = new Proxy(a, { get(target, key) { if (key == 'price') { return target[key] + 20 }else { return target[key] } } }) console.log(a.price)
JavaScript中的Reflect
let obj = {x: '312', y: '666'} let arr = ['1','2','3'] // Object.freeze(obj) let d = new Date() console.log(Reflect.apply(Math.floor, null, [3.99])) // 3 Reflect.apply 反射 console.log(Reflect.construct(Date, []).getTime()) // 1573292254566 实例化 console.log(Reflect.getPrototypeOf(d))// 打印原型方法 1 console.log(Reflect.has(d, 'constructdor')) // 判断原型链上有没有属性或元素 2 console.log(Reflect.isExtensible(d)) // 判断对象是否可以扩展3 Object.freeze(d) // 冻结对象 使对象不可扩展 console.log(Reflect.isExtensible(d)) // 变成了false console.log(Reflect.ownKeys(d)) // 查看对象自己的属性 不包含原型链上的属性4 console.log(Reflect.isExtensible(obj)) // true Reflect.preventExtensions(obj) // 冻结对象5 console.log(Reflect.isExtensible(obj)) // 变成了false console.log(Reflect.getPrototypeOf(arr)) Reflect.setPrototypeOf(arr, String.prototype) // 修改arr的原型对象
关于Promise对象
Promise对象是解决ES5中回调地狱的问题
使用Promise 可以使代码的可读性增强。
使用new Promise的时候需要注意 new Promise后里面的代码将被直接执行。New的时候就会被执行
Promise对象有两个参数resolve和reject
resolve是将Promise的状态改为成功
而reject是将Promise的状态改为失败
如何声明一个Promise对象函数?
function loadScript(src) { return new Promise((resolve, reject) => { let script = document.createElement('script') script.src = src script.onload = () => resolve(src) script.onerror = err => reject(err) document.head.append(script) }) }Promise中的整合
可能读者没有理解什么是整合的意思。
就好像我需要从A接口取一些数据 然后从B接口取一些数据,最后将A和B的数据整合成一个数组。
我们可以使用串联的方式解决该问题
但是Promise给我提供了一个很好的方法。这个是Promise的原型方法
大家只需要这样就可以了
Promise.all([promise接口1,promise接口2]).then(values => { console.log(values) // 在这里会输出一个数组。它就是promise接口1和promis接口2 的集合 })写到这里很多工作过的同学一定用过CDN,在使用CDN的时候考虑到CDN的不可靠性会准备备用的CDN
这个时候的需求时如果主CDN无法使用 就使用备用CDN
同样可以使用串联的思路
但是Promise在这里也给我们提供了一个很好的方法
CDN1和CDN2哪一个接口请求速度快就使用哪一个。
Promise.race([promise接口1,Promise接口2]).then(value => { console.log(vlaue) // 这里会返回最先请求到的数据,也就是最先改变状态的Promise而不会再去管后请求到的数据 })
JavaScript中的map
以下是我个人对map对象的一些理解,如有不足之处请多批评指正:
1、map可以传入一个可遍历的对象
2、map传入的数组必须是一个可遍历的对象,不能是任意值。
3、map必须传入key、value的形式 example:[[k,v],[k,v]
4、map的key可以是任意值 函数、对象...
5、map添加数据不是用add而是用set
6、map的set方法可以用作修改
7、使用删除或查找等方法都是使用map的索引,而不是值
8、使用forEach遍历时,第一个参数是value,第二个是key
let map = new Map([['hello', 'world'],['name', 'Joky']]) console.log(map) // Map(2) {"hello" => "world", "name" => "Joky"} // 添加 map.set('age', 18) console.log(map) // Map(3) {"hello" => "world", "name" => "Joky", "age" => 18} // 修改 map.set('age', 12) console.log(map) // Map(3) {"hello" => "world", "name" => "Joky", "age" => 12} // 删除 map.delete('age') console.log(map) // Map(2) {"hello" => "world", "name" => "Joky"} // 全部清空 map.clear() console.log(map) // Map(0) {} map.set('hello', 'world').set('name', 'Joky').set('age', 18) // 获取map的键值对个数 console.log(map.size) // 3 // 判断某个值是否存在 console.log(map.has('hello')) // true // 给定键获取值 console.log(map.get('hello')) // world // 获取map的所有键 console.log(map.keys()) // MapIterator {"hello", "name", "age"} // 获取map的所有值 console.log(map.values()) // MapIterator {"world", "Joky", 18} // 获取map的键值对 console.log(map.entries()) // MapIterator {"hello" => "world", "name" => "Joky", "age" => 18} // map的遍历 map.forEach((v, k) => { console.log(v, k) // world hello // Joky name // 18 "age" }) for (let [k, v] of map) { console.log(k, v) // hello world // name Joky // age 18 } // 函数键的演示 function o () {} map.set(o, '我是一个函数') console.log(map) // Map(4) {"hello" => "world", "name" => "Joky", "age" => 18, ƒ => "我是一个函数"}
JavaScript中的set数据结构
我个人关于set的一些理解
1、set 接受的参数是一个可遍历的对象而不一定是一个数组
2、set加入新的数据使用add('hello')
3、set加入数据的时候可以链式操作
4、set如果存入了重复的数据将会自动过滤
5、set还是一个键值对
6、set很难改
// 删除元素 set.delete('goodbay') console.log(set) // Set(1) {"hello"} // 清空元素 set.clear() console.log(set) // Set(0) {} // 添加元素 set.add('hello').add('goodbay') console.log(set) // Set(2) {"hello", "goodbay"} // 检查元素是否存在 console.log(set.has('hello')) // true // 检查数据元素个数 console.log(set.size) // 2 // 取set所有的键 console.log(set.keys()) // SetIterator {"hello", "goodbay"} // 取set所有的值 console.log(set.values()) // SetIterator {"hello", "goodbay"} // 取set的键值对 console.log(set.entries()) // SetIterator {"hello" => "hello", "goodbay" => "goodbay"} // 可以使用forEach遍历 set.forEach(x => { console.log(x) // hello // goodbay }) // 可以使用for...of...进行遍历 for (i of set) { console.log(i) // hello // goodbay }
JavaScript中的结构赋值
在ES5中如果有一个数组有很多的元素。我们可能需要使用数组的索引一个一个的取出来。
这个明显是比较麻烦的。
在ES6中就有了新的取数组中的元素方式
let arr = ['hello', 'world'] let [firstName, lastName] = arr console.log(firstName, lastName)如果有多个元素的话我只想取指定的第几个,而不取前面的值呢?
let arr = ['one', 'two', 'three', 'four', 'five'] // 如果这个时候我只要最后一个呢?怎么办呢? let [,,,,lastName] = arr console.log(lastName)
JavaScript中的字符串模版
我个人认为这个功能并不是那么简单。
可能有更多实用的功能我只是没有发现。
如果只是那么简单这个功能有点...
let user_type = 'ordinary'; function handle(strings, type) { // 这里指的注意的是第一个参数是个数组,将所有的字符串常量被变量分割的收集起来,第二个参数是一个变量,可以有多个变量对应的有多个参数 let output = strings[0]; let varstr; if (type == 'ordinary') { varstr = '单价是100元人民币'; }else { varstr = '批发价是50元人民币'; } return `${output}${varstr}` } let show = handle`您购买的商品${user_type}` console.log(show)