Related
I have a JS object I would like to save in Local Storage for future use, and I cannot parse it to a string.
Code:
JSON.stringify({
a: 5,
b: function (param) {
return param;
}
})
Result:
"{"a":5}"
How do I save it for future use, if not with JSON?
(And creating my own Lexer-Parser to interupt string function I dont think is an option)
I'd recommend this approach:
Store arguments and the body in your json:
{"function":{"arguments":"a,b,c","body":"return a*b+c;"}}
Now parse json and instantiate the function:
var f = new Function(function.arguments, function.body);
I think it's save
Usually a question like this indicates an X/Y problem: You need to do X, you think Y will help you do that, so you try to do Y, can't, and ask how to do Y. It would frequently be more useful to ask how to do X instead.
But answering the question asked: You could use replacer and reviver functions to convert the function to a string (during stringify) and back into a function (during parse) to store a string version of the function, but there are all sorts of issues with doing that, not least that the scope in which the function is defined may well matter to the function. (It doesn't matter to the function you've shown in the question, but I assume that's not really representative.) And converting a string from local storage into code you may run means that you are trusting that the local storage content hasn't been corrupted in a malicious way. Granted it's not likely unless the page is already vulnerable to XSS attacks, but it's an issue to keep in mind.
Here's an example, but I don't recommend it unless other options have been exhausted, not least because it uses eval, which (like its close cousin new Function)) can be a vector for malicious code:
// The object
var obj = {
a: 5,
b: function (param) {
return param;
}
};
// Convert to JSON using a replacer function to output
// the string version of a function with /Function(
// in front and )/ at the end.
var json = JSON.stringify(obj, function(key, value) {
if (typeof value === "function") {
return "/Function(" + value.toString() + ")/";
}
return value;
});
// Convert to an object using a reviver function that
// recognizes the /Function(...)/ value and converts it
// into a function via -shudder- `eval`.
var obj2 = JSON.parse(json, function(key, value) {
if (typeof value === "string" &&
value.startsWith("/Function(") &&
value.endsWith(")/")) {
value = value.substring(10, value.length - 2);
return (0, eval)("(" + value + ")");
}
return value;
});
document.body.innerHTML = obj2.b(42);
The construct (0, eval)("(" + value + ")"); ensures that eval runs at global scope rather than within the scope of the reviver function. Normally eval has a magic ability to use the scope you call it in, but that only works when you call it directly. Indirect eval as shown (or just var e = eval; e("(" + value + ")");) doesn't have that magic ability, it runs at global scope.
You can't store functions in JSON.
The value in JSON may contain only string, number, object, array, true, false or null:
Check out it on JSON site.
One simple way of doing this is
var dstr = JSON.stringify( { a: 5
, b: x => x
}
, (k,v) => typeof v === "function" ? "" + v : v
);
I've taken to storing the function name, along with the parameter values, in an array, with the first item in the array being the function name prepended with a $, to separate them from normal arrays.
{
"object": {
"your-function": ["$functionName", "param-1", "param-2"],
"color": ["$getColor", "brand", "brand-2"],
"normal-array": ["normal", "array"]
...
}
}
In the above example I have Sass and JS functions to retrieve color values from a global map/object. Parsing the function in this manner naturally requires custom code, but in terms of "storing" functions in JSON, I like this way of doing it.
I have created JSON.parseIt() and JSON.stringifyIt() functions based on the first answer without using eval
JSON.stringifyIt = (obj)=>{
return(
JSON.stringify(obj, function(key, value) {
if (typeof value === "function") {
return "/Function(" + value.toString() + ")/";
}
if(typeof value === "string"){
return "/String(" + value.toString() + ")/"
}
return value;
})
)
}
JSON.parseIt=(json)=>{
return(
JSON.parse(json, function(key, value) {
if (typeof value === "string" &&
value.startsWith("/Function(") &&
value.endsWith(")/")) {
value = value.substring(10, value.length - 2);
var string = value.slice(value.indexOf("(") + 1, value.indexOf(")"));
if(/\S+/g.test(string)){
return (new Function(string,value.slice(value.indexOf("{") + 1, value.lastIndexOf("}"))))
}else{
return (new Function(value.slice(value.indexOf("{") + 1, value.lastIndexOf("}"))));
}
}
if (typeof value === "string" &&
value.startsWith("/String(") &&
value.endsWith(")/")){
value = value.substring(8, value.length - 2);
}
return value;
})
)
}
// DEMO
var obj = {
string:"a string",
number:10,
func:()=>{
console.log("this is a string from a parsed json function");
},
secFunc:(none,ntwo)=>{console.log(none + ntwo)} ,
confuse:"/Function(hello)/"
}
const stringifiedObj = JSON.stringifyIt(obj);
console.log("the stringified object is: ",stringifiedObj);
const parsedObj = JSON.parseIt(stringifiedObj);
// console.log("the parsed object is: ",parsedObj);
console.log(parsedObj.string);
console.log(parsedObj.number);
console.log(parsedObj.confuse);
parsedObj.func();
parsedObj.secFunc(5,6);
The problems I fixed were
Removed eval.
there was a problem in the stringifying and parsing that if I give a string like
"/Function(hello)/" will be a function when parsed
Made it to two functions
Added parameter insertation
For someone that still need include, for whatever reason, the function definition in JSON, this code can help (but can be slow depending object size):
function Object2JsonWithFunctions(o, space = null) {
var functionList = {}
var fnSeq = 0;
var snrepl = function(k,v){
if(typeof v === 'function'){
fnSeq++;
var funcName = `___fun${fnSeq}___`;
var funcText = ''+v;
functionList[funcName] = funcText
return funcName;
}
return v;
}
var RawJson = JSON.stringify(o, snrepl, space);
for(func in functionList){
var PropValue = `"${func}"`;
RawJson = RawJson.replace(PropValue, functionList[func])
}
return RawJson;}
The code will do the normal convert to JSON.
For functions, the original stringify will return as "prop":"function()..." (function as a string)... The code above will create a placeholder (e.g: "prop":"fn1") and create a function list... After, will replace every placeholder to original function body...
This is a JavaScript function that determines if arguments are strings
just curious if anyone has a way to simplify this function?
I can't help but think there is since there are so many similarities in the parameters
typeof x === "string"
that there is a way to simplify it. I asked my teachers and they told me they were unaware of any.
function isString(string1, string2, string3) {
if (typeof x === "string" && typeof y === "string" && typeof z === "string")
console.log("Yes!" + x + " " + y + " " + z)
else {
console.log("Nope!")
}
}
isString("String1", "String2", "String3")
I really look forward to reading your responses!
Thanks
-Joe
You might be looking for rest parameters or the arguments object that let you handle an arbitrary amount of arguments, together with looping over them (or using a helper method for that):
function areStrings(...args) {
if (args.every(x => typeof x === "string"))
return "Yes!" + args.join(" ");
else
return "Nope!";
}
console.log(areStrings("String1", "String2", "String3"));
console.log(areStrings(5, "someString"));
you can receive your params as an array o, and the use functional parameter to check:
function isString(...strings) {
if (strings.every(s => typeof s === 'string'))
console.log("They are string")
else
console.log("They are not string")
}
If you want to check if multiple variables are strings, you can use this function:
function isString(...args){
for(var i = 0; i<args.length; i++){
if(typeof args[i] !== 'string'){
return false;
}
}
return true;
}
You can pass as much parameters as you want and the result will be only true if all parameters are strings.
Example:
isString(4) returns false
isString("Hello World") returns true
isString("I am a string", 3, true, "Hello") returns false
isString("Hello World", "Welcome") returns true
Clear answer, simple effective. I come to propose another way of doing things. Arrow function + ternary. More verbose but condense.
(I didn't know every cool :))
var resultAmeliored = (...args) => 'Var: '+args.map(val => val+' is '+typeof val).join(', ');
var areStrings = (...args)=>args.every(s => typeof s === 'string')?true:false;
console.log(areStrings("String1", "String2", "String3"));
//true
console.log(resultAmeliored(5, "someString"));
//"Var: 5 is number, someString is string"
I have one rest call that gives me information from query parameters and I need to determine whether they are a string or an int, array, boolean.
For example, if I have:
/.../something?id=1
I'll receive '1', but I know that's an integer.
Furthermore, I have:
/.../something?id=[1,2,3]
I'll receive '[1,2,3]' but I know it's an array. Finally, if I have:
/.../something?id=string
I'll receive 'string' and I should use it as a regular string.
Is regex the only way of doing that check for each type?
You can use JSON.parse with a catch block.
var options = ['12', '[1,2,3]', 'string', 'false', '{"x" : 2}', '/hey/', /hey/];
var parsed = options.map(x => {
try {
return JSON.parse(x)
} catch(e) {
return x;
}
});
var types = parsed.map(x => /\[object (.*)\]$/.exec(Object.prototype.toString.call(x))[1])
console.log(types);
Once you have a value
var value = "1"; //or whatever value after `id=`
you can apply this logic
var type = "";
var isNum = ( s ) => !isNaN(s);
var isObject = ( s ) => {
try { s = JSON.parse(s); return Array.isArray(s) ? "array" : "object" } catch( e ){ return false }
};
Now use them as
type = isNum( value ) ? "number" : ( isObject( s ) || "string" );
Note
If value is a function definition, it will still return a string
One way to solve this is to use trial-and-error methodologies:
If you have the value starting from [ and ending with ], you probably can assume that its an array. Use JSON.parse to be sure.
If first case doesn't match, use /^\d+$/ to test the string against integer values. Use parseInt() to be sure.
Lastly, if above both test cases fail, you can be sure that its a string, because strings can hold almost all type of values.
Savio,
One way you can handle it is to fetch the value and use switch/case statement with 'typeof' to see what they are.
Typeof explanation on MDN
// Numbers
typeof 37 === 'number';
typeof 3.14 === 'number';
// Strings
typeof 'bla' === 'string';
// Booleans
typeof true === 'boolean';
// use Array.isArray or Object.prototype.toString.call
// to differentiate regular objects from arrays
typeof [1, 2, 4] === 'object';
I'm trying to replicate the json stringify method but instead use recursion. I've been able to pass alot of test cases but when it comes to nested arrays I seem to be having issues. if there's any empty array inside an array ('[]'), i get something like [,7,9] instead of [[],7,9]. Also if I pass in:
stringifyJSON([[["test","mike",4,["jake"]],3,4]])
"[[test,mike,4,jake,3,4]]"
I thought I was close in getting this to work, but I might have to start over. Do you guys have any ideas on what I might be able to change to make this work for nested examples? Here's the code I have now:
var testarray = [9,[[],2,3]] //should return '[9,[[],2,3]]'
var count = 0
var stringifyJSON = function(obj,stack) {
var typecheck = typeof obj;
var resarray = stack;
if(resarray == null){ //does resarray exist? Is this the first time through?
var resarray = [];
}
if(typeof obj === "string"){ //Is obj a string?
return '"' + String(obj) + '"';
}
if((Array.isArray(obj)) && (obj.length > 0)){ //If not a string, is it an object?
for(var i = 0; i<obj.length;i++){
if(Array.isArray(obj[i])){
var arraytemp = []
stringifyJSON(arraytemp.push(obj[i]),resarray) // this is probably incorrect, this is how i handle a nested array situation
}
if(typeof obj[i] === 'number'){ //if the number is inside of the array, don't quote it
resarray.push(obj[i]);
}
else if(typecheck === 'object' && Array.isArray(obj[0])){
resarray.push('[' + obj[i] + ']');
}
else{
resarray.push('"' + obj[i] + '"');
}
obj.shift() //delete the first object in the array and get ready to recurse to get to the second object.
stringifyJSON(obj,resarray); //remember the new array when recursing by passing it into the next recursive instance
}
}
if(obj !== null && typeof obj === 'object'){ //is obj an object?
for(var key in obj){
stringifyJSON(resarray.push(key + '"' + ':' + obj[key]),resarray)
}
}
if(typeof obj === "number" || obj == null || obj === true || obj === false){ //special cases and if it's a number
return '' + obj + ''
}
if(typecheck === 'object'){ //a special case where you have an empty array that needs to be quoted.
return '['+resarray+']'
}
return '' + resarray.join('') + '';
};
//JSON values cannot be a function, a date, or undefined
Do you guys have any ideas on what I might be able to change to make this work for nested examples?
Sure, but it's going to scrap you entire function, so I hope you don't mind. I'll provide a bullet-list of points why this approach is essential and yours is essentially flawed from the get-go :(
Corner cases
This function does a simple case analysis on a non-null data's constructor property and encodes accordingly. It manages to cover a lot of corner cases that you're unlikely to consider, such as
JSON.stringify(undefined) returns undefined
JSON.stringify(null) returns 'null'
JSON.stringify(true) returns 'true'
JSON.stringify([1,2,undefined,4]) returns '[1,2,null,4]'
JSON.stringify({a: undefined, b: 2}) returns '{ "b": 2 }'
JSON.stringify({a: /foo/}) returns { "a": {} }
So to verify that our stringifyJSON function actually works properly, I'm not going to test the output of it directly. Instead, I'm going to write a little test method that ensures the JSON.parse of our encoded JSON actually returns our original input value
// we really only care that JSON.parse can work with our result
// the output value should match the input value
// if it doesn't, we did something wrong in our stringifier
const test = data => {
return console.log(JSON.parse(stringifyJSON(data)))
}
test([1,2,3]) // should return [1,2,3]
test({a:[1,2,3]}) // should return {a:[1,2,3]}
Disclaimer: it should be obvious that the code I'm about to share is not meant to be used as an actual replacement for JSON.stringify – there's countless corner cases we probably didn't address. Instead, this code is shared to provide a demonstration for how we could go about such a task. Additional corner cases could easily be added to this function.
Runnable demo
Without further ado, here is stringifyJSON in a runnable demo that verifies excellent compatibility for several common cases
const stringifyJSON = data => {
if (data === undefined)
return undefined
else if (data === null)
return 'null'
else if (data.constructor === String)
return '"' + data.replace(/"/g, '\\"') + '"'
else if (data.constructor === Number)
return String(data)
else if (data.constructor === Boolean)
return data ? 'true' : 'false'
else if (data.constructor === Array)
return '[ ' + data.reduce((acc, v) => {
if (v === undefined)
return [...acc, 'null']
else
return [...acc, stringifyJSON(v)]
}, []).join(', ') + ' ]'
else if (data.constructor === Object)
return '{ ' + Object.keys(data).reduce((acc, k) => {
if (data[k] === undefined)
return acc
else
return [...acc, stringifyJSON(k) + ':' + stringifyJSON(data[k])]
}, []).join(', ') + ' }'
else
return '{}'
}
// round-trip test and log to console
const test = data => {
return console.log(JSON.parse(stringifyJSON(data)))
}
test(null) // null
test('he said "hello"') // 'he said "hello"'
test(5) // 5
test([1,2,true,false]) // [ 1, 2, true, false ]
test({a:1, b:2}) // { a: 1, b: 2 }
test([{a:1},{b:2},{c:3}]) // [ { a: 1 }, { b: 2 }, { c: 3 } ]
test({a:[1,2,3], c:[4,5,6]}) // { a: [ 1, 2, 3 ], c: [ 4, 5, 6 ] }
test({a:undefined, b:2}) // { b: 2 }
test([[["test","mike",4,["jake"]],3,4]]) // [ [ [ 'test', 'mike', 4, [ 'jake' ] ], 3, 4 ] ]
"So why is this better?"
this works for more than just Array types – we can stringify Strings, Numbers, Arrays, Objects, Array of Numbers, Arrays of Objects, Objects containing Arrays of Strings, even nulls and undefineds, and so on – you get the idea
each case of our stringifyJSON object is like a little program that tells us exactly how to encode each type (eg String, Number, Array, Object etc)
no whacked out typeof type checking – after we check for the undefined and null cases, we know we can try to read the constructor property.
no manual looping where we have to mentally keep track of counter variables, how/when to increment them
no complex if conditions using &&, ||, !, or checking things like x > y.length etc
no use of obj[0] or obj[i] that stresses our brain out
no assumptions about Arrays/Objects being empty – and without having to check the length property
no other mutations for that matter – that means we don't have to think about some master return value resarray or what state it's in after push calls happen at various stages in the program
Custom objects
JSON.stringify allows us to set a toJSON property on our custom objects so that when we stringify them, we will get the result we want.
const Foo = x => ({
toJSON: () => ({ type: 'Foo', value: x })
})
console.log(JSON.stringify(Foo(5)))
// {"type":"Foo","value":5}
We could easily add this kind of functionality to our code above – changes in bold
const stringifyJSON = data => {
if (data === undefined)
return undefined
else if (data === null)
return 'null'
else if (data.toJSON instanceof Function)
return stringifyJSON(data.toJSON())
...
else
return '{}'
}
test({toJSON: () => ({a:1, b:2})}) // { a: 1, b: 2 }
So after playing around with your code, I found that if you replace:
resarray.push('[' + obj[i] + ']');
with:
resarray.push(stringifyJSON(obj[i])) it works well with the arrays and still satisfies your recursive process.
Also I found a few quirks with running it through objects like { hello: 5, arr: testarray, arr2: [1, 2,3, "hello"], num: 789, str: '4444', str2: "67494" }; and found that by changing the stringifying of objects from:
stringifyJSON(resarray.push(key + '"' + ':' + obj[key]),resarray)
to something more like:
stringifyJSON(resarray.push('"' + key + '"' + ':' + stringifyJSON(obj[key])),resarray);
it should workout more of how you want. This is really cool though and I had fun playing around with it!
I know that I can test for a JavaScript variable and then define it if it is undefined, but is there not some way of saying
var setVariable = localStorage.getItem('value') || 0;
seems like a much clearer way, and I'm pretty sure I've seen this in other languages.
Yes, it can do that, but strictly speaking that will assign the default value if the retrieved value is falsey, as opposed to truly undefined. It would therefore not only match undefined but also null, false, 0, NaN, "" (but not "0").
If you want to set to default only if the variable is strictly undefined then the safest way is to write:
var x = (typeof x === 'undefined') ? your_default_value : x;
On newer browsers it's actually safe to write:
var x = (x === undefined) ? your_default_value : x;
but be aware that it is possible to subvert this on older browsers where it was permitted to declare a variable named undefined that has a defined value, causing the test to fail.
Logical nullish assignment, ES2020+ solution
New operators are currently being added to the browsers, ??=, ||=, and &&=. This post will focus on ??=.
This checks if left side is undefined or null, short-circuiting if already defined. If not, the right-side is assigned to the left-side variable.
Comparing Methods
// Using ??=
name ??= "Dave"
// Previously, ES2020
name = name ?? "Dave"
// or
if (typeof name === "undefined" || name === null) {
name = true
}
// Before that (not equivalent, but commonly used)
name = name || "Dave" // Now: name ||= "Dave"
Basic Examples
let a // undefined
let b = null
let c = false
a ??= true // true
b ??= true // true
c ??= true // false
Object/Array Examples
let x = ["foo"]
let y = { foo: "fizz" }
x[0] ??= "bar" // "foo"
x[1] ??= "bar" // "bar"
y.foo ??= "buzz" // "fizz"
y.bar ??= "buzz" // "buzz"
x // Array [ "foo", "bar" ]
y // Object { foo: "fizz", bar: "buzz" }
??= Browser Support Oct 2022 - 93%
??= Mozilla Documentation
||= Mozilla Documentation
&&= Mozilla Documentation
The 2018 ES6 answer is:
return Object.is(x, undefined) ? y : x;
If variable x is undefined, return variable y... otherwise if variable x is defined, return variable x.
ES2020 Answer
With the Nullish Coalescing Operator, you can set a default value if value is null or undefined.
const setVariable = localStorage.getItem('value') ?? 0;
However, you should be aware that the nullish coalescing operator does not return the default value for other types of falsy value such as 0 and ''.
However, do take note of the browser support. You may need to use a JavaScript compiler like Babel to convert it into something more backward compatible. If you are using Node.js, it has been supported since version 14.
I needed to "set a variable if undefined" in several places. I created a function using #Alnitak answer. Hopefully it helps someone.
function setDefaultVal(value, defaultValue){
return (value === undefined) ? defaultValue : value;
}
Usage:
hasPoints = setDefaultVal(this.hasPoints, true);
It seems more logical to check typeof instead of undefined? I assume you expect a number as you set the var to 0 when undefined:
var getVariable = localStorage.getItem('value');
var setVariable = (typeof getVariable == 'number') ? getVariable : 0;
In this case if getVariable is not a number (string, object, whatever), setVariable is set to 0
In our days you actually can do your approach with JS:
// Your variable is null
// or '', 0, false, undefined
let x = null;
// Set default value
x = x || 'default value';
console.log(x); // default value
So your example WILL work:
const setVariable = localStorage.getItem('value') || 0;
You can use any of below ways.
let x;
let y = 4;
x || (x = y)
in ES12 or after
let x;
let y = 4;
x ||= y;
If you're a FP (functional programming) fan, Ramda has a neat helper function for this called defaultTo :
usage:
const result = defaultTo(30)(value)
It's more useful when dealing with undefined boolean values:
const result2 = defaultTo(false)(dashboard.someValue)
var setVariable = (typeof localStorage.getItem('value') !== 'undefined' && localStorage.getItem('value')) || 0;
Ran into this scenario today as well where I didn't want zero to be overwritten for several values. We have a file with some common utility methods for scenarios like this. Here's what I added to handle the scenario and be flexible.
function getIfNotSet(value, newValue, overwriteNull, overwriteZero) {
if (typeof (value) === 'undefined') {
return newValue;
} else if (value === null && overwriteNull === true) {
return newValue;
} else if (value === 0 && overwriteZero === true) {
return newValue;
} else {
return value;
}
}
It can then be called with the last two parameters being optional if I want to only set for undefined values or also overwrite null or 0 values. Here's an example of a call to it that will set the ID to -1 if the ID is undefined or null, but wont overwrite a 0 value.
data.ID = Util.getIfNotSet(data.ID, -1, true);
Works even if the default value is a boolean value:
var setVariable = ( (b = 0) => b )( localStorage.getItem('value') );
It seems to me, that for current javascript implementations,
var [result='default']=[possiblyUndefinedValue]
is a nice way to do this (using object deconstruction).