Is this javascript implementation of properties valid? - javascript

So I am fairly new to javascript and I have been looking into ways to create properties with getters and setters (similar to C# properties).
I came up with this function, but I am not entirely sure if I am missing something that could go wrong. Is this a valid way to create properties?
var property = function(instance, propname, acc)
{
acc = acc || {};
acc.get = acc.get || function(val){ return val; };
acc.set = acc.set || function(set){ return set; };
var val = acc.val === undefined ? {} : acc.set(acc.val);
instance[propname] = {};
instance[propname].get = function() { return acc.get(val); };
instance[propname].set = function(set) { val = acc.set(set); };
}
usage:
var obj = {};
property(obj, 'prop1');
obj.prop1.get(); // returns Object {}
obj.prop1.set(13);
obj.prop1.get(); // returns 13
property(obj, 'prop2',
{
val : 13,
set : function(val) { return Math.min(val, 10); }
});
obj.prop2.get(); // returns 10
obj.prop2.set(122);
obj.prop2.get(); // returns 10
obj.prop2.set(3);
obj.prop2.get(); // returns 3
property(obj, 'percent',
{
val : 100,
get : function(val) { return (val * 100).toFixed(2); },
set : function(val) { return (Math.min(Math.max(val, 0), 100) / 100.0); },
});
obj.percent.get(); //returns "100.00"
obj.percent.set(33.12543);
obj.percent.get(); //returns "33.13"
obj.percent.set(122.12);
obj.percent.get(); //returns "100.00"
obj.percent.set(-122.12);
obj.percent.get(); //returns "0.00"

Im still reading through but i can tell you this is wrong
var val = acc.set(acc.val) || {};
if acc.set is undefined it will Throw a error you need to check it is set
before you try and use it like this
var val = acc.set && acc.set(acc.val) || {};

Related

Why am I getting "TypeError: 'x' is read-only" error without problems with 'use strict' or 'constant'? [duplicate]

I'd like to store a JavaScript object in HTML5 localStorage, but my object is apparently being converted to a string.
I can store and retrieve primitive JavaScript types and arrays using localStorage, but objects don't seem to work. Should they?
Here's my code:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
console.log('typeof testObject: ' + typeof testObject);
console.log('testObject properties:');
for (var prop in testObject) {
console.log(' ' + prop + ': ' + testObject[prop]);
}
// Put the object into storage
localStorage.setItem('testObject', testObject);
// Retrieve the object from storage
var retrievedObject = localStorage.getItem('testObject');
console.log('typeof retrievedObject: ' + typeof retrievedObject);
console.log('Value of retrievedObject: ' + retrievedObject);
The console output is
typeof testObject: object
testObject properties:
one: 1
two: 2
three: 3
typeof retrievedObject: string
Value of retrievedObject: [object Object]
It looks to me like the setItem method is converting the input to a string before storing it.
I see this behavior in Safari, Chrome, and Firefox, so I assume it's my misunderstanding of the HTML5 Web Storage specification, not a browser-specific bug or limitation.
I've tried to make sense of the structured clone algorithm described in 2 Common infrastructure. I don't fully understand what it's saying, but maybe my problem has to do with my object's properties not being enumerable (???).
Is there an easy workaround?
Update: The W3C eventually changed their minds about the structured-clone specification, and decided to change the spec to match the implementations. See 12111 – spec for Storage object getItem(key) method does not match implementation behavior. So this question is no longer 100% valid, but the answers still may be of interest.
Looking at the Apple, Mozilla and Mozilla again documentation, the functionality seems to be limited to handle only string key/value pairs.
A workaround can be to stringify your object before storing it, and later parse it when you retrieve it:
var testObject = { 'one': 1, 'two': 2, 'three': 3 };
// Put the object into storage
localStorage.setItem('testObject', JSON.stringify(testObject));
// Retrieve the object from storage
var retrievedObject = localStorage.getItem('testObject');
console.log('retrievedObject: ', JSON.parse(retrievedObject));
A minor improvement on a variant:
Storage.prototype.setObject = function(key, value) {
this.setItem(key, JSON.stringify(value));
}
Storage.prototype.getObject = function(key) {
var value = this.getItem(key);
return value && JSON.parse(value);
}
Because of short-circuit evaluation, getObject() will immediately return null if key is not in Storage. It also will not throw a SyntaxError exception if value is "" (the empty string; JSON.parse() cannot handle that).
You might find it useful to extend the Storage object with these handy methods:
Storage.prototype.setObject = function(key, value) {
this.setItem(key, JSON.stringify(value));
}
Storage.prototype.getObject = function(key) {
return JSON.parse(this.getItem(key));
}
This way you get the functionality that you really wanted even though underneath the API only supports strings.
Creating a facade for the Storage object is an awesome solution. That way, you can implement your own get and set methods. For my API, I have created a facade for localStorage and then check if it is an object or not while setting and getting.
var data = {
set: function(key, value) {
if (!key || !value) {return;}
if (typeof value === "object") {
value = JSON.stringify(value);
}
localStorage.setItem(key, value);
},
get: function(key) {
var value = localStorage.getItem(key);
if (!value) {return;}
// assume it is an object that has been stringified
if (value[0] === "{") {
value = JSON.parse(value);
}
return value;
}
}
Stringify doesn't solve all problems
It seems that the answers here don't cover all types that are possible in JavaScript, so here are some short examples on how to deal with them correctly:
// Objects and Arrays:
var obj = {key: "value"};
localStorage.object = JSON.stringify(obj); // Will ignore private members
obj = JSON.parse(localStorage.object);
// Boolean:
var bool = false;
localStorage.bool = bool;
bool = (localStorage.bool === "true");
// Numbers:
var num = 42;
localStorage.num = num;
num = +localStorage.num; // Short for "num = parseFloat(localStorage.num);"
// Dates:
var date = Date.now();
localStorage.date = date;
date = new Date(parseInt(localStorage.date));
// Regular expressions:
var regex = /^No\.[\d]*$/i; // Usage example: "No.42".match(regex);
localStorage.regex = regex;
var components = localStorage.regex.match("^/(.*)/([a-z]*)$");
regex = new RegExp(components[1], components[2]);
// Functions (not recommended):
function func() {}
localStorage.func = func;
eval(localStorage.func); // Recreates the function with the name "func"
I do not recommend to store functions, because eval() is evil and can lead to issues regarding security, optimisation and debugging.
In general, eval() should never be used in JavaScript code.
Private members
The problem with using JSON.stringify() for storing objects is, that this function can not serialise private members.
This issue can be solved by overwriting the .toString() method (which is called implicitly when storing data in web storage):
// Object with private and public members:
function MyClass(privateContent, publicContent) {
var privateMember = privateContent || "defaultPrivateValue";
this.publicMember = publicContent || "defaultPublicValue";
this.toString = function() {
return '{"private": "' + privateMember + '", "public": "' + this.publicMember + '"}';
};
}
MyClass.fromString = function(serialisedString) {
var properties = JSON.parse(serialisedString || "{}");
return new MyClass(properties.private, properties.public);
};
// Storing:
var obj = new MyClass("invisible", "visible");
localStorage.object = obj;
// Loading:
obj = MyClass.fromString(localStorage.object);
Circular references
Another problem stringify can't deal with are circular references:
var obj = {};
obj["circular"] = obj;
localStorage.object = JSON.stringify(obj); // Fails
In this example, JSON.stringify() will throw a TypeError "Converting circular structure to JSON".
If storing circular references should be supported, the second parameter of JSON.stringify() might be used:
var obj = {id: 1, sub: {}};
obj.sub["circular"] = obj;
localStorage.object = JSON.stringify(obj, function(key, value) {
if(key == 'circular') {
return "$ref" + value.id + "$";
} else {
return value;
}
});
However, finding an efficient solution for storing circular references highly depends on the tasks that need to be solved, and restoring such data is not trivial either.
There are already some question on Stack Overflow dealing with this problem: Stringify (convert to JSON) a JavaScript object with circular reference
There is a great library that wraps many solutions so it even supports older browsers called jStorage
You can set an object
$.jStorage.set(key, value)
And retrieve it easily
value = $.jStorage.get(key)
value = $.jStorage.get(key, "default value")
I arrived at this post after hitting on another post that has been closed as a duplicate of this - titled 'how to store an array in localstorage?'. Which is fine except neither thread actually provides a full answer as to how you can maintain an array in localStorage - however I have managed to craft a solution based on information contained in both threads.
So if anyone else is wanting to be able to push/pop/shift items within an array, and they want that array stored in localStorage or indeed sessionStorage, here you go:
Storage.prototype.getArray = function(arrayName) {
var thisArray = [];
var fetchArrayObject = this.getItem(arrayName);
if (typeof fetchArrayObject !== 'undefined') {
if (fetchArrayObject !== null) { thisArray = JSON.parse(fetchArrayObject); }
}
return thisArray;
}
Storage.prototype.pushArrayItem = function(arrayName,arrayItem) {
var existingArray = this.getArray(arrayName);
existingArray.push(arrayItem);
this.setItem(arrayName,JSON.stringify(existingArray));
}
Storage.prototype.popArrayItem = function(arrayName) {
var arrayItem = {};
var existingArray = this.getArray(arrayName);
if (existingArray.length > 0) {
arrayItem = existingArray.pop();
this.setItem(arrayName,JSON.stringify(existingArray));
}
return arrayItem;
}
Storage.prototype.shiftArrayItem = function(arrayName) {
var arrayItem = {};
var existingArray = this.getArray(arrayName);
if (existingArray.length > 0) {
arrayItem = existingArray.shift();
this.setItem(arrayName,JSON.stringify(existingArray));
}
return arrayItem;
}
Storage.prototype.unshiftArrayItem = function(arrayName,arrayItem) {
var existingArray = this.getArray(arrayName);
existingArray.unshift(arrayItem);
this.setItem(arrayName,JSON.stringify(existingArray));
}
Storage.prototype.deleteArray = function(arrayName) {
this.removeItem(arrayName);
}
example usage - storing simple strings in localStorage array:
localStorage.pushArrayItem('myArray','item one');
localStorage.pushArrayItem('myArray','item two');
example usage - storing objects in sessionStorage array:
var item1 = {}; item1.name = 'fred'; item1.age = 48;
sessionStorage.pushArrayItem('myArray',item1);
var item2 = {}; item2.name = 'dave'; item2.age = 22;
sessionStorage.pushArrayItem('myArray',item2);
common methods to manipulate arrays:
.pushArrayItem(arrayName,arrayItem); -> adds an element onto end of named array
.unshiftArrayItem(arrayName,arrayItem); -> adds an element onto front of named array
.popArrayItem(arrayName); -> removes & returns last array element
.shiftArrayItem(arrayName); -> removes & returns first array element
.getArray(arrayName); -> returns entire array
.deleteArray(arrayName); -> removes entire array from storage
In theory, it is possible to store objects with functions:
function store (a)
{
var c = {f: {}, d: {}};
for (var k in a)
{
if (a.hasOwnProperty(k) && typeof a[k] === 'function')
{
c.f[k] = encodeURIComponent(a[k]);
}
}
c.d = a;
var data = JSON.stringify(c);
window.localStorage.setItem('CODE', data);
}
function restore ()
{
var data = window.localStorage.getItem('CODE');
data = JSON.parse(data);
var b = data.d;
for (var k in data.f)
{
if (data.f.hasOwnProperty(k))
{
b[k] = eval("(" + decodeURIComponent(data.f[k]) + ")");
}
}
return b;
}
However, function serialization/deserialization is unreliable because it is implementation-dependent.
You cannot store a key value without a string format.
LocalStorage only supports string formats for keys/values.
That is why you should convert your data to string whatever it is an array or object.
To store data in localStorage, first of all stringify it using the JSON.stringify() method.
var myObj = [{name:"test", time:"Date 2017-02-03T08:38:04.449Z"}];
localStorage.setItem('item', JSON.stringify(myObj));
Then when you want to retrieve data, you need to parse the string to object again.
var getObj = JSON.parse(localStorage.getItem('item'));
It is recommended using an abstraction library for many of the features discussed here, as well as better compatibility. There are lots of options:
jStorage or simpleStorage ← my preference
localForage
alekseykulikov/storage
Lawnchair
Store.js ← another good option
OMG
localDataStorage
You can use localDataStorage to transparently store JavaScript data types (Array, BigInt, Boolean, Date, Float, Integer, String and Object). It also provides lightweight data obfuscation, automatically compresses strings, facilitates query by key (name) as well as query by (key) value, and helps to enforce segmented shared storage within the same domain by prefixing keys.
[DISCLAIMER] I am the author of the utility [/DISCLAIMER]
Examples:
localDataStorage.set( 'key1', 'Belgian' )
localDataStorage.set( 'key2', 1200.0047 )
localDataStorage.set( 'key3', true )
localDataStorage.set( 'key4', { 'RSK' : [1,'3',5,'7',9] } )
localDataStorage.set( 'key5', null )
localDataStorage.get( 'key1' ) // --> 'Belgian'
localDataStorage.get( 'key2' ) // --> 1200.0047
localDataStorage.get( 'key3' ) // --> true
localDataStorage.get( 'key4' ) // --> Object {RSK: Array(5)}
localDataStorage.get( 'key5' ) // --> null
As you can see, the primitive values are respected.
You can use ejson to store the objects as strings.
EJSON is an extension of JSON to support more types. It supports all JSON-safe types, as well as:
Date (JavaScript Date)
Binary (JavaScript Uint8Array or the result of EJSON.newBinary)
User-defined types (see EJSON.addType. For example, Mongo.ObjectID is implemented this way.)
All EJSON serializations are also valid JSON. For example an object with a date and a binary buffer would be serialized in EJSON as:
{
"d": {"$date": 1358205756553},
"b": {"$binary": "c3VyZS4="}
}
Here is my localStorage wrapper using ejson
https://github.com/UziTech/storage.js
I added some types to my wrapper including regular expressions and functions
Another option would be to use an existing plugin.
For example persisto is an open source project that provides an easy interface to localStorage/sessionStorage and automates persistence for form fields (input, radio buttons, and checkboxes).
(Disclaimer: I am the author.)
For TypeScript users willing to set and get typed properties:
/**
* Silly wrapper to be able to type the storage keys
*/
export class TypedStorage<T> {
public removeItem(key: keyof T): void {
localStorage.removeItem(key);
}
public getItem<K extends keyof T>(key: K): T[K] | null {
const data: string | null = localStorage.getItem(key);
return JSON.parse(data);
}
public setItem<K extends keyof T>(key: K, value: T[K]): void {
const data: string = JSON.stringify(value);
localStorage.setItem(key, data);
}
}
Example usage:
// write an interface for the storage
interface MyStore {
age: number,
name: string,
address: {city:string}
}
const storage: TypedStorage<MyStore> = new TypedStorage<MyStore>();
storage.setItem("wrong key", ""); // error unknown key
storage.setItem("age", "hello"); // error, age should be number
storage.setItem("address", {city:"Here"}); // ok
const address: {city:string} = storage.getItem("address");
https://github.com/adrianmay/rhaboo is a localStorage sugar layer that lets you write things like this:
var store = Rhaboo.persistent('Some name');
store.write('count', store.count ? store.count+1 : 1);
store.write('somethingfancy', {
one: ['man', 'went'],
2: 'mow',
went: [ 2, { mow: ['a', 'meadow' ] }, {} ]
});
store.somethingfancy.went[1].mow.write(1, 'lawn');
It doesn't use JSON.stringify/parse because that would be inaccurate and slow on big objects. Instead, each terminal value has its own localStorage entry.
You can probably guess that I might have something to do with rhaboo.
localStorage.setItem('obj',JSON.stringify({name:'Akash'})); // Set Object in localStorage
localStorage.getItem('obj'); // Get Object from localStorage
sessionStorage.setItem('obj',JSON.stringify({name:'Akash'})); // Set Object in sessionStorage
sessionStorage.getItem('obj'); // Get Object from sessionStorage
I made another minimalistic wrapper with only 20 lines of code to allow using it like it should:
localStorage.set('myKey',{a:[1,2,5], b: 'ok'});
localStorage.has('myKey'); // --> true
localStorage.get('myKey'); // --> {a:[1,2,5], b: 'ok'}
localStorage.keys(); // --> ['myKey']
localStorage.remove('myKey');
https://github.com/zevero/simpleWebstorage
I made a thing that doesn't break the existing Storage objects, but creates a wrapper so you can do what you want. The result is a normal object, no methods, with access like any object.
The thing I made.
If you want 1 localStorage property to be magic:
var prop = ObjectStorage(localStorage, 'prop');
If you need several:
var storage = ObjectStorage(localStorage, ['prop', 'more', 'props']);
Everything you do to prop, or the objects inside storage will be automatically saved into localStorage. You're always playing with a real object, so you can do stuff like this:
storage.data.list.push('more data');
storage.another.list.splice(1, 2, {another: 'object'});
And every new object inside a tracked object will be automatically tracked.
The very big downside: it depends on Object.observe() so it has very limited browser support. And it doesn't look like it'll be coming for Firefox or Edge anytime soon.
I found a way to make it work with objects that have cyclic references.
Let's make an object with cyclic references.
obj = {
L: {
L: { v: 'lorem' },
R: { v: 'ipsum' }
},
R: {
L: { v: 'dolor' },
R: {
L: { v: 'sit' },
R: { v: 'amet' }
}
}
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;
We can't do JSON.stringify here, because of the circular references.
LOCALSTORAGE.CYCLICJSON has .stringify and .parse just like normal JSON, but works with objects with circular references. ("Works" meaning parse(stringify(obj)) and obj are deep equal AND have identical sets of 'inner equalities')
But we can just use the shortcuts:
LOCALSTORAGE.setObject('latinUncles', obj)
recovered = LOCALSTORAGE.getObject('latinUncles')
Then, recovered will be "the same" to obj, in the following sense:
[
obj.L.L.v === recovered.L.L.v,
obj.L.R.v === recovered.L.R.v,
obj.R.L.v === recovered.R.L.v,
obj.R.R.L.v === recovered.R.R.L.v,
obj.R.R.R.v === recovered.R.R.R.v,
obj.R.L.uncle === obj.L,
obj.R.R.uncle === obj.L,
obj.R.R.L.uncle === obj.R.L,
obj.R.R.R.uncle === obj.R.L,
obj.L.L.uncle === obj.R,
obj.L.R.uncle === obj.R,
recovered.R.L.uncle === recovered.L,
recovered.R.R.uncle === recovered.L,
recovered.R.R.L.uncle === recovered.R.L,
recovered.R.R.R.uncle === recovered.R.L,
recovered.L.L.uncle === recovered.R,
recovered.L.R.uncle === recovered.R
]
Here is the implementation of LOCALSTORAGE
LOCALSTORAGE = (function(){
"use strict";
var ignore = [Boolean, Date, Number, RegExp, String];
function primitive(item){
if (typeof item === 'object'){
if (item === null) { return true; }
for (var i=0; i<ignore.length; i++){
if (item instanceof ignore[i]) { return true; }
}
return false;
} else {
return true;
}
}
function infant(value){
return Array.isArray(value) ? [] : {};
}
function decycleIntoForest(object, replacer) {
if (typeof replacer !== 'function'){
replacer = function(x){ return x; }
}
object = replacer(object);
if (primitive(object)) return object;
var objects = [object];
var forest = [infant(object)];
var bucket = new WeakMap(); // bucket = inverse of objects
bucket.set(object, 0);
function addToBucket(obj){
var result = objects.length;
objects.push(obj);
bucket.set(obj, result);
return result;
}
function isInBucket(obj){ return bucket.has(obj); }
function processNode(source, target){
Object.keys(source).forEach(function(key){
var value = replacer(source[key]);
if (primitive(value)){
target[key] = {value: value};
} else {
var ptr;
if (isInBucket(value)){
ptr = bucket.get(value);
} else {
ptr = addToBucket(value);
var newTree = infant(value);
forest.push(newTree);
processNode(value, newTree);
}
target[key] = {pointer: ptr};
}
});
}
processNode(object, forest[0]);
return forest;
};
function deForestIntoCycle(forest) {
var objects = [];
var objectRequested = [];
var todo = [];
function processTree(idx) {
if (idx in objects) return objects[idx];
if (objectRequested[idx]) return null;
objectRequested[idx] = true;
var tree = forest[idx];
var node = Array.isArray(tree) ? [] : {};
for (var key in tree) {
var o = tree[key];
if ('pointer' in o) {
var ptr = o.pointer;
var value = processTree(ptr);
if (value === null) {
todo.push({
node: node,
key: key,
idx: ptr
});
} else {
node[key] = value;
}
} else {
if ('value' in o) {
node[key] = o.value;
} else {
throw new Error('unexpected')
}
}
}
objects[idx] = node;
return node;
}
var result = processTree(0);
for (var i = 0; i < todo.length; i++) {
var item = todo[i];
item.node[item.key] = objects[item.idx];
}
return result;
};
var console = {
log: function(x){
var the = document.getElementById('the');
the.textContent = the.textContent + '\n' + x;
},
delimiter: function(){
var the = document.getElementById('the');
the.textContent = the.textContent +
'\n*******************************************';
}
}
function logCyclicObjectToConsole(root) {
var cycleFree = decycleIntoForest(root);
var shown = cycleFree.map(function(tree, idx) {
return false;
});
var indentIncrement = 4;
function showItem(nodeSlot, indent, label) {
var leadingSpaces = ' '.repeat(indent);
var leadingSpacesPlus = ' '.repeat(indent + indentIncrement);
if (shown[nodeSlot]) {
console.log(leadingSpaces + label + ' ... see above (object #' + nodeSlot + ')');
} else {
console.log(leadingSpaces + label + ' object#' + nodeSlot);
var tree = cycleFree[nodeSlot];
shown[nodeSlot] = true;
Object.keys(tree).forEach(function(key) {
var entry = tree[key];
if ('value' in entry) {
console.log(leadingSpacesPlus + key + ": " + entry.value);
} else {
if ('pointer' in entry) {
showItem(entry.pointer, indent + indentIncrement, key);
}
}
});
}
}
console.delimiter();
showItem(0, 0, 'root');
};
function stringify(obj){
return JSON.stringify(decycleIntoForest(obj));
}
function parse(str){
return deForestIntoCycle(JSON.parse(str));
}
var CYCLICJSON = {
decycleIntoForest: decycleIntoForest,
deForestIntoCycle : deForestIntoCycle,
logCyclicObjectToConsole: logCyclicObjectToConsole,
stringify : stringify,
parse : parse
}
function setObject(name, object){
var str = stringify(object);
localStorage.setItem(name, str);
}
function getObject(name){
var str = localStorage.getItem(name);
if (str===null) return null;
return parse(str);
}
return {
CYCLICJSON : CYCLICJSON,
setObject : setObject,
getObject : getObject
}
})();
obj = {
L: {
L: { v: 'lorem' },
R: { v: 'ipsum' }
},
R: {
L: { v: 'dolor' },
R: {
L: { v: 'sit' },
R: { v: 'amet' }
}
}
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;
// LOCALSTORAGE.setObject('latinUncles', obj)
// recovered = LOCALSTORAGE.getObject('latinUncles')
// localStorage not available inside fiddle ):
LOCALSTORAGE.CYCLICJSON.logCyclicObjectToConsole(obj)
putIntoLS = LOCALSTORAGE.CYCLICJSON.stringify(obj);
recovered = LOCALSTORAGE.CYCLICJSON.parse(putIntoLS);
LOCALSTORAGE.CYCLICJSON.logCyclicObjectToConsole(recovered);
var the = document.getElementById('the');
the.textContent = the.textContent + '\n\n' +
JSON.stringify(
[
obj.L.L.v === recovered.L.L.v,
obj.L.R.v === recovered.L.R.v,
obj.R.L.v === recovered.R.L.v,
obj.R.R.L.v === recovered.R.R.L.v,
obj.R.R.R.v === recovered.R.R.R.v,
obj.R.L.uncle === obj.L,
obj.R.R.uncle === obj.L,
obj.R.R.L.uncle === obj.R.L,
obj.R.R.R.uncle === obj.R.L,
obj.L.L.uncle === obj.R,
obj.L.R.uncle === obj.R,
recovered.R.L.uncle === recovered.L,
recovered.R.R.uncle === recovered.L,
recovered.R.R.L.uncle === recovered.R.L,
recovered.R.R.R.uncle === recovered.R.L,
recovered.L.L.uncle === recovered.R,
recovered.L.R.uncle === recovered.R
]
)
<pre id='the'></pre>
This question has been answered sufficiently from the JavaScript-only perspective, and others have already noted that both localStorage.getItem and localStorage.setItem have no concept of objects—they handle strings and strings only. This answer provides a TypeScript-friendly solution that incorporates what others have suggested in JavaScript-only solutions.
TypeScript 4.2.3
Storage.prototype.setObject = function (key: string, value: unknown) {
this.setItem(key, JSON.stringify(value));
};
Storage.prototype.getObject = function (key: string) {
const value = this.getItem(key);
if (!value) {
return null;
}
return JSON.parse(value);
};
declare global {
interface Storage {
setObject: (key: string, value: unknown) => void;
getObject: (key: string) => unknown;
}
}
Usage
localStorage.setObject('ages', [23, 18, 33, 22, 58]);
localStorage.getObject('ages');
Explanation
We declare both setObject and getObject functions on the Storage prototype—localStorage is an instance of this type. There's nothing special we really need to note besides the null handling in getObject. Since getItem can return null, we must exit early since calling JSON.parse on a null value will throw a runtime exception.
After declaring the functions on the Storage prototype, we include their type definitions on the Storage type in the global namespace.
Note: If we defined these functions with arrow functions, we'd need to assume that the storage object we're calling is always localStorage, which might not be true. For instance, the above code will add setObject and getObject support to sessionStorage as well.
Here is some extended version of the code posted by danott:
It'll also implement a delete value from localstorage and shows how to adds a Getter and Setter layer so instead of,
localstorage.setItem(preview, true)
you can write
config.preview = true
Okay, here were go:
var PT=Storage.prototype
if (typeof PT._setItem >='u')
PT._setItem = PT.setItem;
PT.setItem = function(key, value)
{
if (typeof value >='u') //..undefined
this.removeItem(key)
else
this._setItem(key, JSON.stringify(value));
}
if (typeof PT._getItem >='u')
PT._getItem = PT.getItem;
PT.getItem = function(key)
{
var ItemData = this._getItem(key)
try
{
return JSON.parse(ItemData);
}
catch(e)
{
return ItemData;
}
}
// Aliases for localStorage.set/getItem
get = localStorage.getItem.bind(localStorage)
set = localStorage.setItem.bind(localStorage)
// Create ConfigWrapperObject
var config = {}
// Helper to create getter & setter
function configCreate(PropToAdd){
Object.defineProperty( config, PropToAdd, {
get: function () { return (get(PropToAdd) )},
set: function (val) { set(PropToAdd, val)}
})
}
//------------------------------
// Usage Part
// Create properties
configCreate('preview')
configCreate('notification')
//...
// Configuration Data transfer
// Set
config.preview = true
// Get
config.preview
// Delete
config.preview = undefined
Well, you may strip the aliases part with .bind(...). However, I just put it in since it's really good to know about this. I took me hours to find out why a simple get = localStorage.getItem; don't work.
I suggest using Jackson-js. It is a library that handles serializing and deserializing of Objects while retaining their structure, based on decorators.
The library handles all the pitfalls such as cyclic reference, attributes aliasing, etc.
Simply describe your class using the #JsonProperty() and #JsonClassType() decorators.
Serialize your object using:
const objectMapper = new ObjectMapper();
localstore.setItem(key, objectMapper.stringify<yourObjectType>(yourObject));
For slightly more detailed explanation, check my answer here:
Typescript objects serialization?
And the Jackson-js tutorial here:
Jackson-js: Powerful JavaScript decorators to serialize/deserialize objects into JSON and vice versa (Part 1)
Circular References
In this answer I focus on data-only objects (without functions, etc.) with circular references and develop ideas mentioned by maja and mathheadinclouds (I use his test case and
my code is several times shorter).
Actually, we can use JSON.stringify with a proper replacer - if the source object contains multi-references to some object, or contains circular references then we reference it by special path-string (similar to JSONPath).
// JSON.strigify replacer for objects with circ ref
function refReplacer() {
let m = new Map(), v = new Map(), init = null;
return function(field, value) {
let p = m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field);
let isComplex = value === Object(value)
if (isComplex) m.set(value, p);
let pp = v.get(value)||'';
let path = p.replace(/undefined\.\.?/, '');
let val = pp ? `#REF:${pp[0] == '[' ? '$':'$.'}${pp}` : value;
!init ? (init=value) : (val===init ? val="#REF:$" : 0);
if(!pp && isComplex) v.set(value, path);
return val;
}
}
// ---------------
// TEST
// ---------------
// Generate obj with duplicate/circular references
let obj = {
L: {
L: { v: 'lorem' },
R: { v: 'ipsum' }
},
R: {
L: { v: 'dolor' },
R: {
L: { v: 'sit' },
R: { v: 'amet' }
}
}
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;
testObject = obj;
let json = JSON.stringify(testObject, refReplacer(), 4);
console.log("Test Object\n", testObject);
console.log("JSON with JSONpath references\n", json);
Parse such JSON content with JSONpath-like references:
// Parse JSON content with JSONpath references to object
function parseRefJSON(json) {
let objToPath = new Map();
let pathToObj = new Map();
let o = JSON.parse(json);
let traverse = (parent, field) => {
let obj = parent;
let path = '#REF:$';
if (field !== undefined) {
obj = parent[field];
path = objToPath.get(parent) + (Array.isArray(parent) ? `[${field}]` : `${field ? '.' + field : ''}`);
}
objToPath.set(obj, path);
pathToObj.set(path, obj);
let ref = pathToObj.get(obj);
if (ref) parent[field] = ref;
for (let f in obj) if (obj === Object(obj)) traverse(obj, f);
}
traverse(o);
return o;
}
// ---------------
// TEST 1
// ---------------
let json = `
{
"L": {
"L": {
"v": "lorem",
"uncle": {
"L": {
"v": "dolor",
"uncle": "#REF:$.L"
},
"R": {
"L": {
"v": "sit",
"uncle": "#REF:$.L.L.uncle.L"
},
"R": {
"v": "amet",
"uncle": "#REF:$.L.L.uncle.L"
},
"uncle": "#REF:$.L"
}
}
},
"R": {
"v": "ipsum",
"uncle": "#REF:$.L.L.uncle"
}
},
"R": "#REF:$.L.L.uncle"
}`;
let testObject = parseRefJSON(json);
console.log("Test Object\n", testObject);
// ---------------
// TEST 2
// ---------------
console.log('Tests from mathheadinclouds answer: ');
let recovered = testObject;
let obj = { // Original object
L: {
L: { v: 'lorem' },
R: { v: 'ipsum' }
},
R: {
L: { v: 'dolor' },
R: {
L: { v: 'sit' },
R: { v: 'amet' }
}
}
}
obj.R.L.uncle = obj.L;
obj.R.R.uncle = obj.L;
obj.R.R.L.uncle = obj.R.L;
obj.R.R.R.uncle = obj.R.L;
obj.L.L.uncle = obj.R;
obj.L.R.uncle = obj.R;
[
obj.L.L.v === recovered.L.L.v,
obj.L.R.v === recovered.L.R.v,
obj.R.L.v === recovered.R.L.v,
obj.R.R.L.v === recovered.R.R.L.v,
obj.R.R.R.v === recovered.R.R.R.v,
obj.R.L.uncle === obj.L,
obj.R.R.uncle === obj.L,
obj.R.R.L.uncle === obj.R.L,
obj.R.R.R.uncle === obj.R.L,
obj.L.L.uncle === obj.R,
obj.L.R.uncle === obj.R,
recovered.R.L.uncle === recovered.L,
recovered.R.R.uncle === recovered.L,
recovered.R.R.L.uncle === recovered.R.L,
recovered.R.R.R.uncle === recovered.R.L,
recovered.L.L.uncle === recovered.R,
recovered.L.R.uncle === recovered.R
].forEach(x => console.log('test pass: ' + x));
To load/save the resulting JSON content into storage, use the following code:
localStorage.myObject = JSON.stringify(testObject, refReplacer()); // Save
testObject = parseRefJSON(localStorage.myObject); // Load
localStorage.setItem('user', JSON.stringify(user));
Then to retrieve it from the store and convert to an object again:
var user = JSON.parse(localStorage.getItem('user'));
If we need to delete all entries of the store we can simply do:
localStorage.clear();

Checking existence of nested property in an object javascript

I have already reviewed some of the answers to similar questions, however, I want to ask my question differently.
Let's say we have a string like "level1.level2.level3. ..." that indicates a nested property in an object called Obj.
The point is that we may not know how many nested properties exist in this string. For instance, it may be "level1.level2" or "level1.level2.level3.level4".
Now, I want to write a function, that given the Obj and the string of properties as input, to simply tell us if such a nested property exists in the object or not (let's say true or false as output).
Update:
Thanks to #Silvinus, I found the solution with a minor modification:
private checkNestedProperty(obj, props) {
var splitted = props.split('.');
var temp = obj;
for (var index in splitted) {
if (temp[splitted[index]] === 'undefined' || !temp[splitted[index]]) return false;
temp = temp[splitted[index]];
}
return true;
}
You could use Array#every() and thisArg of it, by iterating the keys and checking if it is in the given object.
var fn = function (o, props) {
return props.split('.').every(k => k in o && (o = o[k], true));
}
console.log(fn({}, "toto.tata")); // false
console.log(fn({ toto: { tata: 17 } }, "toto.tata")); // true
console.log(fn({ toto: { tata: { tutu: 17 } } }, "toto.foo.tata")); // false
console.log(fn({ toto: { tata: false } }, "toto.tata")); // true
You can explore your Obj with this function :
var fn = function(obj, props) {
var splited = props.split('.');
var temp = obj;
for(var index in splited) {
if(typeof temp[splited[index]] === 'undefined') return false;
temp = temp[splited[index]]
}
return true
}
var result = fn({ }, "toto.tata");
console.log(result); // false
var result = fn({ toto: { tata: 17 } }, "toto.tata");
console.log(result); // true
var result = fn({ toto: { tata: { tutu: 17 } } }, "toto.foo.tata");
console.log(result); // false
This function allow to explore nested property of Obj that depends of props passed in parameter
This answer provides the basic answer to your question. But it needs to be tweaked to handle the undefined case:
function isDefined(obj, path) {
function index(obj, i) {
return obj && typeof obj === 'object' ? obj[i] : undefined;
}
return path.split(".").reduce(index, obj) !== undefined;
}
Based on the solution given by #Silvinus here is a solution if you deal with array inside nested objects (as it is often the case in results from databases queries) :
checkNested = function(obj, props) {
var splited = props.split('.');
var temp = obj;
for(var index in splited) {
var regExp = /\[([^)]+)\]/;
var matches = regExp.exec(splited[index])
if(matches) {
splited[index] = splited[index].replace(matches[0], '');
}
if(matches) {
if(matches && typeof temp[splited[index]][matches[1]] === 'undefined') return false;
temp = temp[splited[index]][matches[1]];
}
else {
if(!matches && typeof temp[splited[index]] === 'undefined') return false;
temp = temp[splited[index]]
}
}
return true
}
obj = {ok: {ao: [{},{ok: { aa: ''}}]}}
console.log(checkNested(obj, 'ok.ao[1].ok.aa')) // ==> true
console.log(checkNested(obj, 'ok.ao[0].ok.aa')) // ==> false

Why isn't my implementation of a Where function working?

I'm trying to implement a Where clause. My attempt
Object.prototype.Where = function ( boofunc ) {
// Returns an object whose own properties are
// those properties p of this object that satisify
// the condition boofunc(p)
var that = {};
for ( var prop in this )
{
var val = this[prop];
if ( boofunc(val) )
{
that.prop = val;
}
}
return that;
}
var obj = { x : 10, y : 11, z : 12 };
var evens = obj.Where(function(prop){obj.prop%2==0});
console.log(evens); // TEST
is not working (the object printed to the console doesn't have x, y or z). Or is there a better way of getting a filtered version of an existing object?
Try this:
Object.prototype.Where = function ( boofunc ) {
// Returns an object whose own properties are
// those properties p of this object that satisify
// the condition boofunc(p)
var that = {};
for ( var prop in this )
{
var val = this[prop];
if ( boofunc(val) )
{
that[prop] = val;
}
}
return that;
}
var obj = { x : 10, y : 11, z : 12 };
var evens = obj.Where(function(prop){ return prop % 2==0; });
console.log(evens); // TEST
Basically, you need to return the value from your boofunc, instead of just checking prop % == 0 you have to actually return its result.
Next, you had a couple of typos, such as obj.prop where obj doesn't exist and also setting the property like that[prop] = val; instead of that.prop = val;
Working fiddle: https://jsfiddle.net/t32jywje/1/

Access JavaScript property case-insensitively?

Assume I have an object:
var obj = {
foo:"bar",
fizz:"buzz"
};
I need to access a property of that object dynamically like so:
var objSetter = function(prop,val){
obj[prop] = val;
}
No problems there, except for that prop needs to be case insensitive in case the property name is passed into the function as, say, Foo instead of foo.
So how can I point to an object's property by name without regard to case? I would like to avoid iterating the entire object if possible.
Try this:
var myObject = { "mIxeDCaSEKeY": "value" };
var searchKey = 'mixedCaseKey';
var asLowercase = searchKey.toLowerCase();
myObject[Object.keys(myObject).find(key => key.toLowerCase() === asLowercase)];
You can alternatively already provide the searchKey in lowercase.
If you want it as a function:
/**
* #param {Object} object
* #param {string} key
* #return {any} value
*/
function getParameterCaseInsensitive(object, key) {
const asLowercase = key.toLowerCase();
return object[Object.keys(object)
.find(k => k.toLowerCase() === asLowercase)
];
}
If the key can't be found, then it'll return undefined, just like normal.
If you need to support older browsers, then you can use filter instead:
function getParameterCaseInsensitive(object, key) {
const asLowercase = key.toLowercase();
return object[Object.keys(object).filter(function(k) {
return k.toLowerCase() === asLowercase;
})[0]];
}
I suggest using the polyfills for Object.keys() and Array.filter() if you need even older support.
Note: If you want to also check non-enumerable keys, use Object.getOwnPropertyNames() instead of Object.keys().
Nerdy Note: This assumes your Object doesn't have a key undefined (eg: const foo = {[undefined]: 'bar'};). That's just weird.
Compare all the properties of obj with prop.
var objSetter = function(prop,val){
prop = (prop + "").toLowerCase();
for(var p in obj){
if(obj.hasOwnProperty(p) && prop == (p+ "").toLowerCase()){
obj[p] = val;
break;
}
}
}
For this, I prefer using the prototype over a standalone function just for ease of use and expressiveness. I just don't like funneling objects into functions if I don't have to.
Also, while the accepted answer works, I wanted a more comprehensive solution for both getting and setting that would behave as much like the native dot notation or bracket notation as possible.
With that in mind, I created a couple prototype functions for setting/getting an object property without regard to case. You have to remember to be VERY responsible when adding to the Object prototype. Especially when using JQuery and other libraries. Object.defineProperty() with enumerable set to false was used specifically to avoid conflict with JQuery. I also didn't bother naming the functions anything that indicates they are case-insensitive, but you certainly could. I like shorter names.
Here's the getter:
Object.defineProperty(Object.prototype, "getProp", {
value: function (prop) {
var key,self = this;
for (key in self) {
if (key.toLowerCase() == prop.toLowerCase()) {
return self[key];
}
}
},
//this keeps jquery happy
enumerable: false
});
Here's the setter:
Object.defineProperty(Object.prototype, "setProp", {
value: function (prop, val) {
var key,self = this;
var found = false;
if (Object.keys(self).length > 0) {
for (key in self) {
if (key.toLowerCase() == prop.toLowerCase()) {
//set existing property
found = true;
self[key] = val;
break;
}
}
}
if (!found) {
//if the property was not found, create it
self[prop] = val;
}
return val;
},
//this keeps jquery happy
enumerable: false
});
Now that we've created those functions, our code is super clean and concise and just works.
Case-insensitive getting:
var obj = {foo: 'bar', camelCase: 'humpy'}
obj.getProp("FOO"); //returns 'bar'
obj.getProp("fOO"); //returns 'bar'
obj.getProp("CAMELCASE"); //returns 'humpy'
obj.getProp("CamelCase"); //returns 'humpy'
Case-insensitive setting:
var obj = {foo: 'bar', camelCase: 'humpy'}
obj.setProp('CAmelCasE', 'super humpy'); //sets prop 'camelCase' to 'super humpy'
obj.setProp('newProp', 'newval'); //creates prop 'newProp' and sets val to 'newval'
obj.setProp('NewProp', 'anotherval'); //sets prop 'newProp' to 'anotherval'
Yet another variation on those already presented which pushes the iteration down into the Underscore/Lodash findKey function:
var _ = require('underscore');
var getProp = function (obj, name) {
var realName = _.findKey(obj, function (value, key) {
return key.toLowerCase() === name.toLowerCase();
});
return obj[realName];
};
For example:
var obj = { aa: 1, bB: 2, Cc: 3, DD: 4 };
getProp(obj, 'aa'); // 1
getProp(obj, 'AA'); // 1
getProp(obj, 'bb'); // 2
getProp(obj, 'BB'); // 2
getProp(obj, 'cc'); // 3
getProp(obj, 'CC'); // 3
getProp(obj, 'dd'); // 4
getProp(obj, 'DD'); // 4
getProp(obj, 'EE'); // undefined
This answer requires ES6.
const x = { 'aB': 1, 'X-Total-Count': 10, y3: 2 }
console.log(x[Object.keys(x).find(key=>{return key.match(/^ab$/i)})])
console.log(x[Object.keys(x).find(key=>{return key.match(/^x-total-count$/i)})])
console.log(x[Object.keys(x).find(key=>{return key.match(/^y3$/i)})])
It seems to me like a good candidate for Proxy with traps to convert string keys to either upper case or lower case and behaving like a regular object.
This works with either notation: dots or braquets
Here is the code:
'use strict';
function noCasePropObj(obj)
{
var handler =
{
get: function(target, key)
{
//console.log("key: " + key.toString());
if (typeof key == "string")
{
var uKey = key.toUpperCase();
if ((key != uKey) && (key in target))
return target[key];
return target[uKey];
}
return target[key];
},
set: function(target, key, value)
{
if (typeof key == "string")
{
var uKey = key.toUpperCase();
if ((key != uKey) && (key in target))
target[key] = value;
target[uKey] = value;
}
else
target[key] = value;
},
deleteProperty: function(target, key)
{
if (typeof key == "string")
{
var uKey = key.toUpperCase();
if ((key != uKey) && (key in target))
delete target[key];
if (uKey in target)
delete target[uKey];
}
else
delete target[key];
},
};
function checkAtomic(value)
{
if (typeof value == "object")
return new noCasePropObj(value); // recursive call only for Objects
return value;
}
var newObj;
if (typeof obj == "object")
{
newObj = new Proxy({}, handler);
// traverse the Original object converting string keys to upper case
for (var key in obj)
{
if (typeof key == "string")
{
var objKey = key.toUpperCase();
if (!(key in newObj))
newObj[objKey] = checkAtomic(obj[key]);
}
}
}
else if (Array.isArray(obj))
{
// in an array of objects convert to upper case string keys within each row
newObj = new Array();
for (var i = 0; i < obj.length; i++)
newObj[i] = checkAtomic(obj[i]);
}
return newObj; // object with upper cased keys
}
// Use Sample:
var b = {Name: "Enrique", last: "Alamo", AdDrEsS: {Street: "1233 Main Street", CITY: "Somewhere", zip: 33333}};
console.log("Original: " + JSON.stringify(b)); // Original: {"Name":"Enrique","last":"Alamo","AdDrEsS":{"Street":"1233 Main Street","CITY":"Somewhere","zip":33333}}
var t = noCasePropObj(b);
console.log(JSON.stringify(t)); // {"NAME":"Enrique","LAST":"Alamo","ADDRESS":{"STREET":"1233 Main Street","CITY":"Somewhere","ZIP":33333}}
console.log('.NaMe:' + t.NaMe); // .NaMe:Enrique
console.log('["naME"]:' + t["naME"]); // ["naME"]:Enrique
console.log('.ADDreSS["CitY"]:' + t.ADDreSS["CitY"]); // .ADDreSS["CitY"]:Somewhere
console.log('check:' + JSON.stringify(Object.getOwnPropertyNames(t))); // check:["NAME","LAST","ADDRESS"]
console.log('check2:' + JSON.stringify(Object.getOwnPropertyNames(t['AddresS']))); // check2:["STREET","CITY","ZIP"]
You could do this in order to "normalize" prop
var normalizedProp = prop.toLowerCase();
obj[normalizedProp] = val;
const getPropertyNoCase = (obj, prop) => obj[Object.keys(obj).find(key => key.toLowerCase() === prop.toLowerCase() )];
or
const getPropertyNoCase = (obj, prop) => {
const lowerProp = prop.toLowerCase(obj[Object.keys(obj).find(key => key.toLowerCase() === prop.toLowerCase() )];
}
The ES6 example posted by #nilloc is incorrect and will break in use.
Here is a working example:
const x = {'first':5,'X-Total-Count':10,'third':20};
console.log(x[Object.keys(x).reduce((result,key)=>{
if (!result) {
return key.match(/x-total-count/i)
} else {
return result;
}
},null)]);
or better yet, it should return undefined if the key doesn't exist:
const x = {'first':5,'X-Total-Count':10,'third':20};
console.log(x[Object.keys(x).reduce((result,key)=>{
if (!result) {
return key.match(/x-total-count/i) || undefined
} else {
return result;
}
},undefined)]);
One consideration is that the above example will return the last matching key in the object if there are multiple keys that match.
Here is an example with the code made into a function:
/**
* #param {Object} object
* #param {string} key
* #return {string||undefined} value || undefined
*/
function getKeyCase(obj,key) {
const re = new RegExp(key,"i");
return Object.keys(obj).reduce((result,key)=>{
if (!result) {
return key.match(re) || undefined
} else {
return result;
}
},undefined);
const x = {'first':5,'X-Total-Count':10,'third':20};
console.log(x[getKeyCase(x,"x-total-count")]);
Its really sad that the iteration can't be skipped as it seems. For me what is acceptable but may not be for everyone is to shape the object one time via iteration and then use it in regular hashmap fashion.
const hashmap = {
'FOO': 'foo as in function programming',
'bar': 'bar is in baz',
};
const shapedmap = Object.entries(hashmap).reduce(
(acc, [key, val]) => (acc[key.toUpperCase()] = val, acc), {}
);
for (const term of ['foo', 'bar', 'baz']) {
const match = shapedmap[term.toUpperCase()]
match && console.log('awesome, we got the term.', match);
};
Even if it just one time lookup has to be performed, it shouldn't less performant as any other iteration solution since after 1 pass, the lookup speed is constant. (I guess).
This is an old question, but it was the first one I found.
As #ZachSmith says, you can use a Proxy.
Here's some example code:
function lowercase(oldKey) {
// Check that it's a string.
return typeof oldKey === 'string' ? oldKey.toLowerCase() : oldKey;
}
const propertiesMap = new Map(
Object.keys(obj).map(propKey => [lowercase(propKey), obj[propKey]])
);
const caseInsensitiveGetHandler = {
get: function(target, property, receiver) {
return propertiesMap.get(lowercase(property));
}
};
obj = new Proxy(obj, caseInsensitiveGetHandler);
For my use case, I only needed to proxy the object's getter, but you may need to implement more of the Proxy methods.
There is no need for any iteration. Since prop might not be a string, it should be coerced to a string first where appropriate since that's what objects do natively. A simple getter function is:
function objGetter(prop) {
return obj[String(prop).toLowerCase()];
}
If there is a requirement is to restring access to own properties:
function objGetter(prop) {
prop = String(prop).toLowerCase();
if (obj.hasOwnProperty(prop)) {
return obj.prop;
}
}
and a setter:
function objSetter(prop, val) {
obj[String(prop).toLowerCase()] = val;
}
Heres a very simple code to do this
Assuming that data is the array of objects like
data=[{"A":"bc","B":"nn"}]
var data=data.reduce(function(prev, curr) {
var cc = curr; // current value
var K = Object.keys(cc); // get all keys
var n = {};
for (var i = 0; i < K.length; i++) {
var key = K[i];//get hte key
n[key.toLowerCase()] = cc[key] // convert to lowercase and assign
}
prev.push(n) // push to array
return prev;
}, [])
Output will be
data=[{"a":"bc","b":"nn"}]
You might only need to do case-insensitive matching (usually expensive because of object iteration) IF a case-sensitive match (cheap and quick) fails.
Say you have:
var your_object = { "Chicago" : 'hi' , "deTroiT" : 'word' , "atlanta" : 'get r dun' } ;
And you have, for whatever reason, the_value, Detroit:
if( your_object.hasOwnProperty( the_value ) )
{
// do what you need to do here
}
else
{ // since the case-sensitive match did not succeed,
// ... Now try a the more-expensive case-insensitive matching
for( let lvs_prop in your_object )
{ if( the_value.toLowerCase() == lvs_prop.toLowerCase() )
{
// do what you need to do here
break ;
} ;
}
} ;
why would we do it that complicated when we simply can make it all lower case:
var your_object = {
"chickago" : 'hi' ,
"detroit" : 'word',
"atlanta" : 'get r dun',
GetName: function (status) {
return this[status].name;
} };
to call it: your_object.GetName(your_var.toLowerCase());
Another simple way:
function getVal(obj, prop){
var val;
prop = (prop + "").toLowerCase();
for(var p in obj){
if(obj.hasOwnProperty(p) && prop == (p+ "").toLowerCase()){
val = obj[p]
break;
}
}
return val;
}
Use it like this:
var obj = {
foo:"bar",
fizz:"buzz"
};
getVal(obj,"FoO") -> returns "bar"
Here is a nice recursive function that allows you to traverse a javascript object in a case-insensitive way:
let testObject = {'a': {'B': {'cC': [1,2,3]}}}
let testSeq = ['a','b','cc']
function keySequence(o, kseq) {
if(kseq.length==0){ return o; }
let validKeys = Object.keys(o).filter(k=>k.toLowerCase()==kseq[0].toLowerCase());
if(validKeys.length==0) { return `Incorrect Key: ${kseq[0]}` }
return keySequence(o[validKeys[0]], kseq.slice(1))
}
keySequence(testObject, testSeq); //returns [1,2,3]
This will convert everything to lowercase, but in a bind this could help if you are not concerned with retaining case.
var somedata = {
"MixEdCase": 1234
}
var temp = JSON.parse(JSON.stringify(somedata).toLowerCase());
console.log(temp.mixedcase);
// or
console.log(temp["mixedcase"]);
So, you will need to get the object key that matches the case of the existing object, then use this to do your object update.
const obj = {
foo:"bar",
fizz:"buzz"
};
// to get obj.foo or obj.FOO or obj.foO returning "bar"
// create regex expression of case insensitive version of the key string
const regex=passedKey=> new RegExp(`^${passedKey}$`,'gi');
// find the key that matches the string you are passing
const formattedKey=passedKey=>Object.keys(obj).find(key=>regex(passedKey).test(key));
formattedKey('Foo'); // returns foo
formattedKey('FoO'); // returns foo
// consequently you can can use it like wise
obj[formattedKey('Foo')] // returns bar
obj[formattedKey('FoO')] // returns bar
obj[formattedKey('foo')] // returns bar

In javascript how can I dynamically get a nested property of an object

var arr = { foo : 1, bar: { baz : 2 }, bee : 3 }
function getter(variable) {
return arr[variable];
}
If I want 'foo' vs 'bee' I can just do arr[variable] - that's easy, and the function does that.
But what if I want to get arr.bar.baz AKA arr[bar][baz]?
What can I pass to the getter function that will let me do that, (and of course also let me get non-nested properties using the same function).
I tried getter('bar.baz') and getter('[bar][baz]') but those didn't work.
I suppose I can parse for dots or brackets (like here: In javascript, test for property deeply nested in object graph?). Is there a cleaner way? (Besides eval of course.)
Especially because I need to get the deeply set properly many many times in a loop for a bunch of array elements.
You can use a deep access function based on a string for the path. Note that you can't have any periods in the property names.
function getPropByString(obj, propString) {
if (!propString)
return obj;
var prop, props = propString.split('.');
for (var i = 0, iLen = props.length - 1; i < iLen; i++) {
prop = props[i];
var candidate = obj[prop];
if (candidate !== undefined) {
obj = candidate;
} else {
break;
}
}
return obj[props[i]];
}
var obj = {
foo: {
bar: {
baz: 'x'
}
}
};
console.log(getPropByString(obj, 'foo.bar.baz')); // x
console.log(getPropByString(obj, 'foo.bar.baz.buk')); // undefined
If the access string is empty, returns the object. Otherwise, keeps going along access path until second last accessor. If that's an ojbect, returns the last object[accessor] value. Otherwise, returns undefined.
Using ES6:
var arr = { foo : 1, bar: { baz : 2 }, bee : 3 };
var {foo, bar, bar: {baz}, bee} = arr;
Same as:
// var foo = 1;
// var bar = {baz: 2};
// var baz = 2;
// var bee = 3;
Using lodash:
https://lodash.com/docs#get
_.get(arr, 'bar.baz'); //returns 2;
_.get(arr, 'bar.baz[5].bazzz'); //returns undefined wont throw error;
_.get(arr, 'bar.baz[5].bazzz', 'defaultvalue'); // Returns defaultValue because result is undefined
A recursive way :
function getValue(obj, path) {
if (!path) return obj;
const properties = path.split('.');
return getValue(obj[properties.shift()], properties.join('.'))
}
const myObj = {
foo: {
bar: {
value: 'good'
}
}
}
console.log(getValue(myObj, 'foo.bar.value')); // good
How about change the getter function signature as getter('bar', 'baz') instead
function getter() {
var v = arr;
for(var i=0; i< arguments.length; i++) {
if(!v) return null;
v = v[arguments[i]];
}
return v;
}
ps. didn't test, but you get the idea ;)
A one liner for you:
const mock = {
target: {
"prop1": {
"prop2": {
"prop3": "sad"
}
}
},
path: "prop1.prop2.prop3",
newValue: "happy"
};
mock.path.split(".").reduce(
(acc, curr, i, src) =>
(curr === src[src.length - 1]) ? acc[src[src.length - 1]] = mock.newValue : acc[curr], mock.target);
console.log(mock.target); //? { prop1: { prop2: { prop3: 'happy' } } }
Here's a very simple one liner which grants you dynamic access via "foo.bar.baz" mechanism,
var obj = {
foo: {
bar: {
baz: 'foobarbaz'
}
}
}
const nestedAccess = "foo.bar.baz";
console.log(nestedAccess.split('.').reduce((prev, cur) => prev[cur], obj)) //'foobarbaz'
I have recently developed my own Object method to get an object property nested among objects and arrays regardless how deep it is. It utilizes a single line of recursive approach. Check this out.
Object.prototype.getNestedValue = function(...a) {
return a.length > 1 ? (this[a[0]] !== void 0 && this[a[0]].getNestedValue(...a.slice(1))) : this[a[0]];
};
var myObj = { foo : 1, bar: { baz : 2 }, bee : 3 },
bazval = myObj.getNestedValue("bar","baz");
document.write(bazval);
Now let's check a deeper nested array object combo data structure
Object.prototype.getNestedValue = function(...a) {
return a.length > 1 ? (this[a[0]] !== void 0 && this[a[0]].getNestedValue(...a.slice(1))) : this[a[0]];
};
var myArr = [{fox: [{turn:[857, 432]}]}, {sax: [{pana:[777, 987]}]}, {ton: [{joni:[123, 567]}]}, {piu: [{burn:[666, 37]}]}, {sia: [{foxy:[404, 696]}]}];
document.write(myArr.getNestedValue(3,"piu",0,"burn",1));
I believe being able to pass search parameters dynamically to existing array methods would make actions like searching, filtering or replacing of deeply nested structures much easy.
Using reduce we can fetch the value in single line of code.
const testobj = {b:{c:'1', d:{e:'2',f:'3'}}, g:{h:'3'}}
function fetchByDotOperator(object, value) {
return value.split('.').reduce((acc, curr) => acc[curr], object);
}
console.log(fetchByDotOperator(testobj,'b.d.e'))
You can access the functions arguments where you can pass any number of strings.
I also recommend using arr as a parameter for better encapsulation:
function getter() {
var current = arguments[0];
for(var i = 1; i < arguments.length; i++) {
if(current[arguments[i]]) {
current = current[arguments[i]];
} else {
return null;
}
}
return current;
}
var arr = { foo : 1, bar: { baz : 2 }, bee : 3 };
var baz = getter(arr, 'bar', 'baz');
function getPropertyByString(object, propString) {
let value = object;
const props = propString.split('.');
for (let index = 0; index < props.length; index += 1) {
if (props[index] === undefined) break;
value = value[props[index]];
}
return value;
};
const object = {
name: 'any_name',
address: {
number: 77,
test: {
name: 'test'
}
}
}
console.log(getPropertyByString(object, 'address.test.name'))
// test
Above answers help you access nested objects only, however you might also want to access data in an object/array data type. You can try this recusive method:
const getValue = (obj, key) => {
const keyParts = key.split(".");
return getValueHelper(obj, keyParts);
};
const getValueHelper = (obj, keyParts) => {
if (keyParts.length == 0) return obj;
let key = keyParts.shift();
if (Array.isArray(obj[key])) {
return obj[key].map((x) => getValueHelper(x, [...keyParts])).flat();
}
return getValueHelper(obj[key], [...keyParts]);
};
//Examples
let data1 = {
a: [{ b: { c: [{ d: [{ e: 1 }] }] } }, { b: { c: [{ d: [{ e: 2 }] }] } }],
};
console.log(getValue(data1, "a.b.c.d.e"));
//Output
//[ 1, 2 ]
let data2 = {
a:{b:1},
};
console.log(getValue(data2, "a.b"));
//Output
//1
p.s. Remove .flat() to get desired output for arrays.
Theres a function defined on this blog to safely read nested properties from a JS object
It allows you to mine an object for properties... ie.
safeRead(arr, 'foo', 'bar', 'baz');
and if any part of the object chain is null or undefined it returns an empty string....
let obj = {foo : {bar: {baz:1}}};
// -- simply
console.log(eval('obj.foo.bar.baz')); //-- 1
// -- safer
val = "";
try {
val = eval('Obj.foo.bar.baz')
}
catch(e) {
val = "empty"
}
// -- val = 1
// -- use at your risk ;)
Here I created a small suite of functions to 'get / 'set' / 'push' / 'pull' from object nested properties.
inputObject : Target object.
Ex: obj = {a:1, b:{c:2,d:3}}
propertyString : String containing the key to access.
Ex: "b.c"
Finally:
_getObjectValueByPathString(obj, "b.c") would return 2
function _getObjectValueByPathString(inputObject, propertyString) {
let splitStr = propertyString.split('.');
if (!inputObject.hasOwnProperty(splitStr[0])) return undefined;
if (splitStr.length === 1) {
return inputObject[splitStr[0]];
}
else if (splitStr.length > 1) {
let newPropertyString = "";
let firstValue = splitStr.shift();
splitStr.forEach((subStr, i) => {
newPropertyString = i === 0 ? subStr : newPropertyString.concat(`.${subStr}`);
});
return _getObjectValueByPathString(inputObject[firstValue], newPropertyString);
}
else {
throw "Invalid property string provided";
}
}
function _setObjectValueByPathString(inputObject, propertyString, inputValue) {
let splitStr = propertyString.split('.');
if (splitStr.length === 1) {
inputObject[splitStr[0]] = inputValue;
return;
}
else if (splitStr.length > 1) {
let newPropertyString = "";
let firstValue = splitStr.shift();
splitStr.forEach((subStr, i) => {
newPropertyString = i === 0 ? subStr : newPropertyString.concat(`.${subStr}`);
});
_setObjectValueByPathString(inputObject[firstValue], newPropertyString, inputValue);
return;
}
else {
throw "Invalid property string provided";
}
}
function _pushObjectValueByPathString(inputObject, propertyString, inputValue) {
let splitStr = propertyString.split('.');
if (splitStr.length === 1) {
inputObject[splitStr[0]].push(inputValue);
return;
}
else if (splitStr.length > 1) {
let newPropertyString = "";
let firstValue = splitStr.shift();
splitStr.forEach((subStr, i) => {
newPropertyString = i === 0 ? subStr : newPropertyString.concat(`.${subStr}`);
});
_pushObjectValueByPathString(inputObject[firstValue], newPropertyString, inputValue);
return;
}
else {
throw "Invalid property string provided";
}
}
function _pullObjectValueByPathString(inputObject, propertyString, inputValue) {
let splitStr = propertyString.split('.');
if (splitStr.length === 1) {
inputObject[splitStr[0]].pull(inputValue);
return;
}
else if (splitStr.length > 1) {
let newPropertyString = "";
let firstValue = splitStr.shift();
splitStr.forEach((subStr, i) => {
newPropertyString = i === 0 ? subStr : newPropertyString.concat(`.${subStr}`);
});
_pullObjectValueByPathString(inputObject[firstValue], newPropertyString, inputValue);
return;
}
else {
throw "Invalid property string provided";
}
}

Categories

Resources