Set observable property of object by path - javascript

I have an object that could have any number of levels and could have any existing properties. Besides, the object and/or its properties may be observable.
For example:
var obj = {
prop: "qqq",
obsProp: ko.observable({
nestedObsProp: ko.observable({
prop: 'value'
})
})
};
I need to have a way to set all properties (both observable and non-observables) of any nested levels in a way something like this:
set(obj, 'obsProp.nestedObsProp.prop', 'new value');
I tried to use lodash function _.set(), it works only if properties are not observables, but properties in obj (which I have to update) can be both - observable or not.
I'd appreciate any help, thanks.

function set (source, key, newValue) {
const hierarchy = key.split('.');
const property = hierarchy.pop();
const retrived = hierarchy.reduce((source, key) => {
return source[key];
}, source) || source;
if (ko.isObservable(retrived[property])) {
retrived[property](newValue);
} else {
retrived[property] = newValue;
}
}

yoel neuman, thank you for your answer, it helped me a lot. I took your function as base approach with small corrections:
function set(source, path, newValue) {
const hierarchy = path.split('.');
const property = hierarchy.pop();
var retrieved = hierarchy.reduce((source, key) => {
return ko.isObservable(source) ? source()[key] : source[key];
}, source) || source;
retrieved = ko.isObservable(retrieved) ? retrieved() : retrieved;
if (ko.isObservable(retrieved[property])) {
retrieved[property](newValue);
} else {
retrieved[property] = newValue;
}
}
Unfortunately, I have not enough reputation to vote for your answer. Much appreciated your help!

Related

Save Console.log output to variable [duplicate]

