Cannot redefine property in Node.js (though can in browser) - javascript

I'd like to redefine an existing property inside a class (it's for experimenting purposes; I know I shouldn't).
For some reason, the following code works in a browser (Chrome), but not Node.js (v18.12.0).
function re(instance, name, val) {
let _value = val;
Object.defineProperty(instance, name, {
get: () => { return _value },
set: (v) => { return _value = v }
})
return val;
}
class A {
prop = re(this, 'prop', 456)
}
const a = new A()
console.log(a.prop)
The Chrome console output would be 456, but Node.js will be like nope, no redefining today, instead take this: TypeError: Cannot redefine property: prop. Which is sad. I tested on my PC plus at some online Node.js interpreter (replit.com).

You need to the provide configurable attribute.
function re(instance, name, val) {
let _value = val;
Object.defineProperty(instance, name, {
get: () => { return _value },
set: (v) => { return _value = v },
configurable: true
}, )
return val;
}
class A {
prop = re(this, 'prop', 456)
}
const a = new A()
console.log(a.prop)
You can refer to the MDN documentation: TypeError: can't redefine non-configurable property "x"

Related

Javascript use decorator to change static class field value?

Is it possible for a JS field decorator to change its value?
A simplified use case would be something like this:
const addItem = (newValue) => {
return function (target) {
target.value.push(newValue);
};
};
class Test {
#addItem(4)
static values = [1,2,3];
}
const test = new Test();
console.log(test.constructor.values) // [1,2,3,4]
Using the following experimental decorators:
'#babel/plugin-proposal-decorators',
{
version: '2018-09',
decoratorsBeforeExport: true,
},
End goal is to make a decorator to inject tailwind style sheets into a lit elements static styles. Currently using a mixin for this but just doing this for fun and to learn whats possible with decorators.
Update to Barmars comments
When trying to return a value from the inner function, I end up getting an error:
export const addItem = (value) => {
return function (target) {
return [value];
};
};
Uncaught TypeError: An element descriptor's .kind property must be either "method" or "field", but a decorator created an element descriptor with .kind "undefined"
Looking at the documentation, the variables getting passed to each of these functions doesn't seem to match either.
function logged(value, { kind, name }) {
if (kind === "field") {
return function (initialValue) {
console.log(`initializing ${name} with value ${initialValue}`);
return initialValue;
};
}
}
When running that example, the 2nd parameter to logged() is undefined. "initialValue" also is an object, not the value:
Object { kind: "field", key: "styles", placement: "own", descriptor: {…}, initializer: value(), … }
Nicolo Ribaudo was able to help me over on Babel's discussions. The correct way to do this is to use the initializer function:
const addItem = (newValue) => {
return function (target) {
const { initializer } = target;
target.initializer = function () {
return [
...initializer.call(this),
newValue,
];
};
};
};
class Test {
#addItem(4)
static values = [1,2,3];
}
const test = new Test();
console.log(test.constructor.values) // [1,2,3,4]

Is there a way to use an Accessor Function to define a property of an object in JS?

I have two inputs:
data = {"year": 2021}
valueAccessor = (d) => d["year"];
These two input can be dynamic, so I do not know what will be in the object or function.
I want create a new object, and set values to that object using the Accessor function. Is there a way to do this?
output = {}
output = definePropertyUsingFunction(output, valueAccessor, newValue)
Short answer is "no". But...
If you're in some sort of controlled learning environment, there are some things you could try.
If you know for a fact that:
Your getter function is pure (no side effects), and
Your getter function always accesses 1 property,
you could
Use a Proxy that tracks property access
Make a call to the getter to trigger it (again, if this is a pure function it should not have any side effects)
Write a value to the logged property
Here's a proof of concept:
const data = { year: 2000, month: 1, day: 28 };
const getYear = obj => obj.year;
const getMonth = obj => obj.month;
const toString = obj => `${obj.year}/${obj.month}/${obj.day}`;
const writeViaSimpleGetter = (obj, value, getter) => {
let propName = null;
const proxy = new Proxy(obj, {
get: (target, key) => {
if (propName) throw "Multiple properties accessed";
propName = key;
}
});
// Call the getter
getter(proxy);
if (!propName) throw "No property accessed";
return { ...obj, [propName]: value };
}
// Simple getters work:
console.log(
writeViaSimpleGetter(data, 2021, getYear)
)
console.log(
writeViaSimpleGetter(data, 12, getMonth)
)
// This throws:
try {
console.log(
writeViaSimpleGetter(data, "oops", toString)
)
} catch(e) { console.log("ERROR:", e) }
// This also throws:
try {
console.log(
writeViaSimpleGetter(data, "oops", () => {})
)
} catch(e) { console.log("ERROR:", e) }

javascript print error when calling missing object property

I would like to write a proxy object to automatically print errors when calling some property in original object which is not found.
const proxyObjectFn = () => {
const _obj = Object.assign({}, originalObject);
const get = (key) => {
const value = _obj[key];
if (value === undefined) {
console.error(`${key} not found`);
}
return value;
};
return {
get,
};
};
const proxyObject = proxyObjectFn();
export default proxyObject;
// caller
proxyObject.get('someProperty')
This works, but is there any elegant way so that I can call through proxyObject.someProperty instead of proxyObject.get('someProperty')?
Update
Let me make it more specific. Actually I am writing a translation object.
Original object may be from json, like { "HELLO_KEY": "Hello World" }. I am to call like { label: _t.SOME_I18N_KEY } in UI display code, assuming _t is the proxy object above. I can print the warning to tell me there is missing translation.
You can use the Proxy object:
const handler = {
get: (obj, prop) => {
if(!obj.hasOwnProperty(prop)) console.error(`${prop} not found`);
return obj[prop];
}
};
const _t = new Proxy({ "HELLO_KEY": "Hello World" }, handler);
console.log(_t.HELLO_KEY);
console.log(_t.SOME_NONEXISTENT_KEY);

