ES6 `static get length()` не наследуется, как ожидалось

У меня возникла проблема с предоставлением статической функции получения для свойства length расширения моего класса ES6. Как оказалось, фактический геттер Function.length всегда имеет приоритет над моей собственной реализацией.

class Foo {
  static get value() {
    return 'Foo';
  }

  static get length() {
    return this.value.length;
  }
}

class Bar extends Foo {
  static get value() {
    return `${super.value}Bar`;
  }
}

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 0

В приведенном выше примере Foo делает именно то, что я ожидал, а Bar не очень. Bar.value действительно возвращает 'FooBar', но Bar.length, будучи 0, меня удивило.

Мне потребовалось некоторое время, чтобы понять, откуда взялось число 0, так как я полностью ожидал, что это будет 6 (и в какой-то степени понял бы 3). Как оказалось, значение 0, предоставленное Bar.length, на самом деле является длиной функции constructor Bar, я понял это, когда писал тот же пример в нотации ES5, хотя есть быстрый способ доказать это; просто добавьте constructor к Bar.

class Bar extends Foo {
  constructor(a, b, c, d) {
    //  four configured parameters
  }

  static get value() {
    return `${super.value}Bar`;
  }
}

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 4

Есть способы обойти это:

  • добавить static get length() ко всем расширениям (не моя идея наследования)
  • используйте другое имя свойства (например, static get size() работает по назначению, но не является обычно используемым свойством в JS)
  • расширить базу из встроенного класса, который имеет рабочий length (например, class Foo extends Array {...}) -

Ничего из этого я не хочу делать, если есть более подходящий способ сделать это.

Итак, мой вопрос; кто-нибудь знает правильный способ переопределения пользовательского свойства, которое наследуется, как и ожидалось, или я слишком упрям?


Как уже упоминалось, я понял, что пошло не так, написав синтаксис класса (как я считаю), который будет эквивалентен ES5, поскольку он может быть полезен другим разработчикам и может пролить некоторый свет на то, как я думаю Классы ES6 работают, я оставлю это здесь. (Если у кого-то есть совет, как сделать этот бит сворачиваемым в Stackoverflow, не стесняйтесь редактировать/предлагать)

Что, я полагаю, происходит в синтаксисе ES5

Я знаю, что классы ES6 в основном представляют собой синтаксический сахар вокруг прототипного наследования JS, поэтому то, что, похоже, происходит для Bar, выглядит примерно так:

function Foo() {}
Object.defineProperties(Foo, {
  value: {
    configurable: true, 
    get: function() {
      return 'Foo';
    }
  },
  length: {
    configurable: true, 
    get: function() {
      return this.value.length;
    }
  }
});

function Bar() {}
Bar.prototype = Object.create(Object.getPrototypeOf(Foo));
Object.defineProperties(Bar, {
  value: { 
    configurable: true, 
    get: function() { 
      return 'Bar' + Foo.value; 
    }
  }
});

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 0

Я ожидал, что дескрипторы свойств Foo будут приняты во внимание, например:

function Bar() {}
Bar.prototype = Object.create(Object.getPrototypeOf(Foo));
Object.defineProperties(Bar, Object.assign(
  //  inherit any custom descriptors
  Object.getOwnPropertyDescriptors(Foo),
  {
    configurable: true, 
    value: { 
      get: function() { 
        return 'Bar' + Foo.value;
      }
    }
  }
));


console.log(Foo.value, Foo.length);  //  'foo', 3
console.log(Bar.value, Bar.length);  //  'bar', 6

person Rogier Spieker    schedule 09.09.2017    source источник


Ответы (1)


О статических членах

Статические члены класса ES6 на самом деле являются членами функционального объекта, а не его объекта-прототипа. Рассмотрим следующий пример, где я буду использовать обычные методы вместо геттера, но механика идентична геттеру:

class Foo {
    static staticMethod() {
        return 'Foo static';
    }

    nonStaticMethod() {
        return 'Foo non-static';
    }
}

staticMethod станет членом объекта функции-конструктора, тогда как nonStaticMethod станет членом прототипа этого объекта функции:

function Foo() {}

Foo.staticMethod = function() {
    return 'Foo static';
}

Foo.prototype.nonStaticMethod = function() {
    return 'Foo non-static';
}

Если вы хотите запустить staticMethod из "экземпляра" Foo, вам придется сначала перейти к его constructor, который является объектом функции, членом которого является staticMethod:

let foo = new Foo();

foo.staticMethod(); // Uncaught TypeError: foo.staticMethod is not a function

// Get the static member either on the class directly
// (this works IF you know that Foo is foo's constructor)

Foo.staticMethod(); // > 'Foo static'


// this is the same, provided that neither 'prototype' nor
// 'prototype.constructor' has been overridden afterwards:

Foo.prototype.constructor.staticMethod(); // > 'Foo static'


// ...or by getting the prototype of foo
// (If you have to perform a computed lookup of an object's constructor)
// You'll want to perform such statements in a try catch though...

