Run a function when deep property is set - javascript

I have an object like
const obj = { field1: obj1, field2: obj2 }
and now I'd like to run a function when anything in obj was changed:
function objChanged() { ... }
// decorate obj somehow ...
obj.field3 = data; // objChanged should be called (Proxy can see it)
obj.field1.val = data; //objChanged should be called (Proxy can't see it?)
AFAIK there is a MutationObserver which works only for DOM and Proxy which intercepts only own properties, right?
I do not own obj1 so I can not change it. Is there a way to achieve this functionality?

Following the piece of code will listen to object property you can iterate over object properties to listen all. I am curious, what are you trying to achieve?
const dog = { bark: true };
function Observer(o, property) {
var _this = this;
this.observers = [];
this.Observe = function (notifyCallback) {
_this.observers.push(notifyCallback);
};
Object.defineProperty(o, property, {
set: function (val) {
_this.value = val;
for (var i = 0; i < _this.observers.length; i++) {
_this.observers[i](val);
}
},
get: function () {
return _this.value;
},
});
}
const observer = new Observer(dog, "bark");
observer.Observe(function (value) {
l("Barked");
});
dog.bark = true;
dog.bark = true;
dog.bark = true;
dog.bark = true;

Orgil's answer works only with a single property that needs to be known and encoded. I wanted a solution which works for all properties, including later added. Inspired by his idea to create an observing object, I created a dynamic Proxy that adds another Proxies when needed.
In the following code dog1 serves as proxy: setting its properties modifies the original dog object and logs the assigned value to console.
function AssignProxy(o, fn, path) {
var tree = {};
if(!path) path = "obj";
return new Proxy(o, {
get: (_, prop) => {
if(typeof o[prop] != "object") return o[prop];
if(tree[prop] === undefined) tree[prop] = AssignProxy(o[prop], fn, `${path}.${prop}`);
return tree[prop];
},
set: (_, prop, val) => fn(o[prop] = val, prop, o, path) || 1
});
}
/****** TEST *******/
const dog = {
sounds: {},
name: "Spike"
};
let callback = (val, prop, o, path) => console.log(`assigning ${path}.${prop} to ${val}`)
const dog1 = AssignProxy(dog, callback, "dog1");
dog1.name = "Tyke"; // overwrite property
dog1.age = 4; // create a property
dog1.sounds.howl = "hoooooowl"; // create a deep property
dog1.sounds.howl = {text: "hoowl", pitch: 5000}; // overwrite the deep property
var howl = dog1.sounds.howl; // access by reference
howl.pitch = 6000; // overwrite later added property
console.log(dog); // verify the original object

Related

JavaScript use original getter/setter in defineProperty

I would like to create a TypeScript decorator that can extend the logic of a property's getter/setter. I have tried to copy the original property under a symbol and call that when I redefine the property. The problem is it turns into an infinite loop.
//Find the latest version of 'attribute' getter setter in the prototype chain
let obj = _object;
while(obj && !(Object.getOwnPropertyDescriptor(obj, 'attribute'))){
obj = Object.getPrototypeOf(obj);
}
//Copy original 'attribute' logic under a symbol
const attributeDesc = Object.getOwnPropertyDescriptor(obj, 'attribute');
let id=Symbol('__attribute');
Object.defineProperty(obj, id, attributeDesc);
//Redefine 'attribute' logic
Object.defineProperty(_object, 'attribute', {
get: () => {
//call original
const attribute = obj[id]; //It crashes the page (probably infinite loop)
//extend original logic
attribute['extend'] = 'property';
return attribute;
},
enumerable: false,
configurable: true
});
If you could explain me why it ends up this way that would help me out. I thought the new getter function reference nothing to do with the original. Please suggest me a solution to achive this in JavaScript.
Thank you for your time and answers!
I don't quite see the error. In the repro you provided, it's logical that there is one: the getter for attribute property is calling itself on the line var attributes = obj[id], so there is an infinite loop. However if you edit your code to be like the snippet you provided in the question:
class A {
get attribute() {
return { a: 1 }
}
}
var _object = new A()
let obj = _object
while (obj && !Object.getOwnPropertyDescriptor(obj, 'attribute')) {
obj = Object.getPrototypeOf(obj)
}
const attributeDesc = Object.getOwnPropertyDescriptor(obj, 'attribute')
let id = Symbol('__attribute')
Object.defineProperty(obj, id, attributeDesc)
Object.defineProperty(obj, 'attribute', {
get: function () {
var attributes = obj[id]
attributes['extend'] = 'property'
return attributes
},
enumerable: false,
configurable: true,
})
console.log('result:', obj.attribute)
There is no error and it works as expected.
You don't really need the symbol though, you could do something like
function extendAttributes(_object) {
let obj = _object
while (obj && !Object.hasOwnProperty(obj, 'attributes')) {
obj = Object.getPrototypeOf(obj)
}
if(!obj) return;
const oldContainer = {}
const attributesDescriptor = Object.getOwnPropertyDescriptor(obj, 'attributes')
Object.defineProperty(oldContainer, 'attributes', attributesDescriptor)
Object.defineProperty(obj, 'attributes', {
get() {
const attribute = oldContainer.attributes;
//extend original logic
attribute['extend'] = 'property';
return attribute;
}
})
}
class A {
get attributes() { return {a: 1} }
}
const obj = new A()
extendAttributes(obj)
console.log(obj.attributes)
Which also works like expected

