Перейти к основному содержимому
Блокчейн26ГЛАВА 26Классы на TypeScript: объекты, поля и инкапсуляцияclass

Классы на TypeScript: объекты, поля и инкапсуляция

Прочитано0%
Квизы0/3
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

В Solidity контракт сам по себе был объектом: состояние и поведение в одной коробке, и другого способа их разложить язык не давал. В TypeScript объект — не единственный вариант: те же данные можно держать в обычной записи, а работу с ними — в отдельных функциях. Значит, появляется выбор, и вместе с ним вопрос: зачем всё-таки класс.

Эта глава — про то, как класс записывается в TypeScript, что при этом происходит в памяти, где он ломается и во что превращается после компиляции. Наследование, интерфейсы и то, что под капотом, — в следующей главе.

Разница между «понимать идею ООП» и «уметь писать классы» примерно как между «понимать, что такое рычаг» и «уметь чинить велосипед». Идея простая, а деталей — тридцать штук, и каждая ломается по-своему.

Что дальше встретится в настоящем коде:

  • SDK любого блокчейна — это классы. new Signer(...), wallet.sign(tx), provider.getBalance(addr) — за каждым вызовом класс с полями и методами.
  • Ошибки — тоже классы: class InsufficientFunds extends Error. Без наследования не написать своё исключение.
  • Тесты, контроллеры, сервисы — везде классы с интерфейсами.

Глава разбирает всё по порядку: от class и new до прототипов, на которых это построено. Каждый пример проверен запуском на Node.js 24 и компиляцией TypeScript 6 в строгом режиме — включая тексты ошибок с их настоящими номерами.

Объект руками: почему этого мало​

Начнём с того, что уже умеем. Транзакцию можно описать обычным объектом:

const tx = { id: 'a1', amount: 5000 };
console.log(tx.amount * 0.001); // 5

Работает. Проблемы начинаются, когда транзакций становится много.

const tx1 = { id: 'a1', amount: 5000 };
const tx2 = { id: 'b7', amount: 120 };
const tx3 = { id: 'c3', amount: 48000 };
console.log(tx1.amount * 0.001, tx2.amount * 0.001, tx3.amount * 0.001);

Формула комиссии написана три раза. Меняется ставка — правим в трёх местах, и в одном обязательно забудем. Первое решение мы уже знаем — функция:

function fee(tx) {
return tx.amount * 0.001;
}

Стало лучше, но появилась новая беда: функция fee живёт отдельно от объекта. Ничто не мешает передать в неё что угодно — например, объект пользователя, у которого поля amount нет вовсе. Получится undefined * 0.001, то есть NaN, и без единой ошибки.

Второе решение — фабричная функция: она собирает объект вместе с его методами.

function makeTx(id, amount) {
return {
id,
amount,
fee() {
return this.amount * 0.001;
},
};
}
const t = makeTx('a1', 5000);
console.log(t.fee()); // 5

Уже почти то, что надо: данные и работа с ними в одном месте. Осталась одна неприятность — каждый объект носит с собой свою копию функции fee. Для трёх транзакций всё равно, для тридцати тысяч записей из ответа ноды — уже нет.

Класс решает ровно эту задачу: держит данные у каждого объекта свои, а методы — общие на всех.

Класс: чертёж и изделие​

class Tx {
constructor(id, amount) {
this.id = id;
this.amount = amount;
}
fee() {
return this.amount * 0.001;
}
}

const t = new Tx('a1', 5000);
console.log(t.id, t.amount, t.fee()); // a1 5000 5

Живой запуск даёт a1 5000 5.

Нажми на часть выражения

Три различия, из-за которых класс путают с объектом:

объект { }класс class
Между полямизапятаяничего
Пара «имя — значение»amount: 5000amount = 5000
Что это такоеготовая вещьчертёж вещи
ОДИН ЧЕРТЁЖ — СКОЛЬКО УГОДНО ИЗДЕЛИЙclass Txполя (у каждого свои)id, amountметоды (общие на всех)fee()это чертёж, не объектnewобъектid = 'a1'amount= 5000объектid = 'b7'amount= 120объектid = 'c3'amount= 48000метод fee() один — лежит в прототипе классатри объекта — три набора полей, но не три копии кода метода
Класс — чертёж. Из одного чертежа делают сколько угодно объектов: поля у каждого свои, а метод существует в единственном экземпляре и лежит в прототипе.

Проверим главное обещание — что метод один на всех:

const a = new Tx('a1', 5000);
const b = new Tx('b7', 120);
console.log(a.fee === b.fee); // true
console.log(a.hasOwnProperty('fee')); // false
console.log('fee' in a); // true

a.fee === b.fee даёт true: это буквально одна и та же функция. При этом сам объект поля fee не имеет (hasOwnProperty — false), но найти его через объект можно (in — true). Куда именно смотрит поиск, разберём в разделе про прототипы.

Конструктор: что происходит при new​

Слово new делает четыре вещи подряд:

  1. Создаёт новый пустой объект.
  2. Привязывает к нему прототип класса — так объект получает доступ к методам.
  3. Запускает constructor, где this — этот новый объект.
  4. Возвращает объект (return писать не нужно).

Отсюда следствия, которые полезно проверить руками.

class Tx {
constructor(id, amount) {
this.id = id;
this.amount = amount;
}
}
console.log(typeof Tx); // function
console.log(new Tx('a', 1)); // Tx { id: 'a', amount: 1 }

Вывод — Tx { id: 'a', amount: 1 }: Node печатает имя класса перед фигурными скобками, и по этому имени в консоли удобно отличать «просто объект» от объекта класса.

Вызов без new запрещён:

Tx('a', 1);
// TypeError: Class constructor Tx cannot be invoked without 'new'

Конструктор можно не писать вовсе — тогда используется пустой:

class Empty {}
console.log(new Empty()); // Empty {}

Значения по умолчанию задаются как у обычной функции:

class Tx {
constructor(id, amount = 0, fee = amount * 0.001) {
this.id = id;
this.amount = amount;
this.fee = fee;
}
}
console.log(new Tx('a')); // Tx { id: 'a', amount: 0, fee: 0 }
Важно

Конструктор — не место для тяжёлой работы. Сетевой запрос, чтение файла, разбор большого файла — всё это внутри конструктора превращается в мину: объект нельзя создать быстро, нельзя создать в тесте, и ошибку из конструктора неудобно ловить.

Приём, который применяют вместо этого: конструктор только раскладывает переданное по полям, а «дорогое» создание выносят в статический метод:

class Wallet {
constructor(key) { this.key = key; }
static async fromFile(path) {
const key = await readFile(path, 'utf8');
return new Wallet(key.trim());
}
}

Такой метод называют фабричным. Он ещё и читается лучше: Wallet.fromFile('key.txt') говорит больше, чем new Wallet(...) с непонятным аргументом.

Методы: код, общий для всех объектов​

Метод — обычная функция, записанная внутри тела класса без слова function:

class Tx {
constructor(amount) { this.amount = amount; }
fee() { return this.amount * 0.001; }
total() { return this.amount + this.fee(); }
toString() { return `${this.amount} (комиссия ${this.fee()})`; }
}
const t = new Tx(5000);
console.log(t.total()); // 5005
console.log(`${t}`); // 5000 (комиссия 5)

Три вещи, которые здесь стоит заметить.

Метод зовёт метод через this. Внутри total нельзя написать просто fee() — такой функции в области видимости нет, будет ReferenceError. Только this.fee().

toString — особое имя. Его вызывает сам язык, когда объект попадает в строку: в шаблонной строке, при склейке через +, в String(t). Свой toString — самый дешёвый способ сделать объект читаемым в логах.

В JSON.stringify попадают только поля.

console.log(JSON.stringify(t)); // {"amount":5000}

Методов в JSON нет и не будет: они лежат не в объекте. Отсюда важное следствие для сетевого кода — объект, вернувшийся из JSON.parse, не является объектом класса. Он выглядит похоже, но методов у него нет:

const back = JSON.parse(JSON.stringify(t));
console.log(back.amount); // 5000
console.log(back instanceof Tx); // false
back.fee(); // TypeError: back.fee is not a function

Это одна из самых частых неожиданностей у тех, кто пришёл из языков со строгими типами: данные по сети ходят без классов, и класс приходится собирать заново — new Tx(back.amount).

this: главная ловушка​

