JavaScript: зачем конструктору return …
- четверг, 21 августа 2025 г. в 00:00:08
Этот вопрос интересовал меня настолько давно, что за прошедшие годы даже стал как-то про него забывать. И не то, чтобы меня это прям как-то сильно интересовало, но всё же лучше понимать назначение было бы приятно.
И, ведь, ну в самом деле, подумаешь, ну может разработчику хочеться вернуть не эземпляр, не instance, а какой-нибудь другой объект, и зачем-то при этом ему нужен именно вызов конструктора. Ну, допустим, он хочет чтобы new.target
был заполнен и т.п., ну, мало ли какие варианты зачем-то иметь возможность в противном случае вернуть объект. Или может он хочет асинхронных конструкторов и вернуть new Promise, где уже в resolve
передать this
как вариант для создания await new MyConstructor
. Или может быть хочет вернуть Proxy
над this
для отслеживания операций с этим экземпляром. В общем есть всякие разные причины когда гуманно было бы иметь этот "сахар", но всё же, может быть есть что-то, что иначе никак не сделать? И, самое важное, что эта функциональность была всегда, с самой первой версии же. Понятно, что может быть про это вообще не думали, просто делали "как привычно". И, да, функции-конструкторы, в отличие от class-ов
можно вызвать без new
. И в те времена никаких классов в JavaScript не было конечно, и return
объективно был нужен, но может быть есть какие-нибудь варианты когда и с new
имеется глубокий практический смысл. То есть может быть назначение операции возврата иного значения конкретно у конструкторов вполне себе приемлемое.
И вот недавно нашёл одну интересную особенность, которую реализовать иначе совсем вот вообще никак нельзя, только так, и никак иначе. И, да, она с самых ранних версий существует, и да, более того, в последние годы получила широкое развитие. Только когда я это осознал, то стал абсолютно уверен, что никто это так не использует, и поэтому решил написать статью, чтобы и вам всем это стало известно. Такой вот альтруизм, вдруг случится что, а так хоть статя эта останется. И ладно, что давно не пишу на хабр, все, кому это нужно тут прочтут и разберутся, может быть, когда-нибудь.
Как обычно, самое главное -- это сформулировать вопрос. Без правильного вопроса не будет ответа. Наш вопрос уже вроде бы поставлен, переформулируем ещё раз:
-- Существует ли в JavaScript какой-либо паттерн кода, который невозможно сделать иначе, чем использовать return
в конструкторе?
-- Но, позвольте, а что вообще мы можем из конструктора вернуть?
-- Мы можем вернуть объекты, и на этом, в принципе, всё, вместо примитивов возвращаются экземпляры, возвратная операция для примитивов не осуществляется.
То есть нужно найти какой-нибудь такой объект, возврат которого вместо this
имеет конкретный практический смысл, при этом Иначе это сделать невозможно.
Что же это может быть? По сути всё, что может конструктор вернуть кроме this
, можно сделать "иначе", просто создав этот объект. Зачем тогда вообще использовать конструкторы? Понятно, что, например, классы -- это удобно. Но есть ещё функциональные конструкторы, которые от function
, они исторически существуют и были, и тоже вполне себе полноценны, их даже можно использовать для class extends
, например:
const MyFn = function () {
this.prop = 123;
}
class MyClass extends MyFn {}
const instance = new MyClass;
console.log('instance instanceof MyClass', instance instanceof MyClass);
console.log('instance instanceof MyFunction', instance instanceof MyFn);
console.log('instance .prop : ', instance.prop);
И вот, а для них была ли какая-нибудь такая функциональность, которую нельзя было иначе сделать? Тогда же ведь в спецификации ещё даже new.target
не было, а return
уже был, и вот "зачем" вообще это нужно, если мы всего лишь экземпляры создаём?
Хорошо, ладно, есть объекты. Мы можем вернуть любой объект.
-- А, что такое объект?
-- Да, в общем-то всё, что не примитив.
И, значит, все объекты можно, получается возвращать. Значит мы должны найти какой-то такой объект, который можно вернуть только при вызове функции-конструктора с оператором new
и по-другому совсем никак нельзя вернуть, только именно, чтобы конструктор вернул этот самый такой своеобразный объект. Что это может быть? Какая такая странная сущность, нуждающаяся в операции конструирования, может обладать функциональностью, которую иначе получить нельзя?
Что нам известно?
Конструкторы возвращают экземпляры, которые на самом деле наследуют их свойство .prototype
и они при этом дополнены всеми теми свойствами, которыми мы в конструкторе через this.
положим в этот экземпляр. При этом ничего не мешает просто создать пустой объект и наполнить его теми же самыми свойствами без необходимости операции конструирования. Но этот объект будет обладать одним существенным минусом, он не будет instanceof
этой самой функции. Конечно, вы, может быть скажете, что instanceof
не нужен, и вообще, это всего лишь сравнение вида:
Object.getPrototypeOf(this).constructor === MyConstructor;
И, то есть, как бы да, мы это можем поменять, просто установив другой конструктор, именно поэтому сейчас появился Symbol.hasInstance
, которым мы можем обогатить проверку instanceof
для получения преимуществ перед наивным поведением. То есть -- мы можем, в целом, полагаться на то, что в саом деле instanceof
сейчас уже можно создать таким, чтобы оно действительно ломалось в тех случаях, когда что-то сделано неверно. И, значит тезис о том, что мы можем нуждаться в проверке на то, что экземпляр является действительно наследником конкретного конструктора в целом верен, и "простым" способом получить это поведение можно только через оператор new
.
Отсюда следует, что спектр исследуемых объектов может быть ограничен только действительными экземплярами-наследниками конструтора. Но, опять же, без этого можно обойтись, проверку на "вхождение" в "семейство" можно организовать как-то иначе. И по прежнему, существует ли какой-нибудь такой вид экземпляров, которому обязательно нужен return
через оператор new
?
Как будто со всеми объектами ситуация такая, что да, мы можем обойтись. Но есть такой особый вид объектов, который, если не рассматривать пристально, не кажется даже чем-то необычным. И вот недавно нашёл одну для него ситуацию, когда иначе никак нельзя:
-- Конструктор может вернуть Функцию function
или Класс class
.
Функции и Классы ведь тоже объекты. И тогда, значит, получается, что конструктор сам возвращает конструктор. Логично, что вы сразу спросите меня как быть с instanceof
, ведь в этом случае мы его потеряем. Это действительно так, но нам ничто не мешает унаследовать эту функцию от this
, и в этом случае, конечно же, она будет экземпляром исходного конструктора:
const Cstr = function () {
const Self = function () {};
Object.setPrototypeOf(Self, this);
return Self;
};
const item = new Cstr;
console.log('item instanceof Cstr : ', item instanceof Cstr);
const itemInstance = new item;
console.log('itemInstance instanceof item : ', itemInstance instanceof item);
Безусловно, Object.setPrototypeOf
не существовал в ранних версиях спецификации, и простой и внятный способ обойти это ограничение придумать сложно.
Но, то в целом на сегодняшний день -- это единственный способ получить экземпляр, который сам по себе является конструктором. Мы можем попробовать завернуть конструирование в Proxy
, но с этим, увы, ничего не выйдет:
'use strict';
const Cstr = function() {
const proxy = new Proxy(this, {
construct(target, args) {
return new target(...args);
}
});
return proxy;
};
const instance = new Cstr;
console.log(instance instanceof Cstr);
try {
new instance;
} catch (error) {
console.error(error);
/*
TypeError: instance is not a constructor
at Object.<anonymous> (/code/ConstructibleProxy.js:17:5)
...
*/
}
И на этом в принципе почти всё.
Дальше получаются ещё более интересные вещи. Например, что мы можем наследовать классы от экземпляров других классов. Кроме того мы можем "докрутить" проверку на new.target
при вызове и использовать эти экземпляры как обычные функции.
Да, конечно, в случае с TypeScript придётся ему объяснять, что, мол, тут у нас конструктор возвращает интерфейс, а не тип, но как будто бы это уже и не самая сложная задачка.
Если интересно посмотреть на много кода, вот ссылка на Gist:
https://gist.github.com/wentout/8a2631fd5cc5827df5946b9b6598bf99
Там примерно 200 строк console.log
со всеми возможными проверками и какая-то реализация для TypeScript с Generic'ами.
Итого ... просто оставлю это здесь, кому нужно тот найдёт.