JavaScript: default arguments in automatic getters and setters in closures without eval?

Note: this question is a follow up of the recently asked question JavaScript: automatic getters and setters in closures without eval? .
The gist of that question was as follows: "How can one automatically provide getters and setters for scoped variables in a closure - without the use of the eval statement". There the poster, provided code demonstrating how to do so with eval and the user gave the following answer which does not require eval:
function myClosure() {
var instance = {};
var args = Array.prototype.slice.call(arguments);
args.forEach(function(arg) {
instance[arg] = function(d) {
if (!arguments.length) return arg;
arg = d;
return instance;
};
})
return instance;
};
This question is about how to have default values for the scoped variables which are to be set / get with the above function.
If we simply add a default value to the variable v3 we get the following:
function myClosure() {
var v3 = 2
var instance = {};
var args = Array.prototype.slice.call(arguments);
args.forEach(function(arg) {
instance[arg] = function(d) {
if (!arguments.length) return arg;
arg = d;
return instance;
};
})
return instance;
}
var test = myClosure("v1", "v2", "v3") // make setters/getters for all vars
test.v1(16).v2(2) // give new values to v1, v2
console.log(test.v1() + test.v2() + test.v3()) // try to add with default v3
// 18v3
I was not expecting that.
So how can I provide a default value to the variables?
Note: please build off the following implementation which generates the getters / setters on initialization (allowing the code author to pre-define all variables which should have getters and setters)
function myClosure() {
var instance = function () {};
var publicVariables =['v1', 'v2', 'v3']
function setup() {
var args = Array.prototype.slice.call(arguments);
// if called with a list, use the list, otherwise use the positional arguments
if (typeof args[0] == 'object' && args[0].length) { args = args[0] }
args.forEach(function(arg) {
instance[arg] = function(d) {
if (!arguments.length) return arg;
arg = d;
return instance;
};
})
}
setup(publicVariables)
// setup('v1', 'v2', 'v3') also works
return instance;
}
var test = myClosure()
test.v1(16).v2(2)
console.log(test.v1() + test.v2() + test.v3())
Question:
How to use default values in this set up (above code block) with automatic getters and setters?
The gist of that question was as follows: "How can one automatically provide getters and setters for scoped variables in a closure - without the use of the eval statement". There the poster, provided code demonstrating how to do so with eval and the user gave an answer which does not require eval.
No, you cannot do without eval. All the answers here that don't use any form of eval do not access scoped variables, but rather just plain properties - or they create their own local variables.
Providing a default value is rather simple with that:
function myClosure(...args) {
var instance = {v3: 2};
// ^^^^^ not a `var`
for (const arg of args) {
let val = instance[arg];
instance[arg] = function(d) {
if (!arguments.length) return val;
val = d;
return instance;
};
}
return instance;
}
Do you mean something like this:
function myClosure(...vars) {
const instance = {};
vars.forEach(varArg => {
let name = undefined;
let value = undefined;
if (typeof varArg == 'string')
{
name = varArg;
}
else
{
name = Object.keys(varArg)[0];
value = varArg[name];
}
instance[name] = function(d) {
if (!arguments.length) return value;
value = d;
return instance;
};
})
return instance;
}
const test = myClosure(
{ "v1": 1 },
"v2",
{ "v3": 3 },
);
// Print some defaults.
console.log(test.v1());
console.log(test.v2());
test.v1(16).v2(42) // give new values to v1, v2
console.log(test.v1(), test.v2(), test.v3())
Proxies, for the heck of it.
function myClosure(...vars) {
const instance = vars.reduce((obj, { name, value }) => {
obj[name] = value;
return obj;
}, {});
let proxy;
const handler = {
get: function(target, prop) {
return (...args) => {
if (args.length == 0)
return instance[prop];
instance[prop] = args[0];
return proxy;
};
}
};
proxy = new Proxy(instance, handler);
return proxy;
}
const test = myClosure(
{ name: "v1", value: 1 },
{ name: "v2" },
{ name: "v3", value: 3 }
);
// Print some defaults.
console.log(test.v1());
console.log(test.v2());
console.log(test.vNew());
test.v1(16).v2(42).vNew(50); // give new values to some variables.
console.log(test.v1(), test.v2(), test.v3(), test.vNew())
Note: I am posting my own answer for reference only. I will not be marking this as the answer to the question.
Building off the answer provided by #H.B., I update the answer in the following ways:
getters and setters are made on initialization of the closure itself
make the getter and setter production function a bit more messy to allow for more lazy definition of variables
instance is now a function, not an object
function myClosure() {
var instance = function () {
console.log(this.v1(), this.v2(), this.v3())
};
var publicVariables =[ 'v1', 'v2', {'v3': 3} ]
function setup() {
var args = Array.prototype.slice.call(arguments);
// if called with a list, use the list,
// otherwise use the positional arguments
if (typeof args[0] == 'object' && args[0].length) { args = args[0] }
args.forEach(function(arg) {
var name, value
if(typeof arg == 'object') {
name = Object.keys(arg)[0]
value = arg[name]
} else {
name = arg
value = undefined
}
instance[name] = function(d) {
if (!arguments.length) return value;
value = d;
return instance;
}; // end instance function
}) // end for each
} // end setup
setup(publicVariables)
return instance;
}
var test = myClosure().v1(10).v2(2)
console.log(test.v1(), test.v2(), test.v3())
test.v1(20).v3(1)
console.log(test.v1(), test.v2(), test.v3())

Call an objects setter when its children are assigned to

Let there be an object userSingleton defined as such:
var userSingleton = new function() {
var _user = undefined;
Object.defineProperty(this, 'activeUser', {
get: function() {
console.log("Getter called, done something cool");
return _user;
},
set: function(val) {
console.log("Setter called, do something cooler");
_user = val;
}
});
}
Now if I go to use it, userSingleton.activeUser = {name: 'John Doe'}; works great! I get a "Setter called, do something cooler".
However, if I try to do userSingleton.activeUser.name = 'John Doe'; I instead get a "Getter called, done something cool" and userSingleton._user is not updated.
What's happening is it's trying to set the name property of the object returned by the getter (userSingleton.activeUser).
How do I make it call a particular function when any (unknown at definition time) property is assigned to / modified?
A revisited solution Proxy based with a deeply nested example (NB: ECMA Script 2015):
var handler = {
get: function (target, key) {
return target[key];
},
set: function (target, key, value) {
do_action(target, key, value);
if (typeof value === 'object') {
target[key] = new Proxy(value, handler);
} else {
target[key] = value;
}
}
};
function do_action(target, key, value) {
console.log("firing action on:", key, value)
}
function singletonUser() {
if (!this._singleton) {
const _user = {}
this._singleton = {
activeUser: new Proxy(_user, handler)
};
}
return this._singleton;
}
var userSingleton = singletonUser();
userSingleton.activeUser.name = 'pippo';
userSingleton.activeUser.age = 10;
// a deeply nested example
userSingleton.activeUser.github = {};
userSingleton.activeUser.github.followers = ["gino", "pino"]

Using call to pass context

