Do JavaScript property descriptors support custom attributes? - javascript

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.

Related

Javascript: how to assign setter to object via `=` sign?

Is there way to assign setter/getter to existing object via = sign?
window.somesetter = set(v) {};
window.somesetter = function set(v) {};
window.somesetter = set settername(v) {};
No, there isn't.
If you want to assign a setter or getter to an existing object, use defineProperty.
Object.defineProperty(window, 'setter', {
set: function(value) { window.test = value }
});
window.setter = "Hello";
console.log(window.test);
I'd say no. But the answer is "kinda yes" if you use a proxy instead of the object itself.
But this is black magic and by all means is NOT recommended for production :)
const winProxy = new Proxy(window, {
set(target, name, value) {
if (typeof value === 'function' && {set: 1, get: 1}[value.name]) {
const descriptor = Reflect.getOwnPropertyDescriptor(target, name) || {
enumerable: false,
configurable: true,
}
descriptor[value.name] = value
Reflect.defineProperty(target, name, descriptor)
} else {
Reflect.set(target, name, value)
}
}
})
winProxy.someProp = function set(v) {
console.log('setter', v);
}
winProxy.someProp = function get() {
console.log('getter')
}
window.someProp = 1;
window.someProp
Try this form:
window.somesetter = function (v) {};

Object defineProperty setter for properties

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

Javascript equivalent function to PHP __call [duplicate]

