-
-
Save DmitrySoshnikov/607668 to your computer and use it in GitHub Desktop.
/** | |
* This library extends standard "Object.create" method | |
* with ability to create object with specified class. | |
* | |
* Also it defines concept of a meta-constructor which | |
* creates other construcotrs, which in turn create | |
* object with specified class. | |
* | |
* Currently, non-standard __proto__ extension | |
* is used to inject the needed prototype for arrays. | |
* | |
* Tested in BESEN r.122 and FF4 beta | |
* | |
* @author Dmitry A. Soshnikov <[email protected]> | |
* (C) Mit Style License | |
*/ | |
(function initialize() { | |
var | |
global = ("globalEval", eval)("this"), | |
hasOwn = Object.prototype.hasOwnProperty, | |
nativeObjectCreate = Object.create; | |
/** | |
* An augmented "Object.create" | |
*/ | |
Object.defineProperty(Object, "create", { | |
value: function objectCreate(proto, desc, kind) { | |
// default case | |
if (!kind) { | |
return nativeObjectCreate.call(Object, proto, (desc || {})); | |
} | |
// others case | |
var res = new global[kind]; | |
res.__proto__ = proto; | |
return Object.defineProperties(res, desc); | |
} | |
}); | |
/** | |
* Ad-hoc case with arrays | |
*/ | |
Object.defineProperty(Array, "create", { | |
value: function arrayCreate(proto, desc) { | |
return Object.create(proto, desc, "Array"); | |
} | |
}); | |
/** | |
* simply extends an object | |
* with another object, but only for | |
* absent properties of the object | |
*/ | |
Object.defineProperty(Object, "extend", { | |
value: function (self, another) { | |
Object.getOwnPropertyNames(another).forEach(function (name) { | |
if (hasOwn.call(self, name)) { | |
return; | |
} | |
Object.defineProperty( | |
self, | |
name, | |
Object.getOwnPropertyDescriptor(another, name) | |
); | |
}); | |
return self; | |
} | |
}); | |
/** | |
* A meta-constructor which creates | |
* other constructor, which in turn | |
* create objects with specified class | |
*/ | |
Object.defineProperty(global, "Constructor", { | |
value: Object.defineProperties({}, { | |
/** | |
* creates a new constructor | |
*/ | |
create: { | |
value: function constructorCreate(settings) { | |
// prototype of a constructor; | |
var prototype = Object.extend( | |
// inherits from prototype of the same | |
// name global constructor as settings.class | |
Object.create(global[settings.class].prototype, { | |
constructor: { | |
enumerable: false, | |
value: settings.constructor | |
} | |
}), | |
// and is initialized with | |
// the passed properties | |
settings.prototype | |
); | |
// define it | |
Object.defineProperty( | |
settings.constructor, | |
"prototype", | |
prototype | |
); | |
// default case | |
if (settings.class == "Object") { | |
return settings.constructor; | |
} | |
// other cases | |
var constructor = function () { | |
// create an object | |
var object = Object.create( | |
constructor.prototype, {}, | |
settings.class | |
); | |
// and initialzie it | |
settings.constructor.apply(object, arguments); | |
return object; | |
}; | |
constructor.prototype = prototype; | |
return constructor; | |
} | |
} | |
}) | |
}); | |
})(); | |
// tests | |
// there is no much convenience | |
// in the first level of hierarchi, | |
// since with the same success we could | |
// just create a simple array and augment | |
// it with own properties | |
var foo = Object.create(Array.prototype, { | |
size: { | |
get: function getSize() { | |
return this.length; | |
} | |
} | |
}, "Array"); | |
// but on the second hierarhy level | |
// it may be convinient to have | |
// "bar" as a real array with its | |
// overloaded [[DefineOwnProperty]] | |
var bar = Object.create(foo, { | |
count: { | |
get: function getCount() { | |
return this.size; | |
} | |
} | |
}, "Array"); | |
bar.push(1, 2, 3); | |
console.log( | |
bar.length, // 3 | |
bar.size, // 3 | |
bar.count // 3 | |
); | |
bar[4] = 5; | |
console.log(bar); // 1,2,3,,5 | |
console.log(bar.size); // 5 | |
bar.length = 0; | |
console.log(bar.count); // 0 | |
console.log(bar instanceof Array); // true | |
console.log(bar.constructor === Array); // true | |
console.log(Array.isArray(bar)); // true | |
// test with ad-hoc Array.create | |
var baz = Array.create(bar, { | |
// array elements (very inconvenient) | |
0: {value: 1, writable: true, enumerable: true, configurable: true}, | |
1: {value: 2, writable: true, enumerable: true, configurable: true}, | |
2: {value: 3, writable: true, enumerable: true, configurable: true}, | |
// methods | |
info: { | |
value: function getInfo() { | |
return [this.length, this.size, this.count].join(","); | |
} | |
} | |
}); | |
console.log(baz); // 1,2,3 | |
console.log(baz.info()); // 3,3,3 | |
console.log(Array.isArray(baz)); // true | |
console.log(baz instanceof Array); // true | |
// create new constructor, objects of which | |
// inherit from Array.prototype and are real | |
// arrays, additionally pass prototype properties | |
var Foo = Constructor.create({ | |
// objects kind | |
class: "Array", | |
// an initializer | |
constructor: function Foo(args) { | |
this.push.apply(this, args); | |
}, | |
// prototype properties (also may | |
// be added after "Foo" is created | |
prototype: { | |
size: { | |
get: function getSize() { | |
return this.length; | |
} | |
} | |
} | |
}); | |
var foo = new Foo([1, 2, 3]); | |
console.log(foo.length); // 3 | |
foo.push(4, 5, 6); | |
console.log(foo); // 1,2,3,4,5,6 | |
console.log(foo.length); // 6 | |
foo[7] = 8; | |
console.log(foo); // 1,2,3,4,5,6,,8 | |
foo.length = 3; | |
console.log(foo); // 1,2,3 | |
// augment the prototype | |
Foo.prototype.z = 10; | |
console.log(foo.z); // 10 | |
// change it to the new object | |
Foo.prototype = { | |
constructor: Foo, | |
// since we don't inherit from | |
// Array.prototype, reuse just | |
// one method to be able apply | |
// Foo constructor which uses it | |
push: Array.prototype.push, | |
x: 100 | |
}; | |
// new object | |
var bar = new Foo([1, 2, 3]); | |
console.log(bar instanceof Foo); // true | |
console.log(bar instanceof Array); // false, we changed prototype | |
console.log(Array.isArray(bar)); // but still, it's an array -- true | |
console.log(bar.x); // 100 | |
bar.push(4, 5, 6); | |
console.log(bar.length); // 6 | |
console.log(bar); // [object Array], use toString from new prototype | |
console.log(Array.prototype.join.call(bar)); // 1,2,3,4,5,6 |
Why is initialize a named function expression instead of an anonymous function expression?
Just for the same reason a name is required for function expressions in strict mode of ES5 -- just to make it more useful for debugging, i.e. in a call stack is shown initialize
, but not anonymous
(taking into account that there can be many anonymous
functions there). For the same reason, other functions, (e.g. objectCreate
) have name. Notice however, that this is for more-less complaint ES implementation, because e.g. IE (up to version 8, and possibly some subversions of 9) has a known bug with specifying the name of FE (details: http://bit.ly/aoAQDL)
In global = ("globalEval", eval)("this"), is the comma the comma operator and the code is therefore equivalent to global=eval("this")?
Yeah, but not exactly. There is one subtle issue with strict mode when this
value is undefined
(but not the global object
as in ES3 of non-strict ES5) in a normal function call. To apply eval
for the global context we can use a so-called indirect eval call (one way -- is to use a value of non-Reference type -- after comma operator eval
is a function object, but not the value of Reference
type). For details see: http://bit.ly/cNCHk2 So this is used as a protection from the global strict mode applying.
Dmitry.
Looks great. I have two questions:
Why is initialize a named function expression instead of an anonymous function expression?
In
global = ("globalEval", eval)("this")
, is the comma the comma operator and the code is therefore equivalent toglobal=eval("this")
?