36

I have an ES6 class (transcompiled with babeljs) with a getter property. I understand that these properties are not enumerable by default. However, I do not understand why I am not able to make the property enumerable using Object.defineProperty

// Declare class
class Person {
  constructor(myName) {
    this.name = myName;
  }

  get greeting() {
    return `Hello, I'm ${this.name}`;
  }
}

// Make enumerable (doesn't work)
Object.defineProperty(Person, 'greeting', {enumerable: true});

// Create an instance and get enumerable properties
var person = new Person('Billy');
var enumerableProperties = Object.keys(person);
// => ['name']

Plunker Example

lightswitch05
  • 9,058
  • 7
  • 52
  • 75

4 Answers4

54

ES6 style getters are defined on the prototype, not on each individual person. To set the greeting property to enumerable you need to change:

// Make enumerable (doesn't work)
Object.defineProperty(Person, 'greeting', {enumerable: true});

To:

// Make enumerable
Object.defineProperty(Person.prototype, 'greeting', {enumerable: true});

Object.keys only returns that object's own enumerable properties, so properties on the prototype are not returned. You will find the greeting property in Object.keys( Object.getPrototypeOf( person ) ), or in a for...in loop. Updated Plunker

If instead, you want each individual instance of Person to have its own greeting you can define it in the constructor:

class Person {
  constructor(myName) {
    this.name = myName;

    Object.defineProperty( this, 'greeting', {
      enumerable: true,
      get: function ( ) { return `Hello, I'm ${this.name}`; }
    } );
  }
}

Updated Plunker

Paul
  • 139,544
  • 27
  • 275
  • 264
  • You'll need to use the descriptor returned from `Object.getOwnPropertyDescriptor(Person.prototype, 'greeting')` otherwise you are *overriding* rather than extending the `greeting` descriptor and you loose the `get` behavior. – Sean Vieira Dec 29 '15 at 20:48
  • 3
    @SeanVieira No you don't need to. `defineProperty` can be used to modify/configure existing properties too. As long as you don't specify a `value` or a new `get`, setting enumerable won't stop the old `get` from working. (Notice in the first plunker I linked in my answer that `person[name]` returns the greeting correctly). – Paul Dec 29 '15 at 20:52
16

What is a class?

Non-static methods and accessors of a class lie on the prototype of the class so that every instance of it inherits them. You can access them through the instances, but they are not the own properties of the instances. Static methods and accessors lie on the class (which is a function) itself.

class Test {
 #private_field = "A private field.";
 public_field = "A public field.";
 static get static_getter() {
  return "A static getter.";
 }
 static static_method() {
  return "A static method.";
 }
 get getter() {
  return "A non-static getter.";
 }
 method() {
  return "A non-static method.";
 }
}

console.log(`Class ("${typeof Test}" type)`, Object.getOwnPropertyDescriptors(Test));
console.log("Its prototype", Object.getOwnPropertyDescriptors(Test.prototype));
console.log("Its instance", Object.getOwnPropertyDescriptors(new Test));
Class ("function" type) {
    "length": {
        "value": 0,
        "writable": false,
        "enumerable": false,
        "configurable": true
    },
    "prototype": {
        "value": {……},
        "writable": false,
        "enumerable": false,
        "configurable": false
    },
    "static_getter": {
        "get": ƒ static_getter() {……},
        "set": undefined,
        "enumerable": false,
        "configurable": true
    },
    "static_method": {
        "value": ƒ static_method() {……},
        "writable": true,
        "enumerable": false,
        "configurable": true
    },
    "name": {
        "value": "Test",
        "writable": false,
        "enumerable": false,
        "configurable": true
    }
}
Its prototype {
    "constructor": {
        "value": class Test {……},
        "writable": true,
        "enumerable": false,
        "configurable": true
    },
    "getter": {
        "get": ƒ getter() {……},
        "set": undefined,
        "enumerable": false,
        "configurable": true
    },
    "method": {
        "get": ƒ method() {……},
        "writable": true,
        "enumerable": false,
        "configurable": true
    }
}
Its instance {
    "public_field": {
        "value": "A public field",
        "writable": true,
        "enumerable": true,
        "configurable": true
    }
}

How to set properties enumerable

You can make non-static accessors, which are properties on a prototype, enumerable using Object.defineProperty.

