JavaScript method to over-ride default behaviour of object destructuring - javascript

Is there a way in JS to over-ride the default behavior of an object when it is de-structured?
// Normally destructing lifts properties from an object
const foo = {
a: 1,
b: 2,
};
const { a, b } = foo; // a = 1, b = 2
// I would like to have a method return the properties to be
// destructured
const bar = {
toObject: () => {
return { a, b };
},
};
const { a, b } = bar; // a = undefiner, b = undefined
I know that I could simply use const { a, b } = bar.toObject(); but that requires the consumer of the object to know how it's internals work and breaks the principle of least astonishment.
The closest thing I can think of to what I want is the toJSON magic method.

Nope. The specification requires the right hand side to resolve to a value that can be converted to an object via ToObject, which simply returns the object itself if it is passed one (i.e. no special method on the object is called to convert it to something else).

If you'd use array destructuring, that would work:
const [a, b] = {
*[Symbol.iterator]() {
yield "some"; yield "stuff";
}
};

You can make your toObject work as intended by decorating the target with a Proxy that intercepts ownKeys and get to fake an object for destructuring:
let withToObject = obj => new Proxy(obj, {
ownKeys(o) {
return Object.keys(o.toObject())
},
get(o, prop) {
return o.toObject()[prop]
}
});
let bar = withToObject({
aa: 11,
bb: 22,
cc: 33,
toObject() {
return {
a: this.aa,
b: this.bb
};
}
});
const {a, b} = bar;
console.log(a, b)
Of course, this affects not only destructuring, but also any other interaction with the object, like serialization, so you have to take measures to make these work too. For example, to support JSON, patch get like this:
get(o, prop) {
if (prop === 'toJSON')
return () => o; // or o.toObject(), whatever fits better
return o.toObject()[prop]

Related

Add nested properties to non existent properties using dot notation

In javascript, we can add new properties to an object using dot notation
const obj = {}
obj.a = "hello"
console.log(obj) // prints { a: "hello" }
However, with dot notation it is not possible to add a property to a not yet existent object
obj.a.b = "hello" // <-- cannot set properties of undefined (setting 'b')
obj.a = { b: "hello" } // <-- OK
I would like to achieve this behaviour
const obj = {}
obj.a.b = "hello"
console.log(obj) // prints { a: { b: "hello" } }
My Idea
The only thing i could think of which could get close to this, would be using a proxy
const obj = new Proxy({}, {
set(target, key, receiver) {
// if a.b could make it here before the error is thrown, i'd handle this
// btw, this means that "key" should contain [a,b] which is not how this works.
}
})
obj.a.b = "hello"
The proxy idea cannot work and probably there is absolutely no way of changing JS's native behaviour like I'm asking but maybe I'm missing something instead?
A proxy does work. You need to use the get trap instead of set:
const obj = new Proxy({}, {
get(target, key, receiver) {
if (!(key in target)) return (target[key] = {});
return Reflect.get(target, key);
},
});
obj.a.b = "hello";
console.log(obj);

Execution context of the ' this ' keyword in clone function implementation

I was looking into a function that creates a copy of provided object. I understand mostly what's happening except for the line that involves this keyword. I do understand that the original design of the this keyword was meant to point to an instance of an object in class definitions if we go back to the origins of the this keyword that was borrowed from C++. But JavaScript decided to use this keyword to provide one extra feature, carrying a link to execution context. In the following example I am trying to understand why are we using this keyword. If you have any thoughts, I would really appreciate it.
function clone(obj) {
const replace = {};
let idx = 0;
const undefCache = [];
const replacer = (key, value) => {
let result;
if (value === undefined) {
result = '__undefined__';
} else if (typeof value === 'symbol' || typeof value === 'function') {
const keyIdx = `__replaced__${idx}`;
idx += 1;
replace[keyIdx] = [this, key]; // I understand mostly what's happening except for the line
result = keyIdx;
} else {
result = value;
}
return result;
};
function reviver(key, value) {
let result;
if (value === '__undefined__') {
undefCache.push([this, key]);// I understand mostly what's happening except for the line
} else if (replace[value] !== undefined) {
result = replace[value][0][key];
} else {
result = value;
}
return result;
}
const json = JSON.stringify(obj, replacer);
console.log(json);
const newObject = JSON.parse(json, reviver);
undefCache.forEach(el => {
const [o, key] = el;
o[key] = undefined;
});
return newObject;
}
const source = {
a: 2,
b: '2',
c: false,
g: [
{ a: { j: undefined }, func: () => {} },
{ a: 2, b: '2', c: false, g: [{ a: { j: undefined }, func: () => {} }] }
]
};
const targetOne = clone(source);
console.log(targetOne);
It's used to handle nested objects when doing serialization/deserialization with JSON.parse/stringify on special values.
Within the replacer/reviver functions, the this context is the current object that the serializer (stringify) or deserializer (parse) is working on.
For example, for the object below:
myObject = {
"foo": {
"bar": function () {}
},
"bar": "Different bar"
}
When it's processing the item myObject["foo"]["bar"], this inside the replacer will be a reference to myObject["foo"] with key = "bar" and value = function () {}". This is useful because without the reference, we wouldn't know whether we were processing myObject["bar"] or myObject["foo"]["bar"].
Thus when it is saved into the array, it really just saved pair = [myObject["foo"], "bar"]. Later when it's recovered, for each of these pairs, it can just do pair[0][pair[1]] to recover myObject["foo"]["bar"].
This works similarly with the reviver and undefined. Here the problem is that the reviver cannot return undefined and have the value set to undefined, so instead the code snippet remembers which keys are like this and post-processes the copy of the object to set them properly.
Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter

object building not working as expected

I have 3 different classes A, B, C.
A is, essentially, the parent of both B and C. When building both B and C object, A has to be included too.
Eventually, the class returns an object, which is currently B (inherits A), or C (inherits A), or just A.
Initialising of new object:
const A = new A("a", "b", "c").json(); or const C = new C("a", "b", "c", "cClass1", "cClass2").json();
At the moment, I use inheritance to achieve this:
export class A {
constructor (option1, option2, option3) {
this.option1 = option1;
this.option2 = option2;
this.option3 = option3;
}
json () {
return {
key1: this.option1,
key2: this.option2,
key3: this.option3,
};
}
}
export class B extends A {
constructor (option1, option2, option3, bClass1, bClass2) {
super(option1, option2, option3);
this.bClassKey1 = bClass1;
this.bClassKey2 = bClass2;
}
json () {
return {
...super.json(),
bClassKey1: this.bClass1,
bClassKey2: this.bClass2
};
}
}
export class C extends A {
constructor (option1, option2, option3, cClass1, cClass2) {
super(option1, option2, option3);
this.cClassKey1 = cClass1;
this.cClassKey2 = cClass2;
}
json () {
return {
...super.json(),
cClassKey1: this.cClass1,
cClassKey2: this.cClass2
};
}
}
I now need to change how the objects are built, because I need to achieve the following:
I need an object that contains all of the classes unique parameters, like so:
{
key1: option1,
key2: option2,
key3: option3,
bClassKey1: bClass1,
bClassKey2: bClass2,
cClassKey1: cClass1,
cClassKey2: cClass2
}
However, I cannot use multiple inheritance in JS (apart from mixin NPM, but I'd rather attempt to achieve it natively).
How can I return a object, that's built with A parameters, B parameters (without A) and C parameters (without A). However, there's still situations where B and C need to be built, that extends the parent of A.
It sounds like you want to use aggregation rather than inheritance. Then, each of the classes would have a method that added its information to an object, and you'd use any combination of those methods you wanted.
/*export*/ class A {
constructor (option1, option2, option3) {
this.option1 = option1;
this.option2 = option2;
this.option3 = option3;
}
json (target = {}) {
// Could also use Object.assign here if **all** enumerable properties are desired
target.key1 = this.option1;
target.key2 = this.option2;
target.key3 = this.option3;
return target;
}
}
/*export*/ class B {
constructor (bClass1, bClass2) {
this.bClassKey1 = bClass1;
this.bClassKey2 = bClass2;
}
json (target = {}) {
// Could also use Object.assign here if **all** enumerable properties are desired
// (Duplicating the names introduces the opportunity of error, which in fact
// there was in the question)
target.bClassKey1 = this.bClassKey1;
target.bClassKey2 = this.bClassKey2;
return target;
}
}
/*export*/ class C {
constructor (cClass1, cClass2) {
this.cClassKey1 = cClass1;
this.cClassKey2 = cClass2;
}
json (target = {}) {
// Could also use Object.assign here if **all** enumerable properties are desired
// (Duplicating the names introduces the opportunity of error, which in fact
// there was in the question)
target.cClassKey1 = this.cClassKey1;
target.cClassKey2 = this.cClassKey2;
return target;
}
}
const a = new A("option1value", "option2value", "option3value");
const b = new B("bClass1value", "bClass2value");
const c = new C("cClass1value", "cClass2value");
const o = c.json(b.json(a.json()));
/* Or, but it requires more temporary objects:
const o = {...a.json(), ...b.json(), ...c.json()};
*/
console.log(o);

How to make a property of an object always have the same value as another

I have the following Object
{
a: "123"
b: "$a"
}
b should always have the value of a.
Any idea of how I can make it with JavaScript?
You could use a getter method for the property b with an object.
var obj = {
a: "123",
get b() {
return this.a;
}
};
console.log(obj.b);
Otherwise, you are looking for an ES6 feature, Proxy
The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).
Then take the prop and check if the value contains a $ and return this value of this property.
var obj = { a: "123", b: "$a" },
p = new Proxy(obj, {
get: function(target, prop) {
return target[prop][0] === '$' ? target[target[prop].slice(1)] : target[prop];
}
});
console.log(p.b);
If you're meaning a real JavaScript object you can do this with a getter and a setter via Object.defineProperty().
Or you can create a "class"
var obj= new function()
{
this.a=1212,
this.b=this.a
}
console.log(obj.b)

Assign key to multiple objects

Is it possible to assign a value to multiple JavaScript objects at the same time?
Obviously this can be done with a for loop etc, but I'm curious if there's something in the new version of the language that makes this possible. Similar syntax already exists in a number of other languages, I just can't find the JavaScript equivalent.
Ideally, the syntax would look something like this:
{App1, App2, App3}.foo = "bar"
App1.foo === "bar" // true
App2.foo === "bar" // true
You are effectively looking for lenses, which can abstract over such operations and also provide multiple targets. There are various JS implementations around, though I didn't find any that uses lists. With them, it would look something like
set(onList(property("foo")), [App1, App2, App3]);
But that's ugly, right? And you were asking for new ES6 features. Yes, a Proxy can help us make this a lot more beautiful indeed:
ListProxy(App1, App2, App3).foo = "bar";
Here's how you'd implement such a function:
const ListProxy = (() => {
const handler = {
set(target, property, value) {
for (const t of target)
t[property] = value;
},
get(target, property) {
if (typeof target == "function")
target = target.values;
const maybe = target.filter(x => property in Object(x));
if (maybe.length == 0) return undefined;
let values = maybe.map(x => x[property]);
if (values.every(v => typeof v == "function")) {
function fnList(...args) {
return maybe.map(v => v[property](...args));
}
fnList.values = values;
values = fnList;
}
return new Proxy(values, handler);
}
};
return function ListProxy(...args) { return new Proxy(args, handler); };
})();
The get method is not so vitally important, but it does allow for deeper chaining and even function calls instead of assignments:
ListProxy({value:"ax"}, {value:"by"}).value[0].toUpperCase(); // ["A","B"]
There is no native way to do it. However, if you are just looking for similar syntax, you can do something similar. You can create a proxy function which will do it for you.
var _ = (...args) => {
var proxy = new Proxy(args, {
set: (target, property, value) => {
target.forEach(object => object[property] = value);
}
});
return proxy;
};
var App1 = {}, App2 = {}, App3 = {};
_(App1, App2, App3).value = {
foo: 'bar'
};
_(App1, App2, App3).someOtherValue = {
foo: 'baz'
};
console.log(App1); // { value: { foo: 'bar' }, someOtherValue: { foo: 'baz' } }
console.log(App2); // { value: { foo: 'bar' }, someOtherValue: { foo: 'baz' } }
console.log(App3); // { value: { foo: 'bar' }, someOtherValue: { foo: 'baz' } }
The only way to make something like the syntax you propose work is to extend the Object prototype, whether or not one thinks that's a good idea (it's not).
const App1 = {}, App2 = {}, App3 = {};
Object.defineProperty(Object.prototype, 'values', {
set(value) {
for (let prop in this) this[prop].value = value;
}
});
({App1, App2, App3}).values = "foo";
console.log(App1.value);
You would never be able to write {App1, App2, App2}.value, because the JS parser would interpret the leading { as the beginning of a block. Hence the need to enclose it in parentheses.
You cannot use value to set all the values, since that would conflict with the value property you want to set on the individual objects. Hence we use values instead.
I don't think a special syntax is required for this, I'd rather assign it using basic ES6:
const baz = { foo: "bar" };
[App1, App2, App3].forEach(app => app.value = baz);
you can use:
App1.value = App2.value = {foo: "bar"};
Or
App1.value.foo = App2.value.foo = "bar";

Categories