Created
October 6, 2010 19:27
-
-
Save DmitrySoshnikov/613924 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 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 <[email protected]> | |
* (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