将 ES6 class getter 设置为可枚举

Setting an ES6 class getter to enumerable

我有一个 ES6 class(用 babeljs 转译)和 getter 属性。我知道默认情况下这些属性不可枚举。但是,我不明白为什么我无法使用 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

ES6 风格的 getter 是在原型上定义的,而不是在每个个体上定义的 person。要将 greeting 属性 设置为可枚举,您需要更改:

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

收件人:

// 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

相反,如果您希望 Person 的每个单独实例都有自己的 greeting,您可以在构造函数中定义它:

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

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

Updated Plunker

你可以这样玩:

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
  }
}

好处是默认情况下普通对象上的 getter 是可枚举和可配置的,您不必每次都关心这些修饰符。

但是...看起来有点奇怪)不确定是否应该真正使用它。

什么是 class?

Non-static class 的方法和访问器位于 class 的原型上,因此它的每个实例 继承 它们。您可以通过实例访问它们,但它们不是实例自己的属性。静态方法和访问器位于 class(这是一个函数)本身。

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
    }
}

如何设置可枚举的属性

您可以使用 Object.defineProperty.

使 non-static 访问器(原型上的属性)可枚举
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});
}

但是这样一来就基本没用了,因为大部分有用的函数,比如Object.keys, Object.values, Object.entries, JSON.stringify,等等,只查找对象的 自己的 属性。


将原型上的属性转化为实例

您还可以将原型上的属性(复制)带到实例中。这样他们就不再继承原型的属性,而是将它们作为自己的属性。

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));


将每个 non-static getter 归结为实例,将它们枚举。

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);
    }
}

如果您愿意使用 JavaScript decorators (still, and possibly always, stuck in Stage 2 of the TC39 process),您可以编写自己的 class 装饰器,它采用多个 属性 名称和 returns 一个新的 class 构造函数,其中 属性 描述符是可枚举的,如有必要,在构造时从原型复制到每个实例中(尽管,就像所有装饰器和 class 构造函数包装一样,我可以对奇怪的边缘情况进行成像)。例如:

@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"]

在这种情况下,我已经实现了 makeOwnEnumerable() 这样的:

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);
                    }
                });
            }
        };
    };
}

同样,边缘情况可能比比皆是。


如果您不愿意使用装饰器,您可以将makeOwnEnumerable用作普通函数,即使它不太漂亮:

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"]

这里有一个 runnable example in a web IDE 演示了这一点。请注意,它是用 TypeScript 编写的,它编译为此处包含的 JavaScript 代码。