Programmatically defining Javascript properties in the object constructor - javascript

I have a Javascript constructor with an object containing all of its fields, and functions that read/write to them:
function myObj(){
var _settingsStore {
setting1: 1, //default value
setting2: 2, //default value
}
//gets/sets _settingsStore.settingName, does other stuff
function _genericSet(settingName, settingValue){}
function _genericGet(settingName){}
I'd like to programmatically create properties around each of the fields:
for (var _setting in _settingsStore){
var _str = _setting.toString();
Object.defineProperties(this, {
_str: {
get: function() {return _genericGet(_str)},
set: function(value) {return _genericSet(_str, value)}
}
})
}
My intent is for _str to be purely a variable, and that myObj.setting1 and myObj.setting2 should be defined. Instead, the loop attempts to define myObj._str twice:
Uncaught TypeError: Cannot redefine property: _str
What's going on? And is there a way to get my desired behavior?
EDIT: To clarify, this is what I effectively want to happen, just unrolled outside of the loop:
Object.defineProperties(this, {
settings1: {
get: function() {return _genericGet("settings1")},
set: function(value) {return _genericSet("settings1", value)}
},
settings2: {
get: function() {return _genericGet("settings2")},
set: function(value) {return _genericSet("settings2", value)}
},
//and so on, if there are more of them
})

I'm starting over. If you just want to use setters and getters that do some extra work, and each object has its on store hidden in the constructor, and you wanted to reuse some generic functions, you could do this:
function myObj(){
var _settingsStore = {
setting1: 1, //default value
setting2: 2, //default value
}
for (var _setting in _settingsStore){
makeGetSet(this, _setting, _settingStore);
}
}
function makeGetSet(obj, key, store) {
Object.defineProperty(obj, key, {
get: function() { return _genericGet(key, store)}
set: function(val) {_genericSet(key, val, store)}
})
}
function _genericGet(key, store){
var val = store[key];
// manipulate the val
return val
}
function _genericSet(key, val, store){
// manipulate the new val
store[key] = val
}
I really don't know what sort of manipulations you're doing when getting/setting, so some of this could maybe be shortened.
Also, you could make the _settingsStore generic, and just use the variable scope for the value storage since you need that anyway.

You're misunderstanding object literals.
{ _str: blah } is an object with a property named _str; it doesn't use the value of the _str variable.
Therefore, you were actually defining the same name every time.
Since you didn't set configurable: true, that threw an error.
Instead, you should call Object.defineProperty (singular), which takes a string followed by a property name.
(alternatively, you could build an object to pass to defineProperties and give it the correct name using indexer notation)
However, your code still won't work correctly, since all of the callbacks will close over the same variable (Javascript does not have block scope).
To solve that, you need to wrap each iteration in a separate function.

Related

Validate/Preprocess property before assigning it to object in JavaScript

What is the best-practice to validate and/or preprocess a property before assigning it to an object in JavaScript?
The application for that would be to create an object and to guarantee that a specific property of it will always have a specific type or maybe do some preprocessing with it.
For example, if I create an object:
var obj = {
settings: {}
};
Then when I do something like:
obj.settings = "{foo: bar}";
It would automatically check the type of the assignment - if it is a string, it will try to parse it to an object; if it's an object, it will just assign it; else it will throw an error. This would protect the object's property against being assigned to "anything".
Also, does this make sense at all to do in JavaScript or am I just trying to have strong typed features in a language that is weak typed?
You can do this with Object.defineProperty:
var obj = {}
Object.defineProperty(obj, 'settings', {
set: function (x) {
if (typeof x === 'string') {
this._settings = JSON.parse(x)
} else {
this._settings = x
}
},
get: function () {
return this._settings
}
})
obj.settings = {foo: 'bar'}
console.log(obj.settings)
obj.settings = '{foo: "baz"}'
console.log(obj.settings)
However, if this is desirable depends on your specific use case. I frankly never used it so far. My recommendation is: don't get fancy :)
IMHO this is not strong typing, but the opposite as you are more dynamic. If you want strong typing you could try flow or TypeScript
A simple solution could be to use a getter/setter like below that gets triggered if a value is assigned to a property and how to process it :
let obj = {}
Object.defineProperty(obj, "settings", {
set: function (value) { // preprocess
this._value = value;
},
get: function () {
return "changed";
}
});
You could do this afterward:
obj.settings = "{foo: bar}";
Let me know if this makes any sense.
Reference:
MDN Reference: Object.defineProperty()

Typescript / Javascript custom Property decorators

I've just started to learn in more depth Typescript and ES6 capabilities and I have some misunderstandings regarding how you should create and how it actually works a custom Property Decorator.
This are the sources that I'm following
Source1
Source2
And this is my custom deocrator.
export const Required = (target: Object, key: string) => {
let value: any = target[key];
const getter = () => {
if (value !== undefined) return value;
throw new RequiredPropertyError(MetadataModule.GetClassName(target), key, ErrorOptions.RequiredProperty)
}
const setter = (val) => value = val;
if (delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
}
It is applied like so
export classMyClass{
#Required
public Items: number[];
}
What I don't understand is why it works differently then what would you expect. Well it works but I don't know if it works accordingly to the let's call it "decorator philosophy",
Let me explain what I don't understand
Starting with the first line of code.
let value: any = target[key];
I would expect that the value would be initialized with Items value, but instead is undefined, why? how? i really don't understand.
I've followed both sources and that first thing that I find it confusing is the fact that one used target[key] to initialize the value while the other used this[key], shouldn't this refer to the Required actually.
What I also find confusing is this part
if (delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
Firstly, why I need to delete this[key] ? from my understanding this should refer to the current object context in my this case Required, and when debugged it is as so.
Secondly, that Object.defineProperty will create a property with the name of the key on the target class in my case MyClass, but isn't this property already there?
Moving forward and setting the value using the setter, how does the setter parameter val know what data it should hold ? I mean where is it coming ?
Thanks all.
There's a lot of questions in there, but I'll try to answer them all :)
I would expect that the value would be initialized with Items value, but instead is undefined, why?
When your MyClass class is instantiated and your decorator code is run, the value of all properties of the object are undefined (even if you use a TypeScript property initializer, which you aren't).
Shouldn't this refer to the Required?
Yes, I think it does in your example. But in your Source1 link, the decorator is defined using a function expression (function logProperty()); in your example, you have switched this to an arrow function expression (const Required = ( ... ) =>). It's likely this switch will change what this refers to. To be safe, switch this to target[key].
Why I need to delete this[key]?
This block of code is deleting the original Items property and replacing it with a property of the same name that allows you to spy on the getting and setting of the variable. The delete this[key] line is guarding against the case where the property isn't configurable. (The delete operator returns false if the property in question is non-configurable:).
Object.defineProperty will create a property with the name of the key on the target class in my case MyClass, but isn't this property already there?
Yes, as mentioned above - this code is replacing the property with a new property. The new property is set up to allow you observe the getting and setting of this variable.
Moving forward and setting the value using the setter, how does the setter parameter val know what data it should hold ? I mean where is it coming ?
When your new Items property is set, the setter function is called with the soon-to-be new value of the property as a parameter (this setter function was previously set up as a setter function using Object.defineProperty). The implementation of this setter function sets value, which is a reference to target[key]. As a result, the value of Items gets updated.
To better understand how all this is working, try adding in some logging statements and then play around with getting/setting the Items property:
export const Required = (target: Object, key: string) => {
let value: any = target[key];
console.log('Initialize the Required decorator; value:', value);
const getter = () => {
console.log('Inside the getter; value is:', value);
if (value !== undefined) return value;
throw new RequiredPropertyError(MetadataModule.GetClassName(target), key, ErrorOptions.RequiredProperty);
};
const setter = val => {
console.log('Inside the setter; val is: ', val, 'value is:', value);
return (value = val);
};
if (delete this[key]) {
console.log('Replacing the "' + key + '" property with a new, configured version');
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
};
export class MyClass {
#Required
public Items: number[];
}
// instantiated your class
var mc = new MyClass();
// set the Items property
mc.Items = [4, 5, 6];
// get the value with no Errors
mc.Items;
// set the Items property to undefined
mc.Items = undefined;
// get the value - an Error is thrown
mc.Items;

Can Object.defineProperty only modify the setter?

I want my object to have a field that when read returns the fields value, and when a val is written to the field, I want to modify the val before writing it. My current code is this:
function Cat(lives) {
var self = this;
var privateLives;
Object.defineProperty(self, 'publicLives', {
get: function() {return privateLives;},
set: function(val) {privateLives = 7 * val;}
});
}
Is there a way to do this without making a private variable? Ideally I would simply have the setter be this:
function(val) {self.publicLives = 7 * val;}
but that causes an overflow as the setter calls itself. Is there some way to make so it just doesn't loop the setter (so only assignment outside the setter's scope calls the setter and assignment in the setter just does normal assignment)? If that's possible, I wouldn't need to explicitly define a getter either as the setter writes to a public field.
No, this is not possible - a property can only be either a data property or an accessor property, not both. Of course you don't necessarily need to store the value in a private variable from your setter, you can use a different property or a property on a different object (like in #Oriol's proxy) as well. If you want to avoid private variables, "private" properties are the standard approach:
function Cat(lives) {
this.publicLives = lives;
}
Object.defineProperty(Cat.prototype, 'publicLives', {
get: function() {return this._privateLives;},
set: function(val) { this._privateLives = 7 * val;}
});
But you can also do some tricky things and hide the "private variable" by using a constant getter function that is repeatedly redefined:
Object.defineProperty(Cat.prototype, 'publicLives', {
set: function setter(val) {
val *= 7;
Object.defineProperty(this, 'publicLives', {
get: function() { return val; }
set: setter,
configurable: true
});
}
});
In ES6, an alternative would be using a Proxy object with a [[Set]] trap:
function Cat(lives) {
return new Proxy(this, {
set: function(target, prop, val) {
target[prop] = val;
if (prop === 'publicLives') target[prop] *= 7;
return true;
}
});
}

Overriding innerHTML property access

I am looking for a way to enable setter method on a DOM element. So basically I want to intercept all calls to this specific property. I've tried with defining get/set properties on a specific DOM element, but it isn't of much help:
(function(object, property) {
var __value;
Object.defineProperty(object, property, {
// Create a new getter for the property
get: function () {
console.log('access gettter');
return __value;
},
// Create a new setter for the property
set: function (val) {
__value = val;
}
})
})(document.body, 'innerHTML');
The result of this is that calling:
document.body.innerHTML = 'testValue';
is:
document.body.innerHTML
access gettter
"testValue"
However this doesn't have any impact on the page, I mean body of the page isn't changed, just the value of a private variable inside the closure is returned. Is there a way to achieve this functionality in JavaScript ?
Yes, this can be done. Here is the code in working form:
(function(object, property) {
var ownObjectProto = Object.getPrototypeOf(object);
// exit if bad property
if (!object[property]) {
console.error(property + " is not a property of " + object.toString());
return;
}
while (!Object.getOwnPropertyDescriptor(ownObjectProto, property)) {
ownObjectProto = Object.getPrototypeOf(ownObjectProto);
}
var ownProperty = Object.getOwnPropertyDescriptor(ownObjectProto, property);
Object.defineProperty(object, property, {
// Create a new getter for the property
get: function () {
console.log('access gettter');
return ownProperty.get.call(this);
},
// Create a new setter for the property
set: function (val) {
return ownProperty.set.call(this, val);
}
})
})(document.body, 'innerHTML');
You are not actually replacing the set and get functions here, but you are able to augment them. Seriously though, you shouldn't use this unless you have a very particular need for it, and you really know what you are doing. If not used properly, you could really screw up the functionality of plug-ins on your page or any other number of unintended consequences.
I also have a playground fiddle here: jsfiddle

How to define a constructor function that makes objects with getters and setters?

I'm working on creating an object definition to encapsulate several properties. I have 4 requirements for this object:
more than one instance can be created at any time
only properties defined on the object can be set/get
the object must be stringify-able (just the properties and associated values)
the stringified version must be parse-able and return the object
This is a simplified version of what I have so far:
function Car() {
var _make;
Object.defineProperty(this, 'make', {
get: function() {
return _make;
},
set: function(make) {
_make = make;
}
});
Object.preventExtensions(this);
}
I'm unsure if this is the simplest approach for defining an object with getters and setters. Is there an alternative to using Object.defineProperty, or is this the new norm?
This approach also requires me to write my own stringify method (see below) since calling JSON.stringify will strip off functions, and this will strip off make. Is there an alternative to writing my own? Can I change my object definition somehow?
Car.prototype.stringify = function () {
return JSON.stringify({ make: this.make});
}
Additionally, I must supply an optional constructor arg to be able to instantiate a Car from a JSON object:
function Car(car) {
var _make;
if(car !== undefined && car != null) {
_make = car.make;
}
...
}
Again, is this the best approach to meet all the requirements?
To show up make property while using JSON.stringify, you have to set the enumerable to True (By default, False).
configurable: true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.
Defaults to false.
enumerable: true if and only if this property shows up during enumeration of the properties on the corresponding object.
Defaults to false.
function Car() {
var _make;
Object.defineProperty(this, 'make', {
get: function() {
return _make;
},
set: function(make) {
_make = make;
},
enumerable:true
});
Object.preventExtensions(this);
}

Categories

Resources