How to create a Deep Proxy (aka Proxy Membrane)? - javascript

How can I create a deep/recursive Proxy?
Specifically, I want to know whenever a property is set or modified anywhere in the object tree.
Here's what I've got so far:
function deepProxy(obj) {
return new Proxy(obj, {
set(target, property, value, receiver) {
console.log('set', property,'=', value);
if(typeof value === 'object') {
for(let k of Object.keys(value)) {
if(typeof value[k] === 'object') {
value[k] = deepProxy(value[k]);
}
}
value = deepProxy(value);
}
target[property] = value;
return true;
},
deleteProperty(target, property) {
if(Reflect.has(target, property)) {
let deleted = Reflect.deleteProperty(target, property);
if(deleted) {
console.log('delete', property);
}
return deleted;
}
return false;
}
});
}
And here's my test:
const proxy = deepProxy({});
const baz = {baz: 9, quux: {duck: 6}};
proxy.foo = 5;
proxy.bar = baz;
proxy.bar.baz = 10;
proxy.bar.quux.duck = 999;
baz.quux.duck = 777;
delete proxy.bar;
delete proxy.bar; // should not trigger notifcation -- property was already deleted
baz.quux.duck = 666; // should not trigger notification -- 'bar' was detached
console.log(proxy);
And the output:
set foo = 5
set bar = { baz: 9, quux: { duck: 6 } }
set baz = 10
set duck = 999
set duck = 777
delete bar
set duck = 666
{ foo: 5 }
As you can see, I've just about got it working, except baz.quux.duck = 666 is triggering the setter even though I've removed it from proxy's object tree. Is there any way to de-proxify baz after the property has been deleted?

Fixed a bunch of bugs in my original question. I think this works now:
function createDeepProxy(target, handler) {
const preproxy = new WeakMap();
function makeHandler(path) {
return {
set(target, key, value, receiver) {
if (value != null && typeof value === 'object') {
value = proxify(value, [...path, key]);
}
target[key] = value;
if (handler.set) {
handler.set(target, [...path, key], value, receiver);
}
return true;
},
deleteProperty(target, key) {
if (Reflect.has(target, key)) {
unproxy(target, key);
let deleted = Reflect.deleteProperty(target, key);
if (deleted && handler.deleteProperty) {
handler.deleteProperty(target, [...path, key]);
}
return deleted;
}
return false;
}
}
}
function unproxy(obj, key) {
if (preproxy.has(obj[key])) {
// console.log('unproxy',key);
obj[key] = preproxy.get(obj[key]);
preproxy.delete(obj[key]);
}
for (let k of Object.keys(obj[key])) {
if (obj[key][k] != null && typeof obj[key][k] === 'object') {
unproxy(obj[key], k);
}
}
}
function proxify(obj, path) {
for (let key of Object.keys(obj)) {
if (obj[key] != null && typeof obj[key] === 'object') {
obj[key] = proxify(obj[key], [...path, key]);
}
}
let p = new Proxy(obj, makeHandler(path));
preproxy.set(p, obj);
return p;
}
return proxify(target, []);
}
let obj = {
foo: 'baz',
}
let proxied = createDeepProxy(obj, {
set(target, path, value, receiver) {
console.log('set', path.join('.'), '=', JSON.stringify(value));
},
deleteProperty(target, path) {
console.log('delete', path.join('.'));
}
});
proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
proxied.null = null;
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'
You can assign full objects to properties and they'll get recursively proxified, and then when you delete them out of the proxied object they'll get deproxied so that you don't get notifications for objects that are no longer part of the object-graph.
I have no idea what'll happen if you create a circular linking. I don't recommend it.

Here's a simpler one that does what I think you wanted.
This example allows you to get or set any properties deeply, and calls a change handler on any property (deep or not) to show that it works:
function createOnChangeProxy(onChange, target) {
return new Proxy(target, {
get(target, property) {
const item = target[property]
if (item && typeof item === 'object') return createOnChangeProxy(onChange, item)
return item
},
set(target, property, newValue) {
target[property] = newValue
onChange()
return true
},
})
}
let changeCount = 0
const o = createOnChangeProxy(() => changeCount++, {})
o.foo = 1
o.bar = 2
o.baz = {}
o.baz.lorem = true
o.baz.yeee = {}
o.baz.yeee.wooo = 12
console.log(changeCount === 6)
const proxy = createOnChangeProxy(() => console.log('change'), {})
const baz = {baz: 9, quux: {duck: 6}};
proxy.foo = 5;
proxy.bar = baz;
proxy.bar.baz = 10;
proxy.bar.quux.duck = 999;
baz.quux.duck = 777;
delete proxy.bar;
delete proxy.bar; // should not trigger notifcation -- property was already deleted
baz.quux.duck = 666; // should not trigger notification -- 'bar' was detached
console.log(proxy);
In the part that uses your code sample, there are no extra notifications like your comments wanted.

