Skip to content

Instantly share code, notes, and snippets.

@DmitrySoshnikov
Created October 6, 2010 19:27
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save DmitrySoshnikov/613924 to your computer and use it in GitHub Desktop.
Save DmitrySoshnikov/613924 to your computer and use it in GitHub Desktop.
/**
* This library defines a new style ES objects
* which support delegation based mixins. A mixin
* chain is stored in the internal [[Mixin]] property.
*
* Used features: Harmony (ES6) proxies.
*
* Tested in FF4 beta.
*
* @author Dmitry A. Soshnikov <dmitry.soshnikov@gmail.com>
* (C) 2010 Mit Style License
*/
/**
* Adds a module to a mixin
* chain of an object
*
* @param {Object} module
* - an object being mixed
* @param {Object} to
* - an object being extended by the module
*
* Both, "module" and "to" are new style objects with
* internal property __mixin__. Formally, "module" may
* be a simple object. However, it will be hard then to
* extend the mixin itself later (if it already has been
* mixed as a simple object before to some other object).
*
* In traits mode (enabled by default) checks naming
* conflicts at early stage and warnings an issue.
*
*/
Object.defineProperty(Object, "mixin", {
value: function objectMixin(module, to) {
// in traits mode check naming conflicts
if (Object.mixin.traitsMode) {
for (var name in module) if (name in to) {
console.log('Warning: "' + name + '" is already in the object.');
// throw "Naming conflict";
}
}
// mixin the module to the object
to.__mixin__.push(module);
return to;
}
});
/**
* traits mode: checks whether
* a property is already in object;
* in this case shows a warning message
*/
Object.defineProperty(Object.mixin, "traitsMode", {
value: true,
writable: true
});
/**
* New style objects with
* internal [[Mixin]] property -
* a chain of mixied objects
*/
Object.defineProperty(Object, "new", {
value: function objectNew(object) {
// proxied object
object || (object = {});
// chain of mixins
Object.defineProperty(object, "__mixin__", {
value: []
});
// a proxy handler
var handler = Proxy.noopHandler(object);
// helpers
var hasOwn = Object.prototype.hasOwnProperty;
/**
* generic [[Get]] which resolves
* a property by the chain: own ->
* the mixin chain -> the prototype chain;
* Every object in the mixin chain
* has the same property resolution
*/
handler.get = function (r, name) {
// first check an own property;
if (hasOwn.call(object, name)) {
return object[name];
}
// then consider the mixin chain;
var mixin = object.__mixin__;
var k = mixin.length; while (k--) {
// consider the prototype chain
// as well; these objects may
// be proxies themselves.
// TODO: remove overhead
if (name in mixin[k]) {
return mixin[k][name];
}
}
// if the property is not found
// in the mixin chain, consider
// the prototype chain
return object[name];
};
/**
* Test for in operator
* considers both [[Prototype]]
* and [[Mixin]] chains
*/
handler.has = function (name) {
if (name in object) {
return true;
}
// then consider the mixin chain;
var mixin = object.__mixin__;
var k = mixin.length; while (k--) {
if (name in mixin[k]) {
return true;
}
}
return false;
};
// 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
// a mixin module
var m = Object.new({
foo: function () {
console.log('m.foo', this === o);
},
baz: function () {
console.log('m.baz', this === o);
}
});
var o = Object.mixin(m, Object.new());
o.foo(); // "m.foo", true
console.log(o.foo === m.foo); // true, delegation is used
// another mixin
var m2 = Object.new({
foo: function () {
console.log('m2.foo', this === o);
},
bar: function () {
console.log('m2.bar', this === o);
}
});
// mixin it too; a warning is
// shown since we are in traitsMode
Object.mixin(m2, o); // Warning: "foo" is already in the object
o.foo(); // shadows "m", now "m2.foo", true
o.bar(); // "m2.bar", true
o.baz(); // "m.baz", true, from previous module "m"
// third mixin module
var m3 = Object.new({
test: function () {
console.log('m3.test', this === o, this === m2);
}
});
// mixin it to another module
m2 = Object.mixin(m3, m2);
o.test(); // "m3.test", true, false
m2.test(); // "m3.test", false, true
console.log("test" in o); // true
delete m2.foo; // remove shadowing "foo"
o.foo(); // again is taken from "m", "m.foo"
// another module to check
// traitsMode == false
var m4 = {
foo: function () {
console.log('m4.foo', this === o);
}
};
// turn off traitsMode
Object.mixin.traitsMode = false;
// add this trait to "o";
// no warning is shown since
// we have "traitsMode" turned off
Object.mixin(m4, o);
// but the method is added
// though, naming shadowing detection
// may throw exceptions
o.foo(); // "m4.foo", true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment