JavaScript: Adding a nested property to an object that may not exist - javascript

Suppose I have a json object in which I record the number of visitors to my site, grouped by browser / version.
let data = {
browsers: {
chrome: {
43 : 13,
44 : 11
},
firefox: {
27: 9
}
}
}
To increment a particular browser, I need to check if several keys exist, and if not, create them.
let uap = UAParser(request.headers['user-agent']);
if (typeof uap.browser !== 'undefined') {
if (typeof data.browsers === 'undefined')
data.browsers = {}
if (typeof data.browsers[uap.browser.name] === 'undefined')
data.browsers[uap.browser.name] = {}
if (typeof data.browsers[uap.browser.name][uap.browser.version] === 'undefined')
data.browsers[uap.browser.name][uap.browser.version] = 0
data.browsers[uap.browser.name][uap.browser.version] += 1;
}
The deeper my data structure the crazier things get.
It feels like there must be a neater way to do this in javascript. There's always a neater way. Can anyone enlighten me here?

This shorter code should do the trick:
if (uap.browser) { // typeof is pretty much redundant for object properties.
const name = uap.browsers.name; // Variables for readability.
const version = uap.browser.version;
// Default value if the property does not exist.
const browsers = data.browsers = data.browsers || {};
const browser = browsers[name] = browsers[name] || {};
browser[version] = browser[version] || 0;
// Finally, increment the value.
browser[version]++;
}
Note that you were using === where you should've been using = (in === {}).
Let's explain this line:
const browsers = data.browsers = data.browsers || {};
The last part: data.browsers = data.browsers || {} sets data.browsers to be itself if it exists. If it doesn't yet, it's set to be a new empty object.
Then, that whole value gets assigned to browsers, for ease of access.
Now, shorter code shouldn't be top priority, but in cases like this, you can make the code a lot more readable.

You can give up the if statements and do it like this:
uap.browser = uap.browser || {}
essentially it does the same as the if only much shorter

Here's a very clean and generic solution using Proxy() I have a second solution which is standard ECMAscript 5 if you don't need it so cleanly or need it less browser dependant.
var handler = {
get: function (target, property) {
if (property !== "toJSON" && target[property] === undefined) {
target[property] = new Proxy ({}, handler);
}
return target[property];
}
}
var jsonProxy = new Proxy ({}, handler);
jsonProxy.non.existing.property = 5;
jsonProxy.another.property.that.doesnt.exist = 2;
jsonProxy["you"]["can"]["also"]["use"]["strings"] = 20;
console.log (JSON.stringify (jsonProxy));
You can do the same thing with classes but with a more verbose syntax:
var DynamicJSON = function () {};
DynamicJSON.prototype.get = function (property) {
if (this[property] === undefined) {
this[property] = new DynamicJSON ();
}
return this[property];
};
DynamicJSON.prototype.set = function (property, value) {
this[property] = value;
};
var jsonClass = new DynamicJSON ();
jsonClass.get("non").get("existing").set("property", 5);
jsonClass.get("you").get("have").get("to").get("use").set("strings", 20);
console.log (JSON.stringify (jsonClass));

Related

Is there any way to intercept methods triggered with [] in js? [duplicate]

I can't seem to find the way to overload the [] operator in javascript. Anyone out there know?
I was thinking on the lines of ...
MyClass.operator.lookup(index)
{
return myArray[index];
}
or am I not looking at the right things.
You can do this with ES6 Proxy (available in all modern browsers)
var handler = {
get: function(target, name) {
return "Hello, " + name;
}
};
var proxy = new Proxy({}, handler);
console.log(proxy.world); // output: Hello, world
console.log(proxy[123]); // output: Hello, 123
Check details on MDN.
You can't overload operators in JavaScript.
It was proposed for ECMAScript 4 but rejected.
I don't think you'll see it anytime soon.
The simple answer is that JavaScript allows access to children of an Object via the square brackets.
So you could define your class:
MyClass = function(){
// Set some defaults that belong to the class via dot syntax or array syntax.
this.some_property = 'my value is a string';
this['another_property'] = 'i am also a string';
this[0] = 1;
};
You will then be able to access the members on any instances of your class with either syntax.
foo = new MyClass();
foo.some_property; // Returns 'my value is a string'
foo['some_property']; // Returns 'my value is a string'
foo.another_property; // Returns 'i am also a string'
foo['another_property']; // Also returns 'i am also a string'
foo.0; // Syntax Error
foo[0]; // Returns 1
foo['0']; // Returns 1
Use a proxy. It was mentioned elsewhere in the answers but I think that this is a better example:
var handler = {
get: function(target, name) {
if (name in target) {
return target[name];
}
if (name == 'length') {
return Infinity;
}
return name * name;
}
};
var p = new Proxy({}, handler);
p[4]; //returns 16, which is the square of 4.
We can proxy get | set methods directly. Inspired by this.
class Foo {
constructor(v) {
this.data = v
return new Proxy(this, {
get: (obj, key) => {
if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
return obj.data[key]
else
return obj[key]
},
set: (obj, key, value) => {
if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
return obj.data[key] = value
else
return obj[key] = value
}
})
}
}
var foo = new Foo([])
foo.data = [0, 0, 0]
foo[0] = 1
console.log(foo[0]) // 1
console.log(foo.data) // [1, 0, 0]
As brackets operator is actually property access operator, you can hook on it with getters and setters. For IE you will have to use Object.defineProperty() instead. Example:
var obj = {
get attr() { alert("Getter called!"); return 1; },
set attr(value) { alert("Setter called!"); return value; }
};
obj.attr = 123;
The same for IE8+:
Object.defineProperty("attr", {
get: function() { alert("Getter called!"); return 1; },
set: function(value) { alert("Setter called!"); return value; }
});
For IE5-7 there's onpropertychange event only, which works for DOM elements, but not for other objects.
The drawback of the method is you can only hook on requests to predefined set of properties, not on arbitrary property without any predefined name.
one sneaky way to do this is by extending the language itself.
step 1
define a custom indexing convention, let's call it, "[]".
var MyClass = function MyClass(n) {
this.myArray = Array.from(Array(n).keys()).map(a => 0);
};
Object.defineProperty(MyClass.prototype, "[]", {
value: function(index) {
return this.myArray[index];
}
});
...
var foo = new MyClass(1024);
console.log(foo["[]"](0));
step 2
define a new eval implementation. (don't do this this way, but it's a proof of concept).
var MyClass = function MyClass(length, defaultValue) {
this.myArray = Array.from(Array(length).keys()).map(a => defaultValue);
};
Object.defineProperty(MyClass.prototype, "[]", {
value: function(index) {
return this.myArray[index];
}
});
var foo = new MyClass(1024, 1337);
console.log(foo["[]"](0));
var mini_eval = function(program) {
var esprima = require("esprima");
var tokens = esprima.tokenize(program);
if (tokens.length == 4) {
var types = tokens.map(a => a.type);
var values = tokens.map(a => a.value);
if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
if (values[1] == '[' && values[3] == ']') {
var target = eval(values[0]);
var i = eval(values[2]);
// higher priority than []
if (target.hasOwnProperty('[]')) {
return target['[]'](i);
} else {
return target[i];
}
return eval(values[0])();
} else {
return undefined;
}
} else {
return undefined;
}
} else {
return undefined;
}
};
mini_eval("foo[33]");
the above won't work for more complex indexes but it can be with stronger parsing.
alternative:
instead of resorting to creating your own superset language, you can instead compile your notation to the existing language, then eval it. This reduces the parsing overhead to native after the first time you use it.
var compile = function(program) {
var esprima = require("esprima");
var tokens = esprima.tokenize(program);
if (tokens.length == 4) {
var types = tokens.map(a => a.type);
var values = tokens.map(a => a.value);
if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
if (values[1] == '[' && values[3] == ']') {
var target = values[0];
var i = values[2];
// higher priority than []
return `
(${target}['[]'])
? ${target}['[]'](${i})
: ${target}[${i}]`
} else {
return 'undefined';
}
} else {
return 'undefined';
}
} else {
return 'undefined';
}
};
var result = compile("foo[0]");
console.log(result);
console.log(eval(result));
You need to use Proxy as explained, but it can ultimately be integrated into a class constructor
return new Proxy(this, {
set: function( target, name, value ) {
...}};
with 'this'. Then the set and get (also deleteProperty) functions will fire. Although you get a Proxy object which seems different it for the most part works to ask the compare ( target.constructor === MyClass ) it's class type etc. [even though it's a function where target.constructor.name is the class name in text (just noting an example of things that work slightly different.)]
So you're hoping to do something like
var whatever = MyClassInstance[4];
?
If so, simple answer is that Javascript does not currently support operator overloading.
Have a look at Symbol.iterator. You can implement a user-defined ##iterator method to make any object iterable.
The well-known Symbol.iterator symbol specifies the default iterator for an object. Used by for...of.
Example:
class MyClass {
constructor () {
this._array = [data]
}
*[Symbol.iterator] () {
for (let i=0, n=this._array.length; i<n; i++) {
yield this._array[i]
}
}
}
const c = new MyClass()
for (const element of [...c]) {
// do something with element
}

There is a bug in this object extender using hasOwnProperty, I'm uncertain what that bug is or where this extender

The following code is supposed to extend foo with bar, the assignment was to find the "bug" in this snippet but for the life of my I can't seem to find the bug. Is there something I'm missing? Some major case where this code would break when extending objects with other objects?
var foo = {a:1,b:"2",c:[3]}, bar = {d:"3",e:4,f:5.0};
var extend = function (obj, extension) {
if (typeof obj === "object" && typeof extension === "object") {
for (var i in extension) {
if (extension.hasOwnProperty(i) && !obj.hasOwnProperty(i)) {
obj[i] = extension[i];
}
}
return obj;
}
}
var foo_bar = extend(foo,bar);
console.log(foo_bar); //this logs as expected
Maybe it's when you have repeated keys in both objects. Isn't the extending object able to override the extended object's properties?
var foo = {a:1,b:"2",c:[3]}, bar = {c:"3",d:4,e:5.0};
var foo_bar = extend(foo,bar); //will result in {a:1,b:"2",c:"3",d:4,e:5.0};

How to stringify inherited objects to JSON?

json2.js seems to ignore members of the parent object when using JSON.stringify(). Example:
require('./json2.js');
function WorldObject(type) {
this.position = 4;
}
function Actor(val) {
this.someVal = 50;
}
Actor.prototype = new WorldObject();
var a = new Actor(2);
console.log(a.position);
console.log(JSON.stringify(a));
The output is:
4
{"someVal":50}
I would expect this output:
4
{"position":0, "someVal":50}
Well that's just the way it is, JSON.stringify does not preserve any of the not-owned properties of the object. You can have a look at an interesting discussion about other drawbacks and possible workarounds here.
Also note that the author has not only documented the problems, but also written a library called HydrateJS that might help you.
The problem is a little bit deeper than it seems at the first sight. Even if a would really stringify to {"position":0, "someVal":50}, then parsing it later would create an object that has the desired properties, but is neither an instance of Actor, nor has it a prototype link to the WorldObject (after all, the parse method doesn't have this info, so it can't possibly restore it that way).
To preserve the prototype chain, clever tricks are necessary (like those used in HydrateJS). If this is not what you are aiming for, maybe you just need to "flatten" the object before stringifying it. To do that, you could e.g. iterate all the properties of the object, regardless of whether they are own or not and re-assign them (this will ensure they get defined on the object itself instead of just inherited from the prototype).
function flatten(obj) {
var result = Object.create(obj);
for(var key in result) {
result[key] = result[key];
}
return result;
}
The way the function is written it doesn't mutate the original object. So using
console.log(JSON.stringify(flatten(a)));
you'll get the output you want and a will stay the same.
Another option would be to define a toJSON method in the object prototype you want to serialize:
function Test(){}
Test.prototype = {
someProperty: "some value",
toJSON: function() {
var tmp = {};
for(var key in this) {
if(typeof this[key] !== 'function')
tmp[key] = this[key];
}
return tmp;
}
};
var t = new Test;
JSON.stringify(t); // returns "{"someProperty" : "some value"}"
This works since JSON.stringify searches for a toJSON method in the object it receives, before trying the native serialization.
Check this fiddle: http://jsfiddle.net/AEGYG/
You can flat-stringify the object using this function:
function flatStringify(x) {
for(var i in x) {
if(!x.hasOwnProperty(i)) {
// weird as it might seem, this actually does the trick! - adds parent property to self
x[i] = x[i];
}
}
return JSON.stringify(x);
}
Here is a recursive version of the snippet #TomasVana included in his answer, in case there is inheritance in multiple levels of your object tree:
var flatten = function(obj) {
if (obj === null) {
return null;
}
if (Array.isArray(obj)) {
var newObj = [];
for (var i = 0; i < obj.length; i++) {
if (typeof obj[i] === 'object') {
newObj.push(flatten(obj[i]));
}
else {
newObj.push(obj[i]);
}
}
return newObj;
}
var result = Object.create(obj);
for(var key in result) {
if (typeof result[key] === 'object') {
result[key] = flatten(result[key]);
}
else {
result[key] = result[key];
}
}
return result;
}
And it keeps arrays as arrays. Call it the same way:
console.log(JSON.stringify(flatten(visualDataViews)));
While the flatten approach in general works, the snippets in other answers posted so far don't work for properties that are not modifiable, for example if the prototype has been frozen. To handle this case, you would need to create a new object and assign the properties to this new object. Since you're just stringifying the resulting object, object identity and other JavaScript internals probably don't matter, so it's perfectly fine to return a new object. This approach is also arguably more readable than reassigning an object's properties to itself, since it doesn't look like a no-op:
function flatten(obj) {
var ret = {};
for (var i in obj) {
ret[i] = obj[i];
}
return ret;
}
JSON.stringify takes three options
JSON.stringify(value[, replacer[, space]])
So, make use of the replacer, which is a function, that is called recursively for every key-value-pair.
Next Problem, to get really everything, you need to follow the prototpes and you must use getOwnPropertyNames to get all property names (more than you can catch with keysor for…in):
var getAllPropertyNames = () => {
const seen = new WeakSet();
return (obj) => {
let props = [];
do {
if (seen.has(obj)) return [];
seen.add(obj);
Object.getOwnPropertyNames(obj).forEach((prop) => {
if (props.indexOf(prop) === -1) props.push(prop);
});
} while ((obj = Object.getPrototypeOf(obj)));
return props;
};
};
var flatten = () => {
const seen = new WeakSet();
const getPropertyNames = getAllPropertyNames();
return (key, value) => {
if (value !== null && typeof value === "object") {
if (seen.has(value)) return;
seen.add(value);
let result = {};
getPropertyNames(value).forEach((k) => (result[k] = value[k]));
return result;
}
return value;
};
};
Then flatten the object to JSON:
JSON.stringify(myValue, flatten());
Notes:
I had a case where value was null, but typeof value was "object"
Circular references must bee detected, so it needs seen

jQuery Plugin - Deep option modification

I am currently working a plugin with a settings variable that is fairly deep (3-4 levels in some places). Following the generally accepted jQuery Plugin pattern I have implemented a simple way for users to modify settings on the fly using the following notation:
$('#element').plugin('option', 'option_name', 'new_value');
Here is the code similar to what I am using now for the options method.
option: function (option, value) {
if (typeof (option) === 'string') {
if (value === undefined) return settings[option];
if(typeof(value) === 'object')
$.extend(true, settings[option], value);
else
settings[option] = value;
}
return this;
}
Now consider that I have a settings variable like so:
var settings = {
opt: false,
another: {
deep: true
}
};
If I want to change the deep settings I have to use the following notation:
$('#element').plugin('option', 'another', { deep: false });
However, since in practice my settings can be 3-4 levels deep I feel the following notation would be more useful:
$('#element').plugin('option', 'another.deep', false);
However I'm not sure how feasible this is, nor how to go about doing it. As a first attempt I tried to "traverse" to the option in question and set it, but if I set my traversing variable it doesn't set what it references in the original settings variable.
option: function (option, value) {
if (typeof (option) === 'string') {
if (value === undefined) return settings[option];
var levels = option.split('.'),
opt = settings[levels[0]];
for(var i = 1; i < levels.length; ++i)
opt = opt[levels[i]];
if(typeof(value) === 'object')
$.extend(true, opt, value);
else
opt = value;
}
return this;
}
To say that another way: By setting opt after traversing, the setting it actually refers to in the settings variable is unchanged after this code runs.
I apologize for the long question, any help is appreciated. Thanks!
EDIT
As a second attempt I can do it using eval() like so:
option: function (option, value) {
if (typeof (option) === 'string') {
var levels = option.split('.'),
last = levels[levels.length - 1];
levels.length -= 1;
if (value === undefined) return eval('settings.' + levels.join('.'))[last];
if(typeof(value) === 'object')
$.extend(true, eval('settings.' + levels.join('.'))[last], value);
else
eval('settings.' + levels.join('.'))[last] = value;
}
return this;
}
But I really would like to see if anyone can show me a way to not use eval. Since it is a user input string I would rather not run eval() on it because it could be anything. Or let me know if I am being paranoid, and it shouldn't cause a problem at all.
The issue you're running into here comes down to the difference between variables pointing to Objects and variables for other types like Strings. 2 variables can point to the same Object, but not to the same String:
var a = { foo: 'bar' };
var b = 'bar';
var a2 = a;
var b2 = b;
a2.foo = 'hello world';
b2 = 'hello world';
console.log(a.foo); // 'hello world'
console.log(b); // 'bar'
Your traversal code works great until the last iteration of the loop, at which point opt is a variable containing the same value as deep inside the object settings.opt.another. Instead, cut your loop short and use the last element of levels as a key, like
var settings = {
another: {
deep: true
}
};
var levels = 'another.deep'.split('.')
, opt = settings;
// leave the last element
var i = levels.length-1;
while(i--){
opt = opt[levels.shift()];
}
// save the last element in the array and use it as a key
var k = levels.shift();
opt[k] = 'foobar'; // settings.another.deep is also 'foobar'
At this stage opt is a pointer to the same Object as settings.another and k is a String with the value 'deep'
How about using eval rather than traversal?
var settings = {
opt: false,
another: {
deep: true,
}
};
var x = "settings.another";
eval(x).deep = false;
alert(settings.another.deep);
Would the built in jQuery $().extend() not be exactly what you need?
http://api.jquery.com/jQuery.extend/
*note the second method signature with the first agument of true performs a deep merge...