I am aware of how to create getters and setters for properties whose names one already knows, by doing something like this:
// A trivial example:
function MyObject(val){
this.count = 0;
this.value = val;
}
MyObject.prototype = {
get value(){
return this.count < 2 ? "Go away" : this._value;
},
set value(val){
this._value = val + (++this.count);
}
};
var a = new MyObject('foo');
alert(a.value); // --> "Go away"
a.value = 'bar';
alert(a.value); // --> "bar2"
Now, my question is, is it possible to define sort of catch-all getters and setters like these? I.e., create getters and setters for any property name which isn't already defined.
The concept is possible in PHP using the __get() and __set() magic methods (see the PHP documentation for information on these), so I'm really asking is there a JavaScript equivalent to these?
Needless to say, I'd ideally like a solution that is cross-browser compatible.
This changed as of the ES2015 (aka "ES6") specification: JavaScript now has proxies. Proxies let you create objects that are true proxies for (facades on) other objects. Here's a simple example that turns any property values that are strings to all caps on retrieval, and returns "missing" instead of undefined for a property that doesn't exist:
"use strict";
if (typeof Proxy == "undefined") {
throw new Error("This browser doesn't support Proxy");
}
let original = {
example: "value",
};
let proxy = new Proxy(original, {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
let rv = Reflect.get(target, name, receiver);
if (typeof rv === "string") {
rv = rv.toUpperCase();
}
return rv;
}
return "missing";
}
});
console.log(`original.example = ${original.example}`); // "original.example = value"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = VALUE"
console.log(`proxy.unknown = ${proxy.unknown}`); // "proxy.unknown = missing"
original.example = "updated";
console.log(`original.example = ${original.example}`); // "original.example = updated"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = UPDATED"
Operations you don't override have their default behavior. In the above, all we override is get, but there's a whole list of operations you can hook into.
In the get handler function's arguments list:
target is the object being proxied (original, in our case).
name is (of course) the name of the property being retrieved, which is usually a string but could also be a Symbol.
receiver is the object that should be used as this in the getter function if the property is an accessor rather than a data property. In the normal case this is the proxy or something that inherits from it, but it can be anything since the trap may be triggered by Reflect.get.
This lets you create an object with the catch-all getter and setter feature you want:
"use strict";
if (typeof Proxy == "undefined") {
throw new Error("This browser doesn't support Proxy");
}
let obj = new Proxy({}, {
get(target, name, receiver) {
if (!Reflect.has(target, name)) {
console.log("Getting non-existent property '" + name + "'");
return undefined;
}
return Reflect.get(target, name, receiver);
},
set(target, name, value, receiver) {
if (!Reflect.has(target, name)) {
console.log(`Setting non-existent property '${name}', initial value: ${value}`);
}
return Reflect.set(target, name, value, receiver);
}
});
console.log(`[before] obj.example = ${obj.example}`);
obj.example = "value";
console.log(`[after] obj.example = ${obj.example}`);
The output of the above is:
Getting non-existent property 'example'
[before] obj.example = undefined
Setting non-existent property 'example', initial value: value
[after] obj.example = value
Note how we get the "non-existent" message when we try to retrieve example when it doesn't yet exist, and again when we create it, but not after that.
Answer from 2011 (obsoleted by the above, still relevant to environments limited to ES5 features like Internet Explorer):
No, JavaScript doesn't have a catch-all property feature. The accessor syntax you're using is covered in Section 11.1.5 of the spec, and doesn't offer any wildcard or something like that.
You could, of course, implement a function to do it, but I'm guessing you probably don't want to use f = obj.prop("example"); rather than f = obj.example; and obj.prop("example", value); rather than obj.example = value; (which would be necessary for the function to handle unknown properties).
FWIW, the getter function (I didn't bother with setter logic) would look something like this:
MyObject.prototype.prop = function(propName) {
if (propName in this) {
// This object or its prototype already has this property,
// return the existing value.
return this[propName];
}
// ...Catch-all, deal with undefined property here...
};
But again, I can't imagine you'd really want to do that, because of how it changes how you use the object.
Preface:
T.J. Crowder's answer mentions a Proxy, which will be needed for a catch-all getter/setter for properties which don't exist, as the OP was asking for. Depending on what behavior is actually wanted with dynamic getters/setters, a Proxy may not actually be necessary though; or, potentially, you may want to use a combination of a Proxy with what I'll show you below.
(P.S. I have experimented with Proxy thoroughly in Firefox on Linux recently and have found it to be very capable, but also somewhat confusing/difficult to work with and get right. More importantly, I have also found it to be quite slow (at least in relation to how optimized JavaScript tends to be nowadays) - I'm talking in the realm of deca-multiples slower.)
To implement dynamically created getters and setters specifically, you can use Object.defineProperty() or Object.defineProperties(). This is also quite fast.
The gist is that you can define a getter and/or setter on an object like so:
let obj = {};
let val = 0;
Object.defineProperty(obj, 'prop', { //<- This object is called a "property descriptor".
//Alternatively, use: `get() {}`
get: function() {
return val;
},
//Alternatively, use: `set(newValue) {}`
set: function(newValue) {
val = newValue;
}
});
//Calls the getter function.
console.log(obj.prop);
let copy = obj.prop;
//Etc.
//Calls the setter function.
obj.prop = 10;
++obj.prop;
//Etc.
Several things to note here:
You cannot use the value property in the property descriptor (not shown above) simultaneously with get and/or set; from the docs:
Property descriptors present in objects come in two main flavors: data descriptors and accessor descriptors. A data descriptor is a property that has a value, which may or may not be writable. An accessor descriptor is a property described by a getter-setter pair of functions. A descriptor must be one of these two flavors; it cannot be both.
Thus, you'll note that I created a val property outside of the Object.defineProperty() call/property descriptor. This is standard behavior.
As per the error here, don't set writable to true in the property descriptor if you use get or set.
You might want to consider setting configurable and enumerable, however, depending on what you're after; from the docs:
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.
On this note, these may also be of interest:
Object.getOwnPropertyNames(obj): gets all properties of an object, even non-enumerable ones (AFAIK this is the only way to do so!).
Object.getOwnPropertyDescriptor(obj, prop): gets the property descriptor of an object, the object that was passed to Object.defineProperty() above.
obj.propertyIsEnumerable(prop);: for an individual property on a specific object instance, call this function on the object instance to determine whether the specific property is enumerable or not.
The following could be an original approach to this problem:
var obj = {
emptyValue: null,
get: function(prop){
if(typeof this[prop] == "undefined")
return this.emptyValue;
else
return this[prop];
},
set: function(prop,value){
this[prop] = value;
}
}
In order to use it the properties should be passed as strings.
So here is an example of how it works:
//To set a property
obj.set('myProperty','myValue');
//To get a property
var myVar = obj.get('myProperty');
Edit:
An improved, more object-oriented approach based on what I proposed is the following:
function MyObject() {
var emptyValue = null;
var obj = {};
this.get = function(prop){
return (typeof obj[prop] == "undefined") ? emptyValue : obj[prop];
};
this.set = function(prop,value){
obj[prop] = value;
};
}
var newObj = new MyObject();
newObj.set('myProperty','MyValue');
alert(newObj.get('myProperty'));
You can see it working here.
I was looking for something and I figured out on my own.
/*
This function takes an object and converts to a proxy object.
It also takes care of proxying nested objectsa and array.
*/
let getProxy = (original) => {
return new Proxy(original, {
get(target, name, receiver) {
let rv = Reflect.get(target, name, receiver);
return rv;
},
set(target, name, value, receiver) {
// Proxies new objects
if(typeof value === "object"){
value = getProxy(value);
}
return Reflect.set(target, name, value, receiver);
}
})
}
let first = {};
let proxy = getProxy(first);
/*
Here are the tests
*/
proxy.name={} // object
proxy.name.first={} // nested object
proxy.name.first.names=[] // nested array
proxy.name.first.names[0]={first:"vetri"} // nested array with an object
/*
Here are the serialised values
*/
console.log(JSON.stringify(first)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}
console.log(JSON.stringify(proxy)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}
var x={}
var propName = 'value'
var get = Function("return this['" + propName + "']")
var set = Function("newValue", "this['" + propName + "'] = newValue")
var handler = { 'get': get, 'set': set, enumerable: true, configurable: true }
Object.defineProperty(x, propName, handler)
this works for me

Object.defineProperty for IE

var o = {}; // Creates a new object
Example of an object property added with defineProperty with a data property descriptor
Object.defineProperty(o, "a", {value : 37,
writable : true,
enumerable : true,
configurable : true});
How to implement for IE7-8?(with attributes writable,enumerable,configurable)
if (!Object.defineProperty) {
Object.defineProperty = function (obj, prop, descriptor) {
if (arguments.length < 3) { // all arguments required
throw new TypeError("Arguments not optional");
}
prop += ""; // convert prop to string
...

Defining read-only properties in JavaScript

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)

Categories