-
-
Save rwaldron/617381 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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