Looking into Mirrors

April 27, 2011

in JavaScript,Reflection

In my last post I introduced the programming language concept of Mirrors and mentioned jsmirrors, the prototype I’ve been working on to explore using mirrors to support reflection within JavaScript.  In this post I’m going to take a deeper look into jsmirrors itself.  I had three goals for my first iteration of jsmirrors:

  1. Define basic mirror-based interfaces for reflection upon upon JavaScript objects and properties.
  2. Demonstrate that jsmirrors  can support different levels of reflection privilege.
  3. Demonstrate that the jsmirrors interface can work with both local and external objects.

In this post I’m going to concentrate on showing details of the basic interfaces I designed to meet the first goal. In subsequent posts I talk about the other two goals.

The actual implementation of jsmirrors is contained in the file mirrors.js.  Note that jsmirrors requires an ECMAScript 5 compatible JavaScript implementation. The jsmirrors implementation is structured using the module pattern and when loaded defines a single global named Mirrors whose properties are factory functions that can be used to create various kinds of mirror objects. The most basic mirror factory is called introspect and creates a mirror on a local object that only supports introspection (examination without modification):

//create a test object
var obj = {a:1, get b() {return "b value"}, c: undefined};
obj.c = {back: obj};  //make a circular reference to obj

//create an introspection mirror on obj
var m=Mirrors.introspect(obj);
console.log(m);   //output:  "Object Introspection Mirror #0"

In the above example, lines 2-3 create a couple of test objects and line 6 is creating an introspection mirror on one of them. We see from the output of line 7 how such mirror objects identify themselves using the toString method. Once we have such a mirror, we can use it to examine the structure and state of its reflected object:

console.log(m.ownPropertyNames) ;  //output:  "a,b,c"
console.log(m.extensible); //output:  true
console.log(m.has("toString")); //output:  true
console.log(m.hasOwn("toString")); //output:  false
var p=m.prototype;
console.log(p); //output:  "Object Introspection Mirror #3"
console.log(p.hasOwn("toString")); //output:  true

Lines 8-11 are querying various characteristics of the object reflected by the mirror m such as a list of its own property names, whether or not additional properties may be added, and whether it locally defines or inherits a specific property. Line 12 queries for the object that is the prototype object for the reflected object. Note from line 13 that the value returned is also an introspection mirror. This is one of the important characteristics of this style of mirror interface. When an object value is accessed a mirror on the object is always returned rather than the actual object. You may be curious why the mirror p is “Mirror #3″ rather than “Mirror #1″. The reason is that some of the preceding method calls internally generated Mirrors #1-2 as part of their internal implementation.

Mirror objects aren’t unique. Multiple mirror objects may simultaneously exist that reflect on the same underlying object. The sameAs method can be used to determine if two mirrors are reflecting the same object:

console.log(m.sameAs(p)) ;  //output:  false
var opm = Mirrors.introspect(Object.prototype);
console.log(p.sameAs(opm)); //output:  true

Introspection mirrors support several other methods. The complete list can be seen by looking at the objectMirrorInterface specification in mirrorsInterfaceSpec.js. Some of the most important methods provide access to information about specific properties. Property mirrors are returned to enable introspection of actual property definitions:

var pmb = m.lookup("b");
  //output: "Accessor Property Introspection Mirror name: b #6"

In line 18 the method lookup on a mirror object is used to retrive the property named “b”. What is return in this case is a property introspection mirror. The interface specifications propertyMirrorInterface, dataPropertyMirrorInteface, and accessorPropertyMirrorInteface in mirrorsInterfaceSpec.js describe the operations that can be performed on property introspection mirrors. For example:

console.log(pmb.isData);  //output: false
console.log(pmb.isAccessor); //output: true
console.log(pmb.enumerable); //output: true
Object.defineProperty(obj,"b",{enumerable: false});
console.log(pmb.enumerable); //output: false

Lines 21-22 show tests to determine whether the reflected property is a data property or an accessor property and line 23 reports the state of the property’s enumerable attribute. Lines 24-25 demonstrate that the mirror is presenting a live view of the reflected object. Line 24 modifies the enumerable attribute of the “b” property of the reflected object. When the mirror is again used in line 25 we see that the reported state of the enumerable attribute has changed to false. Note that we had to use a built-in reflection function to change the enumerable attribute because the mirrors we are using in the above examples only support introspection and don’t allow any changes to the reflected objects to be made using the mirrors.

console.log(pmb.definedOn.sameAs(m)); //output: true
var fm=pmb.getter;
console.log(fm);  //output: "Function Introspection Mirror #8"
console.log(fm.source); //output: "function () {return \"b value\";}"

Property mirrors know what object “owns” the reflected property. Line 26 shows using definedOn to get a mirror on the owning object. We then use sameAs to verify that this mirror is actually reflecting the same object as our original mirror m. Because the property we are reflecting upon is an accessor property it has getter and setter functions. In line 27 we use the property mirror to access the property’s getter function and in line 28 we see that the results is yet another kind of mirror, a “Function Introspection Mirror”. As specified by the functionMirrorInterface in mirrorsInterfaceSpec.js this is a kind of object mirror that adds reflection capabilities that are specific to function objects. For example, in line 29 we see that we can use the function mirror to retrieve the source code of the getter function.

The above examples provide just a quick overview of the capability of jsmirrors introspection mirrors and how they are used. But these mirrors only allow the inspection of objects. In many situations that is the only kind of reflection you need or that you will want to permit. However, there are situations where reflection needs to be able to perform other operations such as modifying the definitions of properties or calling reflected functions. In my next post, I’ll explore how jsmirrors supports those kinds of reflection and how it can be used to control or limit access to them.


Previous post:

Next post: