JavaScript use original getter/setter in defineProperty - javascript

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

Related

I have a size property I want to keep read-only to clients in JavaScript [duplicate]

Given an object obj, I would like to define a read-only property 'prop' and set its value to val. Is this the proper way to do that?
Object.defineProperty( obj, 'prop', {
get: function () {
return val;
}
});
The result should be (for val = 'test'):
obj.prop; // 'test'
obj.prop = 'changed';
obj.prop; // still 'test' since it's read-only
This method works btw: http://jsfiddle.net/GHMjN/
I'm just unsure if this is the easiest / smoothest / most proper way to do it...
You could instead use the writable property of the property descriptor, which prevents the need for a get accessor:
var obj = {};
Object.defineProperty(obj, "prop", {
value: "test",
writable: false
});
As mentioned in the comments, the writable option defaults to false so you can omit it in this case:
Object.defineProperty(obj, "prop", {
value: "test"
});
This is ECMAScript 5 so won't work in older browsers.
In new browsers or node.js it is possible to use Proxy to create read-only object.
var obj = {
prop: 'test'
}
obj = new Proxy(obj ,{
setProperty: function(target, key, value){
if(target.hasOwnProperty(key))
return target[key];
return target[key] = value;
},
get: function(target, key){
return target[key];
},
set: function(target, key, value){
return this.setProperty(target, key, value);
},
defineProperty: function (target, key, desc) {
return this.setProperty(target, key, desc.value);
},
deleteProperty: function(target, key) {
return false;
}
});
You can still assign new properties to that object, and they would be read-only as well.
Example
obj.prop
// > 'test'
obj.prop = 'changed';
obj.prop
// > 'test'
// New value
obj.myValue = 'foo';
obj.myValue = 'bar';
obj.myValue
// > 'foo'
In my case I needed an object where we can set its properties only once.
So I made it throw an error when somebody tries to change already set value.
class SetOnlyOnce {
#innerObj = {}; // private field, not accessible from outside
getCurrentPropertyName(){
const stack = new Error().stack; // probably not really performant method
const name = stack.match(/\[as (\w+)\]/)[1];
return name;
}
getValue(){
const key = this.getCurrentPropertyName();
if(this.#innerObj[key] === undefined){
throw new Error('No global param value set for property: ' + key);
}
return this.#innerObj[key];
}
setValue(value){
const key = this.getCurrentPropertyName();
if(this.#innerObj[key] !== undefined){
throw new Error('Changing global parameters is prohibited, as it easily leads to errors: ' + key)
}
this.#innerObj[key] = value;
}
}
class GlobalParams extends SetOnlyOnce {
get couchbaseBucket() { return this.getValue()}
set couchbaseBucket(value){ this.setValue(value)}
get elasticIndex() { return this.getValue()}
set elasticIndex(value){ this.setValue(value)}
}
const _globalParams = new GlobalParams();
_globalParams.couchbaseBucket = 'some-bucket';
_globalParams.elasticIndex = 'some-index';
console.log(_globalParams.couchbaseBucket)
console.log(_globalParams.elasticIndex)
_globalParams.elasticIndex = 'another-index'; // ERROR is thrown here
console.log(_globalParams.elasticIndex)
Because of the old browsers (backwards compatibility) I had to come up with accessor functions for properties. I made it part of bob.js:
var obj = { };
//declare read-only property.
bob.prop.namedProp(obj, 'name', 'Bob', true);
//declare read-write property.
bob.prop.namedProp(obj, 'age', 1);
//get values of properties.
console.log(bob.string.formatString('{0} is {1} years old.', obj.get_name(), obj.get_age()));
//set value of read-write property.
obj.set_age(2);
console.log(bob.string.formatString('Now {0} is {1} years old.', obj.get_name(), obj.get_age()));
//cannot set read-only property of obj. Next line would throw an error.
// obj.set_name('Rob');
//Output:
//========
// Bob is 1 years old.
// Now Bob is 2 years old.
I hope it helps.
I tried and it Works ...
element.readOnly = "readOnly" (then .readonly-> true)
element.readOnly = "" (then .readonly-> false)