class Person {
    constructor(name) {
        this.name = name;
    }
    get greeting() {
        return `Hello from ${this.name}.`;
    }
}
for(const property of ["greeting"]) {
    Object.defineProperty(Person.prototype, property, {enumerable: true});
}

But this way it’ll be mostly of no use, as most of the useful functions, such as Object.keys, Object.values, Object.entries, JSON.stringify, et cetera, do only look for own properties of objects.


Bringing properties on a prototype down to instances

You can also bring (copy) properties on a prototype down to instances. This way they no longer inherit the properties from the prototype, but have them as their own properties.

class Person {
 constructor(name) {
  this.name = name;
  for(const property of ["greeting"]) {
   const descriptor = Object.getOwnPropertyDescriptor(Person.prototype, property);
   const modified_descriptor = Object.assign(descriptor, {enumerable: true});
   Object.defineProperty(this, property, modified_descriptor);
  }
 }
 get greeting() {
  return `Hello from ${this.name}.`;
 }
}

const alice = new Person("Alice");
console.log(alice.greeting);
console.log(JSON.stringify(alice));
console.log(Object.entries(alice));

Bringing every non-static getter down to instances, enumerablizing them.

const prototype = Object.getPrototypeOf(this);
const prototype_property_descriptors = Object.getOwnPropertyDescriptors(prototype);
for(const [property, descriptor] of Object.entries(prototype_property_descriptors)) {
    const is_nonstatic_getter = (typeof descriptor.get === "function");
    if(is_nonstatic_getter) {
        descriptor.enumerable = true;
        Object.defineProperty(this, property, descriptor);
    }
}
3

If you're willing to use JavaScript decorators (still, and possibly always, stuck in Stage 2 of the TC39 process), you can write your own class decorator which takes a number of property names and returns a new class constructor where the property descriptors are made enumerable and, if necessary, copied down from the prototype into each instance upon being constructed (although, just like with all decorators and class constructor wrapping, I can imaging weird edge cases). For example:

@makeOwnEnumerable("greeting")
class Person {

    constructor(myName) {
        this.name = myName;
    }

    get greeting() {
        return `Hello, I'm ${this.name}`;
    }
}

var person = new Person('Billy');
var enumerableProperties = Object.keys(person);
console.log(enumerableProperties) // ["name", "greeting"]

In this case I've implemented makeOwnEnumerable() like this:

function makeOwnEnumerable(...properties) {
    return (ctor) => {
        const c = ctor;
        const descriptors = {};

        for (const p of properties) {
            const descriptor = Object.getOwnPropertyDescriptor(c.prototype, p);
            if (descriptor) {
                descriptors[p] = descriptor;
                delete c.prototype[p];
            }
        }

        return class extends c {
            constructor(...args) {
                super(...args);

                Object.keys(descriptors).forEach(k => {
                    Object.defineProperty(this, k, descriptors[k]);
                });

                properties.forEach(k => {
                    const d = Object.getOwnPropertyDescriptor(this, k);
                    if (d) {
                        d.enumerable = true;
                        Object.defineProperty(this, k, d);
                    }
                });
            }
        };
    };
}

Again, edge cases probably abound.


If you're not willing to use decorators you can just use makeOwnEnumerable as a plain function, even though it's less pretty:

class Person2 extends makeOwnEnumerable("greeting")(class {
    name: string;
    constructor(myName) {
        this.name = myName;
    }

    get greeting() {
        return `Hello, I'm ${this.name}`;
    }
}) { }


var person2 = new Person('Billy');
var enumerableProperties2 = Object.keys(person2);
console.log(enumerableProperties2) // ["name", "greeting"]

Here's a runnable example in a web IDE which demonstrates this in action. Note that it's written in TypeScript, which compiles to the JavaScript code included here.

jcalz
  • 264,269
  • 27
  • 359
  • 360
1

You may do trick like this:

class Person {
  static createFields({ name }) {
    return {
      name,
      get greeting() {
        return `Hello, I'm ${this.name}`;
      }
    }
  }

  constructor(...args) {
    const inst = this.constructor.createFields(...args)
    const desc = Object.getOwnPropertyDescriptors(inst)
    Object.defineProperties(this, desc)
    return this
  }
}

Benefit is that getters on plain object are enumerable and configurable by default, you don't have to care about these modifiers every time.

But... it looks kinda weird) Not sure whether this should be really used.

jeron-diovis
  • 788
  • 1
  • 8
  • 19