#mpen answer ist awesome. I moved his example into a DeepProxy class that can be extended easily.
class DeepProxy {
constructor(target, handler) {
this._preproxy = new WeakMap();
this._handler = handler;
return this.proxify(target, []);
}
makeHandler(path) {
let dp = this;
return {
set(target, key, value, receiver) {
if (typeof value === 'object') {
value = dp.proxify(value, [...path, key]);
}
target[key] = value;
if (dp._handler.set) {
dp._handler.set(target, [...path, key], value, receiver);
}
return true;
},
deleteProperty(target, key) {
if (Reflect.has(target, key)) {
dp.unproxy(target, key);
let deleted = Reflect.deleteProperty(target, key);
if (deleted && dp._handler.deleteProperty) {
dp._handler.deleteProperty(target, [...path, key]);
}
return deleted;
}
return false;
}
}
}
unproxy(obj, key) {
if (this._preproxy.has(obj[key])) {
// console.log('unproxy',key);
obj[key] = this._preproxy.get(obj[key]);
this._preproxy.delete(obj[key]);
}
for (let k of Object.keys(obj[key])) {
if (typeof obj[key][k] === 'object') {
this.unproxy(obj[key], k);
}
}
}
proxify(obj, path) {
for (let key of Object.keys(obj)) {
if (typeof obj[key] === 'object') {
obj[key] = this.proxify(obj[key], [...path, key]);
}
}
let p = new Proxy(obj, this.makeHandler(path));
this._preproxy.set(p, obj);
return p;
}
}
// TEST DeepProxy
let obj = {
foo: 'baz',
}
let proxied = new DeepProxy(obj, {
set(target, path, value, receiver) {
console.log('set', path.join('.'), '=', JSON.stringify(value));
},
deleteProperty(target, path) {
console.log('delete', path.join('.'));
}
});
proxied.foo = 'bar';
proxied.deep = {}
proxied.deep.blue = 'sea';
delete proxied.foo;
delete proxied.deep; // triggers delete on 'deep' but not 'deep.blue'

Related

Copy properties of object to another skipping undefined ones

How can I copy a list of properties from a object to another skipping the null or undefined values?
Something like this
const obj1 = { prop1: 'value', prop2: undefined }
const obj2 = copy(obj1, ['prop1', 'prop2', 'prop3']) // prop3 doesn't exist and prop2 should be excluded
console.log(obj2) // should print { prop1: 'value' }
Here using Object.fromEntries() and Array.prototype.filter().
see: Object.fromEntries(): Object transformations
const obj1 = { prop1: 'value', prop2: undefined }
const copy = (obj, props) => {
return Object.fromEntries(Object.entries(obj)
.filter(([k, v]) => (
(props.includes(k) && v !== undefined && v !== null)
)));
}
const obj2 = copy(obj1, ['prop1', 'prop2', 'prop3'])
console.log(obj2)
Try this:
const obj1 = { prop1: 'value', prop2: undefined }
function copy(obj1, props)
{
return props.reduce(
(acum, current) =>
(obj1[current] !== undefined && obj1[current] !== null) ?
({...acum, [current]:obj1[current]}):
acum
,{}
)
}
const obj2 = copy(obj1, ['prop1', 'prop2', 'prop3']) // prop3 doesn't exist and prop2 should be excluded
console.log(obj2) // should print { prop1: 'value' }
function copy(object, keys) {
const clone = {};
for(const key of keys) {
if(!isNaN(object[key])) clone[key] = object[key];
}
return clone;
}
It's not clear what the purpose of the array is in your code, but this should work for the problem as you stated it. It reduces over the entries of the original object with a new object as an accumulator, and only copies over properties with defined values. To include null in the check, you can change the condition to be != null (because undefined == null).
const copyExcludingUndefined = (obj = {}) =>
Object.entries(obj)
.reduce((a, [k, v]) => {
if (v !== undefined) {
a[k] = v
}
return a
}, {})
Write your own copy function :)
let copy = function(o, args) {
let obj = {};
if (Array.isArray(args)) {
args.forEach(arg => {
if(o[arg] !== undefined) {
obj[arg] = o[arg];
}
});
}
return obj;
}

