阿里云最近在作活动,低至2折,有兴趣能够看看:
https://promotion.aliyun.com/...
为了保证的可读性,本文采用意译而非直译。javascript
使用Proxy
,你能够将一只猫假装成一只老虎。下面大约有6个例子,我但愿它们能让你相信,Proxy 提供了强大的 Javascript 元编程。html
尽管它不像其余ES6功能用的广泛,但Proxy
有许多用途,包括运算符重载,对象模拟,简洁而灵活的API建立,对象变化事件,甚至Vue 3背后的内部响应系统提供动力。前端
Proxy
用于修改某些操做的默认行为,也能够理解为在目标对象以前架设一层拦截,外部全部的访问都必须先经过这层拦截,所以提供了一种机制,能够对外部的访问进行过滤和修改。这个词的原理为代理,在这里能够表示由它来“代理”某些操做,译为“代理器”。vue
ES6原生提供了Proxy
构造函数,用来生成Proxy
实例。java
var proxy = new Proxy(target, handler);
Proxy
对象的全部用法,都是上面的这种形式。不一样的只是handle
参数的写法。其中new Proxy
用来生成Proxy
实例,target
是表示所要拦截的对象,handle
是用来定制拦截行为的对象。node
下面是 Proxy 最简单的例子是,这是一个有陷阱的代理,一个get
陷阱,老是返回42
。git
let target = { x: 10, y: 20 } let hanler = { get: (obj, prop) => 42 } target = new Proxy(target, hanler) target.x //42 target.y //42 target.x // 42
结果是一个对象将为任何属性访问操做都返回“42”。 这包括target.x
,target['x']
,Reflect.get(target, 'x')
等。es6
可是,Proxy 陷阱固然不限于属性的读取。 它只是十几个不一样陷阱中的一个:github
在 Go 语言中,有零值的概念,零值是特定于类型的隐式默认结构值。其思想是提供类型安全的默认基元值,或者用gopher的话说,给结构一个有用的零值。golang
虽然不一样的建立模式支持相似的功能,但Javascript没法用隐式初始值包装对象。Javascript中未设置属性的默认值是undefined
。但 Proxy 能够改变这种状况。
const withZeroValue = (target, zeroValue) => new Proxy(target, { get: (obj, prop) => (prop in obj) ? obj[prop] : zeroValue })
函数withZeroValue
用来包装目标对象。 若是设置了属性,则返回属性值。 不然,它返回一个默认的“零值”。
从技术上讲,这种方法也不是隐含的,但若是咱们扩展withZeroValue
,以Boolean (false
), Number (0
), String (""
), Object ({}
),Array ([]
)等对应的零值,则多是隐含的。
let pos = { x: 4, y: 19 } console.log(pos.x, pos.y, pos.z) // 4, 19, undefined pos = withZeroValue(pos, 0) console.log(pos.z, pos.y, pos.z) // 4, 19, 0
此功能可能有用的一个地方是坐标系。 绘图库能够基于数据的形状自动支持2D和3D渲染。 不是建立两个单独的模型,而是始终将z
默认为 0
而不是undefined
,这多是有意义的。
在JS中获取数组中的最后一个元素方式经过写的很冗长且重复,也容易出错。 这就是为何有一个TC39提案定义了一个便利属性Array.lastItem
来获取和设置最后一个元素。
其余语言,如Python和Ruby,使用负组索引更容易访问最后面的元素。例如,能够简单地使用arr[-1]
替代arr[arr.length-1]
访问最后一个元素。
使用 Proxy 也能够在 Javascript 中使用负索引。
const negativeArray = (els) => new Proxy(els, { get: (target, propKey, receiver) => Reflect.get(target, (+propKey < 0) ? String(target.length + +propKey) : propKey, receiver) });
一个重要的注意事项是包含handler.get的陷阱字符串化全部属性。 对于数组访问,咱们须要将属性名称强制转换为Numbers
,这样就可使用一元加运算符简洁地完成。
如今[-1]
访问最后一个元素,[-2]
访问倒数第二个元素,以此类推。
const unicorn = negativeArray(['🐴', '🎂', '🌈']); unicorn[-1] // '🌈'
众所周知 JS 没有私有属性。 Symbol
最初是为了启用私有属性而引入的,但后来使用像Object.getOwnPropertySymbols
这样的反射方法进行了淡化,这使得它们能够被公开发现。
长期以来的惯例是将私有属性命名为前下划线_
,有效地标记它们“不要访问”。Prox
提供了一种稍微更好的方法来屏蔽这些属性。
const hide = (target, prefix = '_') => new Proxy(target, { has: (obj, prop) => (!prop.startsWith(prefix) && prop in obj), ownKeys: (obj) => Reflect.ownKeys(obj) .filter(prop => (typeof prop !== "string" || !prop.startsWith(prefix))), get: (obj, prop, rec) => (prop in rec) ? obj[prop] : undefined })
hide
函数包装目标对象,并使得从in
运算符和Object.getOwnPropertyNames
等方法没法访问带有下划线的属性。
let userData = hide({ firstName: 'Tom', mediumHandle: '@tbarrasso', _favoriteRapper: 'Drake' }) userData._favoriteRapper // undefined ('_favoriteRapper' in userData) // false
更完整的实现还包括诸如deleteProperty
和defineProperty
之类的陷阱。 除了闭包以外,这多是最接近真正私有属性的方法,由于它们没法经过枚举,克隆,访问或修改来访问。
可是,它们在开发控制台中可见。 只有闭包才能免于这种命运。
在客户端和服务器之间同步状态时遇到困难并不罕见。数据可能会随着时间的推移而发生变化,很难确切地知道什么时候从新同步的逻辑。
Proxy
启用了一种新方法:根据须要将对象包装为无效(和从新同步)属性。 全部访问属性的尝试都首先检查缓存策略,该策略决定返回当前在内存中的内容仍是采起其余一些操做。
const ephemeral = (target, ttl = 60) => { const CREATED_AT = Date.now() const isExpired = () => (Date.now() - CREATED_AT) > (ttl * 1000) return new Proxy(target, { get: (obj, prop) => isExpired() ? undefined : Reflect.get(obj, prop) }) }
这个函数过于简化了:它使对象上的全部属性在一段时间后都没法访问。然而,将此方法扩展为根据每一个属性设置生存时间(TTL),并在必定的持续时间或访问次数以后更新它并不困难。
let bankAccount = ephemeral({ balance: 14.93 }, 10) console.log(bankAccount.balance) // 14.93 setTimeout(() => { console.log(bankAccount.balance) // undefined }, 10 * 1000)
这个示例简单地使银行账户余额在10秒后没法访问。
这些例子来自Csaba Hellinge 关于[代理用例][23]和[Mozilla黑客][24]的文章。方法是包装一个对象以防止扩展或修改。虽然
object.freeze`如今提供了将对象渲染为只读的功能,可是能够对这种方法进行扩展,以便访问不存在属性的枚举对象能更好地处理抛出错误。
const NOPE = () => { throw new Error("Can't modify read-only view"); } const NOPE_HANDLER = { set: NOPE, defineProperty: NOPE, deleteProperty: NOPE, preventExtensions: NOPE, setPrototypeOf: NOPE } const readOnlyView = target => new Proxy(target, NOPE_HANDLER)
const createEnum = (target) => readOnlyView(new Proxy(target, { get: (obj, prop) => { if (prop in obj) { return Reflect.get(obj, prop) } throw new ReferenceError(`Unknown prop "${prop}"`) } }))
如今咱们能够建立一个Object
,若是尝试访问不存在的属性如今不是返回undefined
,而是会抛出异常。 这使得在早期捕获和解决问题变得更加容易。
咱们的enum
示例也是代理上的代理的第一个示例,它确认代理是另外一个代理的有效目标对象。这经过组合代理功能促进了代码重用。
let SHIRT_SIZES = createEnum({ S: 10, M: 15, L: 20 }) SHIRT_SIZES.S // 10 SHIRT_SIZES.S = 15 // Uncaught Error: Can't modify read-only view SHIRT_SIZES.XL // Uncaught ReferenceError: Unknown prop "XL"
这种方法能够进一步扩展,包括模拟方法nameOf
,它返回给定enum
值的属性名,模仿Javascript等语言中的行为。
虽然其余框架和语言超集(好比TypeScript)提供enum
类型,可是这个解决方案的独特之处在于,它使用普通Javascript,而不使用特殊的构建工具或转置器。
也许从语法上讲,最吸引人的 Proxy
用例是重载操做符的能力,好比使用handler.has的in操做符。
in
操做符用于检查指定的属性是否位于指定的对象或其原型链中。但它也是语法上最优雅的重载操做符。这个例子定义了一个连续range
函数来比较数字。
const range = (min, max) => new Proxy(Object.create(null), { has: (_, prop) => (+prop >= min && +prop <= max) })
与Python不一样,Python使用生成器与有限的整数序列进行比较,这种方法支持十进制比较,能够扩展为支持其余数值范围。
const X = 10.5 const nums = [1, 5, X, 50, 100] if (X in range(1, 100)) { // true // ... } nums.filter(n => n in range(1, 10)) // [1, 5]
尽管这个用例不能解决复杂的问题,但它确实提供了干净、可读和可重用的代码。
除了in
运算符,咱们还能够重载delete
和new
。
若是你曾经与cookie
进行交互,那么必须处理document.cookie。 这是一个不寻常的API,由于API是一个String
,它读出全部cookie
,以分号分隔。
document.cookie
是一个看起来像这样的字符串:
_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1
简而言之,处理document.cookie
比较麻烦且容易出错。 一种方法是使用简单的cookie框架,能够适用于使用 Proxy。
const getCookieObject = () => { const cookies = document.cookie.split(';').reduce((cks, ck) => ({[ck.substr(0, ck.indexOf('=')).trim()]: ck.substr(ck.indexOf('=') + 1), ...cks}), {}); const setCookie = (name, val) => document.cookie = `${name}=${val}`; const deleteCookie = (name) => document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; return new Proxy(cookies, { set: (obj, prop, val) => (setCookie(prop, val), Reflect.set(obj, prop, val)), deleteProperty: (obj, prop) => (deleteCookie(prop), Reflect.deleteProperty(obj, prop)) }) }
此函数返回一个键值对对象,但代理对document.cookie
进行持久性的全部更改。
let docCookies = getCookieObject() docCookies.has_recent_activity // "1" docCookies.has_recent_activity = "2" // "2" delete docCookies2["has_recent_activity"] // true
在11行代码中,修改cookie
提供了更好的交互,尽管在生产环境中还须要诸如字符串规范化之类的附加功能。
细节决定成败,Proxy 也不例外。
在撰写本文时(2019年5月),Proxy 没有完整的 polyfill。然而,有一个由谷歌编写的 partial polyfill for Proxy ,它支持get
、set
、apply
和construct trap
,并适用于IE9+。
肯定一个对象是不是代理是不可能的
根据Javascript语言规范,没法肯定对象是不是代理。 可是,在 Node 10+上,可使用util.types.isProxy方法。
给定一个代理对象,就不可能得到或更改目标对象。也不可能获取或修改处理程序对象。
最近似的是Ben Nadel的文章Using Proxy to Dynamically Change THIS Binding,它使用一个空对象做为Proxy
目标和闭包来巧妙地从新分配对象的Proxy
操做。
new Proxy("To be, or not to be...", { }) // TypeError: Cannot create proxy with a non-object as target or handler
不幸的是,Proxy的一个限制是目标必须是Object。 这意味着咱们不能直接使用像String这样的原语。 😞
Proxy的一个主要缺点是性能。 因浏览器和使用而异,可是对于性能有要求的代码来讲,代理不是最好的方法。 固然,能够衡量影响并肯定代理的优点是否超过对性能的影响。
Proxy 提供虚拟化接口来控制任何目标 Object的行为。 这样作能够在简单性和实用性之间取得平衡,而不会牺牲兼容性。
也许使用Proxy
的最使人信服的理由是,上面的许多示例只有几行,而且能够轻松组合以建立复杂的功能。 最后一个例子,咱们能够从几个用例中组合函数来建立一个只读cookie
对象,该对象返回不存在或“私有”隐藏cookie的默认值。
// document.cookie = "_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1" let docCookies = withZeroValue(hide(readOnlyView(getCookieObject())), "Cookie not found") docCookies.has_recent_activity // "1" docCookies.nonExistentCookie // "Cookie not found" docCookies._ga // "Cookie not found" docCookies.newCookie = "1" // Uncaught Error: Can't modify read-only view
我但愿这些例子已经代表,对于Javascript中的小众元编程来讲,代理不只仅是一个深奥的特性。
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug。
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
https://github.com/qq44924588...
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。