Run a function when deep property is set

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

How to write a global get+set method for object

My question is easy to understand, I have an object (or class), and I want to have ONE method which can getting AND setting a property.
In fact, I have no problem to write it for "simple" properties. It becomes difficult when my class has object properties, and that I want to access or alter a nested one.
My class:
var MyClass = function() {
this.name = 'defaultName';
this.list = {
a: 1,
b: 6
};
}
Simple class, isn't it? Then, what I write for my method:
MyClass.prototype.getset = function(prop) {
let value = arguments[1];
let path = prop.split('.');
prop = this;
$(path).each(function(i) { prop = prop[this]; }
if (value) {
prop = value;
return this;
}
return prop;
}
The "get part" works (MyClass.getset('list.b') returns 6).
But the "set part"... does not work.
I want that when I execute MyClass.getset('list.b', 2), the b property of list becomes 2, and that's not the case.
I know why my version is not working (my prop variable is just a "copy" and does not affect the object itself), but I can't find solution for this...
Thanks for you help!
If you're assigning a primitive, you need to assign to a property of an object for the object to be changed as well. Check if value, and if so, navigate to and change from the next to last property, rather than the final property. Use reduce for brevity:
var MyClass = function() {
this.name = 'defaultName';
this.list = {
a: 1,
b: 6
};
}
MyClass.prototype.getset = function(prop, value) {
const props = prop.split('.');
const lastProp = props.pop();
const lastObj = props.reduce((obj, prop) => obj[prop], this);
if (value) {
lastObj[lastProp] = value;
return this;
} else return lastObj[lastProp];
}
const mc = new MyClass();
mc.getset('list.b', 2);
console.log(mc.list.b);
console.log(mc.getset('list.b'));

Javascript overriding the property setting functionality

JavaScript is dynamic. Cool !
I have the following constructor function :
function Preferences () {
this._preferences = {}
}
var obj = new Preferences()
I want to achieve something like this:
>>> obj.something = 'value'
>>> this._preferences['something']
'value'
That is setting the property of the obj does not actually set it's own property but that of obj._preferences. That is I want to override the default behavior.
Is it possible ?
EDIT : I want to achieve this for all property names i.e the name of the property to be set is not already known.
Object.defineProperty(Preferences.prototype, 'something', {
get: function(){
return this._preferences.something;
},
set: function(value){
this._preferences.something = value;
}
});
should do it. It defines a property, 'something', using an accessor property instead of a data property, and will call the 'get' and 'set' functions to decide what do so when .something is accessed.
SOLUTION 1
Using Proxy object you can do something like this and handle runtime defined properties
function Preferences() {
this._preferences = {};
var prefProxy = new Proxy(this, {
get: function(target, property) {
return property in target?
target[property]:
target._preferences[property];
}
set: function(target, property, value, receiver) {
if(property in target){
target[property] = value;
} else {
target._preferences[property] = value;
}
}
});
return prefProxy;
};
SOLUTION 2
I can be wrong but i think what you are asking is solved returning _preferences
function Preferences () {
this._preferences = {};
return _preferences;
}
var obj = new Preferences()
SOLUTION 3
Using getter and setter you can redirect the property to _preferences
function Preferences () {
this._preferences = {}
Object.defineProperty(Preferences.prototype, 'something', {
get: function() {
return this._preferences['something'];
},
set: function(value) {
this._preferences['something'] = value;
}
});
}
var obj = new Preferences()

Copy object with results of getters

I have an object that contains a getter.
myObject {
id: "MyId",
get title () { return myRepository.title; }
}
myRepository.title = "MyTitle";
I want to obtain an object like:
myResult = {
id: "MyId",
title: "MyTitle"
}
I don't want to copy the getter, so:
myResult.title; // Returns "MyTitle"
myRepository.title = "Another title";
myResult.title; // Should still return "MyTitle"
I've try:
$.extend(): But it doesn't iterate over getters. http://bugs.jquery.com/ticket/6145
Iterating properties as suggested here, but it doesn't iterate over getters.
As I'm using angular, using Angular.forEach, as suggested here. But I only get properties and not getters.
Any idea? Thx!
Update
I was setting the getter using Object.defineProperty as:
"title": { get: function () { return myRepository.title; }},
As can be read in the doc:
enumerable true if and only if this property shows up during
enumeration of the properties on the corresponding object. Defaults to
false.
Setting enumerable: true fix the problem.
"title": { get: function () { return myRepository.title; }, enumerable: true },
$.extend does exactly what you want. (Update: You've since said you want non-enumerable properties as well, so it doesn't do what you want; see the second part of this answer below, but I'll leave the first bit for others.) The bug isn't saying that the resulting object won't have a title property, it's saying that the resulting object's title property won't be a getter, which is perfect for what you said you wanted.
Example with correct getter syntax:
// The myRepository object
const myRepository = { title: "MyTitle" };
// The object with a getter
const myObject = {
id: "MyId",
get title() { return myRepository.title; }
};
// The copy with a plain property
const copy = $.extend({}, myObject);
// View the copy (although actually, the result would look
// the same either way)
console.log(JSON.stringify(copy));
// Prove that the copy's `title` really is just a plain property:
console.log("Before: copy.title = " + copy.title);
copy.title = "foo";
console.log("After: copy.title = " + copy.title);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Syntax fixes:
Added missing variable declarations, =, and ;
Removed duplicate property title
Corrected the getter declaration syntax
If you want to include non-enumerable properties, you'll need to use Object.getOwnPropertyNames because they won't show up in a for-in loop, Object.keys, or $.extend (whether or not they're "getter" or normal properties):
// The myRepository object
const myRepository = { title: "MyTitle" };
// The object with a getter
const myObject = {
id: "MyId",
};
Object.defineProperty(myObject, "title", {
enumerable: false, // it's the default, this is just for emphasis,
get: function () {
return myRepository.title;
},
});
console.log("$.extend won't visit non-enumerable properties, so we only get id here:");
console.log(JSON.stringify($.extend({}, myObject)));
// Copy it
const copy = {};
for (const name of Object.getOwnPropertyNames(myObject)) {
copy[name] = myObject[name];
}
// View the copy (although actually, the result would look
// the same either way)
console.log("Our copy operation with Object.getOwnPropertyNames does, though:");
console.log(JSON.stringify(copy));
// Prove that the copy's `title` really is just a plain property:
console.log("Before: copy.title = " + copy.title);
copy.title = "foo";
console.log("After: copy.title = " + copy.title);
.as-console-wrapper {
max-height: 100% !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
First of all, fix your syntax, though it probably is good in your actual code:
myObject = {
id: "MyId",
get title () { return myRepository.title; }
}
Now, to the answer. :)
You can just use a for..in loop to get all the properties, then save them as-is:
var newObj = {};
for (var i in myObject) {
newObj[i] = myObject[i];
}
No jQuery, Angular, any other plugins needed!
I had the same issue but in TypeScript and the method mentioned by T.J. Crowder didnt work.
What did work was the following:
TypeScript:
function copyObjectIncludingGettersResult(originalObj: any) {
//get the properties
let copyObj = Object.getOwnPropertyNames(originalObj).reduce(function (result: any, name: any) {
result[name] = (originalObj as any)[name];
return result;
}, {});
//get the getters values
let prototype = Object.getPrototypeOf(originalObj);
copyObj = Object.getOwnPropertyNames(prototype).reduce(function (result: any, name: any) {
//ignore functions which are not getters
let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
if (descriptor?.writable == null) {
result[name] = (originalObj as any)[name];
}
return result;
}, copyObj);
return copyObj;
}
Javascript version:
function copyObjectIncludingGettersResult(originalObj) {
//get the properties
let copyObj = Object.getOwnPropertyNames(originalObj).reduce(function (result, name) {
result[name] = originalObj[name];
return result;
}, {});
//get the getters values
let prototype = Object.getPrototypeOf(originalObj);
copyObj = Object.getOwnPropertyNames(prototype).reduce(function (result,name) {
let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
if (descriptor?.writable == null) {
result[name] = originalObj[name];
}
return result;
}, copyObj);
return copyObj;
}

Categories

Resources