I have a big object I want to convert to JSON and send. However it has circular structure, so if I try to use JSON.stringify() I'll get:
TypeError: Converting circular structure to JSON
or
TypeError: cyclic object value
I want to toss whatever circular references exist and send whatever can be stringified. How do I do that?
Thanks.
var obj = {
a: "foo",
b: obj
}
I want to stringify obj into:
{"a":"foo"}
In Node.js, you can use util.inspect(object). It automatically replaces circular links with "[Circular]".
Albeit being built-in (no installation is required), you must import it
import * as util from 'util' // has no default export
import { inspect } from 'util' // or directly
// or
var util = require('util')
To use it, simply call
console.log(util.inspect(myObject))
Also be aware that you can pass options object to inspect (see link above)
inspect(myObject[, options: {showHidden, depth, colors, showProxy, ...moreOptions}])
Please, read and give kudos to commenters below...
Use JSON.stringify with a custom replacer. For example:
// Demo: Circular reference
var circ = {};
circ.circ = circ;
// Note: cache should not be re-used by repeated calls to JSON.stringify.
var cache = [];
JSON.stringify(circ, (key, value) => {
if (typeof value === 'object' && value !== null) {
// Duplicate reference found, discard key
if (cache.includes(value)) return;
// Store value in our collection
cache.push(value);
}
return value;
});
cache = null; // Enable garbage collection
The replacer in this example is not 100% correct (depending on your definition of "duplicate"). In the following case, a value is discarded:
var a = {b:1}
var o = {};
o.one = a;
o.two = a;
// one and two point to the same object, but two is discarded:
JSON.stringify(o, ...);
But the concept stands: Use a custom replacer, and keep track of the parsed object values.
As a utility function written in es6:
// safely handles circular references
JSON.safeStringify = (obj, indent = 2) => {
let cache = [];
const retVal = JSON.stringify(
obj,
(key, value) =>
typeof value === "object" && value !== null
? cache.includes(value)
? undefined // Duplicate reference found, discard key
: cache.push(value) && value // Store value in our collection
: value,
indent
);
cache = null;
return retVal;
};
// Example:
console.log('options', JSON.safeStringify(options))
I wonder why nobody posted the proper solution from MDN page yet...
const circularReference = {otherData: 123};
circularReference.myself = circularReference;
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
const stringified = JSON.stringify(circularReference, getCircularReplacer());
console.log(stringified);
Seen values should be stored in a set, not in array (replacer gets called on every element).
Like in the accepted answer, this solution removes all repeating values, not just the circular ones. But at least it does not have exponential complexity.
just do
npm i --save circular-json
then in your js file
const CircularJSON = require('circular-json');
...
const json = CircularJSON.stringify(obj);
https://github.com/WebReflection/circular-json
NOTE: I have nothing to do with this package. But I do use it for this.
Update 2020
Please note CircularJSON is in maintenance only and flatted is its successor.
I really liked Trindaz's solution - more verbose, however it had some bugs. I fixed them for whoever likes it too.
Plus, I added a length limit on my cache objects.
If the object I am printing is really big - I mean infinitely big - I want to limit my algorithm.
JSON.stringifyOnce = function(obj, replacer, indent){
var printedObjects = [];
var printedObjectKeys = [];
function printOnceReplacer(key, value){
if ( printedObjects.length > 2000){ // browsers will not print more than 20K, I don't see the point to allow 2K.. algorithm will not be fast anyway if we have too many objects
return 'object too long';
}
var printedObjIndex = false;
printedObjects.forEach(function(obj, index){
if(obj===value){
printedObjIndex = index;
}
});
if ( key == ''){ //root element
printedObjects.push(obj);
printedObjectKeys.push("root");
return value;
}
else if(printedObjIndex+"" != "false" && typeof(value)=="object"){
if ( printedObjectKeys[printedObjIndex] == "root"){
return "(pointer to root)";
}else{
return "(see " + ((!!value && !!value.constructor) ? value.constructor.name.toLowerCase() : typeof(value)) + " with key " + printedObjectKeys[printedObjIndex] + ")";
}
}else{
var qualifiedKey = key || "(empty key)";
printedObjects.push(value);
printedObjectKeys.push(qualifiedKey);
if(replacer){
return replacer(key, value);
}else{
return value;
}
}
}
return JSON.stringify(obj, printOnceReplacer, indent);
};
Note that there is also a JSON.decycle method implemented by Douglas Crockford. See his
cycle.js. This allows you to stringify almost any standard structure:
var a = [];
a[0] = a;
a[1] = 123;
console.log(JSON.stringify(JSON.decycle(a)));
// result: '[{"$ref":"$"},123]'.
You can also recreate original object with retrocycle method. So you don't have to remove cycles from objects to stringify them.
However this will not work for DOM Nodes (which are typical cause of cycles in real life use-cases). For example this will throw:
var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a)));
I've made a fork to solve that problem (see my cycle.js fork). This should work fine:
var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a, true)));
Note that in my fork JSON.decycle(variable) works as in the original and will throw an exception when the variable contain DOM nodes/elements.
When you use JSON.decycle(variable, true) you accept the fact that the result will not be reversible (retrocycle will not re-create DOM nodes). DOM elements should be identifiable to some extent though. For example if a div element has an id then it will be replaced with a string "div#id-of-the-element".
#RobW's answer is correct, but this is more performant ! Because it uses a hashmap/set:
const customStringify = function (v) {
const cache = new Set();
return JSON.stringify(v, function (key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
// Circular reference found
try {
// If this value does not reference a parent it can be deduped
return JSON.parse(JSON.stringify(value));
}
catch (err) {
// discard key if value cannot be deduped
return;
}
}
// Store value in our set
cache.add(value);
}
return value;
});
};
I'd recommend checking out json-stringify-safe from #isaacs-- it's used in NPM.
BTW- if you're not using Node.js, you can just copy and paste lines 4-27 from the relevant part of the source code.
To install:
$ npm install json-stringify-safe --save
To use:
// Require the thing
var stringify = require('json-stringify-safe');
// Take some nasty circular object
var theBigNasty = {
a: "foo",
b: theBigNasty
};
// Then clean it up a little bit
var sanitized = JSON.parse(stringify(theBigNasty));
This yields:
{
a: 'foo',
b: '[Circular]'
}
Note that, just like with the vanilla JSON.stringify function as #Rob W mentioned, you can also customize the sanitization behavior by passing in a "replacer" function as the second argument to stringify(). If you find yourself needing a simple example of how to do this, I just wrote a custom replacer which coerces errors, regexps, and functions into human-readable strings here.
For future googlers searching for a solution to this problem when you don't know the keys of all circular references, you could use a wrapper around the JSON.stringify function to rule out circular references. See an example script at https://gist.github.com/4653128.
The solution essentially boils down to keeping a reference to previously printed objects in an array, and checking that in a replacer function before returning a value. It's more constrictive than only ruling out circular references, because it also rules out ever printing an object twice, one of the side affects of which is to avoid circular references.
Example wrapper:
function stringifyOnce(obj, replacer, indent){
var printedObjects = [];
var printedObjectKeys = [];
function printOnceReplacer(key, value){
var printedObjIndex = false;
printedObjects.forEach(function(obj, index){
if(obj===value){
printedObjIndex = index;
}
});
if(printedObjIndex && typeof(value)=="object"){
return "(see " + value.constructor.name.toLowerCase() + " with key " + printedObjectKeys[printedObjIndex] + ")";
}else{
var qualifiedKey = key || "(empty key)";
printedObjects.push(value);
printedObjectKeys.push(qualifiedKey);
if(replacer){
return replacer(key, value);
}else{
return value;
}
}
}
return JSON.stringify(obj, printOnceReplacer, indent);
}
var a={b:"b"};
a.a=a;
JSON.stringify(preventCircularJson(a));
evaluates to:
"{"b":"b","a":"CIRCULAR_REFERENCE_REMOVED"}"
with the function:
/**
* Traverses a javascript object, and deletes all circular values
* #param source object to remove circular references from
* #param censoredMessage optional: what to put instead of censored values
* #param censorTheseItems should be kept null, used in recursion
* #returns {undefined}
*/
function preventCircularJson(source, censoredMessage, censorTheseItems) {
//init recursive value if this is the first call
censorTheseItems = censorTheseItems || [source];
//default if none is specified
censoredMessage = censoredMessage || "CIRCULAR_REFERENCE_REMOVED";
//values that have allready apeared will be placed here:
var recursiveItems = {};
//initaite a censored clone to return back
var ret = {};
//traverse the object:
for (var key in source) {
var value = source[key]
if (typeof value == "object") {
//re-examine all complex children again later:
recursiveItems[key] = value;
} else {
//simple values copied as is
ret[key] = value;
}
}
//create list of values to censor:
var censorChildItems = [];
for (var key in recursiveItems) {
var value = source[key];
//all complex child objects should not apear again in children:
censorChildItems.push(value);
}
//censor all circular values
for (var key in recursiveItems) {
var value = source[key];
var censored = false;
censorTheseItems.forEach(function (item) {
if (item === value) {
censored = true;
}
});
if (censored) {
//change circular values to this
value = censoredMessage;
} else {
//recursion:
value = preventCircularJson(value, censoredMessage, censorChildItems.concat(censorTheseItems));
}
ret[key] = value
}
return ret;
}
Use the JSON.stringify method with a replacer. Read this documentation for more information. http://msdn.microsoft.com/en-us/library/cc836459%28v=vs.94%29.aspx
var obj = {
a: "foo",
b: obj
}
var replacement = {"b":undefined};
alert(JSON.stringify(obj,replacement));
Figure out a way to populate the replacement array with cyclic references. You can use the typeof method to find if an the property is of type 'object' ( reference ) and an exact equality check ( === ) to verify circular reference.
If
console.log(JSON.stringify(object));
results in a
TypeError: cyclic object value
Then you may want to print like this:
var output = '';
for (property in object) {
output += property + ': ' + object[property]+'; ';
}
console.log(output);
I know this is an old question, but I'd like to suggest an NPM package I've created called smart-circular, which works differently from the other ways proposed. It's specially useful if you're using big and deep objects.
Some features are:
Replacing circular references or simply repeated structures inside the object by the path leading to its first occurrence (not just the string [circular]);
By looking for circularities in a breadth-first search, the package ensures this path is as small as possible, which is important when dealing with very big and deep objects, where the paths can get annoyingly long and difficult to follow (the custom replacement in JSON.stringify does a DFS);
Allows personalised replacements, handy to simplify or ignore less important parts of the object;
Finally, the paths are written exactly in the way necessary to access the field referenced, which can help you debugging.
The second argument to JSON.stringify() also allows you to specify an array of key names that should be preserved from every object it encounters within your data. This may not work for all use cases, but is a much simpler solution.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
var obj = {
a: "foo",
b: this
}
var json = JSON.stringify(obj, ['a']);
console.log(json);
// {"a":"foo"}
Note: Strangely, the object definition from OP does not throw a circular reference error in the latest Chrome or Firefox. The definition in this answer was modified so that it did throw an error.
I found circular-json library on github and it worked well for my problem.
Some good features I found useful:
Supports multi-platform usage but I only tested it with node.js so far.
API is same so all you need to do is include and use it as a JSON replacement.
It have it's own parsing method so you can convert the 'circular' serialized data back to object.
To update the answer of overriding the way JSON works (probably not recommended, but super simple), don't use circular-json (it's deprecated). Instead, use the successor, flatted:
https://www.npmjs.com/package/flatted
Borrowed from the old answer above from #user1541685 , but replaced with the new one:
npm i --save flatted
then in your js file
const CircularJSON = require('flatted');
const json = CircularJSON.stringify(obj);
This code will fail for circular reference:
JSON.stringify(circularReference);
// TypeError: cyclic object value
Use the below code:
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
JSON.stringify(circularReference, getCircularReplacer());
This solution fixes the issue reported by user2451227 on accepted answer (when o = {}; JSON.stringify([o, o], getCircularReplacer())).
function newCircularReplacer () {
const seenValues = []
return circularReplacer
function circularReplacer (key, value) {
if (typeof value === 'object' && value !== null && Object.keys(value).length) {
const stackSize= seenValues.length
if (stackSize) {
for (let n = stackSize - 1; seenValues[n][key] !== value; --n)
seenValues.pop() // clean up expired references
if (seenValues.includes(value)) return '[Circular]'
}
seenValues.push(value)
}
return value
}
}
let o = {a: 1}
o.b = o // Circular reference
console.log(
JSON.stringify(o, newCircularReplacer()) // {a:1,b:[Circular]} ✅
)
o = {}
a = [o, o] // NOT circular reference
console.log(
JSON.stringify(a, newCircularReplacer()) // [{},{}] ✅
)
I resolve this problem like this:
var util = require('util');
// Our circular object
var obj = {foo: {bar: null}, a:{a:{a:{a:{a:{a:{a:{hi: 'Yo!'}}}}}}}};
obj.foo.bar = obj;
// Generate almost valid JS object definition code (typeof string)
var str = util.inspect(b, {depth: null});
// Fix code to the valid state (in this example it is not required, but my object was huge and complex, and I needed this for my case)
str = str
.replace(/<Buffer[ \w\.]+>/ig, '"buffer"')
.replace(/\[Function]/ig, 'function(){}')
.replace(/\[Circular]/ig, '"Circular"')
.replace(/\{ \[Function: ([\w]+)]/ig, '{ $1: function $1 () {},')
.replace(/\[Function: ([\w]+)]/ig, 'function $1(){}')
.replace(/(\w+): ([\w :]+GMT\+[\w \(\)]+),/ig, '$1: new Date("$2"),')
.replace(/(\S+): ,/ig, '$1: null,');
// Create function to eval stringifyed code
var foo = new Function('return ' + str + ';');
// And have fun
console.log(JSON.stringify(foo(), null, 4));
Try this:
var obj = {
a: "foo",
b: obj
};
var circular_replacer = (value) => {
var seen = [];
if (value != null && typeof value == "object") {
if (seen.indexOf(value) >= 0) return;
seen.push(value);
}
return value;
};
obj = circular_replacer(obj);
Although this has been answered sufficiently, you could also explicitly delete the property in question before stringification using the delete operator.
delete obj.b;
const jsonObject = JSON.stringify(obj);
delete operator
this will remove the need to build or maintain complex logic to remove circular references.
function myStringify(obj, maxDeepLevel = 2) {
if (obj === null) {
return 'null';
}
if (obj === undefined) {
return 'undefined';
}
if (maxDeepLevel < 0 || typeof obj !== 'object') {
return obj.toString();
}
return Object
.entries(obj)
.map(x => x[0] + ': ' + myStringify(x[1], maxDeepLevel - 1))
.join('\r\n');
}
Most of the answers in this thread are catered to use with JSON.stringify specifically -- they do not show how to actually remove circular-references in the original object-tree. (well, short of calling JSON.parse again afterward -- which requires reassignment, and has a higher performance impact)
For removing circular-references from the source object-tree, you can use a function such as this: https://stackoverflow.com/a/63952549/2441655
These general-purpose circular-reference-remover functions can then be used to make subsequent calls to circular-reference-sensitive functions (like JSON.stringify) safe:
const objTree = {normalProp: true};
objTree.selfReference = objTree;
RemoveCircularLinks(objTree); // without this line, the JSON.stringify call errors
console.log(JSON.stringify(objTree));
Here's a solution that:
removes cycles only (and not all duplicate object references, as do most of the solutions posted here so far),
is not unnecessarily verbose,
is fast,
does not require any library dependency.
function replaceCycles(obj, replacement = undefined, seen = new WeakSet()) {
if (typeof obj === 'object')
if (seen.has(obj))
return replacement
else {
seen.add(obj)
const newObj = {}
for (const key in obj)
newObj[key] = replaceCycles(obj[key], replacement, seen)
seen.delete(obj)
return newObj
}
else
return obj
}
Usage:
const a = {
b: 'v1',
c: {
d: 'v2'
}
}
a.e = a.c
a.c.f = a.c
console.log(JSON.stringify(replaceCycles(a, '[CYCLE]')))
Output:
"{'b':'v1','c':{'d':'v2','f':'[CYCLE]'},'e':{'d':'v2','f':'[CYCLE]'}}"
an other solution for resolving this issue with these kind of objects is that using this library
https://github.com/ericmuyser/stringy
its simple and you can in a few simple step solve this.
Based on the other answers I end up with the following code. It works pretty well with circular references, objects with custom constructors.
From the given object to be serialized,
Cache all the object you come across while traversing the object and assign each of them a unique hashID (an auto-incrementing number also works)
Once a circular reference is found mark that field in the new object as circular and store the hashID of the original object as an attribute.
Github Link - DecycledJSON
DJSHelper = {};
DJSHelper.Cache = [];
DJSHelper.currentHashID = 0;
DJSHelper.ReviveCache = [];
// DOES NOT SERIALIZE FUNCTION
function DJSNode(name, object, isRoot){
this.name = name;
// [ATTRIBUTES] contains the primitive fields of the Node
this.attributes = {};
// [CHILDREN] contains the Object/Typed fields of the Node
// All [CHILDREN] must be of type [DJSNode]
this.children = []; //Array of DJSNodes only
// If [IS-ROOT] is true reset the Cache and currentHashId
// before encoding
isRoot = typeof isRoot === 'undefined'? true:isRoot;
this.isRoot = isRoot;
if(isRoot){
DJSHelper.Cache = [];
DJSHelper.currentHashID = 0;
// CACHE THE ROOT
object.hashID = DJSHelper.currentHashID++;
DJSHelper.Cache.push(object);
}
for(var a in object){
if(object.hasOwnProperty(a)){
var val = object[a];
if (typeof val === 'object') {
// IF OBJECT OR NULL REF.
/***************************************************************************/
// DO NOT REMOVE THE [FALSE] AS THAT WOULD RESET THE [DJSHELPER.CACHE]
// AND THE RESULT WOULD BE STACK OVERFLOW
/***************************************************************************/
if(val !== null) {
if (DJSHelper.Cache.indexOf(val) === -1) {
// VAL NOT IN CACHE
// ADD THE VAL TO CACHE FIRST -> BEFORE DOING RECURSION
val.hashID = DJSHelper.currentHashID++;
//console.log("Assigned", val.hashID, "to", a);
DJSHelper.Cache.push(val);
if (!(val instanceof Array)) {
// VAL NOT AN [ARRAY]
try {
this.children.push(new DJSNode(a, val, false));
} catch (err) {
console.log(err.message, a);
throw err;
}
} else {
// VAL IS AN [ARRAY]
var node = new DJSNode(a, {
array: true,
hashID: val.hashID // HashID of array
}, false);
val.forEach(function (elem, index) {
node.children.push(new DJSNode("elem", {val: elem}, false));
});
this.children.push(node);
}
} else {
// VAL IN CACHE
// ADD A CYCLIC NODE WITH HASH-ID
this.children.push(new DJSNode(a, {
cyclic: true,
hashID: val.hashID
}, false));
}
}else{
// PUT NULL AS AN ATTRIBUTE
this.attributes[a] = 'null';
}
} else if (typeof val !== 'function') {
// MUST BE A PRIMITIVE
// ADD IT AS AN ATTRIBUTE
this.attributes[a] = val;
}
}
}
if(isRoot){
DJSHelper.Cache = null;
}
this.constructorName = object.constructor.name;
}
DJSNode.Revive = function (xmlNode, isRoot) {
// Default value of [isRoot] is True
isRoot = typeof isRoot === 'undefined'?true: isRoot;
var root;
if(isRoot){
DJSHelper.ReviveCache = []; //Garbage Collect
}
if(window[xmlNode.constructorName].toString().indexOf('[native code]') > -1 ) {
// yep, native in the browser
if(xmlNode.constructorName == 'Object'){
root = {};
}else{
return null;
}
}else {
eval('root = new ' + xmlNode.constructorName + "()");
}
//CACHE ROOT INTO REVIVE-CACHE
DJSHelper.ReviveCache[xmlNode.attributes.hashID] = root;
for(var k in xmlNode.attributes){
// PRIMITIVE OR NULL REF FIELDS
if(xmlNode.attributes.hasOwnProperty(k)) {
var a = xmlNode.attributes[k];
if(a == 'null'){
root[k] = null;
}else {
root[k] = a;
}
}
}
xmlNode.children.forEach(function (value) {
// Each children is an [DJSNode]
// [Array]s are stored as [DJSNode] with an positive Array attribute
// So is value
if(value.attributes.array){
// ITS AN [ARRAY]
root[value.name] = [];
value.children.forEach(function (elem) {
root[value.name].push(elem.attributes.val);
});
//console.log("Caching", value.attributes.hashID);
DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
}else if(!value.attributes.cyclic){
// ITS AN [OBJECT]
root[value.name] = DJSNode.Revive(value, false);
//console.log("Caching", value.attributes.hashID);
DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
}
});
// [SEPARATE ITERATION] TO MAKE SURE ALL POSSIBLE
// [CYCLIC] REFERENCES ARE CACHED PROPERLY
xmlNode.children.forEach(function (value) {
// Each children is an [DJSNode]
// [Array]s are stored as [DJSNode] with an positive Array attribute
// So is value
if(value.attributes.cyclic){
// ITS AND [CYCLIC] REFERENCE
root[value.name] = DJSHelper.ReviveCache[value.attributes.hashID];
}
});
if(isRoot){
DJSHelper.ReviveCache = null; //Garbage Collect
}
return root;
};
DecycledJSON = {};
DecycledJSON.stringify = function (obj) {
return JSON.stringify(new DJSNode("root", obj));
};
DecycledJSON.parse = function (json, replacerObject) {
// use the replacerObject to get the null values
return DJSNode.Revive(JSON.parse(json));
};
DJS = DecycledJSON;
Example Usage 1:
var obj = {
id:201,
box: {
owner: null,
key: 'storm'
},
lines:[
'item1',
23
]
};
console.log(obj); // ORIGINAL
// SERIALIZE AND THEN PARSE
var jsonObj = DJS.stringify(obj);
console.log(DJS.parse(jsonObj));
Example Usage 2:
// PERSON OBJECT
function Person() {
this.name = null;
this.child = null;
this.dad = null;
this.mom = null;
}
var Dad = new Person();
Dad.name = 'John';
var Mom = new Person();
Mom.name = 'Sarah';
var Child = new Person();
Child.name = 'Kiddo';
Dad.child = Mom.child = Child;
Child.dad = Dad;
Child.mom = Mom;
console.log(Child); // ORIGINAL
// SERIALIZE AND THEN PARSE
var jsonChild = DJS.stringify(Child);
console.log(DJS.parse(jsonChild));
I know this question is old and has lots of great answers but I post this answer because of it's new flavor (es5+)
Object.defineProperties(JSON, {
refStringify: {
value: function(obj) {
let objMap = new Map();
let stringified = JSON.stringify(obj,
function(key, value) {
// only for objects
if (typeof value == 'object') {
// If has the value then return a reference to it
if (objMap.has(value))
return objMap.get(value);
objMap.set(value, `ref${objMap.size + 1}`);
}
return value;
});
return stringified;
}
},
refParse: {
value: function(str) {
let parsed = JSON.parse(str);
let objMap = _createObjectMap(parsed);
objMap.forEach((value, key) => _replaceKeyWithObject(value, key));
return parsed;
}
},
});
// *************************** Example
let a = {
b: 32,
c: {
get a() {
return a;
},
get c() {
return a.c;
}
}
};
let stringified = JSON.refStringify(a);
let parsed = JSON.refParse(stringified, 2);
console.log(parsed, JSON.refStringify(parsed));
// *************************** /Example
// *************************** Helper
function _createObjectMap(obj) {
let objMap = new Map();
JSON.stringify(obj, (key, value) => {
if (typeof value == 'object') {
if (objMap.has(value))
return objMap.get(value);
objMap.set(value, `ref${objMap.size + 1}`);
}
return value;
});
return objMap;
}
function _replaceKeyWithObject(key, obj, replaceWithObject = obj) {
Object.keys(obj).forEach(k => {
let val = obj[k];
if (val == key)
return (obj[k] = replaceWithObject);
if (typeof val == 'object' && val != replaceWithObject)
_replaceKeyWithObject(key, val, replaceWithObject);
});
}
You could try the JSON parser library: treedoc. it supports circular references and also dedupes the repeated objects with references.
yarn add treedoc
import {TD} from 'treedoc'
TD.stringify(obj);
If you want more customization
import {TD, TDEncodeOption} from 'treedoc'
const opt = new TDEncodeOption();
opt.coderOption.setShowType(true).setShowFunction(true);
opt.jsonOption.setIndentFactor(2);
return TD.stringify(obj, opt);
The generated JSON file can be viewed by the viewer http://treedoc.org, which supports the navigation through JSON node references.
[shameless plug] I'm the author of this library
I created following method for my LoggingUtilities class.
Following method takes source and target objects, and assign source to target by given maxLevel.
static assignObjectByLevel(
sourceObject: any,
targetObject: any,
currentLevel: number = 0,
maxLevel: number = 3,
showUndefinedValues = false
): any {
if (currentLevel >= maxLevel) {
return;
}
const objQueue = [];
for (const key in sourceObject) {
if (sourceObject.hasOwnProperty(key)) {
const value = sourceObject[key];
if (typeof value === "object") {
objQueue.push({ key, value });
} else {
targetObject[key] = value;
}
} else {
if (showUndefinedValues) {
targetObject[key] = "undefined/null";
}
}
}
while (objQueue.length > 0) {
const objVal = objQueue.pop();
currentLevel++;
targetObject[objVal.key] = {};
this.assignObjectByLevel(
objVal.value,
targetObject[objVal.key],
currentLevel,
maxLevel,
false
);
}
}
Usage Example:
const logObjParam = {
level1: "value1",
level2: {
value2: "value2",
level3: {
value3: "value3",
level4: {
value4: " value4",
level5: {
value5: " value5",
},
},
},
},
};
let logObj = {};
this.assignObjectByLevel(logObjParam, logObj);
Result:
{
"level1": "value1",
"level2": {
"value2": "value2",
"level3": {
"value3": "value3",
"level4": {}
}
}
}
superserial fully serializes JavaScript objects.
https://github.com/denostack/superserial
Usage:
const serializer = new Serializer();
const nodes = [{ self: null as any, siblings: [] as any[] }, {
self: null as any,
siblings: [] as any[],
}];
nodes[0].self = nodes[0];
nodes[0].siblings = nodes;
nodes[1].self = nodes[1];
nodes[1].siblings = nodes;
const serialized = serializer.serialize(nodes);
console.log(serialized);
output:
[$1,$2];{"self":$1,"siblings":$0};{"self":$2,"siblings":$0}

Check if key exists in object **at any level** [duplicate]

I'm building an utility function that should search for a property name and return its value once it is found. It should do this recursively:
// Function
util.findVal = (object, propName) => {
for (let key in object) {
if (key === propName) {
console.log(propName)
console.log(object[key])
return object[key]
} else {
util.findVal(object[key], propName)
}
}
}
// Input
object: {
photo: {
progress: 20
}
}
// Usage
util.findVal(object, 'progress')
However the console log goes forever and the browser crashes. What am I doing wrong?
EDIT:
This is how I'm calling the function:
// Input
item: {
photo: {
file: {},
progress: 20
}
}
this.findProgress(item)
methods: {
findProgress (item) {
return util.findVal(item, this.propName)
}
}
You could use Object.keys and iterate with Array#some.
function findVal(object, key) {
var value;
Object.keys(object).some(function(k) {
if (k === key) {
value = object[k];
return true;
}
if (object[k] && typeof object[k] === 'object') {
value = findVal(object[k], key);
return value !== undefined;
}
});
return value;
}
var object = { photo: { progress: 20 }};
console.log(findVal(object, 'progress'));
Your code has a few errors:
You're recursively calling util.findVal but not returning the result of the call. Code should be return util.findVal(...)
You're not passing the attribute name key to the recursive call
You're not handling the possibility of a reference loop
If an object contains a key and also a sub-object that contains the key which value is returned is random (depends on the sequence in which the keys are analyzed)
The third problem is what can cause infinite recursion, for example:
var obj1 = {}, obj2 = {};
obj1.x = obj2; obj2.y = obj1;
if you just keep looking recursively searching in obj1 or obj2 could lead to infinite recursion.
Unfortunately for reasons not clear to me in Javascript is impossible to know the object "identity"... (what Python id(x) does) you can only compare an object to another. This means that to know if an object has already been seen in the past you need a linear scan with known objects.
ES6 added the possibility to check object identity with Set and Map where objects can be used as keys. This allows for faster (sub-linear) search times.
A search solution that runs in depth order could be for example:
function findVal(obj, key) {
var seen = new Set, active = [obj];
while (active.length) {
var new_active = [], found = [];
for (var i=0; i<active.length; i++) {
Object.keys(active[i]).forEach(function(k){
var x = active[i][k];
if (k === key) {
found.push(x);
} else if (x && typeof x === "object" &&
!seen.has(x)) {
seen.add(x);
new_active.push(x);
}
});
}
if (found.length) return found;
active = new_active;
}
return null;
}
given an object and an attribute name, returns all the values found with that name at the first depth they are found (there can be more than one value: for example when searching {x:{z:1}, y:{z:2}} for the key "z" two values are at the same depth).
The function also correctly handles self-referencing structures avoiding infinite search.
Don't write your own utility if you can avoid it.
Use something like jsonpath
Some examples of supported syntax:
JSONPath Description
$.store.book[*].author The authors of all books in the store
$..author All authors
$.store.* All things in store, which are some books and a red bicycle
$.store..price The price of everything in the store
$..book[2] The third book
$..book[(#.length-1)] The last book via script subscript
$..book[-1:] The last book via slice
$..book[0,1] The first two books via subscript union
$..book[:2] The first two books via subscript array slice
$..book[?(#.isbn)] Filter all books with isbn number
try changing else statement like this
return util.findVal(object[key],propName)
I know this is an old post, but I found it helpful to answer a problem I had with recursively finding a value by it's key. I further developed the answer given by Nina Scholz, and came up with the following. It should be quicker as it is not creating an array of all of the keys each time it is recursively invoked. Also, this will explicitly return false if the key is not found.
function findVal(obj, keyToFind) {
if (obj[keyToFind]) return obj[keyToFind];
for (let key in obj) {
if (typeof obj[key] === 'object') {
const value = findVal(obj[key], keyToFind);
if (value) return value;
}
}
return false;
}
var object = { photo: { progress: 20 }};
console.log(findVal(object, 'progress'));
I think you are saying that you want to look for the property name anywhere recursively within the objects tree of properties and sub-properties. If so, here is how I would approach this:
var object1 = _getInstance(); // somehow we get an object
var pname = 'PropNameA';
var findPropertyAnywhere = function (obj, name) {
var value = obj[name];
if (typeof value != 'undefined') {
return value;
}
foreach(var key in obj) {
var v2 = findPropertyAnywhere(obj[key], name);
if (typeof v2 != 'undefined') {
return v2;
}
}
return null;
}
findPropertyAnywhere(object1, pname);
Think about it if there is no key found.
I think you could do something like this instead of search
return object[propName] || null
In your code there was a breakpoint missing, I guess you are trying to search inside the whole object not just the directly related attributes so here is an edit for you code
EDIT:
util.findVal = (object, propName) =>{
if(!!object[propName]){
return object[propName]
}else{
for (let key in object) {
if(typeof object[key]=="object"){
return util.findVal(object[key], propName)
}else{
return null
}
}
}
}
Found this question in the realm of needing a general solution to check if an object contains a specific value anywhere in its hierarchy (regardless of the key), which can include arrays of course. So the following does not answer OPs question directly or improve upon other solutions but it might help others looking for the same thing I did and finding this post:
function hasValue(object, value) {
return Object.values(object).some(function(val) {
if (val === value) {
return true;
}
if (val && typeof val === 'object') {
return hasValue(val, value);
}
if (val && val.isArray()) {
return val.some((obj) => {
return hasValue(obj, value);
})
}
});
}
it is of course inspired by #Nina Scholz 's solution!
An answer depends a on how complex you want to get. For example a JSON parsed array doesn't contain functions - and I'm fairly certain it won't contain property value set to a parent node in object tree.
This version returns the property value of the first property name found whilst searching the object tree. undefined is returned if either the named property was not found or has a value of undefined. Some modifications would be needed to tell the difference. It does not re-search parent nodes already being searched, nor try to scan null objects!
let util = {};
util.findVal = (object, propName, searched=[]) => {
searched.push( object)
for (let key in object) {
if (key === propName) {
return object[key]
}
else {
let obj = object[ key]
if( obj && (typeof obj == "object" || typeof obj == "function")) {
if( searched.indexOf(obj) >=0) {
continue
}
let found = util.findVal(obj, propName, searched)
if( found != searched) {
return found
}
}
}
}
searched.pop();
// not in object:
return searched.length ? searched : undefined
}
I ended up writing this function.
It is a refactor of a function found here: Recursively looping through an object to build a property list
added a depth parameter to avoid stack overflow in chrome devtools.
function iterate(obj, context, search, depth) {
for (var property in obj) {
if (Object.prototype.hasOwnProperty.call(obj, property)) {
if(typeof obj[property] == 'function') continue;
if( property == search ){
console.log(context+property);
return;
}
if (typeof obj[property] == "object" && depth < 7) {
//console.log('--- going in: ' + context+property);
iterate(obj[property], context+property+'.', search, depth+1);
}
/*else {
console.log(context+property);
}*/
}
}
}
Returns the value of the field with the specified name.
data is the root node/object.
keyName is a string name of the field/member.
If keyName specifies a field that is itself an object, then that object is returned.
function find (data, keyName) {
for (const key in data) {
const entry = data[key]
if (key === keyName)
return entry
if (typeof entry === 'object') {
const found = find(entry, keyName)
if (found)
return found
}
}
}
The for loop goes through each field and if that field is an object then it will recurse into that object.
Here is a piece of code which find the key you are looking for in your rootObj tree. And add it to the root object. So by the end you will have access to you key like this rootObj[key].
findKeyVal(object, key, rootObj) {
if(object instanceof Object) {
let keys = Object.keys(object);
if(keys.includes(key) && !isNullOrUndefined(object[key])) {
rootObj[key] = object[key];
return;
}
else {
keys.filter(k => object[k] instanceof Object).forEach( k => {
this.findKeyVal(object[k], key, rootObj);
})
}
}
}
Old question, but to check if the property exists anywhere in the hierarchy of an object, try this simple option
var obj = {
firstOperand: {
firstOperand: {
firstOperand: {
sweptArea: 5
}
}
}
};
function doesPropertyExists ( inputObj, prop )
{
return JSON.stringify(obj).indexOf( "\""+ prop +"\":" ) != -1;
};
console.log( doesPropertyExists( obj, "sweptArea" ) );
console.log( doesPropertyExists( obj, "firstOperand" ) );
console.log( doesPropertyExists( obj, "firstOperand22" ) );

Shortest way to set value to variable in Knockout

In Knockout I have observable variable location. It is of type LocationEdit. This viewModel has observable and not fields.
I have collection of field names : fields. For each field I want to reset values for location
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
if (ko.isObservable(this.location()[field])) {
this.location()[field](locs[0][field]);
} else {
this.location()[field] = locs[0][field];
}
}
});
To make this code more simpler (remove if-clauses), Can I somehow set value to this.location()[field] in one line?
You could use the conditional operator (... ? ... : ... ;) although it doesn't change much:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
ko.isObservable(this.location()[field]) ? this.location()[field](locs[0][field]) : this.location()[field] = locs[0][field];
}
});
Or you could write a function:
function upd(arr, index, val) {
ko.isObservable(arr[index]) ? arr[index](val) : arr[index] = val;
}
Usage:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
upd(this.location(), field, locs[0][field]);
}
});
See demo.
You could even add this function to ko:
if(typeof ko.updatePotentialObservable == 'undefined')
ko.updatePotentialObservable = function (arr[index], val) {
ko.isObservable(obj) ? arr[index](val) : arr[index]= val;
}
Usage:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
ko.updatePotentialObservable(this.location(), field, locs[0][field]);
}
});
See other demo
To be honest, I think Gôtô's answers are definitely your best options. Basically, you'd want to create a utility function similar to ko.unwrap but setting a value.
But since you said "also want to find another solution", here's a different utility function. I think the most confusing part of your code is the returning calls to locs[0][field] and this.location()[field]. I'd want something with this signature:
reset(source, target, keys);
So, in your code, you could do:
reset(
this.location(),
locs[0],
fields.filter(f => this.uniqueField(locs, f))
);
Now, writing this method, I ended up with this:
const mergePropsObs = (function() {
// Return a method for setting a specific property in object
const getSetter = obj => prop => ko.isObservable(obj[prop])
? obj[prop]
: val => obj[prop] = val;
// Return unique keys for two objects
// (I went with a quick oneliner; there are many ways to do this)
const allKeys = (obj1, obj2) =>
Object.keys(Object.assign({}, obj1, obj2));
return (base, ext, onlyProps) => {
const props = onlyProps || allKeys(base, ext);
const values = props.map(p => ko.unwrap(ext[p]));
props
.map(getSetter(base))
.forEach((setter, i) => setter(values[i]));
};
}());
var base = { a: 1, b: ko.observable(2), c: 5 };
mergePropsObs(
base,
{ a: 2, b: 3 },
["a", "b"]);
console.log(base.a);
console.log(base.b());
console.log(base.c);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
This utility method can be used with the signature mentioned above. It also has a fallback for when you don't provide an array of field names.

