Прежде чем кто -либо бросится в раздел комментариев, я должен предварительно предвещать эту статью, подчеркнув, что Нет стандартизированного способа написать асинхронные конструкторы в JavaScript пока что . Однако на данный момент есть некоторые обходные пути. Некоторые из них хороши … но большинство из них довольно однородные (если не сказать больше).
В этой статье мы обсудим ограничения различных способов, которыми мы пытались подражать Асинхронизация
конструкторы. После того, как мы установили недостатки, я продемонстрирую, что я нашел как правильное Асинхронизация
Паттерн конструктора в JavaScript.
До ES6 не было концепции классов в спецификации языка. Вместо этого JavaScript «конструкторы» были просто простыми старыми функциями с тесными отношениями с это
и прототип
. Когда занятия наконец прибыли, конструктор
был (более или менее) синтаксический сахар на простых старых функциях конструктора.
Однако это имеет следствие, что конструктор
наследует некоторые из причудливых поведений и семаней старых конструкторов. В частности, возвращение не примитивный значение Из конструктора возвращает это значение вместо построенного это
объект Анкет
Предположим, у нас есть Человек
Класс с частной строковой поле имя
:
class Person { #name: string; constructor(name: string) { this.#name = name; } }
С тех пор, как конструктор
неявно возвращает неопределенное
(что является примитивным значением), тогда новый человек
Возвращает недавно построенные это
объект. Однако, если бы мы вернули буквальный объект, то у нас больше не было бы доступа к это
Объект, если мы как -то не включим его в буквальный объект.
class Person { #name: string; constructor(name: string) { this.#name = name; // This discards the `this` object! return { hello: 'world' }; } } // This leads to a rather silly effect... const maybePerson = new Person('Some Dood'); console.log(maybePerson instanceof Person); // false
Если мы собираемся сохранить это
объект, мы можем сделать это следующим образом:
class Person { #name: string; constructor(name: string) { this.#name = name; // This preserves the `this` object. return { hello: 'world', inner: this }; } get name() { return this.#name; } } // This leads to another funny effect... const maybePerson = new Person('Some Dood'); console.log(maybePerson instanceof Person); // false console.log(maybePerson.inner instanceof Person); // true console.log(maybePerson.name); // undefined console.log(maybePerson.inner.name); // 'Some Dood'
Ооочень … если можно переопределить тип возврата конструктор
, тогда невозможно вернуть Обещание
изнутри конструктор
?
На самом деле, да! A Обещание
Экземпляр действительно является не примитивным значением, в конце концов. Поэтому конструктор
вернет это вместо это
Анкет
class Person { #name: string; constructor() { // Here, we simulate an asynchronous task // that eventually resolves to a name... return Promise.resolve('Some Dood') .then(name => { // NOTE: It is crucial that we use arrow // functions here so that we may preserve // the `this` context. this.#name = name; return this; }); } }
// We overrode the `constructor` to return a `Promise`! const pending = new Person; console.log(pending instanceof Promise); // true console.log(pending instanceof Person); // false // We then `await` the result... const person = await pending; console.log(person instanceof Promise); // false console.log(person instanceof Person); // true // Alternatively, we may directly `await`... const anotherPerson = await new Person; console.log(anotherPerson instanceof Promise); // false console.log(anotherPerson instanceof Person); // true
Мы по сути реализованы Отложенная инициализация ! Хотя этот обходной путь эмулирует асинхронный конструктор
, это происходит с значительный Недостатки:
- Не поддерживает
Асинхронизация
–ждет
синтаксис. - Требуется ручное Цепочка обещаний Анкет
- Требуется тщательное сохранение
это
контекст 1 - Нарушает много допущений, сделанных поставщиками вывода типа. 2
- Переопределяет поведение по умолчанию
конструктор
, что неожиданный и Unidiomatic Анкет
Поскольку переопределение конструктор
Семантически проблематично, возможно, мы должны использовать какую-то обертку в стиле состояния, где конструктор
является просто «точкой входа» в государственную машину. Затем мы потребуем, чтобы пользователь вызвал другие «методы жизненного цикла» для полной инициализации класса.
class Person { /** * Observe that the field may now be `undefined`. * This encodes the "pending" state at the type-level. */ this.#name: string | null; /** Here, we cache the ID for later usage. */ this.#id: number; /** * The `constructor` merely constructs the initial state * of the state machine. The lifecycle methods below will * drive the state transitions forward until the class is * fully initialized. */ constructor(id: number) { this.#name = null; this.#id = id; } /** * Observe that this extra step allows us to drive the * state machine forward. In doing so, we overwrite the * temporary state. * * Do note, however, that nothing prevents the caller from * violating the lifecycle interface. That is, the caller * may invoke `Person#initialize` as many times as they please. * For this class, the consequences are trivial, but this is not * always true for most cases. */ async initialize() { const db = await initializeDatabase(); const data = await db.fetchUser(this.#id); const result = await doSomeMoreWork(data); this.#name = await result.text(); } /** * Also note that since the `name` field may be `undefined` * at certain points of the program, the type system cannot * guarantee its existence. Thus, we must employ some defensive * programming techniques and assertions to uphold invariants. */ doSomethingWithName() { if (!this.#name) throw new Error('not yet initialized'); // ... } /** * Note that the getter may return `undefined` with respect * to pending initialization. Alternatively, we may `throw` * an exception when the `Person` is not yet initialized, * but this is a heavy-handed approach. */ get name() { return this.#name; } }
// From the caller's perspective, we just have to remember // to invoke the `initialize` lifecycle method after construction. const person = new Person(1234567890); await person.initialize(); console.assert(person.name);
Как и в предыдущем обходном пути, это также поставляется с некоторыми заметными недостатками:
- Производит известную инициализацию на месте вызова.
- Требуется, чтобы вызывающий был знаком с семантикой жизненного цикла и внутренних интервалов класса.
- Требует обширной документации о том, как правильно инициализировать и использовать класс.
- Включает в себя проверку времени выполнения инвариантов жизненного цикла.
- Делает интерфейс менее подлежащим обслуживанию, менее эргономичным и более склонным к неправильному использованию.
Довольно забавно, лучший Асинхронизация
конструктор
нет конструктор
вообще!
В первом обходном пути я намекнул на то, как конструктор
может вернуть произвольные не примитивные объекты. Это позволяет нам обернуть это
объект внутри Обещание
для размещения отложенной инициализации.
Однако все разваливается, потому что при этом мы нарушаем типичную семантику конструктор
(даже если это допустимо по стандарту).
Итак … почему бы нам просто не использовать обычную функцию?
Действительно, это решение! Мы просто придерживаемся функциональных корней JavaScript. Вместо делегирования Асинхронизация
работать над конструктор
, мы косвенно вызвать конструктор
через несколько Асинхронизация
статический
Заводская функция . 3 На практике:
class Person { #name: string; /** * NOTE: The constructor is now `private`. * This is totally optional if we intend * to prevent outsiders from invoking the * constructor directly. * * It must be noted that as of writing, private * constructors are a TypeScript-exclusive feature. * For the meantime, the JavaScript-compatible equivalent * is the @private annotation from JSDoc, which should * be enforced by most language servers. See the annotation * below for example: * * @private */ private constructor(name: string) { this.#name = name; } /** * This static factory function now serves as * the user-facing constructor for this class. * It indirectly invokes the `constructor` in * the end, which allows us to leverage the * `async`-`await` syntax before finally passing * in the "ready" data to the `constructor`. */ static async fetchUser(id: number) { // Perform `async` stuff here... const db = await initializeDatabase(); const data = await db.fetchUser(id); const result = await doSomeMoreWork(data); const name = await result.text(); // Invoke the private constructor... return new Person(name); } }
// From the caller's perspective... const person = await Person.fetchUser(1234567890); console.log(person instanceof Person); // true
Учитывая мой надувший пример, этот шаблон поначалу не может показаться мощным. Но при применении к реальным конструкциям, таким как подключения к базе данных, пользовательские сеансы, клиенты API, рукопожатия протокола и другие асинхронные рабочие нагрузки, быстро становится очевидным, как этот шаблон гораздо более масштабируемый и идиоматический, чем обходные пути, обсуждаемые ранее.
Предположим, мы хотим написать клиента для Spotify Web API , который требует токен доступа . В соответствии с Протокол OAuth 2.0 , мы должны сначала достичь Код авторизации и обменяйте его на токен доступа.
Предположим, что у нас уже есть код авторизации. Используя заводские функции, можно инициализировать клиента, используя код авторизации в качестве параметра.
const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token'; class Spotify { #access: string; #refresh: string; /** * Once again, we set the `constructor` to be private. * This ensures that all consumers of this class will use * the factory function as the entry point. */ private constructor(accessToken: string, refreshToken: string) { this.#access = accessToken; this.#refresh = refreshToken; } /** * Exchanges the authorization code for an access token. * @param code - The authorization code from Spotify. */ static async initialize(code: string) { const response = await fetch(TOKEN_ENDPOINT, { method: 'POST', body: new URLSearchParams({ code, grant_type: 'authorization_code', client_id: env.SPOTIFY_ID, client_secret: env.SPOTIFY_SECRET, redirect_uri: env.OAUTH_REDIRECT, }), }); const { access_token, refresh_token } = await response.json(); return new Spotify(access_token, refresh_token); } }
// From the caller's perspective... const client = await Spotify.initialize('authorization-code-here'); console.assert(client instanceof Spotify);
Обратите внимание, что в отличие от второго обходного пути, существование токена доступа применяется на уровне типа. Нет необходимости в подтверждениях и утверждениях в стиле штата и машины. Мы можем быть уверены, что когда мы реализуем методы Spotify
Класс, поле токена доступа – Правильно по строительству -безвоздмездно!
Статический
Асинхронизация
Заводская функция позволяет нам эмулировать асинхронные конструкторы в JavaScript. В основе этого шаблона лежит косвенное вызов конструктор
Анкет Корражка обеспечивает соблюдение того, что любые параметры передаются в конструктор
являются готовы и правильный на уровне типа. Это буквально отсроченная инициализация плюс один уровень косвенности.
Этот шаблон также рассматривает все недостатки предыдущих обходных путей.
- Позволяет
Асинхронизация
–ждет
синтаксис. - Обеспечивает эргономичную точку входа в интерфейс.
- Обеспечивает правильность построения (с помощью вывода типа).
- Делает Не Требовать знания жизненных циклов и классов.
Хотя этот шаблон поставляется с одним незначительным недостатком. Типичный конструктор
Предоставляет стандартный интерфейс для инициализации объекта. То есть мы просто вызываем Новый
оператор для построения нового объекта. Однако с заводскими функциями вызывающий абонент должен быть знаком с правильной точкой входа класса.
Честно говоря, это не проблема. Быстрая съемка документации должно быть достаточно для подталкивания пользователя в правильном направлении. 4 Просто чтобы быть очень осторожным, вызывая Частный
Конструктор должен издавать ошибку компилятора/времени выполнения, которая информирует пользователя инициализировать класс, используя предоставленную статическую заводскую функцию.
Таким образом, среди всех обходных путей фабричные функции являются наиболее идиоматическими, гибкими и неинтрозивными. Мы должны избегать делегирования Асинхронизация
работать на конструктор
Потому что это никогда не было разработано для этого варианта использования. Кроме того, мы должны избегать государственных машин и сложных жизненных циклов, потому что они слишком громоздки, чтобы иметь дело. Вместо этого мы должны принять функциональные корни JavaScript и использовать заводские функции.
В примере кода это было сделано с помощью функций со стрелками. Поскольку функции стрел не имеют
это
Переплет , они наследуютэто
Связывание его охвата. ↩А именно, языковой сервер типографии неправильно проживает
новый человек
быть типомЧеловек
а не типОбещание <человека>
Анкет Это, конечно, не совсем ошибка, потому чтоконструктор
никогда не предназначался для использования как таковой. ↩Грубо говоря, заводская функция – это функция, которая возвращает новый объект. Перед введением классов заводские функции обычно возвращали литералы объектов. Помимо традиционных функций конструктора, это был без привязанного способа параметризации объектных литералов. ↩
На самом деле, так это делается в экосистеме ржавчины. В ржавчине нет такой вещи, как конструктор. Де -факто способ инициализации объектов либо непосредственно через
struct
выражения (т.е. объектные литералы) или косвенно через заводские функции. Да, заводские функции! ↩
Оригинал: “https://dev.to/somedood/the-proper-way-to-write-async-constructors-in-javascript-1o8c”