Do I have to initialize every level of an object in Javascript?

I'm not terribly good with Javascript so I'm wondering if there is a better way of doing this:
if (games[level] === undefined) {
games[level] = {};
games[level]['pending'] = {};
}
if (!games[level]['pending'].length) {
return game.create(level);
}
In PHP I can just test empty($games[$level]['pending']). Is there a better way of testing for this? Basically all I want to do is create the object if it does not exist.
if (games[level] === undefined) {
games[level] = game.create(level);
}
If there is no such level game create should be called to initialize all of the data needed. I don`t see any point of making it an object and then checking for "pending". It will be always empty, because you just created the object.
If your the second if returns something for games[level]['pending'].length you have a big problem with your code. You can`t create an empty object ( games[level]['pending'] = {} ) and find that it already has properties.
In addition:
games[level] = {};
// games[level]['pending'] = {}; - bad
games[level].pending = {}; // this way object properties should be used
you can make yourself a function to do that, pass it the games object a a string like "level.pending.id.something.something" and goes on and creates the objects.
function makeObj(obj, path) {
var parts = path.split("."), tmp = obj, name;
while (parts.length) {
name = parts.shift();
if (typeof tmp[name] === 'undefined') {
tmp[name] = {};
}
tmp = tmp[name];
}
}
var x = {};
makeObj(x, "this.is.a.test");
games[level] = games[level] || {pending: {}};
if (!games[level].pending.length) {
return game.create(level);
}

Categories

Resources