Search recursively for value in object by property name

I'm building an utility function that should search for a property name and return its value once it is found. It should do this recursively:
// Function
util.findVal = (object, propName) => {
for (let key in object) {
if (key === propName) {
console.log(propName)
console.log(object[key])
return object[key]
} else {
util.findVal(object[key], propName)
}
}
}
// Input
object: {
photo: {
progress: 20
}
}
// Usage
util.findVal(object, 'progress')
However the console log goes forever and the browser crashes. What am I doing wrong?
EDIT:
This is how I'm calling the function:
// Input
item: {
photo: {
file: {},
progress: 20
}
}
this.findProgress(item)
methods: {
findProgress (item) {
return util.findVal(item, this.propName)
}
}
You could use Object.keys and iterate with Array#some.
function findVal(object, key) {
var value;
Object.keys(object).some(function(k) {
if (k === key) {
value = object[k];
return true;
}
if (object[k] && typeof object[k] === 'object') {
value = findVal(object[k], key);
return value !== undefined;
}
});
return value;
}
var object = { photo: { progress: 20 }};
console.log(findVal(object, 'progress'));
Your code has a few errors:
You're recursively calling util.findVal but not returning the result of the call. Code should be return util.findVal(...)
You're not passing the attribute name key to the recursive call
You're not handling the possibility of a reference loop
If an object contains a key and also a sub-object that contains the key which value is returned is random (depends on the sequence in which the keys are analyzed)
The third problem is what can cause infinite recursion, for example:
var obj1 = {}, obj2 = {};
obj1.x = obj2; obj2.y = obj1;
if you just keep looking recursively searching in obj1 or obj2 could lead to infinite recursion.
Unfortunately for reasons not clear to me in Javascript is impossible to know the object "identity"... (what Python id(x) does) you can only compare an object to another. This means that to know if an object has already been seen in the past you need a linear scan with known objects.
ES6 added the possibility to check object identity with Set and Map where objects can be used as keys. This allows for faster (sub-linear) search times.
A search solution that runs in depth order could be for example:
function findVal(obj, key) {
var seen = new Set, active = [obj];
while (active.length) {
var new_active = [], found = [];
for (var i=0; i<active.length; i++) {
Object.keys(active[i]).forEach(function(k){
var x = active[i][k];
if (k === key) {
found.push(x);
} else if (x && typeof x === "object" &&
!seen.has(x)) {
seen.add(x);
new_active.push(x);
}
});
}
if (found.length) return found;
active = new_active;
}
return null;
}
given an object and an attribute name, returns all the values found with that name at the first depth they are found (there can be more than one value: for example when searching {x:{z:1}, y:{z:2}} for the key "z" two values are at the same depth).
The function also correctly handles self-referencing structures avoiding infinite search.
Don't write your own utility if you can avoid it.
Use something like jsonpath
Some examples of supported syntax:
JSONPath Description
$.store.book[*].author The authors of all books in the store
$..author All authors
$.store.* All things in store, which are some books and a red bicycle
$.store..price The price of everything in the store
$..book[2] The third book
$..book[(#.length-1)] The last book via script subscript
$..book[-1:] The last book via slice
$..book[0,1] The first two books via subscript union
$..book[:2] The first two books via subscript array slice
$..book[?(#.isbn)] Filter all books with isbn number
try changing else statement like this
return util.findVal(object[key],propName)
I know this is an old post, but I found it helpful to answer a problem I had with recursively finding a value by it's key. I further developed the answer given by Nina Scholz, and came up with the following. It should be quicker as it is not creating an array of all of the keys each time it is recursively invoked. Also, this will explicitly return false if the key is not found.
function findVal(obj, keyToFind) {
if (obj[keyToFind]) return obj[keyToFind];
for (let key in obj) {
if (typeof obj[key] === 'object') {
const value = findVal(obj[key], keyToFind);
if (value) return value;
}
}
return false;
}
var object = { photo: { progress: 20 }};
console.log(findVal(object, 'progress'));
I think you are saying that you want to look for the property name anywhere recursively within the objects tree of properties and sub-properties. If so, here is how I would approach this:
var object1 = _getInstance(); // somehow we get an object
var pname = 'PropNameA';
var findPropertyAnywhere = function (obj, name) {
var value = obj[name];
if (typeof value != 'undefined') {
return value;
}
foreach(var key in obj) {
var v2 = findPropertyAnywhere(obj[key], name);
if (typeof v2 != 'undefined') {
return v2;
}
}
return null;
}
findPropertyAnywhere(object1, pname);
Think about it if there is no key found.
I think you could do something like this instead of search
return object[propName] || null
In your code there was a breakpoint missing, I guess you are trying to search inside the whole object not just the directly related attributes so here is an edit for you code
EDIT:
util.findVal = (object, propName) =>{
if(!!object[propName]){
return object[propName]
}else{
for (let key in object) {
if(typeof object[key]=="object"){
return util.findVal(object[key], propName)
}else{
return null
}
}
}
}
Found this question in the realm of needing a general solution to check if an object contains a specific value anywhere in its hierarchy (regardless of the key), which can include arrays of course. So the following does not answer OPs question directly or improve upon other solutions but it might help others looking for the same thing I did and finding this post:
function hasValue(object, value) {
return Object.values(object).some(function(val) {
if (val === value) {
return true;
}
if (val && typeof val === 'object') {
return hasValue(val, value);
}
if (val && val.isArray()) {
return val.some((obj) => {
return hasValue(obj, value);
})
}
});
}
it is of course inspired by #Nina Scholz 's solution!
An answer depends a on how complex you want to get. For example a JSON parsed array doesn't contain functions - and I'm fairly certain it won't contain property value set to a parent node in object tree.
This version returns the property value of the first property name found whilst searching the object tree. undefined is returned if either the named property was not found or has a value of undefined. Some modifications would be needed to tell the difference. It does not re-search parent nodes already being searched, nor try to scan null objects!
let util = {};
util.findVal = (object, propName, searched=[]) => {
searched.push( object)
for (let key in object) {
if (key === propName) {
return object[key]
}
else {
let obj = object[ key]
if( obj && (typeof obj == "object" || typeof obj == "function")) {
if( searched.indexOf(obj) >=0) {
continue
}
let found = util.findVal(obj, propName, searched)
if( found != searched) {
return found
}
}
}
}
searched.pop();
// not in object:
return searched.length ? searched : undefined
}
I ended up writing this function.
It is a refactor of a function found here: Recursively looping through an object to build a property list
added a depth parameter to avoid stack overflow in chrome devtools.
function iterate(obj, context, search, depth) {
for (var property in obj) {
if (Object.prototype.hasOwnProperty.call(obj, property)) {
if(typeof obj[property] == 'function') continue;
if( property == search ){
console.log(context+property);
return;
}
if (typeof obj[property] == "object" && depth < 7) {
//console.log('--- going in: ' + context+property);
iterate(obj[property], context+property+'.', search, depth+1);
}
/*else {
console.log(context+property);
}*/
}
}
}
Returns the value of the field with the specified name.
data is the root node/object.
keyName is a string name of the field/member.
If keyName specifies a field that is itself an object, then that object is returned.
function find (data, keyName) {
for (const key in data) {
const entry = data[key]
if (key === keyName)
return entry
if (typeof entry === 'object') {
const found = find(entry, keyName)
if (found)
return found
}
}
}
The for loop goes through each field and if that field is an object then it will recurse into that object.
Here is a piece of code which find the key you are looking for in your rootObj tree. And add it to the root object. So by the end you will have access to you key like this rootObj[key].
findKeyVal(object, key, rootObj) {
if(object instanceof Object) {
let keys = Object.keys(object);
if(keys.includes(key) && !isNullOrUndefined(object[key])) {
rootObj[key] = object[key];
return;
}
else {
keys.filter(k => object[k] instanceof Object).forEach( k => {
this.findKeyVal(object[k], key, rootObj);
})
}
}
}
Old question, but to check if the property exists anywhere in the hierarchy of an object, try this simple option
var obj = {
firstOperand: {
firstOperand: {
firstOperand: {
sweptArea: 5
}
}
}
};
function doesPropertyExists ( inputObj, prop )
{
return JSON.stringify(obj).indexOf( "\""+ prop +"\":" ) != -1;
};
console.log( doesPropertyExists( obj, "sweptArea" ) );
console.log( doesPropertyExists( obj, "firstOperand" ) );
console.log( doesPropertyExists( obj, "firstOperand22" ) );

WeakMap polyfill throwing error when trying to define property on inextensible object

The WeakMap polyfill is throwing an error when trying to define property on inextensible object.
Those are in the middle of a bunch of node, javascript code and libraries so I couldn't actually point out where causes the issue. There are many other libraries which also have their own polyfill. It's difficult to debug in which library causes the error. And, the errors are ONLY on IE10.
To get rid of it, I added a checker before defining a property like (line 26 in the above file):
if (entry && entry[0] === key) {
entry[1] = value;
}
else if (Object.isExtensible(key)) {
defineProperty(key, this.name, {
value: [ key, value ],
writable: true
});
}
My question is, Is it safe/right to fix it as in my code above? If not, how should I fix my problem?
As best I can tell that weakMap polyfill is ONLY designed to work with extensible objects as keys. It simply won't work with a non-extensible object.
Your modification has made it so it doesn't throw an exception, but then the non-extensible item will not be in the weakMap either. So, your fix isn't really a fix. That particular polyfill would have to be rewritten to handle non-extensible keys. It would not be a simple fix as it would need a conceptually different approach.
There are a number of other polyfills that take a different approach. I have not researched which ones might be better in this regard. I suspect it's a little bit of conundrum between actually being "weak" (e.g. allows garbage collection) vs. can handle a non-extensible object. The fundamental issue is that if you are going to be weak, then you can't store a reference to the object itself. So, you need to store a reference to some string representation of the object. Well, JS objects don't have a built-in guaranteed unique string representation. So, the usually fix is to coin one using some sort of counter and store it on the object as a property and then you store that string representation in your map. But, if the object isn't extensible, then you can't do that either. So, you're stuck storing the actual object reference in your map, but then it's not really "weak" any more. You can see how you're kind of stuck.
I think this is one case where a polyfill just can't quite live up to the real thing. Different polyfills will have different tradeoffs in this regard. You picked one that is truly weak, but requires the objects to be extensible so a property can be added.
As in my question, I want a solution which does not have huge impact on other functionality or plugins, so here is my fix for this issue. The fix is based on the answer of #jfriend00 above and reference from other implementation in the internet.
var defineProperty = Object.defineProperty;
var counter = Date.now() % 1e9;
var FrozenStore = function() {
this.a = [];
};
var findFrozen = function(store, key){
return store.a.forEach(function(it){
if (it[0] === key) {
return it;
}
});
};
var findIndexFrozen = function(store, key){
return store.a.forEach(function(it, id){
if (it[0] === key) {
return id;
}
});
};
FrozenStore.prototype = {
get: function(key){
var entry = findFrozen(this, key);
if (entry) return entry[1];
},
has: function(key){
return !!findFrozen(this, key);
},
set: function(key, value){
var entry = findFrozen(this, key);
if (entry) entry[1] = value;
else this.a.push([key, value]);
},
"delete": function(key){
var index = findIndexFrozen(this, key);
if (~index) this.a.splice(index, 1);
return !!~index;
}
};
var WeakMap = function() {
this.name = "__st" + (Math.random() * 1e9 >>> 0) + (counter++ + "__");
};
var frozenStore = function(that){
return that._l || (that._l = new FrozenStore);
};
WeakMap.prototype = {
set: function(key, value) {
var entry = key[this.name];
if (entry && entry[0] === key) {
entry[1] = value;
} else {
if (!Object.isExtensible(key)) {
frozenStore(this).set(key, value);
} else {
defineProperty(key, this.name, {
value: [ key, value ],
writable: true
});
}
}
return this;
},
get: function(key) {
var entry;
if ((entry = key[this.name]) && entry[0] === key) {
return entry[1];
} else if (!Object.isExtensible(key)) {
frozenStore(this).get(key);
} else {
return undefined;
}
},
"delete": function(key) {
var entry = key[this.name];
if (!entry || entry[0] !== key) return false;
if (!Object.isExtensible(key)) frozenStore(this)['delete'](key);
entry[0] = entry[1] = undefined;
return true;
},
has: function(key) {
var entry = key[this.name];
if (!entry) return false;
if(!Object.isExtensible(key)) return frozenStore(this).has(key);
return entry[0] === key;
}
};
window.WeakMap = WeakMap;
This introduces the FrozenStore which will manage all non-extensible keys being added to WeakMap. I'm not sure whether it breaks the concept of WeakMap or not but it does rescue me from the issue.

Categories

Resources