📜 ⬆️ ⬇️

ES5 Harmony Proxy - changing the semantics of JavaScript inside JavaScript itself

Proxies are new JavaScript objects for which the programmer must define their behavior. The standard behavior of all objects is defined in the JavaScript engine, which is most often written in C ++. Proxies allow the programmer to define almost any behavior of the JavaScript object, they are useful for writing basic objects or function wrappers or for creating abstractions of virtual objects and provide an API for meta-programming. Now Proxy is not included in the standard, but its standardization is planned in ECMAScript Harmony. To avoid confusion, I will clarify that these Proxies have nothing to do with proxy servers.

Where can they be used


1. General intermediate abstractions
2. Creation of virtual objects: wrappers of existing objects, remote (from a word far) objects, lazy creation of objects (Example ORM - Ruby ActiveRecord, Groovy GORM)
3. Transparent logging, tracing, profiling
4. Implementing domain-specific languages
5. Dynamic interception of nonexistent methods, creation of missing methods (__ noSuchMethod__)
6. Base for specific iterators

Concepts


1. This language feature is called the “comprehensive mechanism” (orig. Catch-all mechanism) - this name is used to describe this feature. The text will use the original title.
2. Another name for the concept of a "comprehensive mechanism" - intermediary (original. Intercession API)
3. The object that handles the property is called the handler.
4. The object whose properties are replaced is called a proxy - proxy
5. The object / method that creates proxy objects is called a proxy factory.
6. The methods included in the handler that handle any behavior are called traps / interceptors traps (by analogy with operating systems)
7. The proxy can be exciting / active (orig. Trapping) or be dissolved (orig. Fixed)

How to work with them - API


There are two types of proxy factories. One for objects another for functions.
')
Proxy constructor:
var proxy = Proxy.create(handler, proto); 

Proxy Designer:
 var proxy = Proxy.createFunction(handler, callTrap, constructTrap); 

proto is an optional parameter that defines the prototype proxy
callTrap is a function that will replace the original function when directly calling a proxy function (the example below will explain everything). Important: this replacement function (callTrap) coincides with this replaced.
constructTrap is an optional parameter — a function that will replace the original constructor function when called via new. Important: this replacement function (constructTrap) is always undefined. If constructTrap is not passed, then callTrap is used in which this delegates from proxy.prototype (the usual behavior of ES5 constructors, Chapter 13.2.2)
handler - an object that defines the behavior of the proxy. This object should always contain base traps.

Basic Traps / Interceptors (Fundamental traps)

