Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy 的巧用 #78

Open
husky-dot opened this issue Jul 17, 2019 · 0 comments
Open

Proxy 的巧用 #78

husky-dot opened this issue Jul 17, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

原文:https://blog.bitsrc.io/a-practical-guide-to-es6-proxy-229079c3c2f0

译者: 前端小智

为了保证的可读性,本文采用意译而非直译。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

Proxy 介绍

使用Proxy,你可以将一只猫伪装成一只老虎。下面大约有6个例子,我希望它们能让你相信,Proxy 提供了强大的 Javascript 元编程。

尽管它不像其他ES6功能用的普遍,但Proxy有许多用途,包括运算符重载对象模拟简洁而灵活的API创建对象变化事件,甚至Vue 3背后的内部响应系统提供动力

Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。

ES6原生提供了Proxy构造函数,用来生成Proxy实例。

var proxy = new Proxy(target, handler);

Proxy对象的所有用法,都是上面的这种形式。不同的只是handle参数的写法。其中new Proxy用来生成Proxy实例,target是表示所要拦截的对象,handle是用来定制拦截行为的对象。

下面是 Proxy 最简单的例子是,这是一个有陷阱的代理,一个get陷阱,总是返回42

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.xtarget['x']Reflect.get(target, 'x')等。

但是,Proxy 陷阱当然不限于属性的读取。 它只是十几个不同陷阱中的一个:

Proxy 用例

默认值/“零值”

在 Go 语言中,有零值的概念,零值是特定于类型的隐式默认结构值。其思想是提供类型安全的默认基元值,或者用gopher的话说,给结构一个有用的零值。

虽然不同的创建模式支持类似的功能,但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

更完整的实现还包括诸如deletePropertydefineProperty之类的陷阱。 除了闭包之外,这可能是最接近真正私有属性的方法,因为它们无法通过枚举,克隆,访问或修改来访问。

但是,它们在开发控制台中可见。 只有闭包才能免于这种命运。

缓存

在客户端和服务器之间同步状态时遇到困难并不罕见。数据可能会随着时间的推移而发生变化,很难确切地知道何时重新同步的逻辑。

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.hasin操作符。

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运算符,我们还可以重载deletenew

cookie对象

如果你曾经与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 也不例外。

Polyfill

在撰写本文时(2019年5月),Proxy 没有完整的 polyfill。然而,有一个由谷歌编写的 partial polyfill for Proxy ,它支持getsetapplyconstruct trap,并适用于IE9+。

它是 Proxy 吗?

确定一个对象是否是代理是不可能的

根据Javascript语言规范,无法确定对象是否是代理。 但是,在 Node 10+上,可以使用util.types.isProxy方法。

目标是什么?

给定一个代理对象,就不可能获得或更改目标对象。也不可能获取或修改处理程序对象。

最近似的是Ben Nadel的文章Using Proxy to Dynamically Change THIS Binding,它使用一个空对象作为Proxy目标和闭包来巧妙地重新分配对象的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?

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中的小众元编程来说,代理不仅仅是一个深奥的特性。

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant