Browse Source

Link to virtual properties wiki page

pull/320/merge
Sami Vaarala 9 years ago
parent
commit
a11c9676cc
  1. 442
      website/guide/virtualproperties.html

442
website/guide/virtualproperties.html

@ -1,441 +1,15 @@
<a name="propertyvirtualization"></a> <!-- for old links -->
<a name="virtualization-accessors"></a> <!-- for old links -->
<a name="virtualization-proxy-object"></a> <!-- for old links -->
<h1 id="virtualproperties">Virtual properties</h1>
<p>This section describes the two different mechanisms Duktape provides
for interacting with property accesses programmatically: accessor
properties and the <code>Proxy</code> object.</p>
<p>Duktape provides two mechanisms for interacting with property accesses
programmatically:</p>
<h2 id="virtualization-accessors">Ecmascript E5 accessor properties (getters and setters)</h2>
<h3>Overview of accessors</h3>
<p>Ecmascript Edition 5 provides <b>accessor properties</b> (also called
"setters and getters") which allow property read/write operations to be
captured by a user function. Setter/getter functions can be both Ecmascript
and Duktape/C functions.</p>
<p>Accessors are set up using <code>Object.defineProperty()</code> or
<code>Object.defineProperties()</code> from Ecmascript code, or using
<code><a href="api.html#duk_def_prop">duk_def_prop()</a></code> from C code.</p>
<h3>Example</h3>
<p>To capture writes to <code>obj.color</code> so that you can validate the
color value and trigger a redraw as a side effect:</p>
<pre class="ecmascript-code">
var obj = {};
Object.defineProperty(obj, 'color', {
enumerable: false,
configurable: false,
get: function () {
// current color is stored in the raw _color property here
return this._color;
},
set: function (v) {
if (!validateColor(v)) {
// only allow valid color formats to be assigned
throw new TypeError('invalid color: ' + v);
}
this._color = v;
redraw();
}
});
// Change to red and trigger a redraw.
obj.color = '#ff0000';
</pre>
<h3>Limitations</h3>
<p>Setters and getters have the advantage of being part of the E5 standard
and of being widely implemented. However, they have significant limitations:</p>
<ul>
<li>They are limited to interacting with property read and write operations.
You cannot capture property deletions, interact with object enumeration, etc.</li>
<li>They only apply to individual properties which you set up as accessors
beforehand. You cannot capture all property accesses of a certain object,
which limits their usefulness in some scenarios, such as virtualizing arrays.</li>
<li>A standard property setter/getter function doesn't get the property key as an
argument (this behavior is defined by the Ecmascript specification) which
prevents sharing of setter/getter functions for multiple properties. Duktape
provides setter/getter functions with the property name as an additional,
non-standard argument; see more discussion below.</li>
<li>Accessor properties (setters and getters)</li>
<li>The <code>Proxy</code> object</li>
</ul>
<h3>Non-standard getter/setter key argument</h3>
<p>Duktape provides the property key as a non-standard setter/getter function
argument when the setter/getter is triggered by a property access. For instance,
when running <code>print(foo.bar)</code> the getter for the "bar" property would
get called, and that function would get "bar" as a (non-standard) argument:</p>
<pre class="ecmascript-code">
var obj = {};
function myGetter(key) {
// 'this' binding is the target object, 'key' is a non-standard argument
}
function mySetter(val, key) {
// 'this' binding is the target object, 'key' is a non-standard argument
}
Object.defineProperties(obj, {
// Same getter/setter can be used here
key1: { enumerable: true, configurable: true, get: myGetter, set: mySetter },
key2: { enumerable: true, configurable: true, get: myGetter, set: mySetter },
key3: { enumerable: true, configurable: true, get: myGetter, set: mySetter }
// ...
});
</pre>
<p>However, setters and getters can be also called without doing a property
access; in these cases the argument will of course be missing:</p>
<pre class="ecmascript-code">
var desc = Object.getOwnPropertyDescriptor(obj, 'key1');
var getter = desc.get;
print(getter()); // invoke getter directly; key name will be 'undefined'
</pre>
<p>With this technique you can share setter/getter functions, but you still need
to define each accessor property beforehand. In particular, you can't virtualize
array elements in a reasonable manner, except for very small, fixed size arrays.</p>
<p>Also see
<a href="https://github.com/svaarala/duktape/blob/master/tests/ecmascript/test-dev-nonstd-setget-key-argument.js">test-dev-nonstd-setget-key-argument.js</a>.</p>
<h3>Sharing a Duktape/C setter/getter without the non-standard key argument</h3>
<div class="note">
This section should only be useful if you have disabled the non-standard
setter/getter key argument feature, which provides a much easier way of
sharing a setter/getter pair than the approach described in this section.
</div>
<p>You can also share a single pair of Duktape/C functions to virtualize
multiple property keys as follows.</p>
<p>First, a separate Ecmascript function is created for each setter/getter,
with each such function using the same underlying Duktape/C functions. Second,
the Duktape/C function uses properties stored on the Ecmascript function
instance "through" which it was called to specialize its behavior. Below, a
<code>key</code> property is stored in the Ecmascript function instance.</p>
<p>For each property, the setter/getter functions would be created as follows:</p>
<pre class="c-code">
/* Create Ecmascript function objects for 'key1' setter/getter. */
duk_push_c_function(my_setter, 1 /*nargs*/);
duk_push_string(ctx, "key1");
duk_put_prop_string(ctx, -2, "key");
duk_push_c_function(my_getter, 0 /*nargs*/);
duk_push_string(ctx, "key1");
duk_put_prop_string(ctx, -2, "key");
/* ... add accessor property to target object */
/* Create Ecmascript function objects for 'key2' setter/getter in
* the same way, and so on for remaining properties.
*/
</pre>
<p>The Duktape/C getter function would then (similarly for the setter):</p>
<pre class="c-code">
static duk_ret_t my_getter(duk_context *ctx) {
const char *key;
/* There are no positional arguments for the getter. */
/* Get the target object (e.g. if "foo.bar" is accessed, gets "foo")
* from the 'this' binding.
*/
duk_push_this(ctx);
/* Get the 'key' being accessed from the Ecmascript function which
* "wraps" the my_getter native function.
*/
duk_push_current_function(ctx);
duk_get_prop_string(ctx, -1, "key");
key = duk_require_string(ctx, -1);
/* -&gt; [ this func key ] */
if (strcmp(key, "key1") == 0) {
/* Behavior for 'key1' */
} else if (strcmp(key, "key2") == 0) {
/* Behavior for 'key2' */
}
/* ... */
}
</pre>
<p>Separate Ecmascript function objects and pre-defined accessor properties on
the target object are still needed for each virtualized property.</p>
<h2 id="virtualization-proxy-object">Ecmascript E6 Proxy subset</h2>
<h3>Overview of Proxy</h3>
<p>In addition to accessors, Duktape provides a subset implementation of the
Ecmascript E6 <code>Proxy</code> concept, see:</p>
<ul>
<li><a href="#es6-proxy">Proxy object (subset)</a>:
discussion of current limitations in Duktape's <code>Proxy</code> implementation.</li>
<li><a href="http://www.ecma-international.org/ecma-262/6.0/index.html#sec-proxy-objects">Proxy Objects (E6)</a>:
ES6 specification for the <code>Proxy</code> object.</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">Proxy (Mozilla)</a>:
Mozilla's description of the <code>Proxy</code> implemented in Firefox, contains a lot of examples.</li>
</ul>
<p>The Proxy object is much more powerful than setters/getters, but is not yet
a widely used feature of Ecmascript engines.</p>
<h3>Examples of has, get, set, and deleteProperty traps</h3>
<p>To print a line whenever any property is accessed:</p>
<pre class="ecmascript-code">
// Underlying plain object.
var target = { foo: 'bar' };
// Handler table, provides traps for interaction (can be modified on-the-fly).
var handler = {
has: function (targ, key) {
print('has called for key=' + key);
return key in targ; // return unmodified existence status
}
get: function (targ, key, recv) {
print('get called for key=' + key);
return targ[key]; // return unmodified value
},
set: function (targ, key, val, recv) {
print('set called for key=' + key + ', val=' + val);
targ[key] = val; // must perform write to target manually if 'set' defined
return true; // true: indicate that property write was allowed
},
deleteProperty: function (targ, key) {
print('deleteProperty called for key=' + key);
delete targ[key]; // must perform delete to target manually if 'deleteProperty' defined
return true; // true: indicate that property delete was allowed
}
};
// Create proxy object.
var proxy = new Proxy(target, handler);
// Proxy object is then accessed normally.
print('foo' in proxy);
proxy.foo = 321;
print(proxy.foo);
delete proxy.foo;
</pre>
<p>A Proxy object can also be used to create a read-only version of an
underlying object (which is quite tedious otherwise):</p>
<pre class="ecmascript-code">
var proxy = new Proxy(target, {
// has and get are omitted: existence checks and reads go through to the
// target object automatically
// set returns false: rejects write
set: function () { return false; },
// deleteProperty returns false: rejects delete
deleteProperty: function () { return false; }
});
</pre>
<p>You can also create a write-only version of an object (which is not
possible otherwise):</p>
<pre class="ecmascript-code">
var proxy = new Proxy(target, {
has: function() { throw new TypeError('has not allowed'); },
get: function() { throw new TypeError('read not allowed'); }
// set and deleteProperty are omitted: set/delete operations
// are allowed and go through to the target automatically
});
</pre>
<p>The following is a more convoluted example combining multiple (somewhat
artificial) behaviors:</p>
<pre class="ecmascript-code">
var target = { foo: 'bar' };
/*
* - 'color' behaves like in the getter/setter example, cannot be deleted
* (attempt to do so causes a TypeError)
*
* - all string values are uppercased when read
*
* - property names beginning with an underscore are read/write/delete
* protected in a few different ways, and their existence is denied
*/
var handler = {
has: function (targ, key) {
// this binding: handler table
// targ: underlying plain object (= target, above)
if (typeof key === 'string' &amp;&amp; key[0] === '_') {
// pretend that property doesn't exist
return false;
}
return key in targ;
},
get: function (targ, key, recv) {
// this binding: handler table
// targ: underlying plain object (= target, above)
// key: key (can be any value, not just a string)
// recv: object being read from (= the proxy object)
if (typeof key === 'string' &amp;&amp; key[0] === '_') {
throw new TypeError('attempt to access a read-protected property');
}
// Return value: value provided as property lookup result.
var val = targ[key];
return (typeof val === 'string' ? val.toUpperCase() : val);
},
set: function (targ, key, val, recv) {
// this binding: handler table
// targ: underlying plain object (= target, above)
// key: key (can be any value, not just a string)
// val: value
// recv: object being read from (= the proxy object)
if (typeof key === 'string') {
if (key === 'color') {
if (!validateColor(val)) {
throw new TypeError('invalid color: ' + val);
}
targ.color = val;
redraw();
// True: indicates to caller that property write allowed.
return true;
} else if (key[0] === '_') {
// False: indicates to caller that property write rejected.
// In non-strict mode this is ignored silently, but in strict
// mode a TypeError is thrown.
return false;
}
}
// Write to target. We could also return true without writing to the
// target to simulate a successful write without changing the target.
targ[key] = val;
return true;
},
deleteProperty: function (targ, key) {
// this binding: handler table
// targ: underlying plain object (= target, above)
// key: key (can be any value, not just a string)
if (typeof key === 'string') {
if (key === 'color') {
// For 'color' a delete attempt causes an explicit error.
throw new TypeError('attempt to delete the color property');
} else if (key[0] === '_') {
// False: indicates to caller that property delete rejected.
// In non-strict mode this is ignored silently, but in strict
// mode a TypeError is thrown.
return false;
}
}
// Delete from target. We could also return true without deleting
// from the target to simulate a successful delete without changing
// the target.
delete targ[key];
return true;
}
};
</pre>
<p>The ES6 semantics reject some property accesses even if the trap would allow
it. This happens if the proxy's target object has a non-configurable
conflicting property; see E6 Sections 9.5.7, 9.5.8, 9.5.9, and 9.5.10 for details.
You can easily avoid any such behaviors by keeping the target object empty and,
if necessary, backing the virtual properties in an unrelated plain object.</p>
<h3>Examples of enumerate and ownKeys traps</h3>
<p>The <code>enumerate</code> trap is invoked for enumeration
(<code>for (k in obj) { ... }</code>) while the <code>ownKeys</code> trap is
invoked by <code>Object.keys()</code> and <code>Object.getOwnPropertyNames()</code>.</p>
<p>To hide property names beginning with an underscore from enumeration and
<code>Object.keys()</code> and <code>Object.getOwnPropertyNames()</code>:</p>
<pre class="ecmascript-code">
var target = {
foo: 1,
bar: 2,
_quux: 3,
_baz: 4
};
var proxy = new Proxy(target, {
enumerate: function (targ) {
// this binding: handler table
// targ: underlying plain object (= target, above)
return Object.getOwnPropertyNames(targ)
.filter(function (v) { return v[0] !== '_'; });
},
ownKeys: function (targ) {
return Object.getOwnPropertyNames(targ)
.filter(function (v) { return v[0] !== '_'; });
}
});
function test() {
for (var k in proxy) {
print(k); // prints 'foo' and 'bar'
}
}
test();
print(Object.keys(proxy)); // prints 'foo,bar'
print(Object.getOwnPropertyNames(proxy)); // prints 'foo,bar'
</pre>
<h3>Using Proxy with a constructor function</h3>
<p>If a constructor returns an object value, that value replaces the
automatically created default instance available to the constructor
as the <code>this</code> binding
(<a href="http://www.ecma-international.org/ecma-262/5.1/#sec-13.2.2">E5.1 Section 13.2.2</a>).
This allows a Proxy object to be returned from a constructor as the result
of a constructor call.</p>
<p>It's also possible to initialize the <code>this</code> object normally, and
then wrap it behind a proxy (see
<a href="https://github.com/svaarala/duktape/blob/master/tests/ecmascript/test-bi-proxy-in-constructor.js">test-bi-proxy-in-constructor.js</a>):</p>
<pre class="ecmascript-code">
function MyConstructor() {
// Initialize 'this' normally
this.foo = 'bar';
// Wrap it behind a proxy
return new Proxy(this, {
// proxy traps
});
}
var obj = new MyConstructor();
</pre>
<h2>Mechanisms not supported by Duktape</h2>
<p>There are various non-standard mechanisms for property virtualization.
These are <b>not supported</b> by Duktape:</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch">Object.prototype.watch()</a></li>
</ul>
<p>See <a href="http://wiki.duktape.org/HowtoVirtualProperties.html">How to use virtual properties</a>
for examples.</p>

Loading…
Cancel
Save