Object.getPrototypeOf(foo).constructor.staticMethod(); // > 'Foo static'

function.length

Все функции имеют свойство length который говорит вам, сколько аргументов принимает эта функция:

function Foo(a, b) {}
Foo.length; // > 2

Таким образом, в вашем примере поиск прототипа от Bar.length до Foo.length действительно никогда не произойдет, поскольку length уже найден непосредственно в Bar. Простое переопределение не сработает:

Foo.length = 3;
Foo.length; // still 2

Это потому, что свойство недоступно для записи. Давайте проверим с помощью getOwnPropertyDescriptor:

Object.getOwnPropertyDescriptor(Foo, 'length');
/*
    { 
        value: 0,
        writable: false,
        enumerable: false,
        configurable: true
    }
*/

Кроме того, вместо определяемого вами геттера value вы можете просто использовать function.name, чтобы получить имя конструктора функции/класса:

Foo.name; // > 'Foo'

Давайте воспользуемся этим, чтобы переопределить свойство length для Foo. Мы по-прежнему можем переопределить Foo.length, поскольку это свойство настраивается:

Object.defineProperty(Foo, 'length', {
    get() {
        return this.name.length;
    }
});

Foo.length; // 3

Это раздувание кода

Крайне нежелательно делать это для каждого расширяющего класса или определять статический геттер для каждого, что эквивалентно приведенному выше коду. Невозможно полностью переопределить поведение без какого-либо оформления функциональных объектов. Но поскольку мы знаем, что классы — это всего лишь синтаксический сахар, и на самом деле мы имеем дело только с объектами и функциями, написать декоратор несложно!

function decorateClasses(...subjects) {

    subjects.forEach(function(subject) {

        Object.defineProperty(subject, 'value', {
            get() {
                const superValue = Object.getPrototypeOf(this).value || '';

                return superValue + this.name;
            },
            enumerable: true,
            configurable: true,
        });

        Object.defineProperty(subject, 'length', {
            get() {
                return this.value.length;
            },
            enumerable: true,
            configurable: true,
        });
    })

}

Эта функция принимает один или несколько объектов, для которых она переопределяет свойство length и устанавливает свойство value. Оба являются аксессорами с методом get. get value явно выполняет поиск прототипа, а затем объединяет результаты с именем функции, которой он принадлежит. Итак, если у нас есть 3 класса:

class Foo {

}

class Bar extends Foo {

}

class Baz extends Bar {

}

Мы можем украсить все классы сразу:

decorateClasses(Foo, Bar, Baz);

Если мы получим доступ к value и length во всех трех классах (функциях), мы получим желаемые результаты:

Foo.value; // 'Foo'
Foo.length; // 3

Bar.value; // 'FooBar'
Bar.length; // 6

Baz.value; // 'FooBarBaz'
Baz.length; // 9
person JJWesterkamp    schedule 09.09.2017
comment
Моя проблема в том, что функция конструктора Bar не Foo, а Function (как указано для ES6), но каким-то образом все, кроме встроенных статических членов Foo, в конечном итоге становятся доступными для Bar. Так что проблема не в том, что я не могу определить длину, проблема в том, что length на Bar - это не length из Foo, а из Function, если я переименую его в size, все будет работать как положено. Я не ожидал этого. - person Rogier Spieker; 09.09.2017
comment
Понимаю, я понял это слишком поздно. Я посмотрю, смогу ли я придумать лучший ответ. Есть способы сделать то, что вы пытаетесь достичь, но это потребует от вас явного написания собственной реализации, поскольку классы (и фактически прототипирование в целом) не предоставляют вам средств для создания предполагаемого поведения. - person JJWesterkamp; 09.09.2017
comment
Вот что я понял, я надеялся, что сообщество SO узнает что-то, что я пропустил. Если вы укажете, что классы не предоставляют этого ... в вашем ответе я приму это как ответ (пока кто-то не придет с чем-то, что докажет обратное, конечно). Спасибо за ваше время! - person Rogier Spieker; 09.09.2017
comment
Я дополнительно отредактировал свой ответ для большей ясности. Надеюсь, что это помогает вам! - person JJWesterkamp; 09.09.2017
comment
Предложение extends в JS всегда будет создавать только прототип связи между Foo.prototype и Bar.prototype — это только половина того, что делает extends. Он также прототипирует конструктор экземпляров расширенного класса на основе конструктора экземпляров базового класса. Object.getPrototypeOf( Bar) === Foo верно. - person traktor; 09.09.2017
comment
@ Traktor53 Тогда мой ответ неверен. Не могли бы вы отредактировать? - person JJWesterkamp; 09.09.2017
comment
Я уже удалил всю неправильную информацию и обновил ответ. Я изначально неправильно понял вопрос... - person JJWesterkamp; 09.09.2017
comment
Хорошая работа. Учитывая, что статические имена членов, которые не могут быть автоматически унаследованы, являются локальными именами свойств объекта функции ( prototype, length и name), а prototype читаются только для конструкторов классов, отказ от их использования в качестве имен членов остается разумным. - person traktor; 09.09.2017