How to read it: : function(, __) -> { } //
 { getOwnPropertyDescriptor: function(name) -> {PropertyDescriptor | undefined} // Object.getOwnPropertyDescriptor(proxy, name) getPropertyDescriptor: function(name) -> {PropertyDescriptor | undefined} // Object.getPropertyDescriptor(proxy, name) (not in ES5) getOwnPropertyNames: function() -> {String[]} // Object.getOwnPropertyNames(proxy) getPropertyNames: function() -> {String[]} // Object.getPropertyNames(proxy) (not in ES5) defineProperty: function(name, propertyDescriptor) -> {Mixed} // Object.defineProperty(proxy,name,pd) delete: function(name) -> {Boolean} // delete proxy.name fix: function() -> {{String:PropertyDescriptor}[]|undefined} // Object.{freeze|seal|preventExtensions}(proxy) } 


Derived traps

These interceptors are optional, if they are not defined, the default logic will be used.
 { has: function(name) -> {Boolean} // if (name in proxy) ... hasOwn: function(name) -> {Boolean} // ({}).hasOwnProperty.call(proxy, name) get: function(receiver, name) -> {Mixed} // receiver.name; set: function(receiver, name, val) -> {Boolean} // receiver.name = val; enumerate: function() -> {String[]} // for (name in proxy)         keys: function() -> {String[]} // Object.keys(proxy)      } 

By default, the following logic will be executed
In the form of a table you can see here.

Derivatives of traps are called "derivatives" because they can be defined by basic traps. For example, a has trap can be defined using a getPropertyDescriptor hook (check returns undefined or not). With the help of derivatives of interceptors, it is possible to emulate properties with less cost, so they were defined. The fix trap was introduced to allow proxies to interact with Object.preventExtensions , Object.seal and Object.freeze . A non-extensible, sealed or frozen object (non-extensible, sealed, frozen) must in some way restrict the freedom of the handler (i.e. what he should return in future calls to set, get so forth. For example, if the previous call to handler.get(p, “foo”) returned not undefined, then future calls to handler.get(p, "foo") must return the same value if the object is frozen. Each time a proxy tries to freeze, seal, block (non-extensible, sealed or frozen) the trap "fix" called.
The fix handler has 2 options:
1. reject request (fix should return undefined), then a TypeError exception will be thrown.
2. execute the query and return the object description in the form {String:PropertyDescriptor}[] in this case, the “comprehensive mechanism” will create a new object based on the object description. At this point, all references to the handler are deleted (it can be deleted by the garbage collector). In this case, the proxy is called dissolved.

Examples



Hello, proxy!

The following piece of code creates a proxy that interrupts access to the properties and returns the value “Hello, p” for each property “p”:
 var p = Proxy.create({ get: function(proxy, name) { return 'Hello, '+ name; } }); document.write(p.World); //  'Hello, World' 

Living example

Simple profiler

Create a simple wrapper that counts how many times what property was obtained:
 function makeSimpleProfiler(target) { var forwarder = new ForwardingHandler(target); var count = Object.create(null); forwarder.get = function(rcvr, name) { count[name] = (count[name] || 0) + 1; return this.target[name]; }; return { proxy: Proxy.create(forwarder, Object.getPrototypeOf(target)), get stats() { return count; } }; } 

The makeSimpleProfiler function takes as its argument the object we want to monitor. It returns an object that has 2 properties: the proxy itself and stats - the number of calls.
Living example

The ForwardingHandler function in line two functions makeSimpleProfiler creates a simple forwarding proxy that transparently delegates all operations performed on the proxy to the target object. Here's what she looks like:
 function ForwardingHandler(obj) { this.target = obj; } ForwardingHandler.prototype = { has: function(name) { return name in this.target; }, get: function(rcvr,name) { return this.target[name]; }, set: function(rcvr,name,val) { this.target[name]=val;return true; }, delete: function(name) { return delete this.target[name]; } enumerate: function() { var props = []; for (name in this.target) { props.push(name); }; return props; }, iterate: function() { var props = this.enumerate(), i = 0; return { next: function() { if (i === props.length) throw StopIteration; return props[i++]; } }; }, keys: function() { return Object.keys(this.target); }, ... }; Proxy.wrap = function(obj) { return Proxy.create(new ForwardingHandler(obj), Object.getPrototypeOf(obj)); } 

The full version of this feature can be found here . This forwarding handler is likely to become part of the standard .

Remote objects

Proxies allow you to create virtual objects that can emulate remote objects or existing objects. To demonstrate, let's create a wrapper around an existing library for remote communication in JavaScript. The web_send Tyler Close library can be used to create remote connections to objects located on the server. Now we can call methods on HTTP POST requests using this remote connection. Unfortunately, remote connections cannot be used as objects.
Let's compare. To call a remote function, initially it was necessary to call:
 Q.post(ref, 'foo', [a,b,c]); 

Using proxies, we can make this call more natural, write our wrapper:
 function Obj(ref) { return Proxy.create({ get: function(rcvr, name) { return function() { var args = Array.prototype.slice.call(arguments); return Q.post(ref, name, args); }; } }); } 

Now we can do like this Obj(ref).foo(a,b,c) .

Emulation __noSuchMethod__

Using Proxy, it is possible to emulate the __noSuchMethod__ hook in browsers that do not support it (but this is not relevant now).
 function MyObject() {}; MyObject.prototype = Object.create(NoSuchMethodTrap); MyObject.prototype.__noSuchMethod__ = function(methodName, args) { return 'Hello, '+ methodName; }; new MyObject().foo() // returns 'Hello, foo' 

This object uses a NoSuchMethodTrap proxy in which the get trap replaces the original __noSuchMethod__.
 var NoSuchMethodTrap = Proxy.create({ get: function(rcvr, name) { if (name === '__noSuchMethod__') { throw new Error("receiver does not implement __noSuchMethod__ hook"); } else { return function() { var args = Array.prototype.slice.call(arguments); return this.__noSuchMethod__(name, args); } } } }); 

Living example

Higher order messages

Higher-order messages are messages that receive other messages as an argument, as described here .
Higher-order messages are similar to higher-order functions, but they are more capacious in code. Using a proxy is very easy to create higher order messages. Consider the special object "_", which rewrites messages into functions:
 var msg = _.foo(1,2) msg.selector; // "foo" msg.args; // [1,2] msg(x); // x.foo(1,2) 

msg is a function that uses one argument as if it were defined as function(z) { return z.foo(1,2); } function(z) { return z.foo(1,2); } . The following example is a direct interpretation of the SVP from the aforementioned documents, but written with more capacious code:
 var words = "higher order messages are fun and short".split(" "); String.prototype.longerThan = function(i) { return this.length > i; }; //        document.write(words.filter(_.longerThan(4)).map(_.toUpperCase())); //       : // words.filter(function (s) { return s.longerThan(4) }) // .map(function (s) { return s.toUpperCase() }) 

Here is the object code "_":
 //     var _ = Proxy.create({ get: function(_, name) { return function() { var args = Array.prototype.slice.call(arguments); var f = function(rcvr) { return rcvr[name].apply(rcvr, args); }; f.selector = name; f.args = args; return f; } } }); 

Living example

Base Object Emulation

Proxies enable Javascript programmers to emulate the weirdness of basic objects, such as DOM. This allows the authors of libraries to wrap the base objects in order to “tame” them (approx. Sandboxes) or correct them to reduce cross-browser incompatibility.

Proxy functions

The previous examples used objects. Here is a simple example of a proxy function:
 var simpleHandler = { get: function(proxy, name) { // can intercept access to the 'prototype' of the function if (name === 'prototype') return Object.prototype; return 'Hello, '+ name; } }; var fproxy = Proxy.createFunction( simpleHandler, function() { return arguments[0]; }, // call trap function() { return arguments[1]; }); // construct trap fproxy(1,2); // 1 new fproxy(1,2); // 2 fproxy.prototype; // Object.prototype fproxy.foo; // 'Hello, foo' 

Living example

Proxy functions open up possibilities for writing special idioms that were not available to us in pure JavaScript. First of all, proxy functions can create a function (callable object) from any object:
 function makeCallable(target, call, construct) { return Proxy.createFunction( new ForwardingHandler(target), call, construct || call); } 

Second, proxy functions can be used to create pseudo-classes whose entities are instances callable.
 function Thing() { /* initialize state, etc */ return makeCallable(this, function() { /* actions to perform when instance is called like a function */ }); } 

Experiments with new semantics


For lovers of the language: Proxies can be used to create "standard" objects and JavaScript functions in JavaScript itself. The ability to change the semantics of JavaScript objects within JavaScript greatly simplifies the mechanism for making minor changes to the semantics for conducting experiments. Partial implementation of semantics inside JavaScript can be observed in the default trap values . Another example: JavaScript arrays inside JavaScript using Proxy.

Proxy tips


Avoid recursion

Avoid implicit toString calls inside traps. Be careful with the receiver argument of the get and set traps. The receiver represents a link to the proxy, therefore implicitly calling get and set will result in infinite recursion. For example, calling console.log (receiver) for debugging inside a setter will call the toString method, which will cause infinite recursion.
 get: function(receiver, name) { print(receiver); return target[name]; } 

If p is a proxy that uses the trap above, then calling p.foo will result in an infinite loop: First, the get trap will be called with name="foo" , which prints the receiver (ie, p ). This results in a call to p.toString() , which will call the trap again this time with name="toString" . And so on.

Proxy as handlers

Proxy handlers can themselves be proxies. The proxy tracer below domains this pattern, it is used to create a "tunneling" of all operations for the processor through one get trap.

image

Proxy Tracer / Proxy Probe

Tracer simply prints a description of all operations that he processed. This is very useful for debugging or for studying the work of a proxy.
Living example

A probe has a logic similar to a tracer, it logs all meta-level operations applied to it.
Living example

When can it be used?


Now only Firefox 4.0 supports proxies. There is a proxy implementation for Node.js as an extension: node-overload (partial support) node-proxy (almost full support). In any case, Proxies will be added to the standard so that it will soon appear in your browser!

Additional resources


1. ECMAScript Harmony
2. Documentation on The Mozilla Developer Network
3. Standard Development: The first part of this is Google Tech Talk and this is the paper shown on DLS 2010 .
4. Brendan Eich, in his blog briefly explains the basics of Proxy.
5. Partial list of open Proxy problems in Firefox 4.
6. Slides Brendan Eich from his salary on jsconf

Resources used in the article


1. MDC Proxy (DRAFT)
2. ES5 Catch-all Proxies
3. Proxy Inception (Brendan Eich)
4. Tutorial: Harmony Proxies (Tom Van Cutsem)

If you do not understand something, please ask your questions or watch the slides . Suggestions, suggestions, criticism is welcome!

Source: https://habr.com/ru/post/117915/


All Articles