fastest way to get a property by name if it exists anywhere in the object graph

I want to be able to retrieve the value of a property if it exists anywhere within the object graph and I would like to know if there is a faster way to the option I have come up with below.
I have specifically opted for tree traversal instead of recursion, because it uses less stack frames and memory and because traversal is faster.
example:
const obj= {someObj:{}, someOtherObj: { nestedObjet: {featureRole: "canary"}}}
const prop= findProperties(obj, "featureRole");
console.log(prop); //canary
breadth first search properties
findProperty(obj: Record<string, any>, propId: string): any {
if (obj && obj[propId] != undefined) {
return obj[propId];
}
const props = [obj];
while (props.length) {
const prop = props.shift();
if (typeof prop === 'object') {
if (prop && prop[propId] != undefined) {
return prop[propId];
}
for (const innerProp in prop) {
if (Object.prototype.hasOwnProperty.call(prop, innerProp) && typeof prop[innerProp] === 'object') {
props.push(prop[innerProp]);
}
}
}
}
You can do this recursively, either returning as soon as a value is found:
const obj = {
someObj: {}, someOtherObj: { nestedObjet: { featureRole: "canary" } },
obj: { someOtherObj: { nestedObjet: { featureRole: "cockatoo" } } }
}
const findProperties = (obj, prop) => {
if (obj[prop] !== undefined) {
return obj[prop];
}
for (const key in obj) {
if (typeof obj[key] === 'object') {
const res = findProperties(obj[key], prop);
if (res !== null) {
return res
}
}
}
return null;
}
const prop = findProperties(obj, "featureRole");
console.log(prop); //canary
Or collecting all the possible matches:
const obj = {
someObj: {}, someOtherObj: { nestedObjet: { featureRole: "canary" } },
obj: { someOtherObj: { nestedObjet: { featureRole: "cockatoo" } } }
}
const findProperties = (obj, prop, res = []) => {
if (obj[prop] !== undefined) {
res.push(obj[prop]);
}
for (const key in obj) {
if (typeof obj[key] === 'object') {
findProperties(obj[key], prop, res);
}
}
return res;
}
const prop = findProperties(obj, "featureRole");
console.log(prop); // ["canary", "cockatoo"]
Solution with some restrictions like \" in property name/value:
findProperties = (o, prop) => JSON.stringify(o).match(new RegExp(`"\(${prop}\)":"\([^"]*\)"`, 'gi')).map(m => m.split(':').map(str => str.replace(/"/gi, '')));
Usage:
findProperties(obj, "featureRole").map(([,v]) => console.log(v));
You can use recursivity to search in your object.
const object = {
a: {
d: 24
},
b: {
e: 5,
f: {
g: 'B'
}
},
c: 12
};
function getProp(object, propName) {
if (propName in object) {
return object[propName];
} else {
for (const prop in object) {
if (typeof object[prop] === 'object') {
const results = getProp(object[prop], propName);
if(!!results) {
return results;
}
}
}
}
}
You can play with it here: https://jsfiddle.net/tpbhexgw/10/
Here is an iterative solution using object-scan.
This library is build from the ground up for speed. It uses iteration instead of recursion internally.
// const objectScan = require('object-scan');
const obj = { someObj: {}, otherObj: { some: { deep: { nested: { key: {} } } } }, someOtherObj: { nestedObjet: { featureRole: 'canary' } } };
// compile search function (only needs to be done once)
const searchFn = objectScan(['**.featureRole'], { abort: true, rtn: 'bool' });
// execute search
console.log(searchFn(obj));
// => true
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#14.0.0"></script>
Disclaimer: I'm the author of object-scan
While this will not be faster than writing your own iterative solution, it has the major advantage that it will prune the search tree if you have any other meta information about the key position.
For example if you knew that the key is at exactly one or two nestings deep, you could write the following and it would never search deeper than that
// const objectScan = require('object-scan');
const obj = { someObj: {}, otherObj: { some: { deep: { nested: { key: {} } } } }, someOtherObj: { nestedObjet: { featureRole: 'canary' } } };
const searchFn = objectScan(['{*,*.*}.featureRole'], { abort: true, rtn: 'bool' });
console.log(searchFn(obj));
// => true
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#14.0.0"></script>
Disclaimer: I'm the author of object-scan
These kind of changes have typically a much greater impact than micro-optimizing code (that a different compiler might do who knows what with anyways).

Lodash findKey() method

As a part of a challenge I need to implement the .findKey() method myself. Below is the solution proposition, however, I get an error "predicate is not a function".
const _ = {
findKey(object, predicate) {
for (let key in object) {
let value = object[key];
let predicateReturnValue = predicate(value);
if (predicateReturnValue) {
return key;
};
};
undefined
return undefined;
}
};
Can anyone help?
function findKey(object, predicate) {
for (let key in object) {
let value = object[key];
let predicateReturnValue = predicate(value);
if (predicateReturnValue) { // just take the value
return key; // return key
}
}
}
const isEqual = a => b => a === b
const object = { a: 'Apple', b: 'Beer', c: 'Cake' }
alert(findKey(object, isEqual('Apple')));
alert(findKey(object, isEqual('Cakes')));

Why my recursive function works only for one level?

I am trying to learn how to cope with the objects and arrays and I saw many ways of iterating objects but recursing doesn't work for me and I don't understand why. What am I doing wrong?
I need to loop through an object and just slightly change something in an array. In my case, it's uppercasing the keys
Here is what I've got for now
const development = {
port: 8080,
db: {
username: "jkdfs",
password: "dsfsdg",
name: "kslfjskd",
test: { test: 12, another: 'value' }
},
token_secret: "jfkjdsj",
hash_rounds: "kjsfkljdfkl"
};
function throughObject(obj) {
let collection = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let value = obj[key];
if (typeof obj[key] !== 'object') {
collection[key.toUpperCase()] = value;
} else {
collection[key.toUpperCase()] = nestedObject(obj[key]);
}
}
function nestedObject(nested) {
const sub = {};
for (const k in nested) {
let v = nested[k];
if (typeof nested[k] !== 'object') {
sub[k.toUpperCase()] = v;
} else {
nestedObject(v);
}
}
return sub;
}
}
return collection;
}
const r = throughObject(development);
console.log(r);
When you're recursively calling the function on an object value, you still need to assign it to the sub object: sub[k.toUpperCase()] = nestedObject(v). Also, you don't need 2 different functions.
const development = {
port: 8080,
db: {
username: "jkdfs",
password: "dsfsdg",
name: "kslfjskd",
test: { test: 12, another: 'value' }
},
token_secret: "jfkjdsj",
hash_rounds: "kjsfkljdfkl"
};
function nestedObject(nested) {
const sub = {};
for (const k in nested) {
const v = nested[k];
if (typeof nested[k] !== 'object')
sub[k.toUpperCase()] = v;
else
sub[k.toUpperCase()] = nestedObject(v); // <- HERE
}
return sub;
}
console.log(nestedObject(development))
Here's a shorter version using Object.fromEntries()
const development={port:8080,db:{username:"jkdfs",password:"dsfsdg",name:"kslfjskd",test:{test:12,another:"value"}},token_secret:"jfkjdsj",hash_rounds:"kjsfkljdfkl"};
const convert = o =>
Object.fromEntries(
Object.entries(o).map(([k, v]) =>
[k.toUpperCase(), Object(v) === v ? convert(v) : v]
)
)
console.log(convert(development))

javascript implementation of lodash "set" method

Found this excellent code for _.get vanilla js implementation:
const get = (obj, path, defaultValue) => path.split(".")
.reduce((a, c) => (a && a[c] ? a[c] : (defaultValue || null)), obj)
Now I'm looking for _.set implementation, any help would be appreciated.
I think this could cover it:
const set = (obj, path, value) => {
if (Object(obj) !== obj) return obj; // When obj is not an object
// If not yet an array, get the keys from the string-path
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
path.slice(0,-1).reduce((a, c, i) => // Iterate all of them except the last one
Object(a[c]) === a[c] // Does the key exist and is its value an object?
// Yes: then follow that path
? a[c]
// No: create the key. Is the next key a potential array-index?
: a[c] = Math.abs(path[i+1])>>0 === +path[i+1]
? [] // Yes: assign a new array object
: {}, // No: assign a new plain object
obj)[path[path.length-1]] = value; // Finally assign the value to the last key
return obj; // Return the top-level object to allow chaining
};
// Demo
var obj = { test: true };
set(obj, "test.1.it", "hello");
console.log(obj); // includes an intentional undefined value
It is a bit more complex than get, because there is some logic needed to create missing parts of the path in the object, to overwrite primitive values that stand in the way, and to determine whether a new child should better be an array or a plain object.
Check this one:
/**
* #example
* const obj = {id:1, address: {city: 'Minsk', street: 'Prityckogo 12'}}
* setByString(obj, 'address.city', 'Grodno'); obj.address.city => 'Grodno'
* setByString(obj, ['address', 'city'], 'Grodno'); obj.address.city => 'Grodno'
* setByString(obj, ['address', 'city', 'phones'], {mobile: 1234, home: 222}); obj.address.city.phones.home => 222
*/
/**
* #param {any} input
* #return {boolean}
*/
const isObject = (input) => (
null !== input &&
typeof input === 'object' &&
Object.getPrototypeOf(input).isPrototypeOf(Object)
)
**/
* #param {object} obj
* #param {string} path
* #param {any} value
*/
const setByString = (obj, path, value) => {
const pList = Array.isArray(path) ? path : path.split('.');
const len = pList.length;
// changes second last key to {}
for (let i = 0; i < len - 1; i++) {
const elem = pList[i];
if (!obj[elem] || !isObject(obj[elem])) {
obj[elem] = {};
}
obj = obj[elem];
}
// set value to second last key
obj[pList[len - 1]] = value;
};
const set = (obj = {}, paths = [], value) => {
const inputObj = obj === null ? {} : { ...obj };
if (paths.length === 0) {
return inputObj;
}
if (paths.length === 1) {
const path = paths[0];
inputObj[path] = value;
return { ...inputObj, [path]: value };
}
const [path, ...rest] = paths;
const currentNode = inputObj[path];
const childNode = set(currentNode, rest, value);
return { ...inputObj, [path]: childNode };
};
Example:
const input = {};
set(input, ['a', 'b'], 'hello');
Result:
{ a: { b: 'hello' }}
Here's an implementation of lodash 'get' and 'set' without using dot or bracket notation; useful for passing security scans.
https://jsfiddle.net/5amtL8zx/17/
/* lodash implementation of 'get', 'set', and 'unset' without dot or bracket notation
* - supports getting and setting 'prop1.2' array element but not with brackets: 'prop1.[2]'
*/
isObjectKey = (obj, key) => {
return Object.getPrototypeOf(obj) === Object.prototype && /string|number/.test(typeof key);
}
isArrayNumber = (obj, key) => {
const isKey = /string|number/.test(typeof key), path = isKey ? String(key).split('.') : [], prop = isKey && path.length > 1 ? path.shift() : '';
return Object.getPrototypeOf(obj) === Array.prototype && isKey && !isNaN(prop);
}
isValid = (obj, key) => {
const isObj = isObjectKey(obj, key), isArr = isArrayNumber(obj, key);
return isObj || isArr;
}
define = (obj, key, value) => {
Object.defineProperty(obj, String(key), { value, writable: true, configurable: true, enumerable: true });
}
get = (obj, key, value) => {
if (!isValid(obj, key)) {
return undefined;
}
let path = String(key).split('.'), prop = path.shift(), result = new Map(Object.entries(obj)).get(prop);
return path.length && typeof result !== 'undefined' ? get(result, path.join('.'), value) : result || value;
}
set = (obj, key, value) => {
if (!isValid(obj, key)) {
return undefined;
}
let path = key.split('.'), prop = path.shift();
if (!(prop in obj)) {
define(obj, prop, {});
}
const result = get(obj, prop);
return path.length && isValid(result, path.join('.')) ? set(result, path.join('.'), value) : define(obj, prop, value);
}
unset = (obj, key) => {
if (!isValid(obj, key)) {
return undefined;
}
let path = key.split('.'), prop = path.shift();
if (!(prop in obj)) {
return undefined;
}
if (path.length) {
let result = get(obj, prop);
result = unset(result, path.join('.'));
set(obj, prop, result);
return obj;
} else {
const { [prop]: remove, ...rest } = obj;
return rest;
}
}
let obj = {};
set(obj, 'prop1.prop2', 'value1');
console.log(Object.entries(obj));
console.log(get(obj, 'prop1.prop2'));
const prop1 = get(obj, 'prop1');
set(prop1, 'prop2', 'value2');
console.log(get(obj, 'prop1.prop2'));
set(obj, 'prop3', [1, 2, 3]);
console.log(get(obj, 'prop3'));
console.log(get(obj, 'prop3.2'));
console.log(get(obj.prop3, 0));
set(obj, 'prop3.3', 4);
console.log(get(obj, 'prop3.3'));
set(obj, 'prop4', [{'name': 'Bob'}]);
console.log(get(obj, 'prop4.0'));
unset(obj, 'prop4.0.name')
console.log(get(obj, 'prop4.0'));
//[["prop1", {
// prop2: "value1"
//}]]
//"value1"
//"value2"
//[1, 2, 3]
//3
//1
//4
//{
// name: "Bob"
//}
//{ ... }

Categories

Resources