Рассмотрим этот код:
class Klass {
#priv = 42
get pub() {
return this.#priv
}
}
// these two correctly return 42
console.info(new Klass().pub);
console.info((new class extends Klass {}).pub)
// but this one throws "TypeError: Cannot read private member
// #priv from an object whose class did not declare it"
console.info(Object.create(new Klass()).pub) // throws
// even though
console.info(Object.create(new Klass()) instanceof Klass) // true
console.info(Object.getPrototypeOf(Object.create(new Klass())).pub) // 42
Я подумал, что, поскольку у меня есть настоящий экземпляр Klass
в цепочке прототипов, доступ к Object.create(new Klass()).pub
не выдаст ошибку.
Это задумано? И если да, то почему так сделано?
Кроме того, как правильно использовать универсальную функцию для клонирования экземпляров произвольных классов, которые ведут себя аналогично Klass
?
Я столкнулся с этой проблемой, когда что-то тестировал с помощью vitest. Мой код выглядел примерно так:
import { it, expect } from 'vitest';
class Klass {
#priv = 42;
get pub() {
return this.#priv;
}
}
it('works', () => {
expect(new Klass()).toMatchObject({
make_this_test_fail: 'yup',
pub: 42,
});
});
Мой тест, очевидно, провалился, но вместо того, чтобы дать мне хорошую разницу, показывающую, что свойство make_this_test_fail
не было найдено в показанном экземпляре
FAIL test/the.test.js [ test/the.test.js ]
TypeError: Cannot read private member #priv from an object whose class did not declare it
❯ Klass.get pub [as pub] test/the.test.js:7:15
5|
6| get pub() {
7| return this.#priv;
| ^
8| }
9| }
Я отследил это до этой строки, откуда actual
происходит deepClone
, которая создает экземпляры произвольных классов, используя Object.create
здесь.
Я тоже так думаю, но надеюсь, что кто-нибудь здесь сможет пролить больше света на этот вопрос :). Я также хотел бы знать, есть ли более «пуленепробиваемый» способ клонирования произвольных экземпляров классов, чтобы такие доступы не вызывали ошибок (я только что отредактировал вопрос, чтобы отразить это)
Пробовали structuredClone()
?
Да, но это дает пустой объект. Я думаю, что он повторяет только свои свойства, но pub
является геттером прототипа, поэтому он не «достигает» его. structuredClone(new class Klass { #priv = 42; get pub() { return this.#priv; }})
Верно. По сути, я думаю, что большая часть традиционных объектных средств не предназначена для работы с расширенными функциями классов. Это уже не просто синтаксический сахар.
Связанный: Почему доступ к закрытым статическим членам через подкласс запрещен?
Это задумано? И если да, то почему так сделано?
Да, так задумано: частные поля не наследуются по цепочке прототипов, они существуют только в том конкретном экземпляре, в котором они были созданы во время создания. Это делает их совершенно незаметными снаружи, вы не можете вмешиваться в приватные поля, например. обмениваясь прототипом объекта, и вы не можете обманом заставить метод создать частное поле (по назначению, рассмотрите установщик в вашем примере) на объекте, где оно ранее не существовало.
Частные поля являются «жестко приватными» и очень похожи на внутренние слоты встроенных объектов. Они также не пересылаются через прокси. Ментальная модель частного поля подобна WeakMap
хранению значения для каждого экземпляра.
(Тем не менее не все согласны с тем, что это хороший дизайн.)
Кроме того, как правильно использовать универсальную функцию для клонирования экземпляров произвольных классов, которые ведут себя аналогично
Klass
?
Вы не можете написать такую общую функцию. Любой класс, независимо от того, использует ли он частные поля или нет, может иметь частное состояние, которое невозможно клонировать. Единственный способ добиться этой цели — дать каждому классу метод clone
для клонирования себя соответствующим образом. Используйте символ для имени метода, если хотите избежать конфликтов.
Я подозреваю, что
Object.create()
не воспроизводит все, что необходимо для ведения бухгалтерского учета класса, обеспечивающего доступ к частной собственности.