Movatterモバイル変換


[0]ホーム

URL:


28. Metaprogramming with proxies
Table of contents
Please support this book:buy it (PDF, EPUB, MOBI) ordonate
(Ad, please don’t block.)

28.Metaprogramming with proxies



28.1Overview

Proxies enable you to intercept and customize operations performed on objects (such as getting properties). They are ametaprogramming feature.

In the following example,proxy is the object whose operations we are intercepting andhandler is the object that handles the interceptions. In this case, we are only intercepting a single operation,get (getting properties).

consttarget={};consthandler={get(target,propKey,receiver){console.log('get '+propKey);return123;}};constproxy=newProxy(target,handler);

When we get the propertyproxy.foo, the handler intercepts that operation:

> proxy.fooget foo123

Consultthe reference for the complete API for a list of operations that can be intercepted.

28.2Programming versus metaprogramming

Before we can get into what proxies are and why they are useful, we first need to understand whatmetaprogramming is.

In programming, there are levels:

Base and meta level can be different languages. In the following meta program, the metaprogramming language is JavaScript and the base programming language is Java.

conststr='Hello'+'!'.repeat(3);console.log('System.out.println("'+str+'")');

Metaprogramming can take different forms. In the previous example, we have printed Java code to the console. Let’s use JavaScript as both metaprogramming language and base programming language. The classic example for this is theeval() function, which lets you evaluate/compile JavaScript code on the fly. There arenot that many actual use cases foreval(). In the interaction below, we use it to evaluate the expression5 + 2.

> eval('5 + 2')7

Other JavaScript operations may not look like metaprogramming, but actually are, if you look closer:

// Base levelconstobj={hello(){console.log('Hello!');}};// Meta levelfor(constkeyofObject.keys(obj)){console.log(key);}

The program is examining its own structure while running. This doesn’t look like metaprogramming, because the separation between programming constructs and data structures is fuzzy in JavaScript. All of theObject.* methods can be considered metaprogramming functionality.

28.2.1Kinds of metaprogramming

Reflective metaprogramming means that a program processes itself.Kiczales et al. [2] distinguish three kinds of reflective metaprogramming:

Let’s look at examples.

Example: introspection.Object.keys() performs introspection (see previous example).

Example: self-modification. The following functionmoveProperty moves a property from a source to a target. It performs self-modification via the bracket operator for property access, the assignment operator and thedelete operator. (In production code, you’d probably useproperty descriptors for this task.)

functionmoveProperty(source,propertyName,target){target[propertyName]=source[propertyName];deletesource[propertyName];}

UsingmoveProperty():

>constobj1={prop:'abc'};>constobj2={};>moveProperty(obj1,'prop',obj2);>obj1{}>obj2{prop:'abc'}

ECMAScript 5 doesn’t support intercession; proxies were created to fill that gap.

28.3Proxies explained

ECMAScript 6 proxies bring intercession to JavaScript. They work as follows. There are many operations that you can perform on an objectobj. For example:

Proxies are special objects that allow you customize some of these operations. A proxy is created with two parameters:

In the following example, the handler intercepts the operationsget andhas.

consttarget={};consthandler={/** Intercepts: getting properties */get(target,propKey,receiver){console.log(`GET${propKey}`);return123;},/** Intercepts: checking whether properties exist */has(target,propKey){console.log(`HAS${propKey}`);returntrue;}};constproxy=newProxy(target,handler);

When we get propertyfoo, the handler intercepts that operation:

> proxy.fooGET foo123

Similarly, thein operator triggershas:

> 'hello' in proxyHAS hellotrue

The handler doesn’t implement the trapset (setting properties). Therefore, settingproxy.bar is forwarded totarget and leads totarget.bar being set.

> proxy.bar = 'abc';> target.bar'abc'

28.3.1Function-specific traps

If the target is a function, two additional operations can be intercepted:

The reason for only enabling these traps for function targets is simple: You wouldn’t be able to forward the operationsapply andconstruct, otherwise.

28.3.2Intercepting method calls

If you want to intercept method calls via a proxy, there is one challenge: you can intercept the operationget (getting property values) and you can intercept the operationapply (calling a function), but there is no single operation for method calls that you could intercept. That’s because method calls are viewed as two separate operations: First aget to retrieve a function, then anapply to call that function.

Therefore, you must interceptget and return a function that intercepts the function call. The following code demonstrates how that is done.

functiontraceMethodCalls(obj){consthandler={get(target,propKey,receiver){constorigMethod=target[propKey];returnfunction(...args){constresult=origMethod.apply(this,args);console.log(propKey+JSON.stringify(args)+' -> '+JSON.stringify(result));returnresult;};}};returnnewProxy(obj,handler);}

I’m not using a Proxy for the latter task, I’m simply wrapping the original method with a function.

Let’s use the following object to try outtraceMethodCalls():

constobj={multiply(x,y){returnx*y;},squared(x){returnthis.multiply(x,x);},};

tracedObj is a traced version ofobj. The first line after each method call is the output ofconsole.log(), the second line is the result of the method call.

> const tracedObj = traceMethodCalls(obj);> tracedObj.multiply(2,7)multiply[2,7] -> 1414> tracedObj.squared(9)multiply[9,9] -> 81squared[9] -> 8181

The nice thing is that even the callthis.multiply() that is made insideobj.squared() is traced. That’s becausethis keeps referring to the proxy.

This is not the most efficient solution. One could, for example, cache methods. Furthermore, Proxies themselves have an impact on performance.

28.3.3Revocable proxies

ECMAScript 6 lets you create proxies that can berevoked (switched off):

const{proxy,revoke}=Proxy.revocable(target,handler);

On the left hand side of the assignment operator (=), we are using destructuring to access the propertiesproxy andrevoke of the object returned byProxy.revocable().

After you call the functionrevoke for the first time, any operation you apply toproxy causes aTypeError. Subsequent calls ofrevoke have no further effect.

consttarget={};// Start with an empty objectconsthandler={};// Don’t intercept anythingconst{proxy,revoke}=Proxy.revocable(target,handler);proxy.foo=123;console.log(proxy.foo);// 123revoke();console.log(proxy.foo);// TypeError: Revoked

28.3.4Proxies as prototypes

A proxyproto can become the prototype of an objectobj. Some operations that begin inobj may continue inproto. One such operation isget.

constproto=newProxy({},{get(target,propertyKey,receiver){console.log('GET '+propertyKey);returntarget[propertyKey];}});constobj=Object.create(proto);obj.bla;// Output:// GET bla

The propertybla can’t be found inobj, which is why the search continues inproto and the trapget is triggered there. There are more operations that affect prototypes; they are listed at the end of this chapter.

28.3.5Forwarding intercepted operations

Operations whose traps the handler doesn’t implement are automatically forwarded to the target. Sometimes there is some task you want to perform in addition to forwarding the operation. For example, a handler that intercepts all operations and logs them, but doesn’t prevent them from reaching the target:

