How do I make {...myCls}
return {...myCls.instanceVar}
?
I'm trying to implement a custom version of *[Symbol.iterator]() { yield this.internalObj; }
such that object-spreads of my class perform an object-spread operation to myClass.instanceVar
.
Specifically, I want to make {...(new MyClass('a', 'b'}))}
return {...(new MyClass('a', 'b'})).innerVal}
. However, it seems we cannot override object-spread logic, we can only override array-spread logic.
For example, this is a simple class to create an Array
wrapper
class MyArray {
innerArray = [];
getItem(index) {
return (index < this.innerArray.length) ? this.innerArray[index] : null;
}
setItem(index, val) {
const innerArrayLength = this.innerArray.length;
return (index < innerArrayLength)
?
this.innerArray[index] = val
:
this.innerArray.push(val)
;
}
removeItem(index) {
return this.innerArray.splice(index, 1);
}
clear() {
this.innerArray = [];
}
get length() {
return this.innerArray.length;
}
*[Symbol.iterator]() {
return yield* this.innerArray;
}
}
// Usage:
let myArr = new MyArray() // undefined
myArr.setItem(0, 'a') // 1
myArr.setItem(10, 'b') // 2
console.log([...myArr]) // (2) [ 0 => "a", 1 => "b" ]
However, what I want is a way to do that with object class instance variables instead of array class instance variables.
For example, this is what happens when I try to implement a StorageMock
class
class StorageMock {
storage = {};
setItem(key, val) {
this.storage[key] = val;
}
getItem(key) {
return (key in this.storage) ? this.storage[key] : null;
}
removeItem(key) {
delete this.storage[key];
}
clear() {
this.storage = {};
}
get length() {
return Object.keys(this.storage).length;
}
key(index) {
return Object.keys(this.storage)[index] || null;
}
*[Symbol.iterator]() {
return yield* Object.entries(this.storage).map(([ k, v ]) => ({ [k]: v }));
}
}
let myStore = new StorageMock() // undefined
myStore.setItem('a', 'hello'); // undefined
myStore.setItem('b', 'world'); // undefined
console.log({...myStore}); // { storage: { a: "hello", b: "world" } } <== PROBLEM
// Doing the same with localStorage prints out:
// { a: "hello", b: "world" }
// instead of
// { storage: { a: "hello", b: "world" } }
In this case, the Storage API works to spread storage entries when spreading (local|session)Storage
, but creating a special StorageMock
class does not.
Point being that I can't make {...storageMockInstance} === {...(storageMockInstance.storage)}
. So how does one override the object-spreading syntax of an ES class?
I've tried various combinations of Object.create()
, Object.definePropert(y|ies)()
, variants of the in
operator (all of which have relevant access-ability defined here), all depending on the for...in
syntax defininition from the generic-spreading-syntax proposal. But all I've found is that only "standard" destructuring can be used according to references 1, 2, and 3.
But there has to be a way to do this via ESNext classes. None of my attempts to accomplish the ability to use actual native class features instead of those available through AMD module syntax. It doesn't seem reasonable that I couldn't override these fields in a way similar to how other languages do so. i.e. If I could only override how the JS for..in
loop works in the same way that Python allows overriding it, then I could spread the inner variable through a forIn()
method just like toString()
or toJSON()
.
Please do not respond with @babel/polyfill
, core-js
, or babel-jest
for this question. It's not only meant for (local|session)Storage
, but also just a question on a high-level problem.
You cannot.
Unless you cheat. But might not be worth it.
The term "array destructuring" might be a slightly misleading. It actually always starts by getting the iterator of the object and draws values from there until all bindings are satisfied. In fact, it is not only supposed to be used on arrays.
const obj = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
yield 4;
}
};
//1. take iterator
//2. take first three values
const [a, b, c] = obj;
//1. take iterator (do not continue the old one)
//2. take the first value
const [x] = obj;
console.log(a, b, c, x); // 1, 2, 3, 1
Object destructuring, however, does not have a similar mechanism. When using {...x}
the abstract operation CopyDataProperties
is performed. As the name suggests, it will copy properties, rather than invoke some mechanism to get the data to copy.
The properties that will be copied would be
excludedNames
parameter to the abstract operation. Note: this is only relevant when using spread with a rest target, like const {foo, bar, ...rest} = obj;
What could be done is to lie to the runtime about each of these things. This can be done using a Proxy and you need to change the following things:
ownKeys
trap to switch what keys would be used for the destructuring.getOwnPropertyDescriptor
trap to make sure the properties are enumerable.get
trap to give the value for the property.This can be done as a function like this, for example and will make an object behave as if you are using one of its property values:
const obj = {
a: 1,
b: 2,
c: 3,
myProp: {
d: 4,
e: 5,
f: 6
}
};
const x = { ...lieAboutSpread("myProp", obj) };
console.log(x);
function lieAboutSpread(prop, obj) {
//this will be the false target for spreading
const newTarget = obj[prop];
const handler = {
// return the false target's keys
ownKeys() {
return Reflect.ownKeys(newTarget);
},
// return the false target's property descriptors
getOwnPropertyDescriptor(target, property) {
return Reflect.getOwnPropertyDescriptor(newTarget, property);
},
// return the false target's values
get(target, property, receiver) {
return Reflect.get(newTarget, property, receiver);
}
}
return new Proxy(obj, handler);
}
So, this is possible. I am however, not sure it is of that much benefit to simply doing { ...obj.myProp }
. Moreover, the above function could be re-written in a way that does not "lie" at all. But it becomes extremely boring:
const obj = {
a: 1,
b: 2,
c: 3,
myProp: {
d: 4,
e: 5,
f: 6
}
};
const x = { ...lieAboutSpread("myProp", obj) };
console.log(x);
function lieAboutSpread(prop, obj) {
//this will be the target for spreading
return obj[prop];
}
In my opinion, this highlights why the artificial way of masking the object is an overkill.