Skip to content

@DmitrySoshnikov /magic-properties.js
Created

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
/**
* This library defines magic properties and methods
* for objects. Generic hooks are: __get__, __set__,
* __delete__, __count__, __call__, __construct__,
* __noSuchProperty__ and __noSuchMethod__.
*
* 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);
}
});
// additional helpers
/**
* isPrimitive
*/
Object.defineProperty(Object, "isPrimitive", {
value: function (object) {
return object !== Object(object);
}
});
/**
* isObject
*/
Object.defineProperty(Object, "isObject", {
value: function (object) {
return !Object.isPrimitive(object);
}
});
/**
* New style objects with
* generic hooks; see also
* new style objects with [[Mixin]]
* chain: http://gist.github.com/613924
*/
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, "__noSuchMethod__": 1,
"__noSuchProperty__": 1, "__call__": 1, "__construct__": 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,
configurable: true,
writable: true
});
}
});
// 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,
configurable: 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(object).filter(function (name) {
return !hasOwn.call(magics, name);
});
};
// if this is initially a proxied function
if (hasOwn.call(object, "__call__") || hasOwn.call(object, "__construct__")) {
var createFnArgs = [handler];
object.__call__ && (createFnArgs.push(object.__call__));
object.__construct__ && (createFnArgs.push(object.__construct__));
return Proxy.createFunction.apply(Proxy, createFnArgs);
}
// else this is a simple 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 calling of
// missing methods
__noSuchMethod__: function (name, args) {
console.log('__noSuchMethod__ hook for non-existing method "' + name + '" with args ' + args + ' 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.');
},
// foo is callable;
// it's optional, if omitted, then
// __construct__ is called (if present)
__call__: function () {
var args = Array.prototype.slice.call(arguments);
console.log('__call__ hook is called with args: ' + args);
},
// and also we can define separate
// handler for the construction;
// it's optional, if omitted, then
// __call__ is used
__construct__: function () {
var args = Array.prototype.slice.call(arguments);
console.log('__construct__ hook is called with args: ' + args);
// standard implementation of the [[Construct]]:
// create an object which inherits from our "prototype"
var newObject = Object.create(foo.prototype);
// add a new property
newObject.z = 30;
// then initialize it
var initResult = foo.__call__.apply(newObject, args);
// if __call__ returned an object, return it,
// else return our created object
return Object.isObject(initResult) ? initResult : newObject;
},
// since we use __construct__
// we should have a "prototype" property
// for our object
prototype: (function () {
// return standard implementation of the "prototype"
// with non-enumerable "constructor" property
return Object.defineProperty({}, "constructor", {
value: this,
writable: true,
configurable: true
});
}()),
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
// testing native SpiderMonkeys __noSuchMethod__
// 1. with nonExisting property
foo.nonExisting(1, 2, 3); // __get__, __noSuchProperty__, __noSuchMethod__
// 2. with existing property but which it's not a function
try {
foo.y(4, 5, 6); // error: "y" is not a function! __noSuchMethod__ cannot handle it
} catch (e) {
console.log(e); // TypeError: foo.y is not a function
}
// 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
}
// call the object
foo(1, 2, 3); // __call__ with 1,2,3 args
// and use it as a constructor
var bar = new foo(4, 5, 6); // __construct__ with 4,5,6 args
console.log(bar.z); // 30
console.log(typeof foo); // "function"
console.log(Object.prototype.toString.call(foo)); // "[object Function]"
// remove __call__ and __construct__ traps
delete foo.__call__;
delete foo.__construct__;
// better to have it as "object" again, but we don't in the current strawmen
console.log(typeof foo); // still "function", but not "object"
console.log(Object.prototype.toString.call(foo)); // still "[object Function]", but not "[object Object]"
console.log(foo.prototype === Object.getPrototypeOf(bar)); // true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.