this — не «текущий класс». this — тот объект, который стоял слева от точки в момент вызова. Не при объявлении, а при вызове. Отсюда почти все загадочные ошибки в классах.

class Tx {
constructor(amount) { this.amount = amount; }
fee() { return this.amount * 0.001; }
}
const t = new Tx(5000);

console.log(t.fee()); // 5 — слева от точки стоит t

const f = t.fee;
f();
// TypeError: Cannot read properties of undefined (reading 'amount')

Во второй строке метод достали из объекта и положили в переменную. Функция та же самая, но точки при вызове больше нет — и this внутри оказывается undefined. Ошибка при этом жалуется не на this, а на чтение amount у undefined, и найти причину по тексту сложно.

THIS ОПРЕДЕЛЯЕТСЯ В МОМЕНТ ВЫЗОВАt.fee()слева от точки — tthis = tработаетconst f = t.fee;f()точки нет — связь потерянаthis = undefinedTypeError при чтении поляfee = () => …стрелка в поле объектаthis приклеен навсегдаработаетметод, переданный как коллбэк, — это средний случай: обёртка или стрелкаt.fee.bind(t) и () => t.fee() чинят потерю так же надёжно
Три способа вызвать один и тот же метод. Связь с объектом даёт точка при вызове; стоит её потерять — и this становится undefined.

Где это случается на практике: почти всегда там, где метод передают дальше как значение.

// все три строки сломаются одинаково
setTimeout(t.fee, 100);
[1, 2, 3].forEach(t.fee);
button.addEventListener('click', t.fee);

Три надёжных способа починить:

setTimeout(() => t.fee(), 100); // 1. обёртка-стрелка: точка на месте
setTimeout(t.fee.bind(t), 100); // 2. bind: делает новую функцию с приклеенным this
class Tx {
amount = 5000;
fee = () => this.amount * 0.001; // 3. стрелка в поле объекта
}
const g = new Tx().fee;
console.log(g()); // 5 — работает даже без точки

Третий способ работает, потому что у стрелочной функции своего this нет: она берёт его из места объявления, а объявлена она внутри создаваемого объекта. Цена — такое поле принадлежит объекту, а не прототипу, то есть у каждого объекта своя копия функции. Для обработчиков событий это нормально, для класса, которого создают десятки тысяч, — нет.

Интересный факт

TypeScript умеет ловить потерю this заранее. Флаг strictBindCallApply проверяет bind, call и apply, а отдельный псевдопараметр this в сигнатуре метода позволяет прямо запретить вызов без нужного контекста:

class Tx {
amount = 0;
fee(this: Tx) { return this.amount * 0.001; }
}
const f = new Tx().fee;
f(); // ошибка ещё до запуска

Такой параметр не настоящий: он существует только в типах и в JavaScript не попадает.

Отвечено вопросов: 0 из 5
Чем класс отличается от объекта?
Что произойдёт при вызове Tx() без new?
Сколько копий метода fee существует при трёх созданных объектах?
const f = t.fee; f(). Почему падает?
Что вернёт JSON.parse(JSON.stringify(t)) instanceof Tx?
Отвечено верно: 0 из 5

Поля прямо в конструкторе: сокращение TypeScript​

В TypeScript у класса появляется то, чего в JavaScript нет: поля объявляют с типами, а конструктор умеет объявлять их сам.

Длинная запись — та же, что в JavaScript, плюс типы:

class Tx {
id: string;
amount: number;
constructor(id: string, amount: number) {
this.id = id;
this.amount = amount;
}
}

Имя поля повторяется четыре раза. Короткая запись делает то же самое:

class Tx {
constructor(
public readonly id: string,
private amount: number,
protected fee = 0.001,
) {}
total(): number {
return this.amount + this.fee;
}
}
Важно

Короткая запись — единственная конструкция этой главы, которая не запускается через node file.ts. Node срезает типы, но поля в конструкторе — это не тип, а генерация кода, и срезать её нечем:

class Tx {
constructor(public readonly id: string) {}
^^^^^^^^^^
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property
is not supported in strip-only mode

Длинная запись выше работает без всяких оговорок. Короткую увидишь в чужом коде и в проектах со сборщиком — там её обрабатывает tsc, а не Node. Флаг --erasableSyntaxOnly в tsconfig.json запрещает такие конструкции заранее, чтобы поймать это при проверке, а не при запуске.

Тело конструктора пустое, а поля всё равно есть. Это называется параметрами-свойствами: модификатор доступа (public, private, protected, readonly) перед параметром конструктора говорит компилятору «заведи такое поле и положи туда аргумент».

Нажми на часть выражения

Вот что из этого получится после компиляции — реальный вывод tsc --target es2022:

class Tx {
id;
amount;
constructor(id, amount) {
this.id = id;
this.amount = amount;
}
}

Компилятор дописал ровно то, что мы поленились написать руками. Все модификаторы при этом исчезли.

Совет

Поле, объявленное без значения и без присваивания в конструкторе, — ошибка в строгом режиме:

class C {
name: string;
}
// error TS2564: Property 'name' has no initializer
// and is not definitely assigned in the constructor.

Три законных выхода: дать значение по умолчанию (name = ''), присвоить в конструкторе или честно признать, что поля может не быть (name?: string). Четвёртый выход — приписка name!: string — говорит компилятору «поверь мне на слово»; ей пользуются, когда значение проставляет фреймворк, и в обычном коде она означает, что что-то спроектировано неудачно.

Инкапсуляция: private, protected и #​

Инкапсуляция — это ответ на вопрос «к чему снаружи можно прикасаться, а к чему нельзя». Без неё любой чужой код может залезть в середину объекта и сломать его согласованность.

В TypeScript есть три модификатора и одна возможность самого JavaScript.

class Tx {
constructor(
public readonly id: string,
private amount: number,
protected fee = 0.001,
) {}
total(): number { return this.amount + this.fee; }
}

const t = new Tx('a1', 100);
t.id = 'b2'; // error TS2540: Cannot assign to 'id' because it is a read-only property.
console.log(t.amount); // error TS2341: Property 'amount' is private and only accessible within class 'Tx'.
console.log(t.fee); // error TS2445: Property 'fee' is protected and only accessible within class 'Tx' and its subclasses.

Все три ошибки — настоящий вывод компилятора. И у всех трёх есть общая особенность, о которой обязательно нужно знать.

Важно

private в TypeScript — не защита, а договорённость, проверяемая до запуска. В скомпилированном JavaScript от него не остаётся ничего: поле обычное, и любой код, работающий с готовым файлом, спокойно его прочитает.

Настоящую защиту даёт возможность самого JavaScript — поле с решёткой:

class Wallet {
#key = 'секрет';
reveal() { return this.#key.length; }
}
const w = new Wallet();
console.log(w.reveal()); // 6
console.log(Object.keys(w)); // []
console.log(JSON.stringify(w)); // {}
w.#key; // SyntaxError на этапе разбора файла

Живой запуск: 6, пустой массив ключей, пустой JSON. Поле #key не видно ни через Object.keys, ни в JSON, ни отладчиком снаружи, и обращение к нему из чужого кода — синтаксическая ошибка ещё до запуска.

Правило простое: секрет (ключ, пароль, токен) — #. Внутренняя кухня, о которой просто не нужно знать снаружи, — private.

TypeScript знает и про #, и ошибка у него отдельная:

console.log(t.#hash);
// error TS18013: Property '#hash' is not accessible outside class 'Tx'
// because it has a private identifier.

Геттеры и сеттеры​

Иногда поле нужно не хранить, а вычислять — но обращаться к нему хочется как к полю, без скобок. Для этого есть get:

class Block {
constructor(txs) { this.txs = txs; }
get total() { return this.txs.reduce((s, x) => s + x, 0); }
}
const b = new Block([1, 2, 3]);
console.log(b.total); // 6 — без скобок!
console.log(Object.keys(b)); // [ 'txs' ] — поля total не существует

Живой запуск подтверждает: b.total даёт 6, но среди собственных полей объекта total нет — это метод, замаскированный под поле.

Парный set перехватывает присваивание и позволяет проверить значение до того, как оно попадёт внутрь:

class Wallet {
#balance = 0;
get balance() { return this.#balance; }
set balance(v) {
if (v < 0) throw new RangeError('баланс не может быть отрицательным');
this.#balance = v;
}
}
const w = new Wallet();
w.balance = 100; // проходит через сеттер
w.balance = -5; // RangeError: баланс не может быть отрицательным

Такая пара — типовой приём: хранит #balance, а наружу выставляет balance с проверкой. Снаружи это выглядит как обычное поле, и вызывающий код ничего не подозревает.

Совет

Геттер обязан быть дешёвым. Читающий код видит b.total и считает, что это просто чтение поля, — а внутри может оказаться перебор десяти тысяч элементов, который выполнится при каждом обращении в цикле.

Правило: если работа дорогая, делайте её методом со скобками — b.computeTotal(). Скобки честно предупреждают «здесь что-то происходит».

readonly: поле, которое нельзя переписать​

readonly разрешает присвоить значение только один раз — при объявлении или в конструкторе:

class Tx {
constructor(public readonly id: string) {}
rename(next: string) {
this.id = next; // error TS2540: Cannot assign to 'id' because it is a read-only property.
}
}

Ошибка возникает даже внутри класса — в этом весь смысл: идентификатор транзакции не должен меняться никогда и ни у кого.

Разница с const и с Object.freeze:

что защищаеткогда проверяется
constимя переменнойдо запуска и во время работы
readonlyполе объектатолько до запуска, компилятором
Object.freezeвсе поля объектаво время работы, молча не даёт изменить

И readonly, как всё остальное из TypeScript, исчезает при компиляции. Если объект уезжает в чужой код, который на TypeScript не написан, защиты нет.

static: принадлежит классу, а не объекту​

Статическое поле или метод живёт у самого класса. У объектов его нет.

class Counter {
static made = 0;
constructor() { Counter.made += 1; }
}
new Counter();
new Counter();
console.log(Counter.made); // 2
console.log(new Counter().made); // undefined

Живой запуск: 2 и undefined. Это ровно то, что смущает в первый раз — обращаться нужно через имя класса, не через объект.

Для чего это применяют на практике:

class Address {
private constructor(public readonly value: string) {}

static readonly PREFIX = '3P'; // константа класса

static isValid(s: string): boolean { // проверка, которой не нужен объект
return s.startsWith(Address.PREFIX) && s.length === 35;
}

static from(s: string): Address { // фабричный метод
if (!Address.isValid(s)) throw new Error(`не адрес: ${s}`);
return new Address(s);
}
}

const a = Address.from('3P8fH2...');

Три типовых применения статики видны здесь сразу: константа класса, проверка, которой не нужен готовый объект, и фабричный метод, создающий объект вместо new. Обратите внимание на private constructor — из-за него объект нельзя создать напрямую, и единственная дверь внутрь идёт через Address.from, где выполняется проверка.

Интересный факт

static — соблазн написать класс, который на самом деле не нужен:

class MathUtils {
static add(a: number, b: number) { return a + b; }
static sub(a: number, b: number) { return a - b; }
}

Класс, у которого всё статическое, — это модуль, переодетый классом. В JavaScript и TypeScript для этого есть настоящие модули: просто export function add(...) в отдельном файле. Так короче, так лучше работает удаление неиспользуемого кода при сборке, и new по ошибке никто не напишет.

Отвечено вопросов: 0 из 5
Что останется от private в скомпилированном JavaScript?
Что покажет JSON.stringify для объекта с полем #key?
Чем get total отличается от метода total()?
Что вернёт new Counter().made при static made?
Зачем классу private constructor?
Отвечено верно: 0 из 5

Типичные ошибки: текст, перевод, причина, починка​

Что видноПереводПричинаКак чинить
TypeError: Class constructor Tx cannot be invoked without 'new'Конструктор класса нельзя вызвать без newЗабыли newДобавить new
TypeError: Cannot read properties of undefined (reading 'amount') внутри методаЧтение поля у undefinedПотеряли this: метод вызвали без точкиОбёртка-стрелка, bind или стрелка в поле
TypeError: back.fee is not a functionУ объекта нет такого методаОбъект пришёл из JSON.parse — это не объект классаСобрать заново: new Tx(back.amount)
error TS2564: Property 'name' has no initializerУ поля нет начального значенияПоле объявлено, но не заполненоЗначение по умолчанию, присваивание в конструкторе или name?: string
error TS2341: Property 'amount' is privateПоле приватноеОбращение снаружи классаГеттер, публичный метод — или признать, что поле должно быть публичным
error TS2540: Cannot assign to 'id'Поле только для чтенияПрисваивание в readonly после конструктораУбрать readonly или создавать новый объект вместо изменения
error TS18013: Property '#hash' is not accessible outside classПоле с решёткой недоступно снаружиОбращение к #-полю извнеПубличный метод или геттер внутри класса
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]Node не умеет эту конструкциюПараметры-свойства при запуске node file.tsДлинная запись полей либо сборка через tsc

Тренажёр: классы вслепую​

Цель: скорость ≥ 110 зн/мин, точность ≥ 90%

class Tx { constructor(public readonly id: string) {} }
клавиша ``клавиша 11клавиша 22клавиша 33клавиша 44клавиша 55клавиша 66клавиша 77клавиша 88клавиша 99клавиша 00клавиша --клавиша ==клавиша ⌫⌫клавиша TabTabклавиша QQклавиша WWклавиша EEклавиша RRклавиша TTклавиша YYклавиша UUклавиша IIклавиша OOклавиша PPклавиша [[клавиша ]]клавиша \\клавиша Caps LockCaps Lockклавиша AAклавиша SSклавиша DDклавиша FFклавиша GGклавиша HHклавиша JJклавиша KKклавиша LLклавиша ;;клавиша ''клавиша EnterEnterклавиша ShiftShiftклавиша ZZклавиша XXклавиша CCклавиша VVклавиша BBклавиша NNклавиша MMклавиша ,,клавиша ..клавиша //клавиша ShiftShiftклавиша CtrlCtrlклавиша WinWinклавиша AltAltклавиша ПробелПробелклавиша AltAltклавиша WinWinклавиша MenuMenuклавиша CtrlCtrl
следующая: C — средний левой руки

Символы этой главы​

Знак или словоНазваниеЧто делает
classклассобъявляет чертёж объектов
newсоздание объектасоздаёт объект, привязывает прототип, запускает конструктор
constructorконструкторметод, выполняемый один раз при создании
thisтекущий объектто, что стояло слева от точки при вызове
staticстатический членпринадлежит классу, а не объектам
get / setгеттер и сеттерметод, к которому обращаются как к полю
#полеприватное поле JavaScriptнастоящая недоступность снаружи, в том числе после компиляции
privateприватное поле TypeScriptпроверка компилятором; в готовом JS ограничения нет
protectedзащищённое поледоступно классу и его наследникам
publicпубличное полеповедение по умолчанию; пишут ради параметров-свойств
readonlyтолько для чтенияприсвоить можно один раз, в конструкторе

Что дальше​

Класс умеет держать данные и поведение в одном месте, прятать внутренности и раздавать доступ через геттеры. Следующая глава — про то, как классы связывают друг с другом: наследование, абстрактные классы, интерфейсы и композиция. Там же выяснится, что class — надстройка над механизмом, который в языке был всегда, и что типы здесь сравниваются по форме, а не по имени.

Углубиться

new.target. Внутри конструктора доступно new.target — тот класс, от которого на самом деле шёл new. Приём применяют, чтобы запретить прямое создание базового класса.

Классы против чистых функций. Заметная часть современного кода на TypeScript классов почти не использует: данные держат в обычных объектах и типах, а поведение — в функциях. Спор решается по задаче: там, где у объекта есть состояние и жизненный цикл (соединение, кошелёк, сессия), класс уместнее; там, где идёт преобразование данных, — функция.

Object.freeze. Ещё один способ запретить изменение — заморозить объект целиком во время выполнения. В отличие от readonly, работает не до запуска, а в нём.

Дальше по теме:

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоКлассы в JavaScript — объявление, конструктор, наследование, экземпляр, геттеры и сеттерыАлександр Ламков — Friendly Frontend
ВидеоКлассы в TypeScript: создание, синтаксис, свойства и методы. Конструктор и readonly свойстваWebDev с нуля. Канал Алекса Лущенко
ВидеоАбстрактные классы в TypeScript: полное руководствоWebDev с нуля. Канал Алекса Лущенко
ВидеоКонтекст вызова функции. Фундаментальный JavaScriptМихаил Непомнящий
ВидеоПрототипное программирование и прототипное наследование в JavaScriptTimur Shemsedinov

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.