Skip to content

Instantly share code, notes, and snippets.

@rwaldron
Forked from DmitrySoshnikov/magic-properties.js
Created October 8, 2010 19:31
Show Gist options
  • Save rwaldron/617381 to your computer and use it in GitHub Desktop.
Save rwaldron/617381 to your computer and use it in GitHub Desktop.
/**
* This library defines magic properties and methods
* for objects. Generic hooks are: __get__, __set__,
* __delete__, __count__ and __noSuchProperty__.
*
* Used features: Harmony (ES6) proxies.
*
* Tested in FF4 beta.
*
* @author Dmitry A. Soshnikov <dmitry.soshnikov@gmail.com>
* (C) 2010 Mit Style License
*/
/**
* ES6 getPropertyDescriptor
*/
Object.getPropertyDescriptor ||
Object.defineProperty(Object, "getPropertyDescriptor", {
value: function (object, name) {
do {
object = Object.getPrototypeOf(object);
if (object) {
var desc = Object.getOwnPropertyDescriptor(object, name);
if (desc) {
return desc;
}
}
} while (object);
}
});
/**
* New style objects with
* internal [[Mixin]] property -
* a chain of mixied objects
*/
Object.defineProperty(Object, "new", {
value: function objectNew(object) {
// proxied object
object || (object = {});
// helpers
var hasOwn = Object.prototype.hasOwnProperty;
// magic properties
// XXX: don't know how to handle __noSuchMethod__
// in such a generic get of a proxy
var magics = {
"__get__": 1, "__set__": 1, "__delete__": 1,
"__noSuchProperty__": 1, "__noSuchMethod__": 1
};
// set enumerable false for magics in initial structure
Object.getOwnPropertyNames(object).forEach(function (name) {
if (hasOwn.call(magics, name)) {
return Object.defineProperty(object, name, {
enumerable: false
});
}
});
// initial properties count excluding magics
var count = Object.getOwnPropertyNames(object).filter(function (name) {
return !hasOwn.call(magics, name);
}).length;
// __count__ accessor
Object.defineProperty(object, "__count__", {
get: function getCount() {
return count;
}
});
// a proxy handler
var handler = Proxy.noopHandler(object);
/**
* generic [[Get]]
*/
handler.get = function (r, name) {
// __get__ hook
if ("__get__" in object) {
object.__get__(name);
}
// if a property is not found
if ("__noSuchProperty__" in object && !(name in object)) {
return object.__noSuchProperty__(name);
}
return object[name];
};
// set
handler.set = function (r, name, value) {
// __set__ hook
if ("__set__" in object) {
object.__set__(name, value);
}
// update count if needed
if (!hasOwn.call(object, name)) {
// check whether there is an inherited accessor property
// because in this case, assignment sets the inherited property
var inheritedDesc = Object.getPropertyDescriptor(object, name);
// if there no inherited property, or it is a data property,
// then a new own property will be created, so increase the count
if (!inheritedDesc || hasOwn.call(inheritedDesc, "value")) {
count++;
}
}
// if one of the magics is being defined,
// set its enumerable attribute to false
if (hasOwn.call(magics, name)) {
return Object.defineProperty(object, name, {
value: value,
writable: true
});
}
// update/create the property
object[name] = value;
};
// delete
handler.delete = function (name) {
// __delete__ hook
if ("__delete__" in object) {
object.__delete__(name);
}
// decrease count
count--;
return delete object[name];
};
// defineProperty
handler.defineProperty = function (name, desc) {
// increase count if needed
if (!hasOwn.call(object, name)) {
count++;
}
// if one of the magics is being defined,
// set its enumerable attribute to false
if (hasOwn.call(magics, name)) {
desc.enumerable = false;
}
return Object.defineProperty(object, name, desc);
};
// getOwnPropertyNames to filter magics
handler.getOwnPropertyNames = function () {
return Object.getOwnPropertyNames(obj).filter(function (name) {
return !hasOwn.call(magics, name);
});
};
// a proxied object
return Proxy.create(
handler,
Object.getPrototypeOf(object)
);
}
});
/** A no-op forwarding proxy handler
* see: http://wiki.ecmascript.org/doku.php?id=harmony:proxies#examplea_no-op_forwarding_proxy
* It's good to have it as a built-in sugar
*/
Object.defineProperty(Proxy, "noopHandler", {
value: function noopHandler(obj) {
return {
getOwnPropertyDescriptor: function(name) {
var desc = Object.getOwnPropertyDescriptor(obj, name);
// a trapping proxy's properties must always be configurable
desc.configurable = true;
return desc;
},
getPropertyDescriptor: function(name) {
var desc = Object.getPropertyDescriptor(obj, name); // not in ES5
// a trapping proxy's properties must always be configurable
desc.configurable = true;
return desc;
},
getOwnPropertyNames: function() {
return Object.getOwnPropertyNames(obj);
},
getPropertyNames: function() {
return Object.getPropertyNames(obj); // not in ES5
},
defineProperty: function(name, desc) {
Object.defineProperty(obj, name, desc);
},
delete: function(name) { return delete obj[name]; },
fix: function() {
if (Object.isFrozen(obj)) {
return Object.getOwnPropertyNames(obj).map(function(name) {
return Object.getOwnPropertyDescriptor(obj, name);
});
}
// As long as obj is not frozen, the proxy won't allow itself to be fixed
return undefined; // will cause a TypeError to be thrown
},
has: function(name) { return name in obj; },
hasOwn: function(name) { return ({}).hasOwnProperty.call(obj, name); },
get: function(receiver, name) { return obj[name]; },
// bad behavior when set fails in non-strict mode
set: function(receiver, name, val) { obj[name] = val; return true; },
enumerate: function() {
var result = [];
for (name in obj) { result.push(name); };
return result;
},
keys: function() { return Object.keys(obj); }
};
}
});
// tests
var foo = Object.new({
// hook for reading properties
// is called every time
__get__: function (name) {
console.log('__get__ hook for "' + name + '" property is called.');
},
// hook for reading of only
// absent properties
__noSuchProperty__: function (name) {
console.log('__noSuchProperty__ hook for non-existing property "' + name + '" is called.');
},
// hook for writing properties
__set__: function (name, value) {
console.log('__set__ hook for "' + name + '" property with value ' + value + ' is called.');
},
// hook for removing properties
__delete__: function (name) {
console.log('__delete__ hook for "' + name + '" property is called.');
},
x: 10,
bar: function () {
return this.x;
}
});
console.log(foo.x); // __get__ x, 10
console.log(foo.bar()); // __get__ bar, __get__ x, 10
foo.y = 20; // __set__ y, value 20
console.log(foo.__count__); // 3 -- x, y, bar
delete foo.x; // __delete__ x
console.log(foo.__count__); // 2 -- y, bar
console.log(foo.z); // __get__ z, __noSuchProperty__ z
// redefine magic __get__ via assignment
foo.__get__ = function (name) {
console.log('New version of __get__ for "' + name + '" property.');
};
console.log(foo.y); // New version of __get__, y
// __get__ is not enumerable
for (var k in foo) {
console.log(k); // bar, y
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment