JavaScript Mixin function - javascript

The function skeleton is at the top of mixin.js. It takes in a target object (o) and another object with properties to be mixed in (mixin).
You should return a Proxy object from this function. Override the "get" trap so that:
1) If the object already has a property, the object's property is returned.
2) If the object does not have the property, it returns the property from the mixin object.
3) If the property is "__original", it returns the original object "o". (This design provides a way to "unmix" a mixin.)
4) If none of the other cases hold, undefined should be returned as the result.
You should not make any changes to this file outside of the addMixin function definition.
This is what I have so far,
function addMixin(o, mixin) {
let oldValue = {};
return new Proxy(o, {
get: function(target, property) {
if(target.hasOwnProperty(property) === true) {
return target[property];
} else if (target.hasOwnProperty(property) === false) {
oldValue[property] = target[property];
return mixin[property];
} else if (property === '__original') {
return target[property] = oldValue[property];
} else {
return undefined;
}
}
})
// A sample mixin.
let PlayableMixin = {
// Plays a system bell 3 times
play: function() {
console.log("\u0007");
console.log("\u0007");
console.log("\u0007");
},
duration: 100,
};
function Song(name, performer, duration) {
this.name = name;
this.performer = performer;
this.duration = duration;
}
Song.prototype = addMixin(Song.prototype, PlayableMixin);
Song.prototype.display = function() {
console.log(`Now playing "${this.name}", by ${this.performer}. (${this.duration})`);
}
let s = new Song("Gun Street Girl", "Tom Waits", "4:17");
s.display();
s.play();
console.log(s.duration);
s = s.__original;
console.log(s.play);
Expected Output
Now playing "Gun Street Girl", by Tom Waits. (4:17)
4:17
undefined
Gives me an error instead of printing undefined.

You don't really need the if else chain. return already breaks out of the function. __original should be checked first, because it should return the original object regardless of whether it exists as a property or not; it is essentially a reserved symbol name for a computed property.
Your code is also missing a closing bracket.
function addMixin(o, mixin) {
let oldValue = {};
return new Proxy(o, {
get: function(target, property) {
if (property === '__original') {
return o;
}
if (target.hasOwnProperty(property) === true) {
return target[property];
} else if (target.hasOwnProperty(property) === false) {
oldValue[property] = target[property];
return mixin[property];
} else {
return undefined;
}
}
})
}
// A sample mixin.
let PlayableMixin = {
// Plays a system bell 3 times
play: function() {
console.log("\u0007");
console.log("\u0007");
console.log("\u0007");
},
duration: 100,
};
function Song(name, performer, duration) {
this.name = name;
this.performer = performer;
this.duration = duration;
}
Song.prototype = addMixin(Song.prototype, PlayableMixin);
Song.prototype.display = function() {
console.log(`Now playing "${this.name}", by ${this.performer}. (${this.duration})`);
}
let s = new Song("Gun Street Girl", "Tom Waits", "4:17");
s.display();
s.play();
console.log(s.duration);
s = s.__original;
console.log(s, s.play);
Just a side comment: I'd complain about the problem description. The way declared conditions are listed almost seems like it was purposely set up to be confusing. It's like one of those silly riddles where someone makes you repeat what they say and say something stupid about yourself.

Related

Object property that can be getter or function

I am coding a chainable library and I want the API to allow calling part of the chain as static values (with a default value) and sometimes as functions, so parameters could be pass to them.
Simplified example:
var obj = {};
var chainCache = [];
Reflect.defineProperty(obj, 'color', {
get(){
chainCache.push('red');
return obj;
}
})
Reflect.defineProperty(obj, 'background', {
get(){
chainCache.push('black');
return obj;
}
})
Reflect.defineProperty(obj, 'end', {
value(){
var value = chainCache.join(" ");
chainCache.length = 0;
return value;
}
})
console.log( obj.color.background.end() ) // red black
This is a very simplified example and in reality I would also like to include an ability in the above "API" to optionally use the same color key, like this:
obj.color.background.end() // current API (great)
obj.color('#FF0').background.end() // optionally call "color" as function
obj.color().background.end() // bad API, I do not want this
Can color be both function and property at the same time, depending how it is called?
I took a harder look at this than I intended. It looks like with the advent of Proxies this is possible. Building on your example:
let chainCache = [];
let obj = {
color: new Proxy(()=>{}, {
get: (target, prop) => {
chainCache.push("red");
if(prop === "background") {
chainCache.push("black");
return { end: obj.end };
}
},
apply: (target, prop, args) => {
if(args.length > 0) {
chainCache.push(args.pop());
} else {
throw new Error("color expects argument if called like function");
}
return { background: new Proxy(()=>{}, {
get: (target,prop) => { chainCache.push("black"); return obj.end;}
})};
}
}),
end: () => {
let value = chainCache.join(" ");
chainCache = [];
return value;
}
};
console.log(obj.color.background.end());
console.log(obj.color("#FF0").background.end());
console.log(obj.color().background.end());
In the debug console you get:
red black
#FF0 black
Error: color expects argument if called like function
Essentially color is a proxy that wraps an anonymous arrow function, if color is accessed like a property the get() trap gets called, if color gets accessed as a function the apply() trap gets called. This allows for a certain degree of meta-programming like you're looking for.
You will need to make obj a function.
A function is just an object and can have properties as well - obj.color must return a function (for obj.color()) that has properties (for obj.color.background).
But no, when the getter is accessed you don't know yet whether it will be used for a method invocation or not - obj.color() is a property access plus a function call.
var chainCache = [];
var obj = Object.defineProperties(function obj(...args) {
chainCache.push(args);
return obj;
}, {
color: {
get() {
chainCache.push('color');
return obj;
}
},
background: {
get() {
chainCache.push('background');
return obj;
}
},
end: {
value: function() {
var res = chainCache.reduce((acc, val) =>
Array.isArray(val)
? `${acc}(${val.join(',')})`
: `${acc}.${val}`
, "obj");
chainCache.length = 0;
return res;
}
}
})

How to implement more than one method in JS es6 class?

I have a class of validations that I have created in JS:
let test = new Validator(req.body);
Now I want to test something, maybe that a specific key in this object is 2-5 char length, I would do it like this:
let myBoolean = test.selector("firstName").minLength(2).maxLength(5);
// firstName is like: req.body.firstName
And how this could be done in the class?
EDIT
I made something like this:
audit.isLength({selector: "from", gte: 2, lte: 35})
class Validator {
constructor(obj) {
this.obj = obj;
this.isValid = true;
}
isExists(sel) {
if (typeof this.obj[sel] === "undefined") return false;
return true;
}
isLength(info) {
let sel = this.obj[info.selector];
if (typeof sel === "undefined") return false;
if (info.gte) {
if (sel.length<info.gte) return false;
}
if (info.lte) {
if (sel.length>info.lte) return false;
}
if (info.gt) {
if (sel.length<=info.gt) return false;
}
if (info.lt) {
if (sel.length>=info.lt) return false;
}
return true;
}
}
Try something like this - assign the object to validate to a property on the instantiation, return this from each validating call, and when validating, assign to an isValid property on the object (if it isn't already false). Note that you need to access the isValid property finally in order to retrieve the boolean.
class Validator {
constructor(obj) {
this.obj = obj;
this.isValid = true;
}
selector(sel) {
this.sel = sel;
return this;
}
minLength(min) {
if (this.isValid) this.isValid = this.obj[this.sel].length >= min;
return this;
}
maxLength(max) {
if (this.isValid) this.isValid = this.obj[this.sel].length <= max;
return this;
}
}
const test = new Validator({firstName: 'foobar'}); // 6 chars: invalid
console.log(test.selector("firstName").minLength(2).maxLength(5).isValid);
const test2 = new Validator({firstName: 'fooba'}); // 5 chars: valid
console.log(test2.selector("firstName").minLength(2).maxLength(5).isValid);
const test3 = new Validator({firstName: 'f'}); // 1 char: invalid
console.log(test3.selector("firstName").minLength(2).maxLength(5).isValid);
Create a class with fluent methods/chainable methods, that return this, which is an instance of the class itself and when you finally run validation according to the rules, call .validate(), which will act as a final method to return the result:
class Validator {
constructor (body) {
this._body = body;
}
selector(str) {
this._selector = str;
return this;
}
minLength(num) {
this._minLength = num;
return this;
}
maxLength(num) {
this._maxLength = num;
return this;
}
validate() {
// run your validation logic here and return true or false accordingly
return true
}
}
const req = { body: 'body' };
const test = new Validator(req.body);
const myBoolean = test
.selector('firstName')
.minLength(2)
.maxLength(5)
.validate();
console.log('rules:');
console.log(test);
console.log(`result: ${myBoolean}`);
This is the builder pattern (sort of). You'll probably want to define a separate class that has a minLength and maxLength function. Those functions will set some state on the builder, and return either this (the builder its self), or a new builder that's a copy of this. Then you'd have some finalize function on the builder, which looks at the state, handles all the logic based on the min/max, and returns a boolean.

How to make a property/method invokable or not?

I want to achieve this functionality:
I have an object var obj = {};
I have three properties on that obj, obj.zero & obj.one& obj.binaryString
obj.zero & obj.one are methods while obj.binaryString is a string
When I chain the properties, I want them to add their respective digit to the binaryString. So for example:
obj.one.zero.zero.one => makes obj.binaryString = 1001
obj.one.zero.one.one.zero => makes obj.binaryString = 10110
I have achieved the above functionality with this:
function Binary () {
var obj = { binaryString: '' };
Object.defineProperty(obj, 'zero', {
get: function() {
obj.binaryString += '0';
return obj;
}
});
Object.defineProperty(obj, 'one', {
get: function() {
obj.binaryString += '1';
return obj;
}
});
return obj;
}
var binary = new Binary();
binary.one.zero.zero.one // => obj.binaryString becomes '1001'
Now I want to log out the completed binaryString, plus and additionalString which I have accomplished with the code below:
// placed inside Binary constructor function
Object.defineProperty(obj, 'log', {
get: function() {
return function(additionalString) {
console.log(obj.binaryString + additionalString);
};
}
});
So with this current code I can do this:
binary.one.zero.one.zero.one.log(' is the answer');
// logs out `10101 is the answer`
What I want to do is get rid of the log and make the one and zero methods invokable or not so I can achieve this functionality:
binary.one.one.zero.one(' is the result')
// => logs out `1101 is the result`
How can I do this?
I believe it would be similar functionality to how Chalk works:
chalk.blue.bold('Hello world!');
// `blue` is not invoked here but it adds the color blue to the style
chalk.blue('Hello world!');
// `blue` IS invoked here. It adds blue to the style and returns the stylized string
Just Make the obj as function , and print what ever you want .
function Binary () {
var obj = function(msg){ console.log(msg+this.binaryString ) };
obj.binaryString = ''
Object.defineProperty(obj, 'zero', {
get: function() {
obj.binaryString += '0';
return obj;
}
});
Object.defineProperty(obj, 'one', {
get: function() {
obj.binaryString += '1';
return obj;
}
});
return obj;
}
var binary = new Binary();
binary.one.zero.zero.one.zero(" is the result ")
I would like to point out that what you're doing is a very bad/dangerous idea: abusing read properties to mutate the object itself is asking for trouble. You may not see it now, but it's going to lead to pain and heartache down the line in the form of difficult-to-find bugs and convoluted patterns.
What you can do is, which is not so dangerous, is instead of mutating the object itself, return a new instance of Binary with every call to #one or #zero. For example:
function Binary(s) {
this.binaryString = s || ''
}
Object.defineProperty(Binary.prototype, 'zero', {
get: function() {
return new Binary(this.binaryString + '0')
}})
Object.defineProperty(Binary.prototype, 'one', {
get: function() {
return new Binary(this.binaryString + '1')
}})
This is the approach taken by Chalk, and will be much safer and less error-prone.
UPDATE:
After thinking about your problem, and seeing your question, I think the best approach at all is not to use classes at all. You can solve this problem with a pure function-based approach. It's immutable, it's safe, and I believe it's less confusing. Here it is in ES5:
function bin(str) {
if(!str) str = ''
function f(msg) { return str + ' ' + msg }
return Object.defineProperties(f, {
zero: {
get: function() {
return bin(str + '0')
},
},
one: {
get: function() {
return bin(str + '1')
},
},
})
}
And if you can use ES6 (aka ES2015), you can make it much more compact:
function bin(str = '') {
return Object.defineProperties(msg => `${str} ${msg}`, {
zero: { get() { return bin(str + '0') } },
one: { get() { return bin(str + '1') } },
})
}
You would use it like this:
bin().one.zero.one.zero('is the answer') // '1010 is the answer'

Object is created but seems to be sharing a reference

I am building an editor, in the editor when you press play it converts a bunch of objects to a Prefab (which explains to my engine how to build an object). When I then stop them I would like to rebuild the Prefab again and it should be back to its original state (before play was pressed). The issue that I am having is that when stop is pressed the items don't go back to their original state. They seem to stay in the state that they are in when stop was pressed.
Here is the class that I am using to convert Prefabs back and forth:
class Prefab {
public name: string;
public components: PrefabComponent[] = [];
public static create(gameObject: GameObject): Prefab {
let prefab = new Prefab;
prefab.name = gameObject.name;
gameObject.components.forEach(comp => {
let prefabComp = new PrefabComponent;
prefabComp.name = comp.name;
for (let c in comp) {
let prefabProp = new PrefabProperty;
prefabProp.name = c;
if (typeof comp[c] == 'object') {
prefabProp.value = Object.create(comp[c]);
} else {
prefabProp.value = comp[c];
}
prefabComp.properties.push(prefabProp);
}
prefab.components.push(prefabComp);
});
return prefab;
}
public static toObject(prefab: Prefab): GameObject {
let gameObject = new GameObject(prefab.name);
prefab.components.forEach(comp => {
let newComp;
if (comp.name.toLowerCase() != 'transform') {
newComp = gameObject.addComponent(comp.name);
}
if (!newComp) {
newComp = gameObject.getComponent(comp.name);
}
comp.properties.forEach(prop => {
if (prop.value instanceof Sprite) {
newComp[prop.name] = Sprite.create(prop.value.path);
} else if (typeof prop.value == 'object') {
newComp[prop.name] = Object.create(prop.value);
} else {
newComp[prop.name] = prop.value;
}
});
});
return gameObject;
}
}
class PrefabComponent {
public name: string;
public properties: PrefabProperty[] = [];
}
class PrefabProperty {
public name: string;
public value: any;
}
Here is how I am starting and stopping the game:
play.addEventListener('click', (event) => {
event.preventDefault();
// Game has started
// Stop was pressed
if (game instanceof SpyNginMain) {
game.stopGame();
game = null;
play.classList.remove('active');
pause.classList.remove('active');
// Reset the editor
EditorObjectManager.clear();
prefabs.forEach(prefab => {
EditorObjectManager.addItem(Prefab.toObject(prefab));
});
updateScene();
}
// Game has not started
// Play was pressed
else {
game = new SpyNginMain();
prefabs = [];
EditorObjectManager.items.forEach(item => {
prefabs.push(Prefab.create(item));
});
game.init(scene, prefabs);
game.startGame();
play.classList.add('active');
}
});
The issue seems to be when I try to convert Object with the PrefabProperty class. I think that it isn't creating an instance but is creating a reference to the original, but I am not sure...
Your create method will certainly retain references to the original object because of the following code:
if (typeof comp[c] == 'object') {
prefabProp.value = Object.create(comp[c]);
} else {
prefabProp.value = comp[c];
}
The Object.create call creates a new object with the original object as its prototype, so if you subsequently add/modify/delete a property in the original object, the changes will also appear in the newly created object.
I think what you meant to do here is to clone the original object? There are many existing implementations for cloning (eg lodash's cloneDeep).
I would replace all five lines quoted above with a single clone(comp[c]) call, since your typeof check is not reliable in other ways too (eg typeof null == 'object' is true, typeof [1,2,3] == 'object' is true, etc).
This might just be a typescript thing but seeing this without parenthesis looks incorrect:
let prefabComp = new PrefabComponent;
I believe it should be:
let prefabComp = new PrefabComponent();
Also this line here: newComp[prop.name] = prop.value; if prop.value is an object, that component would get referenced from the old one.
A good way to see if the object is the same one is ===.
const a = {'a': 'hello'};
console.log(a === a); // true
const b = {'a': 'hello'};
console.log(a === b); // false

unique object identifier in javascript

I need to do some experiment and I need to know some kind of unique identifier for objects in javascript, so I can see if they are the same. I don't want to use equality operators, I need something like the id() function in python.
Does something like this exist ?
Update My original answer below was written 6 years ago in a style befitting the times and my understanding. In response to some conversation in the comments, a more modern approach to this is as follows:
(function() {
if ( typeof Object.id != "undefined" ) return;
var id = 0;
Object.id = function(o) {
if ( typeof o.__uniqueid != "undefined" ) {
return o.__uniqueid;
}
Object.defineProperty(o, "__uniqueid", {
value: ++id,
enumerable: false,
// This could go either way, depending on your
// interpretation of what an "id" is
writable: false
});
return o.__uniqueid;
};
})();
var obj = { a: 1, b: 1 };
console.log(Object.id(obj));
console.log(Object.id([]));
console.log(Object.id({}));
console.log(Object.id(/./));
console.log(Object.id(function() {}));
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
console.log(k);
}
}
// Logged keys are `a` and `b`
If you have archaic browser requirements, check here for browser compatibility for Object.defineProperty.
The original answer is kept below (instead of just in the change history) because I think the comparison is valuable.
You can give the following a spin. This also gives you the option to explicitly set an object's ID in its constructor or elsewhere.
(function() {
if ( typeof Object.prototype.uniqueId == "undefined" ) {
var id = 0;
Object.prototype.uniqueId = function() {
if ( typeof this.__uniqueid == "undefined" ) {
this.__uniqueid = ++id;
}
return this.__uniqueid;
};
}
})();
var obj1 = {};
var obj2 = new Object();
console.log(obj1.uniqueId());
console.log(obj2.uniqueId());
console.log([].uniqueId());
console.log({}.uniqueId());
console.log(/./.uniqueId());
console.log((function() {}).uniqueId());
Take care to make sure that whatever member you use to internally store the unique ID doesn't collide with another automatically created member name.
So far as my observation goes, any answer posted here can have unexpected side effects.
In ES2015-compatible enviroment, you can avoid any side effects by using WeakMap.
const id = (() => {
let currentId = 0;
const map = new WeakMap();
return (object) => {
if (!map.has(object)) {
map.set(object, ++currentId);
}
return map.get(object);
};
})();
id({}); //=> 1
Latest browsers provide a cleaner method for extending Object.prototype. This code will make the property hidden from property enumeration (for p in o)
For the browsers that implement defineProperty, you can implement uniqueId property like this:
(function() {
var id_counter = 1;
Object.defineProperty(Object.prototype, "__uniqueId", {
writable: true
});
Object.defineProperty(Object.prototype, "uniqueId", {
get: function() {
if (this.__uniqueId == undefined)
this.__uniqueId = id_counter++;
return this.__uniqueId;
}
});
}());
For details, see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty
Actually, you don't need to modify the object prototype and add a function there. The following should work well for your purpose.
var __next_objid=1;
function objectId(obj) {
if (obj==null) return null;
if (obj.__obj_id==null) obj.__obj_id=__next_objid++;
return obj.__obj_id;
}
For browsers implementing the Object.defineProperty() method, the code below generates and returns a function that you can bind to any object you own.
This approach has the advantage of not extending Object.prototype.
The code works by checking if the given object has a __objectID__ property, and by defining it as a hidden (non-enumerable) read-only property if not.
So it is safe against any attempt to change or redefine the read-only obj.__objectID__ property after it has been defined, and consistently throws a nice error instead of silently fail.
Finally, in the quite extreme case where some other code would already have defined __objectID__ on a given object, this value would simply be returned.
var getObjectID = (function () {
var id = 0; // Private ID counter
return function (obj) {
if(obj.hasOwnProperty("__objectID__")) {
return obj.__objectID__;
} else {
++id;
Object.defineProperty(obj, "__objectID__", {
/*
* Explicitly sets these two attribute values to false,
* although they are false by default.
*/
"configurable" : false,
"enumerable" : false,
/*
* This closure guarantees that different objects
* will not share the same id variable.
*/
"get" : (function (__objectID__) {
return function () { return __objectID__; };
})(id),
"set" : function () {
throw new Error("Sorry, but 'obj.__objectID__' is read-only!");
}
});
return obj.__objectID__;
}
};
})();
Typescript version of #justin answer, ES6 compatible, using Symbols to prevent any key collision and added into the global Object.id for convenience. Just copy paste the code below, or put it into an ObjecId.ts file you will import.
(enableObjectID)();
declare global {
interface ObjectConstructor {
id: (object: any) => number;
}
}
const uniqueId: symbol = Symbol('The unique id of an object');
export function enableObjectID(): void {
if (typeof Object['id'] !== 'undefined') {
return;
}
let id: number = 0;
Object['id'] = (object: any) => {
const hasUniqueId: boolean = !!object[uniqueId];
if (!hasUniqueId) {
object[uniqueId] = ++id;
}
return object[uniqueId];
};
}
Example of usage:
console.log(Object.id(myObject));
jQuery code uses it's own data() method as such id.
var id = $.data(object);
At the backstage method data creates a very special field in object called "jQuery" + now() put there next id of a stream of unique ids like
id = elem[ expando ] = ++uuid;
I'd suggest you use the same method as John Resig obviously knows all there is about JavaScript and his method is based on all that knowledge.
For the purpose of comparing two objects, the simplest way to do this would be to add a unique property to one of the objects at the time you need to compare the objects, check if the property exists in the other and then remove it again. This saves overriding prototypes.
function isSameObject(objectA, objectB) {
unique_ref = "unique_id_" + performance.now();
objectA[unique_ref] = true;
isSame = objectB.hasOwnProperty(unique_ref);
delete objectA[unique_ref];
return isSame;
}
object1 = {something:true};
object2 = {something:true};
object3 = object1;
console.log(isSameObject(object1, object2)); //false
console.log(isSameObject(object1, object3)); //true
I faced the same problem and here's the solution I implemented with ES6
code
let id = 0; // This is a kind of global variable accessible for every instance
class Animal {
constructor(name){
this.name = name;
this.id = id++;
}
foo(){}
// Executes some cool stuff
}
cat = new Animal("Catty");
console.log(cat.id) // 1
I've used code like this, which will cause Objects to stringify with unique strings:
Object.prototype.__defineGetter__('__id__', function () {
var gid = 0;
return function(){
var id = gid++;
this.__proto__ = {
__proto__: this.__proto__,
get __id__(){ return id }
};
return id;
}
}.call() );
Object.prototype.toString = function () {
return '[Object ' + this.__id__ + ']';
};
the __proto__ bits are to keep the __id__ getter from showing up in the object. this has been only tested in firefox.
Notwithstanding the advice not to modify Object.prototype, this can still be really useful for testing, within a limited scope. The author of the accepted answer changed it, but is still setting Object.id, which doesn't make sense to me. Here's a snippet that does the job:
// Generates a unique, read-only id for an object.
// The _uid is generated for the object the first time it's accessed.
(function() {
var id = 0;
Object.defineProperty(Object.prototype, '_uid', {
// The prototype getter sets up a property on the instance. Because
// the new instance-prop masks this one, we know this will only ever
// be called at most once for any given object.
get: function () {
Object.defineProperty(this, '_uid', {
value: id++,
writable: false,
enumerable: false,
});
return this._uid;
},
enumerable: false,
});
})();
function assert(p) { if (!p) throw Error('Not!'); }
var obj = {};
assert(obj._uid == 0);
assert({}._uid == 1);
assert([]._uid == 2);
assert(obj._uid == 0); // still
This one will calculate a HashCode for each object, optimized for string, number and virtually anything that has a getHashCode function. For the rest it assigns a new reference number.
(function() {
var __gRefID = 0;
window.getHashCode = function(ref)
{
if (ref == null) { throw Error("Unable to calculate HashCode on a null reference"); }
// already cached reference id
if (ref.hasOwnProperty("__refID")) { return ref["__refID"]; }
// numbers are already hashcodes
if (typeof ref === "number") { return ref; }
// strings are immutable, so we need to calculate this every time
if (typeof ref === "string")
{
var hash = 0, i, chr;
for (i = 0; i < ref.length; i++) {
chr = ref.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
}
// virtual call
if (typeof ref.getHashCode === "function") { return ref.getHashCode(); }
// generate and return a new reference id
return (ref["__refID"] = "ref" + __gRefID++);
}
})();
If you came here because you deal with class instances like me you can use static vars/methods to reference instances by a custom unique id:
class Person {
constructor( name ) {
this.name = name;
this.id = Person.ix++;
Person.stack[ this.id ] = this;
}
}
Person.ix = 0;
Person.stack = {};
Person.byId = id => Person.stack[ id ];
let store = {};
store[ new Person( "joe" ).id ] = true;
store[ new Person( "tim" ).id ] = true;
for( let id in store ) {
console.log( Person.byId( id ).name );
}
Here's a variant of Justin Johnson's answer that provides a scalability benefit when you are creating billions of objects for which you want the ID.
Specifically, rather than solely using a 1-up counter (that might overflow the representational limits of Number, and can't be cycled without risking reusing an ID), we register the object and its newly generated ID with a FinalizationRegistry, such that, at some point after the object is garbage collected, the ID is returned to a freelist for reuse by a newly created object (Python's id function can also return the same ID for multiple objects, so long as the existence of the two objects does not overlap in time).
Limitations:
It only works on objects, not JS primitives (this is somewhat reasonable; unlike Python, where everything is an object, JS primitives typically aren't, and the id function logically only works on objects, since primitives need not "exist" in any reasonably identifiable way).
If the code creates (without discarding) billions of objects, asks for their IDs, then releases them all at once and never asks for an ID again, the recovered IDs in the freelist constitute a memory leak of sorts. Hopefully the JS optimizer stores them efficiently, so the cost remains a small fraction of what the objects themselves cost, but it's still a cost. In cases where objects with IDs are regularly created and destroyed, the wasted memory is roughly tied to the maximum number of such ID-ed objects in existence at any given point in time.
If those limitations aren't a problem though, this works fairly well. I modified the testing code a bit to hand control back to the event loop (and hopefully the garbage collector) now and again while creating 10M garbage objects to ID, and on my browser, nearly half the object IDs get reclaimed for reuse; the final loop making five objects and IDing them produces IDs just above 1M, when over 2M objects had IDs generated at some point. In a realistic scenario with meaningful code executing and real async usage I'd expect better results simply because there would be more opportunities for the finalization registry to perform cleanup.
async function sleep(ms) {
await _sleep(ms);
}
function _sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
(function() {
if ( typeof Object.id != "undefined" ) return;
var freelist = []; // Stores previously used IDs for reuse when an object with
// an ID is garbage collected, so creating and dropping billions
// of objects doesn't consume all available IDs
const registry = new FinalizationRegistry((freeid) => {
freelist.push(freeid);
});
var id = 0;
Object.id = function(o) {
if ( typeof o.__uniqueid != "undefined" ) {
return o.__uniqueid;
}
Object.defineProperty(o, "__uniqueid", {
value: freelist.length ? freelist.pop() : ++id,
enumerable: false,
// This could go either way, depending on your
// interpretation of what an "id" is
writable: false
});
registry.register(o, o.__uniqueid); // Sometime after o is collected, its ID
// will be reclaimed for use by a new object
return o.__uniqueid;
};
})();
var obj = { a: 1, b: 1 };
console.log(Object.id(obj));
console.log(Object.id([]));
console.log(Object.id({}));
console.log(Object.id(/./));
var idsum = 0; // So we do something real to prevent optimizing out code
// Make a ton of temporary objects with IDs, handing control back to the event loop
// every once in a while to (hopefully) see some IDs returned to the pool
for (var i = 0; i < 1000000; ++i) {
idsum += Object.id({c: i});
}
sleep(10).then(() => {
console.log(Object.id(function() { console.log("Hey"); }));
for (var i = 1000000; i < 2000000; ++i) {
idsum += Object.id({c: i});
}
console.log(Object.id(function() { console.log("There"); }));
sleep(10).then(() => {
for (var i = 0; i < 5; ++i) {
console.log(Object.id([i]));
}
console.log(idsum);
});
});
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
console.log(k);
}
}
// Logged keys are `a` and `b`

Categories