Aleksandar Bosakov
Aleksandar Bosakov

Reputation: 21

Javascript: is there "Symbol.iterator" analogue for the object spread syntax - { ...obj }?

There is a well known symbol: Symbol.iterator, that, when defined as a generator function property on an object, allows the object to be used in a [...object] syntax. So, you can do, eg., this:

const newArray = [ ...foo, ...object, ...bar ];

However, I couldn't find an analogue of that functionality, allowing this:

const newObject = { ...foo, ...object, ...etc };

or Object.assign with non-own properties. Case in point: instances of ES6 classes with get prop() / set prop() accessors - they are defined on the .prototype property of the class constructor:

const C = class {
  #num = 42;
  #str = 'aaa';
  
  get num() { return this.#num; }
  set num(val) { /* validate new value */ this.#num = val; return true; }

  get str() { return this.#num; }
  set str(val) { /* validate new value */ this.#num = val; return true; }
}

const obj = new C();

Now obj has built-in validation for obj.num = ... and obj.str = .... However, it cannot be used in {...foo, ...obj}, or in Object.assign(foo, obj), because the accessors are on the prototype. Proxies can be spread, and property acces trapped for validation, but one of my goals is to make accessing obj.prop as close in performance as possible to accessing plain object prop. And look at this:

const p = new Proxy({ num: 0 }, {});

suite.add('instance with class-accessors', () => obj.num++);
suite.add('proxy', () => p.num++);
suite.run();

instance with class-accessors x ***235***,971,841 ops/sec ±0.20% (86 runs sampled)
proxy x ***1***,014,238 ops/sec ±1.91% (84 runs sampled)

Two orders of magnitude slower! So, is there a way to allow { ...obj } / Object.assign for instances with prototype based getters?

I did experiment, just in case, with defining *[Symbol.iterator]() {...} in the class, yielding Object.entries-style pairs of [prop, val], didn't work. Didn't find anything useful on MDN's "Well known symbols". Expected to find something like, eg., Symbol.entries, something that can control spread/assign and make instances with getters usage transparent.

Upvotes: 1

Views: 263

Answers (2)

Daniel
Daniel

Reputation: 1

A other method would be, to define the getter|setter from the class prototype to the obj via Object.defineProperty(). Then the spread syntax would work. A example implementation:

function makeProtoGSetterEnumerable(target=this,classF=this?.constructor??target.constructor, bindGet=true){

    let descriptors= Object.getOwnPropertyDescriptors(classF.prototype)

    const log=false;
    if(log)console.log("before (Only proto holds the g|setter)\ntarget Desc:",Object.getOwnPropertyDescriptors(target),"\nproto Desc",descriptors);

    //You don't want to modify certain props on class e.g. .constructor
    let not=['constructor'];
    const desc_entries=Object.entries(descriptors).filter( ([prop_key])=> false===not.includes(prop_key)  )

    for( const [prop_key,desc] of desc_entries ){
        //setting unconfigurable prop raises an Error
        if(desc.configurable===false || desc.enumerable===true ){continue }
        const new_desc={
            ...desc,
            get: desc?.get,
            set: desc?.set,
            enumerable:true,
            configurable:true,
        }
        if(bindGet){ 
          //Only for better preview in web console, no need to click (...) , but otherwise uneccessary & slightly more inefficent 
          new_desc.get=  new_desc.get.bind(target);
        }
        Object.defineProperty( target , prop_key ,new_desc )            
    }
    if(!log){return}
    const desc_after_proto=Object.getOwnPropertyDescriptors(target.constructor.prototype);
    const desc_after=Object.getOwnPropertyDescriptors(target);
    console.log("after (Both proto hold the g|setter)\ntarget Desc:",desc_after,"\nproto Desc",desc_after_proto)
  }

This can be used either in the constructor or on a specific obj. For you class:

const C = class {
  #num = 42;
  #str = 'aaa';
    
  // constructor(){ makeProtoGSetterEnumerable(this) }
  get num() { return this.#num; }
  set num(val) { /* validate new value */ this.#num = val;}

  get str() { return this.#str; }
  set str(val) { /* validate new value */ this.#str = val;}
}

const obj = new C();
console.log( obj , {...obj} )

// {#num:42,#str:'aaa'} {}  //Spread is empty

makeProtoGSetterEnumerable(obj);
console.log(obj, ({...obj}))
// {num: 42, str: 'aaa', #num: 42, #str: 'aaa'} {num: 42, str: 'aaa'} 
//Spread works now and has only non private getters

//Info chrome allows viewing+editing of .#props in webconsole, but in modules they work as intendet 

Upvotes: 0

Bergi
Bergi

Reputation: 664164

No, there is not. Object literal spread syntax just picks all enumerable own properties, that's it.

The common workaround is to define a getter method to serialise an object that can be spread as you want, often also used for JSON conversion:

class C {
  #num = 42;
  #str = 'aaa';
  get num() { return this.#num; }
  get str() { return this.#num; }
  // …
  toJSON() {
    return {num: this.#num, str: this.#str};
  }
}

const object = new C();

const newObject = { x: 1, ...object.toJSON(), y: 2 };
console.log(newObject);
console.log(JSON.stringify(object));

Upvotes: 1

Related Questions