How to use MutationObserver with an Object?

I have the following object:
mind = {
queries: [],
actions: []
};
and I update queries and actions according to another function.
I wanted to detect every time they're being updated and changed, and i've heard about MutationObserver, so I tried to call it:
var muob = (window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver);
var ob = new muob(function(m) {
console.log('It works!');
});
ob.observe(mind, { subtree: true });
But it doesn't work. I get in return:
Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
What's wrong with my code?
MutationObserver is only something that works for DOM elements, not objects:
var ob = new MutationObserver(function(m) {
console.log('It works!');
});
ob.observe(mind, { childList: true });
mind.textContent = 'foo';
<div id="mind"></div>
For what you're doing, you can make the queries and actions properties have methods to update the arrays instead, eg:
const mind = {
_queries: [],
_actions: [],
queries: {
push(...args) {
console.log('Push detected');
mind._queries.push(...args);
},
get() {
return mind._queries;
}
},
actions: {
push(...args) {
console.log('Push detected');
mind._actions.push(...args);
},
get() {
return mind._actions;
}
}
};
mind.queries.push('foo');
console.log(mind.queries.get());
Or, using a Proxy:
const handler = {
set(obj, prop, newVal) {
console.log('Change detected');
return obj[prop] = newVal;
},
get(obj, prop) {
return obj[prop];
}
};
const mind = {
queries: new Proxy([], handler),
actions: new Proxy([], handler),
};
mind.queries.push('foo');
console.log(mind.queries);
(the above snippet logs Change detected twice because it has to update both the 0 property on the array and change the array's .length)
Still, this is pretty odd - it would be much more elegant if, at the location in the code where you change the array, you also call another function (the It works! part) to indicate an update has occurred.
MutationObserver is for DOM elements, not JavaScript objects.
There is no equivalent for JavaScript objects,¹ but you can use a combination of Proxy objects and accessor properties to get a notification of any change to those arrays. You'd use the Proxy to know when the arrays were modified, and by making x and y accessor properties, you could know when they were changed (and use that as the opportunity to wrap them in proxies).
Here's a rough sketch:
const mind = (() => {
function wrap(array) {
return new Proxy(array, {
set(target, propName, value, receiver) {
beforeChange(target, propName, value, receiver);
const result = Reflect.set(target, propName, value);
afterChange(target, propName, value, receiver);
return result;
}
// ...you may want other traps here...
});
}
function beforeChange(target, name, value, receiver) {
console.log("beforeChange", name, value);
}
function afterChange(target, name, value, receiver) {
console.log("afterChange", name, value);
}
let queries = wrap([]);
let actions = wrap([]);
return {
get queries() {
return queries;
},
set queries(value) {
beforeChange(queries, "*queries*", value);
queries = wrap(value);
afterChange(queries, "*queries*", value);
},
get actions() {
return queries;
},
set queries(value) {
beforeChange(queries, "*actions*", value);
queries = wrap(value);
afterChange(queries, "*actions*", value);
}
};
})();
mind.queries.push(1);
mind.actions.push("two");
console.log(mind.actions);
mind.actions[0] = "TWO";
console.log(mind.actions);
mind.queries = [];
mind.queries[10] = "ten";
console.log(mind.queries);
.as-console-wrapper {
max-height: 100% !important;
}
¹ For a brief time there was going to be Object.observe, but it was abandoned in favor of Proxy.

Recursive Object.defineProperty() getters

I'm trying to recursively assign the object values from class constructor argument as a properties of the class. Can't figure out how to do a recursion - getting 'Maximum call stack size exceeded' and infinity loops most of the time.
Here is the demo:
const Locale = function(rules) {
for (let prop in rules) {
Object.defineProperty(this, prop, {
get: function () {
console.log('getter for "%s" called', prop)
return rules[prop];
}
});
}
}
const rules = {
a: {
b: {
c: 'value'
}
}
}
const locale = new Locale(rules);
console.log(locale.a.b.c);
Now I'm getting the following console output:
getter for "a" called
value
How to assign a getter for each level of the rules object? Expected console output:
getter for "a" called
getter for "b" called
getter for "c" called
value
You need to create a Locale object for each nested level of the rules object:
const Locale = function(rules) {
for (let prop in rules) {
Object.defineProperty(this, prop, {
get: function () {
console.log('getter for "%s" called', prop);
// create new Locale if object, return value if not an object
if( rules[prop] !== null && typeof rules[prop] === 'object' )
return new Locale( rules[prop] );
else
return rules[prop];
}
});
}
}
const rules = {
a: {
b: {
c: 'value'
}
}
}
const locale = new Locale(rules);
console.log(locale.a.b.c);
If you just want to use a function instead of a class you can do:
var locale = (rules) => {
if (rules !== null && typeof rules !== 'object') return rules
return Object.defineProperties({}, Object.keys(rules).reduce((acc, key) => ({
...acc,
[key]: {
get: () => {
console.log('getter for "%s" called', key)
return locale(rules[key])
}
}
}), {}))
}
var l = locale(rules)
console.log('locale', l)
console.log(l.a.b.c)

Categories