I'm trying to use call to pass the context of the Utilities object so I can access its members (allData array, etc) within the myTest function.
I'm getting error:
ReferenceError: allData is not defined
This tells me the context is lost and I guess I'm not binding the context correctly. How do I do this?
var Utilities = {
allData : [],
storage : [],
apiRequest : function () {
this.allData = ["a","b","c"];
var myTest = this.allData.map.call(this, function (i, el) {
var url = 'testPHP.php/translate_tts?ie=utf-8&tl=zh-CN&q=' + i;
return $.get(url, function (returned_data) {
this.storage.push(returned_data);
});
});
$.when.apply($, myTest).done(function () {
log('done!');
log(this.storage[i]);
});
Reminder
There is only function level context in Javascript, and this is nothing more than a local variable. By default, this is set to the global window object:
function me () { return this; };
me() === window; // true
Or to the object from which the function was invoked:
var o = {};
o.me = me;
o.me() === o; // true
Knowing this, read the following carefully:
var me = o.me;
me === o.me; // true, not a copy
me() === o; // false
me() === window; // true
var p = {};
p.me = o.me;
p.me() === p; // true
o.me() === o; // true
As you can see, this is automatically set at function invocation. This is the default behaviour, but you can also do it yourself using either .call() or .apply() (one shot):
me.call(o) === o; // true
me.apply(o) === o; // true
p.me.call(o) === o; // true
me() === window; // true
And more recently, .bind() (permanent):
me = me.bind(o);
me() === o; // true
me() === window; // false
Your question
I would use .bind():
var Utilities = {
allData : [],
storage : [],
apiRequest : function () {
this.allData = ["a","b","c"];
var myTest = this.allData.map(function (i, el) {
var url = 'testPHP.php/translate_tts?ie=utf-8&tl=zh-CN&q=' + i;
return $.get(url, function (returned_data) {
this.storage.push(returned_data);
}.bind(this));
}.bind(this));
$.when.apply($, myTest).done(function () {
log('done!');
// I've added a loop here
var i, l = arguments.length;
for (i = 0; i < l; i++) {
log(this.storage[i]);
}
}.bind(this));
Great answer above. One thing to emphasize at this point: whenever you bind the object at initialization it will be bound and cannot be call/apply'd using another context anymore. IMO it's up to you whether to use call/apply (at runtime) OR .bind (permanently).
Going from here:
I'm trying to use call to pass the context of the Utilities object so I can access its members (allData array, etc) within the myTest function.
var OtherTest = function() {
this.dataStorage.push(["ok"]);
console.log(arguments);
}
var Utilities = {
dataStorage: [],
allData: [],
apiRequest: function() {
this.allData = ["a","b","c"];
OtherTest.apply(this,arguments);
}
}
Utilities.apiRequest('hu','hu');
console.log(Utilities.dataStorage[0]);
Since this is a reference to the object, it can be mutated at any time after initialization making it easy to use call/apply to pass the context which is the Utilities Object in this case.

Insert an object into unknow object path

I want to insert an object into a somewhat predefined object:
var obj = {
"scripts": {
"libs":{}
},
"plugins":{}
}
//....
function addobj(path, obj){
console.log(path); // Object {libs: Object}..
path.push(obj); // TypeError: undefined is not a function
}
// Test cases:
addobj(obj["scripts"],{"test":{}});
console.log(obj);
But an error occurs: TypeError: undefined is not a function Why is this happening?
http://jsfiddle.net/Qn3Tb/
Using jQuery, you can use $.extend():
demo
$.extend(path,obj);
You can't .push onto an Object. An Object is a key-value store, therefore you need to assign a key to the object (value) you want to store on the parent object. How you go about achieving that is another question, but something like this might work:
function addobj(path, obj, key) {
path[key || "unnamed"] = obj;
}
If you wanted to add libs to scripts you would do the following:
addobj(script, libs, "libs");
However given what this addobj method actually does, my suggestion would be to drop the abstraction altogether, it's not needed.
Why not simply do
function addProp(prop, value, targetObject){
targetObject[prop] = value;
}
addProp('scripts', { test:{}}, obj);
Based on your question, you can use this to target a specific property:
var obj = {
"scripts": {
"libs":{
"labs": {
foo: 1
}
}
},
"plugins":{}
};
function setPropByString(obj, propString, value) {
if (!propString)
return obj;
var prop, props = propString.split('.');
for (var i = 0, iLen = props.length - 1; i < iLen; i++) {
prop = props[i];
var candidate = obj[prop];
if (candidate !== undefined) {
obj = candidate;
} else {
break;
}
}
obj[props[i]] = value;
}
setPropByString(obj, 'scripts.libs.labs', { something: 1 });
console.log(obj);
Note that this will overwrite the existing prop. So it's propably easier to just extend with jQuery like #A.Wolff suggest.
http://jsfiddle.net/Mn45R/
You cannot do this in the way mentioned in the question.
I believe you should create a function, like
function Node(key) {
var currentNode = this;
this.getKey = function() {
return key;
};
var children = [];
this.addNode(childKey) {
children[children.length] = new Node(childKey);
}
this.search(searchKey) {
if (searchKey === key) {
return currentNode;
}
for (var childIndex in children) {
var searchResult = children[childIndex].search(searchKey);
if (!!searchResult) {
return searchResult;
}
}
return null;
}
}
You can create your root this way:
var root = new Node();
You can add a child to the root this way:
root.addNode("scripts");
This function can be used to add some node to another node having a key
function addNodeToTree(tree, key, newKey) {
var node = tree.search(key);
if (!!node) {
node.addNode(new Node(newKey));
return true;
}
return false;
}
Finally, you can add a node like this:
addNodeToTree(root, "scripts", "test");

Categories

Resources