Practical JavaScript Meta Programming
Proxy & Reflect
This article will show the basics to building a Proxy object that traps some standard object behavior, and implements some custom logic either in addition to, or in place of, the default behavior. It utilizes the Reflect object to perform the default behavior when applicable.
I'll trap the set
, and deleteProperty
operations in order to have two different states for an object.
- An internal state that reflects values that are persisted, in theory, to disk.
- A current, or transient, state that may or may not be equal to internal state.
First, I create an object named Modela
that, upon initialization, returns a proxied object that has the following properties.
modifiedProperties
is an array that will hold the names of any properties that have been modified from their internal state.__instance__
returns the internal object state.save
method will move property values from the transient state to the internal state.
The proxy traps deleteProperty
to override its default behavior.
- Determines if the property being deleted is on the internal model object.
- If it is an internal model property, add to
modifiedProperties
and flip thedirty
flag. - Then reset the value to either
""
or 0 depending on if it's a number value or not. - If it's not an internal model property, go ahead and delete it with the
Reflect
object.
The proxy also traps set
and does the following:
- Determines if the property being changed is on the internal model object.
- If it is an internal model property, add to
modifiedProperties
and flip thedirty
flag. - Use
Reflect
to forward with changing the value.
const Modela = Object.create(null, {
init: {
/*
On initialization, an object argument should be passed in
representing the fields of resultant object
Example:
Modela.init({
id: 0,
firstName: '',
lastName: ''
});
*/
value: function (model) {
// Makes the target object dirty and stores modified properties since last save
function modify (target, property) {
if (property in model) {
target.dirty = true;
if (target.modifiedProperties.indexOf(property) == -1
&& property !== "dirty"
&& property !== "modifiedProperties") {
target.modifiedProperties.push(property);
}
}
}
/*
Define the base object with public methods
*/
const base = Object.create(null, {
// Array property to hold other modified properties
modifiedProperties: {
value: [],
enumerable: true,
writable: true
},
// Non-enumerable property to display the internal state
__instance__: {
value: model
},
save: {
value: function () {
// For each modified model property, save to internal state
this.modifiedProperties.forEach(property => {
model[property] = this[property];
});
// State saved, clear modified properties
this.modifiedProperties = [];
// Object now clean
this.dirty = false;
return this;
},
enumerable: true
}
});
// Assign the model properties & value to the base object
Object.assign(base, model);
// Save the object to reset dirty flag
base.save();
/*
Define the handler that traps required properties and implement
custom logic for each one
*/
const handler = {
set (target, property, value) {
modify(target, property);
return Reflect.set(target, property, value);
},
deleteProperty (target, property) {
modify(target, property);
if (property in model) {
if (Number.isNaN(model[property])) {
target[property] = '';
} else {
target[property] = 0;
}
return true;
}
return Reflect.deleteProperty(target, property);
}
};
// Construct the new proxy and return it
const proxy = new Proxy(base, handler);
return proxy;
}
}
});
var Product = Modela.init({
'id':0,
'title': '',
'description': '',
'price': 0,
'quantity': 0
});
Proxy in Action
Let's see this in action. First, I modify one of the model properties, title
. You'll see that it gets added to the modifiedProperties
array, the dirty
flag gets set to true, and the transient property value changes. However, the internal title
property does not.
Then, when I invoke save()
, the internal title
property value gets updated.
Next, I add a non-model property to the object.
- Note that the
dirty
flag does not get flipped. - Then I change the value of a internal model property, and both
modifiedProperties
anddirty
are changed appropriately. - The internal property still hasn't changed value.
- Then I invoke
save()
again and the internal property gets the value.
Lastly, I delete the price
property, but because it's an internal model property, my trap stops the deletion from happening, and changes the value of the property back to 0.
Next Steps
From here, it's not a large jump in complexity to add in logic that persists the internal state to disk. You could write SQL with a database adapter, or work with an ORM tool to manage that process.
Also, I could have written better type checking to include booleans, nullable values, etc. but that's beyond the scope of this article.