参考链接

Metaprogramming(元编程) is a powerful technique that enables you to write programs that can create other programs. ES6 made it easier to utilize metaprogramming in JavaScript with the help of proxies and many similar features. ES6 proxies facilitate(促进) the redefinition of fundamental operations in an object, opening the door for a wide variety of possibilities.

In this guide, we’ll show you how to apply ES6 proxies in practical situations.

Prerequisites(前提条件) and outcomes(收获)

This tutorial is primarily aimed at developers who have experience with JavaScript and are at least familiar with the idea of ES6 proxies. If you have a firm understanding of proxies as a design pattern, that knowledge should translate.

After reading this guide, you should be able to:

  • Understand what an ES6 Proxy is, how to implement one, and when to use it
  • Use ES6 Proxies for access control, caching, and data binding

Anatomy(剖析) of an ES6 proxy: Target, handler, and trap

Fundamentally(基本上), a proxy is something or someone that becomes something else’s substitute(替代), so that whatever it is, it has to go through the substitute to reach the real deal. An ES6 proxy works the same way.

To effectively implement and use an ES6 proxy, you must understand three key terms:

  1. Target — The real deal that the proxy is substituting, the target is what stands behind the proxy. This can be any object
  2. Handler — An object that contains the logic of all the proxy’s traps
  3. Trap — Similar to traps in operating systems, traps in this context are methods that provide access to the object in a certain way

Putting all this together, below is the simplest implementation in which you can return something different if a given property doesn’t exist in an object using an ES6 proxy.

  1. const target = {
  2. someProp: 1
  3. }
  4. const handler = {
  5. get: function(target, key) {
  6. return key in target ?
  7. target[key] :
  8. 'Doesn't exist!';
  9. }
  10. }
  11. const proxy = new Proxy(target, handler);
  12. console.log(proxy.someProp) // 1
  13. console.log(proxy.someOtherProp) // Doesn't exist!

An ES6 proxy is a powerful feature that facilitates the virtualization of objects in JavaScript.

Data binding(数据绑定): Syncing(同步) multiple objects

Data binding is often difficult to achieve due to its complexity. The application of ES6 proxies to achieve two-way data binding can be seen among model-view-controller libraries in JavaScript, where an object is modified when the DOM undergoes a change.

To put it simply, data binding is a technique that binds multiple data sources together to synchronize them.

Suppose that there is an with the id of username.

  1. <input type="text" id="username" />

Let’s say you want to keep the value of this input in sync with a property of an object.

  1. const inputState = {
  2. id: 'username',
  3. value: ''
  4. }

It’s quite easy to modify the inputState when the value of the input changes by listening to the change event of the input and then updating inputState‘s value. However, the reverse(相反) — updating the input when the inputState is modified — is quite difficult.

An ES6 proxy can help in such a situation.

  1. const input = document.querySelector('#username')
  2. const handler = {
  3. set: function(target, key, value) {
  4. if (target.id && key === 'username') {
  5. target[key] = value;
  6. document.querySelector(`#${target.id}`)
  7. .value = value;
  8. return true
  9. }
  10. return false
  11. }
  12. }
  13. const proxy = new Proxy(inputState, handler)
  14. proxy.value = 'John Doe'
  15. console.log(proxy.value, input.value) // 'John Doe' will be printed for both

This way, when the inputState changes, the input will reflect(反映) the change that has been made. Combined with listening to the change event, this will produce a simple two-way data binding(双向数据绑定,注:此处也可以作为面试题,编写简易的双向数据数据绑定) of the input and inputState.

While this is a valid use case, it’s generally not encouraged. More on that later(稍后详述).

Caching(缓存): Enhancing code performance(增强代码性能)

Caching is an ancient concept that allows very complex and large applications to remain relatively performant(缓存是一个古老概念,它可以使大型复杂应用保持相对性能). Caching is the process of storing certain pieces of data so they can be served much faster when requested. A cache doesn’t store any data permanently. Cache invalidation(缓存失效) is the process of ensuring that the cache is fresh. This is a common struggle for developers. As Phil Karlton said, “There are only two hard things in computer science: cache invalidation and naming things.”

ES6 proxies make caching easier. If you want to check whether something exists in an object, for example, it would first check the cache and return the data or do something else to obtain that data if it doesn’t exist.

Let’s say(假设) you need to make a lot of API calls to obtain a specific piece of information and do something with it.

  1. const getScoreboad = (player) => {
  2. fetch('some-api-url')
  3. .then((scoreboard) => {
  4. // do something with scoreboard
  5. })
  6. }

This would mean that whenever the scoreboard(记分牌) of a player is required, a new call has to be made. Instead, you could cache the scoreboard when it is first requested, and subsequent(随后的) requests could be taken from the cache.

  1. const cache = {
  2. 'John': ['55', '99']
  3. }
  4. const handler = {
  5. get: function(target, player) {
  6. if(target[player] {
  7. return target[player]
  8. } else {
  9. fetch('some-api-url')
  10. .then((scoreboard => {
  11. target[player] = scoreboard
  12. return scoreboard
  13. }))
  14. }
  15. }
  16. }
  17. const proxy = new Proxy(cache, handler) // access cache and do something with scoreboard

This way, an API call will only be made if the cache doesn’t contain the player’s scoreboard.

Access control(访问控制): Controlling what goes in and out of objects

The simplest use case is access control. Most of what the ES6 proxy is known for falls under access control. The scenario(场景) we walked through to show how to implement proxies is an example of access control.

Let’s explore a few practical applications of access control using an E6 proxy.

1. Validation(验证)

One of the most intuitive(直观) use cases for ES6 proxies is validating what comes inside your object to ensure that the data in your object is as accurate as possible. For example, if you want to enforce a maximum number of characters for a product description, you could do so like this:

  1. const productDescs = {}
  2. const handler = {
  3. set: function(target, key, value) {
  4. if(value.length > 150) {
  5. value = value.substring(0, 150)
  6. }
  7. target[key] = value
  8. }
  9. }
  10. const proxy = new Proxy(productDescs, handler)

Now, even if you add a description that’s longer than 150 characters, it’ll be cut short and added.

注:对某些表单的验证,可以基于Proxy来进行验证。

2. Providing a read-only view of an object(只读属性)

There may come a time when you want to ensure that an object is not modified in any way and can only be used for reading purposes. JavaScript provides Object.freeze() to do this, but the behavior is more customizable when using a proxy(使用proxy可以进行更方便的定制).

  1. const importantData = {
  2. name: 'John Doe',
  3. age: 42
  4. }
  5. const handler = {
  6. set: 'Read-Only',
  7. defineProperty: 'Read-Only',
  8. deleteProperty: 'Read-Only',
  9. preventExtensions: 'Read-Only',
  10. setPrototypeOf: 'Read-Only'
  11. }
  12. const proxy = new Proxy(importantData, handler)

Now when you try to mutate(改变) the object in any way, you’ll only receive a string saying Read Only. Otherwise, you could throw an error to indicate that the object is read-only.

3. Private properties(私有属性)

JavaScript doesn’t have private properties per se, except for closures(闭包). When the Symbol data type was introduced, it was used to mimic(模拟) private properties. But it fell by the wayside(抛到了一边) with the introduction of the Object.getOwnPropertySymbols method. ES6 proxies aren’t a perfect solution, but they do the job in a pinch(凑合用下).

A common convention(惯例) is to identify a private property by prepending an underscore(下划线) before its name. This convention enables you to use ES6 proxies.

  1. const object = {
  2. _privateProp: 42
  3. }
  4. const handler = {
  5. has: function(target, key) {
  6. return !(key.startsWith('_') && key in target)
  7. },
  8. get: function(target, key, receiver) {
  9. return key in receiver ? target[key] : undefined
  10. }
  11. }
  12. const proxy = new Proxy(object, handler)
  13. proxy._privateProp // undefined

Adding the ownKeys and deleteProperty will bring this implementation closer to being a truly private property. Then again, you can still view a proxy object in the developer console. If your use case aligns with the above implementation, it’s still applicable.

Why and when to use proxies

ES6 proxies are not ideal for performance-intensive tasks(ES6对于性能密集型任务表现并不理想). That’s why it’s crucial to perform the necessary testing(所以执行必要的测试至关重要). A proxy can be used wherever an object is expected, and the complex functionality that proxies provide with just a few lines of code makes it an ideal feature for metaprogramming(任何需要对象的地方都可以使用代理,代理通过几行代码就能提供复杂的功能,这使它成为元编程的理想特性).

Proxies are typically used alongside another metaprogramming feature known as Reflect(代理通常与另一个元编程特性Reflect一起使用).

Summary

Hopefully, this guide has helped you understand why ES6 proxies are such a great tool, especially for metaprogramming. You should now know:

  • What an ES6 proxy is
  • How and when to implement a proxy
  • How to use ES6 proxies to perform access control, data binding, and caching
  • That ES6 proxies are not ideal for performance-intensive tasks

To learn more, check out the following resources.