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)
Related
I'm currently trying to write a class that defines a generic getter and setter for all properties of the object. The closest thing I have right now is functions named get and set for the class as a whole which takes a property name and returns it if it exists.
I've tried to use the computed property names option for dynamically defining a getter, but I'm getting errors on either giving the getter arguments, or errors about prop being undefined. Is there any way to define a pair that works for both obj.prop and obj['prop'] that doesn't require writing them for every property in a class?
Current code for reference:
class Holder {
constructor(def="the door"){
this.holds=def
}
// Generic getter for every possible property
get (prop) {
if(prop in this){
return this[prop];
} else {
return `No property found named '${prop}'`;
}
}
// Generic setter, performs magic that sets `changed` as well.
set (prop, value) {
if(prop in this){
this[prop] = value;
this.changed = true;
}
}
}
const hodor = new Holder();
console.log(hodor.holds); // Expected: "the door"
console.log(hodor.derp); // Expected: "No property found named 'derp'"
So if you run the code above you'll find it's not actually working. The console.log(hodor.holds) is going directly to the underlying this.holds instance property.
A quick search on StackOverflow led me to this Is it possible to implement dynamic getters/setters in JavaScript?. I've modified your code to use this Proxy approach and this now works correctly:
class Holder {
constructor(def = 'the door') {
this.holds = def;
return new Proxy(this, {
get(target, name, receiver) {
if (name in target) {
return target[name];
} else {
return `No property found named '${name}'`;
}
},
set(target, name, receiver) {
if (name in target) {
target[name] = receiver;
target.changed = true;
}
},
});
}
}
const hodor = new Holder();
console.log(hodor.holds); // Logs: the door
console.log(hodor.derp); // Logs: "No property found named 'derp'"
hodor.holds = 'bar';
console.log(hodor.holds); // Logs "bar"
Is there any way to define a pair that works for both obj.prop and obj['prop'] that doesn't require writing them for every property in a class?
Two options for you:
In the constructor, build the accessors in a loop over an array of the names of the properties you want to have accessors. This is creating them for each property, but not writing them for each property. :-)
Or use a Proxy, but you may not want the overhead. With a Proxy object, you can intercept the get and set actions regardless of what property is being gotten/set.
But in both cases, you'll need a place to put the actual values the accessors access. That could be a WeakMap keyed by Holder instances, or a private property (either truly private or pseudo-private), or something else.
Here's a quick sketch of #1 with a WeakMap:
const holderData = new WeakMap();
class Holder {
constructor(def = "the door") {
// Create an object to hold the values for this instance
holderData.set(this, Object.create(null));
this.def = def;
}
}
for (const name of ["def", "x", "y", "z"]) {
Object.defineProperty(Holder.prototype, name, {
get() {
console.log(`(Getting "${name}")`);
return holderData.get(this)[name];
},
set(value) {
console.log(`(Setting "${name}" to "${value}")`);
holderData.get(this)[name] = value;
}
});
}
const h = new Holder();
console.log(`h.def = ${h.def}`);
console.log(`h["def"] = ${h["def"]}`);
h.x = "ecks";
console.log(`h.x = ${h.x}`);
When a Holder instance is no longer reachable, it becomes eligible for garbage collection, and the same happens to the object in the WeakMap.
Here's a quick sketch of #2, also with a WeakMap although you could just use the target object (I think when I wrote this I hadn't had enough coffee yet):
const holderData = new WeakMap();
class Holder {
constructor(def = "the door") {
// Create an object to hold the values for this instance
holderData.set(this, Object.create(null));
// Create the proxy
const proxy = new Proxy(this, {
get(target, propKey, receiver) {
if (typeof propKey === "string") { // Or whatever check you want for
// the properties you want to handle
console.log(`(Getting ${propKey.toString()})`);
return holderData.get(target)[propKey];
}
// Default handling
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (typeof propKey === "string") { // Or whatever
console.log(`(Setting "${propKey.toString()}" to "${value}")`);
holderData.get(target)[propKey] = value;
return true;
}
// Default handling
return Reflect.set(target, propKey, receiver, value);
}
});
proxy.def = def;
return proxy;
}
}
const h = new Holder();
console.log(`h.def = ${h.def}`);
console.log(`h["def"] = ${h["def"]}`);
h.x = "ecks";
console.log(`h.x = ${h.x}`);
Here's that same thing using the target object to hold the values:
class Holder {
constructor(def = "the door") {
this.def = def;
// Return the proxy
return new Proxy(this, {
get(target, propKey, receiver) {
if (typeof propKey === "string") { // Or whatever check you want for
// the properties you want to handle
console.log(`(Getting ${propKey.toString()})`);
return target[propKey];
}
// Default handling
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (typeof propKey === "string") { // Or whatever
console.log(`(Setting "${propKey.toString()}" to "${value}")`);
target[propKey] = value;
return true;
}
// Default handling
return Reflect.set(target, propKey, receiver, value);
}
});
}
}
const h = new Holder();
console.log(`h.def = ${h.def}`);
console.log(`h["def"] = ${h["def"]}`);
h.x = "ecks";
console.log(`h.x = ${h.x}`);
I'm working with JS Proxies for fun and have made decent progress. But currently it's all at a single level. What I would like is to have nested Proxies be returned if I'm making a nested call/access, otherwise just return the object.
// example calls/accesses
data.settings = {fire: true};
data.settings.fire // nested call
// returns true
data.settings // top level call
// returns {fire: true}
// the proxy code
const data = new Proxy({}, {
get: function(target, property, receiver) {
// how to figure out nestedCall?
if (nestedCall) {
return new Proxy(target[property], {
get: function(subTarget, subProperty, subReceiver) {
return 'nonsense, there is nothing nested here';
}.
});
}
else {
return target[property];
}
},
});
Is this even possible?
Is this even possible?
No, it is not possible to distinguish
const val = data.settings.fire; // two accesses
from
const obj = data.settings; // one access
const val = obj.fire; // another access
and return a plain object, instead of a proxy for it, for .settings only in the second case.
Just use a 'set' trap. This 'set' trap example proxies objects as they are being assigned. You can alter the criteria to be more sophisticated as needed.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#A_complete_traps_list_example
const whatICareAbout = ['settings', 'mediaSettings', 'fire', 'video'];
const proxy = {
get: function(obj, prop) {
if(whatICareAbout.includes(prop)) console.log('Get...', prop);
return obj[prop];
},
set: function(obj, prop, value) {
if(typeof value === 'object') {
console.log('Proxy...', prop);
obj[prop] = new Proxy(value, proxy);
} else {
obj[prop] = value;
}
}
};
const p = new Proxy({}, proxy);
p.settings = {fire: true};
p.settings.mediaSettings = {video: false};
console.log(p.settings);
console.log(p.settings.mediaSettings);
I'm trying to programmatically add and delete (for caching purposes) getters from an object. I'm adding a getter like this:
Object.defineProperty(obj, 'text', {
get: getter
})
obj.text should only be evaluated the first time it's accessed, and the calculated value cached for subsequent calls.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get shows how to implement a smart getter like this:
get notifier() {
delete this.notifier;
return this.notifier = document.getElementById('bookmarked-notification-anchor');
}
I can't use delete this.text inside my getter function however. What I've found is, that this is the prototype of the Object rather than the instance - is that correct? And if so, how can I delete the getter of the instance and replace it with the calculated value?
edit:
As per the comments, the getter and object looks something like this:
var obj = {}
obj.value = '2018-04-21T12:00:00Z000'
Object.defineProperty(obj, 'text', {
get: function () {
delete this.text // doesn't seem to work
if (this.value == null) return ''
var text = this.value.split('T')[0].split('-').reverse().join('.')
this.text = text
return text // return this.text ends in Maximum call stack size exceeded
}
})
You need to make the property configurable so that you can delete it:
var obj = {value: '2018-04-21T12:00:00Z000'};
Object.defineProperty(obj, 'text', {
get: function () {
delete this.text
if (this.value == null) return ''
var text = this.value.split('T')[0].split('-').reverse().join('.')
console.log("updating")
this.text = text
return text
},
configurable: true
//^^^^^^^^^^^^^^^^^^
});
console.log("first access");
console.log(obj.text);
console.log("second access");
console.log(obj.text);
Apart from that, if you are having issues with properties inherited from a prototype object, you cannot delete it but need to use defineProperty to shadow it.
What I've found is, that this is the prototype of the Object rather than the instance...
Not in the code you've shown, not unless you're calling it oddly.
One way to do it is to use Object.defineProperty to redefine the property.
So for instance, if you're doing this on a one-off object:
var obj = {
get notifier() {
var value = Math.random();
console.log("Getter called");
Object.defineProperty(this, "notifier", {
value: value
});
return value;
}
};
console.log("First use");
console.log(obj.notifier);
console.log("Second use");
console.log(obj.notifier);
Or if it's not a one-off:
function Maker() {
}
Object.defineProperty(Maker.prototype, "notifier", {
get: function() {
var value = Math.random();
console.log("Getter called");
Object.defineProperty(this, "notifier", {
value: value
});
return value;
},
configurable: true
});
var obj = new Maker();
console.log("First use");
console.log(obj.notifier);
console.log("Second use");
console.log(obj.notifier);
I've stuck to ES5-level stuff above since you didn't seem to be using any ES2015+ features.
Using NodeJS, I'm trying to keep track of changes to Model attributes regardless of what type they may be. Using Object.defineProperty setters usually work great, but for Objects and arrays, if a property of the object is set I'm unable to track changes that are made.
I realize that the setter isn't the object itself, but is there a way I can get it to function as though it is?
For example, can I get the below code to trigger the setter when a "property" of the setter is set?
var model = {};
model.attributes = {};
Object.defineProperty(model, 'options', {
get: function() {
return this.attributes.options;
},
set: function(value) {
console.log('changed options from:', this.attributes.options, 'to', value);
this.attributes.options = value;
}
});
model.options = {};
model.options = { a: 50 }; // triggers setter
model.options.b = 60; // doesn't trigger setter, how can I get it to do so?
If you're using an ES6 transpiler that supports proxies, you can solve the problem like the so:
var handler = {
defineProperty (target, key, descriptor) {
console.log('changed options from:', target[key], 'to', descriptor.value);
if(typeof descriptor.value === 'object'){
descriptor.value = new Proxy(descriptor.value,handler);
}
Object.defineProperty(target, key, descriptor);
return true;
}
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.foo = 'bar2' // changed from undefined to bar2
proxy.foo = 'bar' // changed from bar2 to bar
proxy.options = { a: 50 } // changed from undefined to Object { a: 50 }
proxy.options.a = 15 // changed from 50 to 15
proxy.options.b = 60 // changed from undefined to 60
Inspired by this article
I would like to define a JavaScript property using a property descriptor that has custom attributes, in other words, attributes other than the standard value, writable, etc...
In the example below I have defined a property with a property descriptor that has the custom attribute customAttr. The call to Object.defineProperty works fine but later when I try to loop over the attributes of the property descriptor, my custom attribute is not listed.
Is what I am trying to do possible?
const o = {}
Object.defineProperty(o, 'newDataProperty', {
value: 101,
writable: true,
enumerable: true,
configurable: true,
customAttr: 1,
})
const desc = Object.getOwnPropertyDescriptor(o, 'newDataProperty')
// List the descriptor attributes.
for (const prop in desc) {
console.log(`${prop}: ${desc[prop]}`)
}
// PROBLEM: `customAttr` is not listed
No, it's not possible. This is what Object.defineProperty does:
...
3. Let desc be the result of calling ToPropertyDescriptor with Attributes as the argument.
4. Call the [[DefineOwnProperty]] internal method of O with arguments name, desc, and true.
5. Return O.
And in short, ToPropertyDescriptor simply ignores anything that's not "enumerable", "writable", "configurable", "value", "get" or "set":
...
Let desc be the result of creating a new Property Descriptor that initially has no fields.
If the result of calling the [[HasProperty]] internal method of Obj with argument "enumerable" is true, then
...
(repeat step 3 for other valid descriptor properties)
10. Return desc.
Resurrecting an old post here, but I found the idea interesting. You can extract the fact that functions are objects in javascript, and use the get function as the attribute holder :
function setPropertyAttribute(obj, propertyName, attributeName, attributeValue) {
var descriptor = getCustomPropertyDescriptor(obj, propertyName);
descriptor.get.$custom[attributeName] = attributeValue;
}
function getPropertyAttributes(obj, propertyName) {
var descriptor = getCustomPropertyDescriptor(obj, propertyName);
return descriptor.get.$custom;
}
function getPropertyAttribute(obj, propertyName, attributeName) {
return getPropertyAttributes(obj, propertyName)[attributeName];
}
function getCustomPropertyDescriptor(obj, prop) {
var actualDescriptor = Object.getOwnPropertyDescriptor(obj, prop);
if (actualDescriptor && actualDescriptor.get && actualDescriptor.get.$custom) {
return actualDescriptor;
}
var value = obj[prop];
var descriptor = {
get: function() {
return value;
},
set: function(newValue) {
value = newValue;
}
}
descriptor.get.$custom = {};
Object.defineProperty(obj, prop, descriptor);
return Object.getOwnPropertyDescriptor(obj, prop);
}
Then :
var obj = {
text: 'value',
number: 256
}
setPropertyAttribute(obj, 'text', 'myAttribute', 'myAttributeValue');
var attrValue = getPropertyAttribute(obj, 'text', 'myAttribute'); //'myAttributeValue'
fiddle here.