consthandler={deleteProperty(target,propKey){console.log('DELETE '+propKey);returndeletetarget[propKey];},has(target,propKey){console.log('HAS '+propKey);returnpropKeyintarget;},// Other traps: similar}

For each trap, we first log the name of the operation and then forward it by performing it manually. ECMAScript 6 has the module-like objectReflect that helps with forwarding: for each trap

handler.trap(target,arg_1,···,arg_n)

Reflect has a method

Reflect.trap(target,arg_1,···,arg_n)

If we useReflect, the previous example looks as follows.

consthandler={deleteProperty(target,propKey){console.log('DELETE '+propKey);returnReflect.deleteProperty(target,propKey);},has(target,propKey){console.log('HAS '+propKey);returnReflect.has(target,propKey);},// Other traps: similar}

Now what each of the traps does is so similar that we can implement the handler via a proxy:

consthandler=newProxy({},{get(target,trapName,receiver){// Return the handler method named trapNamereturnfunction(...args){// Don’t log args[0]console.log(trapName.toUpperCase()+' '+args.slice(1));// Forward the operationreturnReflect[trapName](...args);}}});

For each trap, the proxy asks for a handler method via theget operation and we give it one. That is, all of the handler methods can be implemented via the single meta methodget. It was one of the goals for the proxy API to make this kind of virtualization simple.

Let’s use this proxy-based handler:

>consttarget={};>constproxy=newProxy(target,handler);>proxy.foo=123;SETfoo,123,[objectObject]>proxy.fooGETfoo,[objectObject]123

The following interaction confirms that theset operation was correctly forwarded to the target:

> target.foo123

28.3.6Pitfall: not all objects can be wrapped transparently by proxies

A proxy object can be seen as intercepting operations performed on its target object – the proxy wraps the target. The proxy’s handler object is like an observer or listener for the proxy. It specifies which operations should be intercepted by implementing corresponding methods (get for reading a property, etc.). If the handler method for an operation is missing then that operation is not intercepted. It is simply forwarded to the target.

Therefore, if the handler is the empty object, the proxy should transparently wrap the target. Alas, that doesn’t always work.

28.3.6.1Wrapping an object affectsthis

Before we dig deeper, let’s quickly review how wrapping a target affectsthis:

consttarget={foo(){return{thisIsTarget:this===target,thisIsProxy:this===proxy,};}};consthandler={};constproxy=newProxy(target,handler);

If you calltarget.foo() directly,this points totarget:

> target.foo(){ thisIsTarget: true, thisIsProxy: false }

If you invoke that method via the proxy,this points toproxy:

> proxy.foo(){ thisIsTarget: false, thisIsProxy: true }

That’s done so that the proxy continues to be in the loop if, e.g., the target invokes methods onthis.

28.3.6.2Objects that can’t be wrapped transparently

Normally, proxies with an empty handler wrap targets transparently: you don’t notice that they are there and they don’t change the behavior of the targets.

If, however, a target associates information withthis via a mechanism that is not controlled by proxies, you have a problem: things fail, because different information is associated depending on whether the target is wrapped or not.

For example, the following classPerson stores private information in the WeakMap_name (more information on this technique is given inthe chapter on classes):

const_name=newWeakMap();classPerson{constructor(name){_name.set(this,name);}getname(){return_name.get(this);}}

Instances ofPerson can’t be wrapped transparently:

> const jane = new Person('Jane');> jane.name'Jane'> const proxy = new Proxy(jane, {});> proxy.nameundefined

jane.name is different from the wrappedproxy.name. The following implementation does not have this problem:

classPerson2{constructor(name){this._name=name;}getname(){returnthis._name;}}constjane=newPerson2('Jane');console.log(jane.name);// Janeconstproxy=newProxy(jane,{});console.log(proxy.name);// Jane
28.3.6.3Wrapping instances of built-in constructors

Instances of most built-in constructors also have a mechanism that is not intercepted by proxies. They therefore can’t be wrapped transparently, either. I’ll demonstrate the problem for an instance ofDate:

consttarget=newDate();consthandler={};constproxy=newProxy(target,handler);proxy.getDate();// TypeError: this is not a Date object.

The mechanism that is unaffected by proxies is calledinternal slots. These slots are property-like storage associated with instances. The specification handles these slots as if they were properties with names in square brackets. For example, the following method is internal and can be invoked on all objectsO:

O.[[GetPrototypeOf]]()

However, access to internal slots does not happen via normal “get” and “set” operations. IfgetDate() is invoked via a proxy, it can’t find the internal slot it needs onthis and complains via aTypeError.

ForDate methods,the language specification states:

Unless explicitly stated otherwise, the methods of the Number prototype object defined below are not generic and thethis value passed to them must be either a Number value or an object that has a[[NumberData]] internal slot that has been initialized to a Number value.

28.3.6.4Arrays can be wrapped transparently

In contrast to other built-ins, Arrays can be wrapped transparently:

> const p = new Proxy(new Array(), {});> p.push('a');> p.length1> p.length = 0;> p.length0

The reason for Arrays being wrappable is that, even though property access is customized to makelength work, Array methods don’t rely on internal slots – they are generic.

28.3.6.5A work-around

As a work-around, you can change how the handler forwards method calls and selectively setthis to the target and not the proxy:

consthandler={get(target,propKey,receiver){if(propKey==='getDate'){returntarget.getDate.bind(target);}returnReflect.get(target,propKey,receiver);},};constproxy=newProxy(newDate('2020-12-24'),handler);proxy.getDate();// 24

The drawback of this approach is that none of the operations that the method performs onthis go through the proxy.

Acknowlegement: Thanks to Allen Wirfs-Brock for pointing out the pitfall explained in this section.

28.4Use cases for proxies

This section demonstrates what proxies can be used for. That will give you the opportunity to see the API in action.

28.4.1Tracing property accesses (get,set)

Let’s assume we have a functiontracePropAccess(obj, propKeys) that logs whenever a property ofobj, whose key is in the ArraypropKeys, is set or got. In the following code, we apply that function to an instance of the classPoint:

classPoint{constructor(x,y){this.x=x;this.y=y;}toString(){return`Point(${this.x},${this.y})`;}}// Trace accesses to properties `x` and `y`constp=newPoint(5,7);p=tracePropAccess(p,['x','y']);

Getting and setting properties of the traced objectp has the following effects:

> p.xGET x5> p.x = 21SET x=2121

Intriguingly, tracing also works wheneverPoint accesses the properties, becausethis now refers to the traced object, not to an instance ofPoint.

> p.toString()GET xGET y'Point(21, 7)'

In ECMAScript 5, you’d implementtracePropAccess() as follows. We replace each property with a getter and a setter that traces accesses. The setters and getters use an extra object,propData, to store the data of the properties. Note that we are destructively changing the original implementation, which means that we are metaprogramming.

functiontracePropAccess(obj,propKeys){// Store the property data hereconstpropData=Object.create(null);// Replace each property with a getter and a setterpropKeys.forEach(function(propKey){propData[propKey]=obj[propKey];Object.defineProperty(obj,propKey,{get:function(){console.log('GET '+propKey);returnpropData[propKey];},set:function(value){console.log('SET '+propKey+'='+value);propData[propKey]=value;},});});returnobj;}

In ECMAScript 6, we can use a simpler, proxy-based solution. We intercept property getting and setting and don’t have to change the implementation.

functiontracePropAccess(obj,propKeys){constpropKeySet=newSet(propKeys);returnnewProxy(obj,{get(target,propKey,receiver){if(propKeySet.has(propKey)){console.log('GET '+propKey);}returnReflect.get(target,propKey,receiver);},set(target,propKey,value,receiver){if(propKeySet.has(propKey)){console.log('SET '+propKey+'='+value);}returnReflect.set(target,propKey,value,receiver);},});}

28.4.2Warning about unknown properties (get,set)

When it comes to accessing properties, JavaScript is very forgiving. For example, if you try to read a property and misspell its name, you don’t get an exception, you get the resultundefined. You can use proxies to get an exception in such a case. This works as follows. We make the proxy a prototype of an object.

If a property isn’t found in the object, theget trap of the proxy is triggered. If the property doesn’t even exist in the prototype chain after the proxy, it really is missing and we throw an exception. Otherwise, we return the value of the inherited property. We do so by forwarding theget operation to the target (the prototype of the target is also the prototype of the proxy).

constPropertyChecker=newProxy({},{get(target,propKey,receiver){if(!(propKeyintarget)){thrownewReferenceError('Unknown property: '+propKey);}returnReflect.get(target,propKey,receiver);}});

Let’s usePropertyChecker for an object that we create:

>constobj={__proto__:PropertyChecker,foo:123};>obj.foo// own123>obj.foReferenceError:Unknownproperty:fo>obj.toString()// inherited'[objectObject]'

If we turnPropertyChecker into a constructor, we can use it for ECMAScript 6 classes viaextends:

functionPropertyChecker(){}PropertyChecker.prototype=newProxy(···);classPointextendsPropertyChecker{constructor(x,y){super();this.x=x;this.y=y;}}constp=newPoint(5,7);console.log(p.x);// 5console.log(p.z);// ReferenceError

If you are worried about accidentallycreating properties, you have two options: You can either wrap a proxy around objects that trapsset. Or you can make an objectobj non-extensible viaObject.preventExtensions(obj), which means that JavaScript doesn’t let you add new (own) properties toobj.

28.4.3Negative Array indices (get)

Some Array methods let you refer to the last element via-1, to the second-to-last element via-2, etc. For example:

> ['a', 'b', 'c'].slice(-1)[ 'c' ]

Alas, that doesn’t work when accessing elements via the bracket operator ([]). We can, however, use proxies to add that capability. The following functioncreateArray() creates Arrays that support negative indices. It does so by wrapping proxies around Array instances. The proxies intercept theget operation that is triggered by the bracket operator.

functioncreateArray(...elements){consthandler={get(target,propKey,receiver){// Sloppy way of checking for negative indicesconstindex=Number(propKey);if(index<0){propKey=String(target.length+index);}returnReflect.get(target,propKey,receiver);}};// Wrap a proxy around an Arrayconsttarget=[];target.push(...elements);returnnewProxy(target,handler);}constarr=createArray('a','b','c');console.log(arr[-1]);// c

Acknowledgement: The idea for this example comes from ablog post by hemanth.hm.

28.4.4Data binding (set)

Data binding is about syncing data between objects. One popular use case are widgets based on the MVC (Model View Controler) pattern: With data binding, theview (the widget) stays up-to-date if you change themodel (the data visualized by the widget).

To implement data binding, you have to observe and react to changes made to an object. In the following code snippet, I sketch how observing changes could work for an Array.

functioncreateObservedArray(callback){constarray=[];returnnewProxy(array,{set(target,propertyKey,value,receiver){callback(propertyKey,value);returnReflect.set(target,propertyKey,value,receiver);}});}constobservedArray=createObservedArray((key,value)=>console.log(`${key}=${value}`));observedArray.push('a');

Output:

0=alength=1

28.4.5Accessing a restful web service (method calls)

A proxy can be used to create an object on which arbitrary methods can be invoked. In the following example, the functioncreateWebService creates one such object,service. Invoking a method onservice retrieves the contents of the web service resource with the same name. Retrieval is handled via an ECMAScript 6 Promise.

constservice=createWebService('http://example.com/data');// Read JSON data in http://example.com/data/employeesservice.employees().then(json=>{constemployees=JSON.parse(json);···});

The following code is a quick and dirty implementation ofcreateWebService in ECMAScript 5. Because we don’t have proxies, we need to know beforehand what methods will be invoked onservice. The parameterpropKeys provides us with that information, it holds an Array with method names.

functioncreateWebService(baseUrl,propKeys){constservice={};propKeys.forEach(function(propKey){service[propKey]=function(){returnhttpGet(baseUrl+'/'+propKey);};});returnservice;}

The ECMAScript 6 implementation ofcreateWebService can use proxies and is simpler:

functioncreateWebService(baseUrl){returnnewProxy({},{get(target,propKey,receiver){// Return the method to be calledreturn()=>httpGet(baseUrl+'/'+propKey);}});}

Both implementations use the following function to make HTTP GET requests (how it works is explained inthe chapter on Promises.

functionhttpGet(url){returnnewPromise((resolve,reject)=>{constrequest=newXMLHttpRequest();Object.assign(request,{onload(){if(this.status===200){// Successresolve(this.response);}else{// Something went wrong (404 etc.)reject(newError(this.statusText));}},onerror(){reject(newError('XMLHttpRequest Error: '+this.statusText));}});request.open('GET',url);request.send();});}

28.4.6Revocable references

Revocable references work as follows: A client is not allowed to access an important resource (an object) directly, only via a reference (an intermediate object, a wrapper around the resource). Normally, every operation applied to the reference is forwarded to the resource. After the client is done, the resource is protected byrevoking the reference, by switching it off. Henceforth, applying operations to the reference throws exceptions and nothing is forwarded, anymore.

In the following example, we create a revocable reference for a resource. We then read one of the resource’s properties via the reference. That works, because the reference grants us access. Next, we revoke the reference. Now the reference doesn’t let us read the property, anymore.

constresource={x:11,y:8};const{reference,revoke}=createRevocableReference(resource);// Access grantedconsole.log(reference.x);// 11revoke();// Access deniedconsole.log(reference.x);// TypeError: Revoked

Proxies are ideally suited for implementing revocable references, because they can intercept and forward operations. This is a simple proxy-based implementation ofcreateRevocableReference:

functioncreateRevocableReference(target){letenabled=true;return{reference:newProxy(target,{get(target,propKey,receiver){if(!enabled){thrownewTypeError('Revoked');}returnReflect.get(target,propKey,receiver);},has(target,propKey){if(!enabled){thrownewTypeError('Revoked');}returnReflect.has(target,propKey);},···}),revoke(){enabled=false;},};}

The code can be simplified via the proxy-as-handler technique from the previous section. This time, the handler basically is theReflect object. Thus, theget trap normally returns the appropriateReflect method. If the reference has been revoked, aTypeError is thrown, instead.

functioncreateRevocableReference(target){letenabled=true;consthandler=newProxy({},{get(dummyTarget,trapName,receiver){if(!enabled){thrownewTypeError('Revoked');}returnReflect[trapName];}});return{reference:newProxy(target,handler),revoke(){enabled=false;},};}

However, you don’t have to implement revocable references yourself, because ECMAScript 6 lets you create proxies that can be revoked. This time, the revoking happens in the proxy, not in the handler. All the handler has to do is forward every operation to the target. As we have seen that happens automatically if the handler doesn’t implement any traps.

functioncreateRevocableReference(target){consthandler={};// forward everythingconst{proxy,revoke}=Proxy.revocable(target,handler);return{reference:proxy,revoke};}
28.4.6.1Membranes

Membranes build on the idea of revocable references: Environments that are designed to run untrusted code, wrap a membrane around that code to isolate it and keep the rest of the system safe. Objects pass the membrane in two directions:

In both cases, revocable references are wrapped around the objects. Objects returned by wrapped functions or methods are also wrapped. Additionally, if a wrapped wet object is passed back into a membrane, it is unwrapped.

Once the untrusted code is done, all of the revocable references are revoked. As a result, none of its code on the outside can be executed anymore and outside objects that it has cease to work, as well. TheCaja Compiler is “a tool for making third party HTML, CSS and JavaScript safe to embed in your website”. It uses membranes to achieve this task.

28.4.7Implementing the DOM in JavaScript

The browser Document Object Model (DOM) is usually implemented as a mix of JavaScript and C++. Implementing it in pure JavaScript is useful for:

Alas, the standard DOM can do things that are not easy to replicate in JavaScript. For example, most DOM collections are live views on the current state of the DOM that change dynamically whenever the DOM changes. As a result, pure JavaScript implementations of the DOM are not very efficient. One of the reasons for adding proxies to JavaScript was to help write more efficient DOM implementations.

28.4.8Other use cases

There are more use cases for proxies. For example:

28.5The design of the proxy API

In this section, we go deeper into how proxies work and why they work that way.

28.5.1Stratification: keeping base level and meta level separate

Firefox has allowed you to do some interceptive metaprogramming for a while: If you define a method whose name is__noSuchMethod__, it is notified whenever a method is called that doesn’t exist. The following is an example of using__noSuchMethod__.

constobj={__noSuchMethod__:function(name,args){console.log(name+': '+args);}};// Neither of the following two methods exist,// but we can make it look like they doobj.foo(1);// Output: foo: 1obj.bar(1,2);// Output: bar: 1,2

Thus,__noSuchMethod__ works similarly to a proxy trap. In contrast to proxies, the trap is an own or inherited method of the object whose operations we want to intercept. The problem with that approach is that base level (normal methods) and meta level (__noSuchMethod__) are mixed. Base-level code may accidentally invoke or see a meta level method and there is the possibility of accidentally defining a meta level method.

Even in standard ECMAScript 5, base level and meta level are sometimes mixed. For example, the following metaprogramming mechanisms can fail, because they exist at the base level:

By now, it should be obvious that making (base level) property keys special is problematic. Therefore, proxies arestratified – base level (the proxy object) and meta level (the handler object) are separate.

28.5.2Virtual objects versus wrappers

Proxies are used in two roles:

An earlier design of the proxy API conceived proxies as purely virtual objects. However, it turned out that even in that role, a target was useful, to enforce invariants (which is explained later) and as a fallback for traps that the handler doesn’t implement.

28.5.3Transparent virtualization and handler encapsulation

Proxies are shielded in two ways:

Both principles give proxies considerable power for impersonating other objects. One reason for enforcinginvariants (as explained later) is to keep that power in check.

If you do need a way to tell proxies apart from non-proxies, you have to implement it yourself. The following code is a modulelib.js that exports two functions: one of them creates proxies, the other one determines whether an object is one of those proxies.

// lib.jsconstproxies=newWeakSet();exportfunctioncreateProxy(obj){consthandler={};constproxy=newProxy(obj,handler);proxies.add(proxy);returnproxy;}exportfunctionisProxy(obj){returnproxies.has(obj);}

This module uses the ECMAScript 6 data structureWeakSet for keeping track of proxies.WeakSet is ideally suited for this purpose, because it doesn’t prevent its elements from being garbage-collected.

The next example shows howlib.js can be used.

// main.jsimport{createProxy,isProxy}from'./lib.js';constp=createProxy({});console.log(isProxy(p));// trueconsole.log(isProxy({}));// false

28.5.4The meta object protocol and proxy traps

This section examines how JavaScript is structured internally and how the set of proxy traps was chosen.

In the context of programming languages and API design,aprotocol is a set of interfaces plus rules for using them. The ECMAScript specification describes how to execute JavaScript code. It includes aprotocol for handling objects. This protocol operates at a meta level and is sometimes called the meta object protocol (MOP). The JavaScript MOP consists of own internal methods that all objects have. “Internal” means that they exist only in the specification (JavaScript engines may or may not have them) and are not accessible from JavaScript. The names of internal methods are written in double square brackets.

The internal method for getting properties is called[[Get]]. If we pretend that property names with square brackets are legal, this method would roughly be implemented as follows in JavaScript.

// Method definition[[Get]](propKey,receiver){constdesc=this.[[GetOwnProperty]](propKey);if(desc===undefined){constparent=this.[[GetPrototypeOf]]();if(parent===null)returnundefined;returnparent.[[Get]](propKey,receiver);// (A)}if('value'indesc){returndesc.value;}constgetter=desc.get;if(getter===undefined)returnundefined;returngetter.[[Call]](receiver,[]);}

The MOP methods called in this code are:

In line A you can see why proxies in a prototype chain find out aboutget if a property isn’t found in an “earlier” object: If there is no own property whose key ispropKey, the search continues in the prototypeparent ofthis.

Fundamental versus derived operations. You can see that[[Get]] calls other MOP operations. Operations that do that are calledderived. Operations that don’t depend on other operations are calledfundamental.

28.5.4.1The MOP of proxies

Themeta object protocol of proxies is different from that of normal objects. For normal objects, derived operations call other operations. For proxies, each operation (regardless of whether it is fundamental or derived) is either intercepted by a handler method or forwarded to the target.

What operations should be interceptable via proxies? One possibility is to only provide traps for fundamental operations. The alternative is to include some derived operations. The advantage of doing so is that it increases performance and is more convenient. For example, if there weren’t a trap forget, you’d have to implement its functionality viagetOwnPropertyDescriptor. One problem with derived traps is that they can lead to proxies behaving inconsistently. For example,get may return a value that is different from the value in the descriptor returned bygetOwnPropertyDescriptor.

28.5.4.2Selective intercession: what operations should be interceptable?

Intercession by proxies isselective: you can’t intercept every language operation. Why were some operations excluded? Let’s look at two reasons.

First, stable operations are not well suited for intercession. An operation isstable if it always produces the same results for the same arguments. If a proxy can trap a stable operation, it can become unstable and thus unreliable.Strict equality (===) is one such stable operation. It can’t be trapped and its result is computed by treating the proxy itself as just another object. Another way of maintaining stability is by applying an operation to the target instead of the proxy. As explained later, when we look at how invariants are enfored for proxies, this happens whenObject.getPrototypeOf() is applied to a proxy whose target is non-extensible.

A second reason for not making more operations interceptable is that intercession means executing custom code in situations where that normally isn’t possible. The more this interleaving of code happens, the harder it is to understand and debug a program. It also affects performance negatively.

28.5.4.3Traps:get versusinvoke

If you want to create virtual methods via ECMAScript 6 proxies, you have to return functions from aget trap. That raises the question: why not introduce an extra trap for method invocations (e.g.invoke)? That would enable us to distinguish between:

There are two reasons for not doing so.

First, not all implementations distinguish betweenget andinvoke. For example,Apple’s JavaScriptCore doesn’t.

Second, extracting a method and invoking it later viacall() orapply() should have the same effect as invoking the method via dispatch. In other words, the following two variants should work equivalently. If there was an extra trapinvoke then that equivalence would be harder to maintain.

// Variant 1: call via dynamic dispatchconstresult=obj.m();// Variant 2: extract and call directlyconstm=obj.m;constresult=m.call(obj);
28.5.4.3.1Use cases forinvoke

Some things can only be done if you are able to distinguish betweenget andinvoke. Those things are therefore impossible with the current proxy API. Two examples are: auto-binding and intercepting missing methods. Let’s examine how one would implement them if proxies supportedinvoke.

Auto-binding. By making a proxy the prototype of an objectobj, you can automatically bind methods:

Auto-binding helps with using methods as callbacks. For example, variant 2 from the previous example becomes simpler:

constboundMethod=obj.m;constresult=boundMethod();

Intercepting missing methods.invoke lets a proxy emulate the previously mentioned__noSuchMethod__ mechanism that Firefox supports. The proxy would again become the prototype of an objectobj. It would react differently depending on how an unknown propertyfoo is accessed:

28.5.5Enforcing invariants for proxies

Before we look at what invariants are and how they are enforced for proxies, let’s review how objects can be protected via non-extensibility and non-configurability.

28.5.5.1Protecting objects

There are two ways of protecting objects:

Non-extensibility. If an object is non-extensible, you can’t add properties and you can’t change its prototype:

'use strict';// switch on strict mode to get TypeErrorsconstobj=Object.preventExtensions({});console.log(Object.isExtensible(obj));// falseobj.foo=123;// TypeError: object is not extensibleObject.setPrototypeOf(obj,null);// TypeError: object is not extensible

Non-configurability. All the data of a property is stored inattributes. A property is like a record and attributes are like the fields of that record. Examples of attributes:

Thus, if a property is both non-writable and non-configurable, it is read-only and remains that way:

'use strict';// switch on strict mode to get TypeErrorsconstobj={};Object.defineProperty(obj,'foo',{value:123,writable:false,configurable:false});console.log(obj.foo);// 123obj.foo='a';// TypeError: Cannot assign to read only propertyObject.defineProperty(obj,'foo',{configurable:true});// TypeError: Cannot redefine property

For more details on these topics (including howObject.defineProperty() works) consult the following sections in “Speaking JavaScript”:

28.5.5.2Enforcing invariants

Traditionally, non-extensibility and non-configurability are:

These and other characteristics that remain unchanged in the face of language operations are calledinvariants. With proxies, it is easy to violate invariants, as they are not intrinsically bound by non-extensibility etc.

The proxy API prevents proxies from violating invariants by checking the parameters and results of handler methods. The following are four examples of invariants (for an arbitrary objectobj) and how they are enforced for proxies (an exhaustive list is given at the end of this chapter).

The first two invariants involve non-extensibility and non-configurability. These are enforced by using the target object for bookkeeping: results returned by handler methods have to be mostly in sync with the target object.

The remaining two invariants are enforced by checking return values:

Enforcing invariants has the following benefits:

The next two sections give examples of invariants being enforced.

28.5.5.3Example: the prototype of a non-extensible target must be represented faithfully

In response to thegetPrototypeOf trap, the proxy must return the target’s prototype if the target is non-extensible.

To demonstrate this invariant, let’s create a handler that returns a prototype that is different from the target’s prototype:

constfakeProto={};consthandler={getPrototypeOf(t){returnfakeProto;}};

Faking the prototype works if the target is extensible:

constextensibleTarget={};constext=newProxy(extensibleTarget,handler);console.log(Object.getPrototypeOf(ext)===fakeProto);// true

We do, however, get an error if we fake the prototype for a non-extensible object.

constnonExtensibleTarget={};Object.preventExtensions(nonExtensibleTarget);constnonExt=newProxy(nonExtensibleTarget,handler);Object.getPrototypeOf(nonExt);// TypeError
28.5.5.4Example: non-writable non-configurable target properties must be represented faithfully

If the target has a non-writable non-configurable property then the handler must return that property’s value in response to aget trap. To demonstrate this invariant, let’s create a handler that always returns the same value for properties.

consthandler={get(target,propKey){return'abc';}};consttarget=Object.defineProperties({},{foo:{value:123,writable:true,configurable:true},bar:{value:456,writable:false,configurable:false},});constproxy=newProxy(target,handler);

Propertytarget.foo is not both non-writable and non-configurable, which means that the handler is allowed to pretend that it has a different value:

> proxy.foo'abc'

However, propertytarget.bar is both non-writable and non-configurable. Therefore, we can’t fake its value:

> proxy.barTypeError: Invariant check failed

28.6FAQ: proxies

28.6.1Where is theenumerate trap?

ES6 originally had a trapenumerate that was triggered byfor-in loops. But it was recently removed, to simplify proxies.Reflect.enumerate() was removed, as well. (Source: TC39 notes)

28.7Reference: the proxy API

This section serves as a quick reference for the proxy API: the global objectsProxy andReflect.

28.7.1Creating proxies

There are two ways to create proxies:

28.7.2Handler methods

This subsection explains what traps can be implemented by handlers and what operations trigger them. Several traps return boolean values. For the trapshas andisExtensible, the boolean is the result of the operation. For all other traps, the boolean indicates whether the operation succeeded or not.

Traps for all objects:

Traps for functions (available if target is a function):

28.7.2.1Fundamental operations versus derived operations

The following operations arefundamental, they don’t use other operations to do their work:apply,defineProperty,deleteProperty,getOwnPropertyDescriptor,getPrototypeOf,isExtensible,ownKeys,preventExtensions,setPrototypeOf

All other operations arederived, they can be implemented via fundamental operations. For example, for data properties,get can be implemented by iterating over the prototype chain viagetPrototypeOf and callinggetOwnPropertyDescriptor for each chain member until either an own property is found or the chain ends.

28.7.3Invariants of handler methods

Invariants are safety constraints for handlers. This subsection documents what invariants are enforced by the proxy API and how. Whenever you read “the handler must do X” below, it means that aTypeError is thrown if it doesn’t. Some invariants restrict return values, others restrict parameters. The correctness of a trap’s return value is ensured in two ways: Normally, an illegal value means that aTypeError is thrown. But whenever a boolean is expected, coercion is used to convert non-booleans to legal values.

This is the complete list of invariants that are enforced:

In the spec, the invariants are listed in the section “Proxy Object Internal Methods and Internal Slots”.

28.7.4Operations that affect the prototype chain

The following operations of normal objects perform operations on objects in the prototype chain. Therefore, if one of the objects in that chain is a proxy, its traps are triggered. The specification implements the operations as internal own methods (that are not visible to JavaScript code). But in this section, we pretend that they are normal methods that have the same names as the traps. The parametertarget becomes the receiver of the method call.

All other operations only affect own properties, they have no effect on the prototype chain.

In the spec, these (and other) operations are described in the section “Ordinary Object Internal Methods and Internal Slots”.

28.7.5Reflect

The global objectReflect implements all interceptable operations of the JavaScript meta object protocol as methods. The names of those methods are the same as those of the handler methods, which,as we have seen, helps with forwarding operations from the handler to the target.

Several methods have boolean results. Forhas andisExtensible, they are the results of the operation. For the remaining methods, they indicate whether the operation succeeded.

28.7.5.1Use cases forReflect besides forwarding

Apart from forwarding operations,why isReflect useful [4]?

28.7.5.2Object.* versusReflect.*

Going forward,Object will host operations that are of interest to normal applications, whileReflect will host operations that are more low-level.

28.8Conclusion

This concludes our in-depth look at the proxy API. For each application, you have to take performance into consideration and – if necessary – measure. Proxies may not always be fast enough. On the other hand, performance is often not crucial and it is nice to have the metaprogramming power that proxies give us. As we have seen, there are numerous use cases they can help with.

28.9Further reading

[1] “On the design of the ECMAScript Reflection API” by Tom Van Cutsem and Mark Miller. Technical report, 2012. [Important source of this chapter.]

[2] “The Art of the Metaobject Protocol” by Gregor Kiczales, Jim des Rivieres and Daniel G. Bobrow. Book, 1991.

[3] “Putting Metaclasses to Work: A New Dimension in Object-Oriented Programming” by Ira R. Forman and Scott H. Danforth. Book, 1999.

[4] “Harmony-reflect: Why should I use this library?” by Tom Van Cutsem. [Explains whyReflect is useful.]

Next:29. Coding style tips for ECMAScript 6

[8]ページ先頭

©2009-2025 Movatter.jp