// redefine
var o = redefine.from(null);
// ES5
var o = Object.create(null);
// redefine
var o = redefine.from(null, {
name: "object",
toString: function () {
return "Hi, I am " + this.name;
}
});
// ES5
var o = Object.create(null, {
name: {
value: "object"
},
toString: {
value: function () {
return "Hi, I am " + this.name;
}
}
});
// redefine
var o = redefine.from(null, {
name: "object",
toString: function () {
return "Hi, I am " + this.name;
}
}, {
configurable : true,
writable: true
});
// ES5
var o = Object.create(null, {
name: {
configurable: true,
writable: true,
value: "object"
},
toString: {
configurable: true,
writable: true,
value: function () {
return "Hi, I am " + this.name;
}
}
});
// redefine
var o = redefine.from({}, {
bday: Date.now(),
age: redefine.as({
set: function () {
throw 'you cannot set your age';
},
get: function () {
return parseInt(
(Date.now() - this.bday) /
(1000 * 60 * 60 * 24 * 365)
); // I know, no leap year
}
})
});
// ES5
var o = Object.create({}, {
bday: {
value: Date.now()
},
age: {
set: function () {
throw 'you cannot set your age';
},
get: function () {
return parseInt(
(Date.now() - this.bday) /
(1000 * 60 * 60 * 24 * 365)
); // I know, no leap year
}
}
});
This a simplified Emitter
class with a logic optimized to save some memory and GC cycle.
// generic basic Emitter constructor
function Emitter(){}
function emit(callback) {
// just a recycled function
callback(this);
}
This is how we can define the prototype of the Emitter
class.
// redefine
redefine(
Emitter.prototype,
{
emit: function (type, data) {
if (type in this._handlers) {
this._handlers[type].forEach(emit, data);
}
return this;
},
on: function (type, handler) {
var list = this._getList(type);
list.indexOf(handler) < 0 && list.push(handler);
return this;
},
off: function (type, handler) {
var list = this._getList(type),
i = list.indexOf(handler);
if (-1 < i) {
list.splice(i, 1);
if (!list.length) {
delete this._handlers[type];
if (!Object.keys(this._handler).length) {
delete this._handlers;
}
}
}
return this;
},
_getList: function (type) {
return this._handlers[type] || (
this._handlers[type] = []
);
},
_handlers: redefine.later(function(){
return {};
})
}
);
The pattern used for the _handlers
property only is an inherited getter replaced on demand with a direct property access, explained in details in The Power Of Getters post.
Long story short: we can create 100 instances of Emitter
and the amount of extra objects will be exactly 100
instead of 200
considering a handler created during initialization.
With this pattern, only when the _handlers
object is needed is created once per instance and set as property to avoid calling the getter per each access.
When listeners are removed, both Array used as type list and _handlers
are removed, if empty. In order to obtain the same behavior in ES5:
// ES5
Object.defineProperties(
Emitter.prototype,
{
emit: {
value: function (type, data) {
if (type in this._handlers) {
this._handlers[type].forEach(emit, data);
}
return this;
}
},
on: {
value: function (type, handler) {
var list = this._getList(type);
list.indexOf(handler) < 0 && list.push(handler);
return this;
},
}
off: {
value: function (type, handler) {
var list = this._getList(type),
i = list.indexOf(handler);
if (-1 < i) {
list.splice(i, 1);
if (!list.length) {
delete this._handlers[type];
if (!Object.keys(this._handler).length) {
delete this._handlers;
}
}
}
return this;
}
},
_getList: {
value: function (type) {
return this._handlers[type] || (
this._handlers[type] = []
);
}
},
_handlers: {
get: function () {
Object.defineProperty(
this, "_handlers", {
configurable: true,
value: {}
}
);
return this._handlers;
}
}
}
);
While Object.defineProperties(object, descriptorsList)
second argument loops only through hasOwnProperty(key)
, once it comes to property definition, and the same is for Object.defineProperty(obj, key, descriptor)
, the object used to described the property looks up for inherited properties too and consider them.
Even if defaults are {writable: false, enumerable: false, configurable: false}
, it's easy to make every property enumerable
and configurable
simply doing this:
Object.prototype.configurable = true;
Object.prototype.enumerable = true;
After above malicious piece of code, if you think a constant variable defined like this cannot be changed you are wrong:
Object.defineProperty(window, "myLibrary", {value:myLibrary});
It is necessary indeed to ensure that defaults are written as well, making defaults meaningless because of inheritance, but even worse, if a malicious code will write Object.prototype.writable = true
and there are getters or setters involved, these will all throw an exception because writable
does not accept getters or setters.
In ES5 every descriptor should inherit from null
otherwise no descriptor will be immune from possible attacks.
Here an example of few problems we might want to avoid:
// set up the nasty environment
Object.prototype.configurable =
Object.prototype.enumerable =
Object.prototype.writable = true;
// verify that there are actually problems
try {
Object.defineProperty(this, "problem", {get: function () {
return "we gonna have bad time";
}});
} catch(o_O) {
console.log("ouch!");
Object.defineProperty(this, "problem", {value:true});
console.log(this.propertyIsEnumerable("problem")); // true
this.problem = {}; // no problems
delete this.problem; // true again
}
Accordingly, every single descriptor should be created via Object.crete(null)
so our code should look like
function descriptor(object) {
// now don't take this wrong
// but this function is almost
// as big as the whole redefine.js library
// I would not go through this pattern
for (var
nullDescriptor = Object.create(null),
properties = [
"configurable",
"enumerable",
"writable",
"get",
"set",
"value"
],
i = properties.length; i--;
object.hasOwnProperty(properties[i]) && (
nullDescriptor[properties[i]] = object[properties[i]]
)
);
return nullDescriptor;
}
Object.defineProperty(this, "problem", descriptor({
value: "now we talk"
}));
this.propertyIsEnumerable("problem"); // false
this.problem = {}; // nope
delete this.problem; // false
this.problem; // "now we talk"
Got it? Now ...
redefine(this, "problem", "solved");
this.propertyIsEnumerable("problem"); // false
this.problem = {}; // nope
delete this.problem; // false
this.problem; // "solved"
If you are wondering about performance there are many things to consider behind redefine.js
and one of these is that performance are really good for what it offers.
While is usually slightly slower on desktop, redefine.js
is almost as fast in older Android 2.3 devices where performance matters the most.
redefine.js
is indeed suitable for mobile phones, even if quite old!