Советы по стилю кода
Это и есть искусство программирования – взять сложную задачу и написать такой код для её решения, который и правильно работает, и легко читается, понятен для людей. Для этого нужен хороший стиль написания кода. В этой главе мы рассмотрим составляющие такого стиля.
Синтаксис
Шпаргалка с правилами синтаксиса (подробнее смотрите ниже по тексту):
Не всё здесь однозначно, так что разберём эти правила подробнее.
Ни одно правило не является жёстко обязательным
Здесь нет железных правил. Это стилевые предпочтения, а не религиозные догмы.
Фигурные скобки
В большинстве JavaScript проектов фигурные скобки пишутся в так называемом «египетском» стиле с открывающей скобкой на той же строке, что и соответствующее ключевое слово – не на новой строке. Перед открывающей скобкой должен быть пробел, как здесь:
if (condition) < // делай это // . и это // . и потом это >
А что если у нас однострочная запись, типа if (condition) doSomething() , должны ли мы использовать фигурные скобки?
Вот различные варианты расстановки скобок с комментариями, посмотрите сами, какой вам кажется самым читаемым:
-
Такое иногда бывает в коде начинающих. Плохо, фигурные скобки не нужны:
if (n < 0) не поддерживается`);>
if (n < 0) alert(`Степень $не поддерживается`);
if (n < 0) alert(`Степень $не поддерживается`);
if (n < 0) < alert(`Степень $не поддерживается`); >
Для очень короткого кода допустима одна строка. Например: if (cond) return null . Но блок кода (последний вариант) обычно всё равно читается лучше.
Длина строки
Никто не любит читать длинные горизонтальные строки кода. Лучше всего разбивать их, например:
// обратные кавычки ` позволяют разделять строку на части let str = ` Рабочая группа TC39 организации Ecma International - это группа JavaScript-разработчиков, теоретиков и авторов движков JavaScript, которые вместе с сообществом занимаются поддержкой и развитием языка JavaScript. `;
if ( 123 && moonPhase === 'Waning Gibbous' && zodiacSign === 'Libra' )
Максимальную длину строки согласовывают в команде. Обычно это 80 или 120 символов.
Отступы
Существует два типа отступов:
-
Горизонтальные отступы: 2 или 4 пробела. Горизонтальный отступ выполняется с помощью 2 или 4 пробелов, или символа табуляции (клавиша Tab ). Какой из них выбрать – это уже на ваше усмотрение. Пробелы больше распространены. Одно из преимуществ пробелов над табуляцией заключается в том, что пробелы допускают более гибкие конфигурации отступов, чем символ табуляции. Например, мы можем выровнять аргументы относительно открывающей скобки:
show(parameters, aligned, // 5 пробелов слева one, after, another ) < // . >
function pow(x, n) < let result = 1; // //
Точка с запятой
Точки с запятой должны присутствовать после каждого выражения, даже если их, казалось бы, можно пропустить.
Есть языки, в которых точка с запятой необязательна и редко используется. Однако в JavaScript бывают случаи, когда перенос строки не интерпретируется, как точка с запятой, что может привести к ошибкам. Подробнее об этом – в главе о структуре кода.
Если вы – опытный разработчик на JavaScript, то можно выбрать стиль кода без точек с запятой, например StandardJS. В ином случае, лучше будет использовать точки с запятой, чтобы избежать подводных камней. Большинство разработчиков их ставят.
Уровни вложенности
Уровней вложенности должно быть немного.
Например, в цикле бывает полезно использовать директиву continue , чтобы избежать лишней вложенности.
Например, вместо добавления вложенного условия if , как здесь:
for (let i = 0; i < 10; i++) < if (cond) < . // >
Мы можем написать:
for (let i = 0; i < 10; i++) < if (!cond) continue; . //
Аналогичная ситуация – с if/else и return .
Например, две нижеследующие конструкции идентичны.
function pow(x, n) < if (n < 0) < alert("Отрицательные значения 'n' не поддерживаются"); >else < let result = 1; for (let i = 0; i < n; i++) < result *= x; >return result; > >
function pow(x, n) < if (n < 0) < alert("Отрицательные значения 'n' не поддерживаются"); return; >let result = 1; for (let i = 0; i < n; i++) < result *= x; >return result; >
Размещение функций
Если вы пишете несколько вспомогательных функций, а затем используемый ими код, то существует три способа организации функций.
-
Объявить функции перед кодом, который их вызовет:
// объявление функций function createElement() < . >function setHandler(elem) < . >function walkAround() < . >// код, который использует их let elem = createElement(); setHandler(elem); walkAround();
// код, использующий функции let elem = createElement(); setHandler(elem); walkAround(); // --- вспомогательные функции --- function createElement() < . >function setHandler(elem) < . >function walkAround()
В большинстве случаев второй вариант является предпочтительным.
Это потому, что при чтении кода мы сначала хотим знать, что он делает. Если сначала идёт код, то это тут же становится понятно. И тогда, может быть, нам вообще не нужно будет читать функции, особенно если их имена хорошо подобраны.
Руководства по стилю кода
Руководство по стилю содержит общие правила о том, как писать код, например: какие кавычки использовать, сколько пробелов отступать, максимальную длину строки и так далее – в общем, множество мелочей.
Когда все участники команды используют одно и то же руководство по стилю, код выглядит одинаково, независимо от того, кто из команды его написал.
Конечно, команда всегда может написать собственное руководство по стилю, но обычно в этом нет необходимости. Существует множество уже готовых.
Некоторые популярные руководства:
- Google JavaScript Style Guide
- Airbnb JavaScript Style Guide (есть перевод)
- Idiomatic.JS (есть перевод)
- StandardJS
- (и ещё множество других)
Если вы – начинающий разработчик, то начните со шпаргалки в начале этой главы. Как только вы освоитесь, просмотрите другие руководства, чтобы выбрать общие принципы и решить, какое вам больше подходит.
Автоматизированные средства проверки (линтеры)
Автоматизированные средства проверки, так называемые «линтеры» – это инструменты, которые могут автоматически проверять стиль вашего кода и вносить предложения по его улучшению.
Самое замечательное в них то, что проверка стиля может также найти программные ошибки, такие как опечатки в именах переменных или функций. Из-за этой особенности использовать линтер рекомендуется, даже если вы не хотите придерживаться какого-то конкретного «стиля кода».
Вот некоторые известные инструменты для проверки:
- JSLint – проверяет код на соответствие стилю JSLint, в онлайн-интерфейсе вверху можно ввести код, а внизу – различные настройки проверки, чтобы попробовать её в действии.
- JSHint – больше проверок, чем в JSLint.
- ESLint – пожалуй, самый современный линтер.
Все они, в общем-то, работают. Автор пользуется ESLint.
Большинство линтеров интегрированы со многими популярными редакторами: просто включите плагин в редакторе и настройте стиль.
Например, для ESLint вы должны выполнить следующее:
- Установите Node.JS.
- Установите ESLint с помощью команды npm install -g eslint (npm – установщик пакетов JavaScript).
- Создайте файл конфигурации с именем .eslintrc в корне вашего JavaScript-проекта (в папке, содержащей все ваши файлы).
- Установите/включите плагин для вашего редактора, который интегрируется с ESLint. У большинства редакторов он есть.
Вот пример файла .eslintrc :
< "extends": "eslint:recommended", "env": < "browser": true, "node": true, "es6": true >, "rules": < "no-console": 0, "indent": ["warning", 2] >>
Здесь директива "extends" означает, что конфигурация основана на наборе настроек «eslint:recommended». После этого мы уточняем наши собственные.
Кроме того, возможно загрузить наборы правил стиля из сети и расширить их. Смотрите https://eslint.org/docs/user-guide/getting-started для получения более подробной информации об установке.
Также некоторые среды разработки имеют встроенные линтеры, возможно, удобные, но не такие гибкие в настройке, как ESLint.
Итого
Все правила синтаксиса, описанные в этой главе (и в ссылках на руководства по стилю), направлены на повышение читаемости вашего кода. О любых можно поспорить.
Когда мы думаем о написании «лучшего» кода, мы должны задать себе вопросы: «Что сделает код более читаемым и лёгким для понимания?» и «Что может помочь избегать ошибок?». Это – основные моменты, о которых следует помнить при выборе и обсуждении стилей кода.
Чтение популярных руководств по стилю позволит вам быть в курсе лучших практик и последних идей и тенденций в стилях написания кода.
Рекомендации по написанию чистого кода на JavaScript
Если вы заботитесь о самом коде, и о том, как он написан, а не заняты лишь тем, чтобы создавать работающие программы, это означает что вы стремитесь к тому, чтобы ваш код был чистым. Профессиональный разработчик пишет код не только в расчёте на компьютеры, но и в расчёте на себя самого, встретившего этот код в будущем, и в расчёте на других программистов. Код, который вы пишете, не исчезает навсегда в недрах компьютера. Он живёт, изменяется, и, если написан плохо, вполне может сильно расстроить того, кому придётся редактировать его после того, как вы его написали. Вполне возможно, что этим «кем-то» будете именно вы.
Исходя из этих идей, чистый код можно определить как код, написанный так, что он сам себя объясняет. Этот код без труда смогут понять люди, его легко будет модифицировать или расширять.
Код и WTF-вопросы
Сущность WTF-вопросов, вроде «WTF is that?», сводится к крайнему удивлению и возмущению. По-русски эти чувства можно выразить вопросом «Какого чёрта?». «Чёрт», в зависимости от ситуации, вполне может уступить место чему-нибудь совершенно непечатному. Сколько раз вам доводилось дописывать чей-то код и при этом задаваться подобными вопросами?
Задавая себе WTF-вопросы о чужом коде, программисты спрашивают себя о том, что это такое (WTF is that?), что пытался сделать автор кода (WTF did you do here?), зачем в коде присутствует та или иная конструкция (WTF is this for?)
Вот картинка, в соответствии с которой единственным достоверным показателем качества кода является количество WTF-вопросов в минуту.

Слева — хороший код. Справа — плохой
А если серьёзно, то для того чтобы помочь вам настроиться на размышления о чистом коде, процитируем Роберта Мартина, известного как дядюшка Боб: «Даже плохой программный код может работать. Однако если код не является «чистым», это всегда будет мешать развитию проекта».
Теперь рассмотрим некоторые практические рекомендации по написанию чистого кода. Мы будем использовать здесь JavaScript, но эти рекомендации можно применить и к разработке на других языках.
1. Строгие проверки на равенство
Стремитесь к тому, чтобы вместо == использовать === .
// Если необдуманно использовать оператор == - это может серьёзно повлиять на программную логику. Это можно сравнить с тем, что некто ожидает, что пойдёт налево, но по какой-то причине вдруг идёт направо. 0 == false // true 0 === false // false 2 == "2" // true 2 === "2" // false // пример const value = "500"; if (value === 500) < console.log(value); // этот код не выполнится >if (value === "500") < console.log(value); // этот код выполнится >
2. Переменные
Называйте переменные так, чтобы их имена раскрывали бы их сущность, их роль в программе. При таком подходе их удобно будет искать в коде, а тот, кто увидит этот код, легче сможет понять смысл выполняемых им действий.
Плохо:
let daysSLV = 10; let y = new Date().getFullYear(); let ok; if (user.age > 30)
const MAX_AGE = 30; let daysSinceLastVisit = 10; let currentYear = new Date().getFullYear(); . const isUserOlderThanAllowed = user.age > MAX_AGE;
Не надо добавлять в имена переменных дополнительные слова, в которых нет необходимости.
let nameValue; let theProduct;
let name; let product;
Не стоит принуждать того, кто читает код, к тому, чтобы ему приходилось бы помнить о том, в каком окружении объявлена переменная.
const users = ["John", "Marco", "Peter"]; users.forEach(u => < doSomething(); doSomethingElse(); // . // . // . // . // Тут перед нами ситуация, в которой возникает WTF-вопрос "Для чего используется `u`?" register(u); >);
const users = ["John", "Marco", "Peter"]; users.forEach(user => < doSomething(); doSomethingElse(); // . // . // . // . register(user); >);
Не нужно снабжать имена переменных избыточными сведениями о контексте, в котором они используются.
const user = < userName: "John", userSurname: "Doe", userAge: "28" >; . user.userName;
const user = < name: "John", surname: "Doe", age: "28" >; . user.name;
3. Функции
Используйте для функций длинные описательные имена. Учитывая то, что функция представляет собой описание выполнения некоего действия, её имя должно представлять собой глагол или фразу, полностью описывающую суть функции. Имена аргументов нужно подбирать так, чтобы они адекватно описывали бы представляемые ими данные. Имена функций должны сообщать читателю кода о том, что именно делают эти функции.
function notif(user) < // реализация >
function notifyUser(emailAddress) < // реализация >
Избегайте использования длинных списков аргументов. В идеале функции следует иметь два или меньшее количество аргументов. Чем меньше у функции аргументов — тем легче будет её тестировать.
function getUsers(fields, fromDate, toDate) < // реализация >
function getUsers(< fields, fromDate, toDate >) < // реализация >getUsers(< fields: ['name', 'surname', 'email'], fromDate: '2019-01-01', toDate: '2019-01-18' >)
Используйте аргументы по умолчанию, отдавая им предпочтение перед условными конструкциями.
function createShape(type) < const shapeType = type || "cube"; // . >
function createShape(type = "cube") < // . >
Функция должна решать одну задачу. Стремитесь к тому, чтобы одна функция не выполняла бы множество действий.
function notifyUsers(users) < users.forEach(user => < const userRecord = database.lookup(user); if (userRecord.isVerified()) < notify(user); >>); >
function notifyVerifiedUsers(users) < users.filter(isUserVerified).forEach(notify); >function isUserVerified(user)
Используйте Object.assign для установки свойств объектов по умолчанию.
const shapeConfig = < type: "cube", width: 200, height: null >; function createShape(config) < config.type = config.type || "cube"; config.width = config.width || 250; config.height = config. height || 250; >createShape(shapeConfig);
const shapeConfig = < type: "cube", width: 200 // Значение ключа 'height' не задано >; function createShape(config) < config = Object.assign( < type: "cube", width: 250, height: 250 >, config ); . > createShape(shapeConfig);
Не используйте флаги в качестве параметров. Их использование означает, что функция выполняет больше действий, чем ей следовало бы выполнять.
function createFile(name, isPublic) < if (isPublic) < fs.create(`./public/$`); > else < fs.create(name); >>
function createFile(name) < fs.create(name); >function createPublicFile(name) < createFile(`./public/$`); >
Не загрязняйте глобальную область видимости. Если вам нужно расширить существующий объект — используйте ES-классы и механизмы наследования вместо того, чтобы создавать функции в цепочке прототипов стандартных объектов.
Array.prototype.myFunc = function myFunc() < // реализация >;
class SuperArray extends Array < myFunc() < // реализация >>
4. Условные конструкции
Старайтесь не называть логические переменные так, чтобы в их именах присутствовало бы отрицание. То же самое касается и функций, возвращающих логические значения. Использование таких сущностей в условных конструкциях затрудняет чтение кода.
function isUserNotBlocked(user) < // реализация >if (!isUserNotBlocked(user)) < // реализация >
function isUserBlocked(user) < // реализация >if (isUserBlocked(user)) < // реализация >
Пользуйтесь сокращённой формой записи условных конструкций. Возможно, эта рекомендация может показаться тривиальной, но об этом стоит упомянуть. Используйте этот подход только для логических переменных, и в том случае, если вы уверены в том, что значением переменной не будет undefined или null .
if (isValid === true) < // что-то сделать. >if (isValid === false) < // что-то сделать. >
if (isValid) < // что-то сделать. >if (!isValid) < // что-то сделать. >
Избегайте логических конструкций везде, где это возможно. Вместо них используйте полиморфизм и наследование.
class Car < // . getMaximumSpeed() < switch (this.type) < case "Ford": return this.someFactor() + this.anotherFactor(); case "Mazda": return this.someFactor(); case "McLaren": return this.someFactor() - this.anotherFactor(); >> >
class Car < // . >class Ford extends Car < // . getMaximumSpeed() < return this.someFactor() + this.anotherFactor(); >> class Mazda extends Car < // . getMaximumSpeed() < return this.someFactor(); >> class McLaren extends Car < // . getMaximumSpeed() < return this.someFactor() - this.anotherFactor(); >>
5. ES-классы
Классы появились в JavaScript сравнительно недавно. Их можно назвать «синтаксическим сахаром». В основе того, что получается при использовании классов, лежат, как и прежде, прототипы объектов. Но код, в котором применяются классы, выглядит иначе. В целом, если есть такая возможность, ES-классы стоит предпочесть обычным функциям-конструкторам.
const Person = function(name) < if (!(this instanceof Person)) < throw new Error("Instantiate Person with `new` keyword"); >this.name = name; >; Person.prototype.sayHello = function sayHello() < /**/ >; const Student = function(name, school) < if (!(this instanceof Student)) < throw new Error("Instantiate Student with `new` keyword"); >Person.call(this, name); this.school = school; >; Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; Student.prototype.printSchoolName = function printSchoolName() < /**/ >;
class Person < constructor(name) < this.name = name; >sayHello() < /* . */ >> class Student extends Person < constructor(name, school) < super(name); this.school = school; >printSchoolName() < /* . */ >>
Организуйте методы так, чтобы их можно было бы объединять в цепочки. Этот паттерн используют многие библиотеки — такие, как jQuery и Lodash. В результате ваш код будет более компактным, чем без использования этого паттерна. Речь идёт о том, что в конце каждой из функций класса нужно возвращать this . Это позволит объединять вызовы таких функций в цепочки.
class Person < constructor(name) < this.name = name; >setSurname(surname) < this.surname = surname; >setAge(age) < this.age = age; >save() < console.log(this.name, this.surname, this.age); >> const person = new Person("John"); person.setSurname("Doe"); person.setAge(29); person.save();
class Person < constructor(name) < this.name = name; >setSurname(surname) < this.surname = surname; // Возвратим this для получения возможности объединять вызовы методов в цепочки return this; >setAge(age) < this.age = age; // Возвратим this для получения возможности объединять вызовы методов в цепочки return this; >save() < console.log(this.name, this.surname, this.age); // Возвратим this для получения возможности объединять вызовы методов в цепочки return this; >> const person = new Person("John") .setSurname("Doe") .setAge(29) .save();
6. О том, чего лучше не делать
Тому, кто хочет, чтобы его код был бы чистым, стоит стремиться к тому, чтобы не повторяться. Речь идёт о том, что нужно избегать ситуаций, в которых приходится писать один и тот же код. Кроме того, не нужно оставлять в кодовой базе неиспользуемые функции и фрагменты программ, которые никогда не выполняются.
Дублирующийся код может появляться по разным причинам. Например, в проекте может быть пара слегка отличающихся друг от друга функций. Природа этих отличий или нехватка времени принуждают программиста, например, к созданию двух самостоятельных функций, содержащих практически идентичный код. В подобной ситуации избавление от дублирующегося кода заключается в абстрагировании различий и в работе с ними на более высоком уровне.
Теперь поговорим о неиспользуемом коде. Это — код, который присутствует в кодовой базе, но совершенно ничего не делает. Такое случается, например, когда на определённом этапе разработки решают, что в некоем фрагменте программы больше нет смысла. Для того чтобы избавиться от подобных фрагментов кода, нужно внимательно просмотреть кодовую базу и убрать их. Легче всего избавляться от такого кода в тот момент, когда решено, что он больше не нужен. Позже можно забыть о том, для чего он использовался. Это значительно усложнит борьбу с ним.
Если отложить борьбу с ненужным кодом, то программа будет напоминать то, что показано на следующем рисунке.

Иногда мой код похож на этот балкон. Не знаю — какую задачу он решает, но опасаюсь от него избавиться
Итоги
Здесь мы обсудили лишь малую часть тех действий, которые можно предпринять для улучшения кода. Автор этого материала полагает, что о рассмотренных здесь принципах часто забывают. Программисты пытаются им следовать, но, по разным причинам, не всегда в этом преуспевают. Возможно, в самом начале работы над проектом все помнят о важности чистого кода, в результате программа получается аккуратной. Потом же, по мере приближения дедлайнов, о чистоте кода часто забывают, обращая внимание лишь на то, что отмечено как TODO или REFACTOR. В такие моменты заказчик проекта будет настаивать на том, чтобы проект завершили в срок, а не на том, чтобы его код был бы чистым.
Мы довольно часто публикуем материалы, посвящённые проблеме написания качественного JavaScript-кода. Если вам эта тема интересна — вот несколько ссылок.
- Пишем чистый и масштабируемый JavaScript-код: 12 советов.
- Как писать чистый и красивый код.
- 7 рекомендаций по оформлению кода на JavaScript.
- Паттерн BIF: чистый код фронтенда и удобная работа с серверными данными.
- Руководство по JavaScript, часть 2: стиль кода и структура программ.
- JavaScript: путь к ясности кода.
Уважаемые читатели! Доводилось ли вам задаваться WTF-вопросами при чтении чужого кода?
Javascript: исходный код и его отображение при отладке
Программисты делятся на две категории: те, которые используют отладчик при разработке, и те, которые обходятся без него. В этом посте я попытался обобщить, какие типы сущностей можно выявить в исходном коде JS-программы, и как эти типы выглядят под отладчиком. JS-программисты из первой категории могут дополнить, если я упустил какой-либо тип сущностей, а JS-программисты из второй категории могут посмотреть на то, чего, возможно, никогда не видели.
Типы сущностей в исходном коде
Сам я сталкивался со следующими типами:
- примитивы: строка, число, логическое значение, null, undefined, символ;
- области видимости (scopes)
- (update) замыкания
- объекты
- массивы
- функции
- классы
- модули
- пакеты
Примитивы
С примитивами ничего интересного, на то они и примитивы. (BigInt) Вот код:
const aBool = true; const aNull = null; const aNum = 128; const aStr = '128'; const aSymLocal = Symbol('local symbol'); const aSymGlobal = Symbol.for('global symbol'); let aUndef;
А вот так примитивы выглядят под отладчиком (слева - в браузере Chrome, справа - в IDE PhpStorm):

Ну разве что обращает на себя внимание стрелка рядом с символом в IDEA (PhpStorm), как будто aSymGlobal и aSymLocal являются составными компонентами, а не примитивными элементами. Стрелку на aSymGlobal я развернул - нет там ничего.
UPDATE: К примитивам можно отнести BigInt, т.к. у него свой собственный тип:
typeof BigInt('1') === 'bigint' // true
Области видимости
Проще всего организовать различные области видимости переменных при помощи блоков:
При остановке в отладчике во внутреннем блоке видны переменные из всех трёх областей:

Также и в браузере, и в nodejs доступна глобальная область видимости (Global), а в nodejs ещё доступна область видимости исполняемого фрагмента кода (скрипта) - Local.
Объекты
В JavaScript'е всё, что не примитив, то объект (включая функции и массивы). В данном разделе я рассматриваю именно объекты (которые не функции и не массивы):
const code = Symbol(); const name = Symbol(); const obj = < [id]: 1, [code]: 'ant', [name]: 'cat', aStr: 'string', aNum: 64, anObj: < [code]: 'dog' >>
Символы рекомендуется использовать в качестве идентификаторов свойств объекта и из кода понятно, что 'ant' - это код для объекта obj , а 'cat' - это имя. Для объекта obj.anObj 'dog' - это код.
В отладчике не всё так однозначно:

Если у символа отсутствует описание, то непонятно, какое свойство является именем, а какое - кодом.
Прототип объекта
В свойстве obj.__proto__ находится ссылка на прототип, по которому создавался данный объект. Объекты создаются при помощи конструктора (функция Object.constructor() ), который в качестве прототипа для новых объектов использует свойство Object.constructor.prototype :
const obj = <>;
Таким образом obj.__proto__ === obj.__proto__.constructor.prototype :

prototype в свою очередь содержит ту же функцию constructor , которая содержит тот же prototype , и т.д. - циклическая зависимость, по которой можно спускаться вглубь, пока хватит ресурсов компьютера.
В отладчике также видно, что, например, функция assign является методом конструктора f Object() (методом класса Object ), а не методом свежесозданного объекта obj .
Таким образом отладчик может быть своего рода кратким справочником по методам соответствующих базовых классов:
obj.__proto__.constructor.assign // Object.assign
Массивы
Массивы - это такие специфические объекты, которые и в коде, и под отладчиком выглядят слегка иначе, чем обычные объекты. Вместо фигурных скобок <> применяются квадратные [] :
let undef; const arr = [1, 'str', null, undef, , ['internal', 'array']];
Массив очень похож на объект, только вместо имён ключей (свойств) применяются числовые индексы:

Прототип массива
Под отладчиком видно, что в основе у массивов находится Array:
arr.__proto__ => Array arr.__proto__.constructor.isArray // Array.isArray
у которого в основе находится Object:
arr.__proto__.__proto__ => Object
Функции
Стрелочные vs. Обычные
Стрелочные функции исполняются в области видимости родителя, обычные - создают собственную область видимости.
// arrow function ((a) => < debugger; return a + 2; >)(1); // regular function (function (a) < debugger; return a + 2; >)(2);
Если запустить данный код в браузере/nodejs, то переменная this в локальной области видимости будет неопределена для стрелочных функций:

и будет соответствовать глобальному объекту (Window или global) для обычных:

Именованные vs. Анонимные
Различия между именованными и анонимными функциями видны в стеке вызовов.
// anonymous functions (function (a) < return 2 + (function (b) < debugger; return b + 4; >)(a); >)(1); // named functions (function outer(a) < return 2 + (function inner(b) < debugger; return b + 4; >)(a); >)(1);
Для анонимных функций в стеке указывается только файл и строка кода:

Для именованных - ещё и имя функции, что удобно:

Прототип функции
Прототипом функции является объект Function, для которого прототипом является Object:
func.__proto__ => Function func.__proto__.constructor.caller // Function.caller func.__proto__.__proto__ => Object
Классы
Именованные vs. Анонимные
< const AnonClass = class < name = 'Anonymous' >; class NamedClass < name = 'Named' >function makeAnonClass() < return class < name = 'Dynamic Anon' >; > function makeNamedClass() < return class DynamicNamed < name = 'Dynamic Named' >; > const DynamicAnonClass = makeAnonClass(); const DynamicNamedClass = makeNamedClass(); const anon = new AnonClass(); const named = new NamedClass(); const dynAnon = new DynamicAnonClass(); const dynNamed = new DynamicNamedClass(); const justObj = new (class < name = 'Just Object' >)(); debugger; >
Объекты, созданные при помощи анонимного класса, приравненного к какой-либо переменной, в отладчике видны под именем этой переменной ( anon ).
Объекты, созданные при помощи именованных классов, в отладчике видны под именами этих классов ( dynNamed и named ).
Имя класса, к которому принадлежит объект, находится в obj.__proto__.constructor.name .
Объекты, созданные при помощи динамически созданного анонимного класса, видны в отладчике IDEA под именем базового класса Object, а в отладчике Хрома - без названия, как и простой объект ( dynAnon ). Т.е., у них obj.__proto__.constructor.name отсутствует.
Объект justObj проще было бы создать при помощи обычных фигурных скобок , чем при помощи одноразовой конструкции new (class )() .

В общем, объекты, созданные при помощи именованных классов, в отладчике маркируются именем соответствующего класса (именем конструктора), что очень сильно облегчает жизнь разработчику.
Отладчик Хрома выводит и классы, и объекты-переменные в едином списке, IDEA выделяет функции и классы в отдельный список Functions внутри соответствующей области видимости.
Класс - это функция
class Demo <>
В отладчике видно, что класс Demo является функцией ( Demo.__proto__ => Function ). IDEA выносит классы в секцию Functions внутри блока:

У класса есть свойство prototype которое он использует в качестве свойства __proto__ для новых объектов, создаваемых при помощи оператора new :
const demo = new Demo(); demo.__proto__ === Demo.prototype // true
Экземпляры класса
Экземпляры, создаваемые при помощи оператора new , являются объектами (не функциями, как сам класс):
< class Demo < propA methodA() <>> const demo = new Demo(); debugger; >

Под отладчиком видно, что методы нового объекта находятся в его прототипе ( demo.__proto__.methodA ), а свойства - в самом объекте ( demo.propA ).
Статические свойства и методы
< class Demo < static propStat static methodStat() < return this.propStat; >> const demo = new Demo(); Demo.methodStat(); debugger; >
Статические члены "вешаются" на саму класс-функцию, а не на объекты, создаваемые при помощи оператора new :

Видно, что у объекта demo нет никаких свойств и методов, зато у класс-функции Demo есть свойство propStat и метод methodStat .
Приватные свойства и методы
< class Demo < #propPriv = 'private' #methodPriv() < return this.#propPriv; >> const demo = new Demo(); debugger; >
Приватные свойства и методы видны в Хроме, а в IDEA прячутся в деталях объекта, но видны в его аннотации:

Акцессоры (get & set)
Акцессоры позволяют реализовать "виртуальное" свойство, позволяя контролировать присвоение данных этому свойству и получение данных от свойства:
< class Demo < #prop get prop() < return this.#prop; >set prop(data) < this.#prop = data; >> const demo = new Demo(); demo.prop = 'access'; debugger; >
И в Хроме, и в IDEA данное "виртуальное" свойство при отладке сразу не отображается (стоит троеточие вместо значения), а для получения данных нужно в явном виде вызвать getter (двойной щелчок мыши по свойству):

В IDEA в аннотации прототипа класс-функции ( Demo.prototype ) видно, что prop: Accessor . Также стоит отметить, что "виртуальное" свойство (являясь парой функций) относится скорее к прототипу объекта, чем к самому объекту: если Хром отображает prop в свойствах объекта и в свойствах его прототипа, то IDEA - только в свойствах прототипа.
Наследование
< class Parent < name = 'parent' parentAge = 64 action() <>actionParent() <> > class Child extends Parent < name = 'child' childAge = 32 action() <>actionChild() <> > const child = new Child(); debugger; >
При наследовании прототипы выстраиваются в цепочку, а при добавлении свойств в новый объект конструктор наследника перекрывает значения таких же свойств родителя ( name в итоге равен "child"):

Также видно, что перекрытые методы родителя доступны через прототипы:
child.__proto__.__proto__.action();
Из необычного, и Хром, и Idea аннотируют прототип child.__proto__ как Parent , хотя прототип по факту содержит методы из класса Child .
Модули
Модуль в JS - это отдельный файл, подключаемый через import . Пусть содержимое модуля находится в файле ./sub.mjs (расширение "*.mjs" означает, что в файл содержит ES6-модуль):
function modFunc() <> class ModClass <> const MOD_CONST='CONSTANT'; export ;
а вызывающий скрипт выглядит так:
import * as sub from './sub.mjs'; debugger;
Под отладчиком в вызывающем скрипте виден элемент sub , который не является обычным JS-объектом (у него нет прототипа):

Также видно, что экспортируемые объекты модуля являются "виртуальными" свойствами (доступны через акцессоры).
Пакеты
Пакет - это способ организации кода в nodejs, в браузере пакеты отсутствуют. Если JS-модуль представляет из себя файл, то пакет - это группа файлов, главным из которых является package.json , в котором задаётся точка входа в пакет (по-умолчанию - index.js ). В точке входа описывается экспорт пакета, аналогично тому, как описывается экспорт в модуле. Поэтому импорт пакета аналогичен импорту модуля, за исключением того, что при импорте указывается не путь к модулю (filepath или URL), а имя пакета:
// import * as sub from './sub.mjs'; import * as express from 'express';
Под отладчиком сущности, импортируемые из пакета, аналогичны импортируемым из модуля:

Резюме
Не знаю, увидели ли вы что-либо новое для себя в этой статье (если нет, то надеюсь, вы хотя бы не читали её внимательно, надеясь найти что-то новое), зато я обнаружил для себя много чего незнакомого, пока её писал. Что уже хорошо, пусть и не в масштабах Вселенной.
Всем спасибо за внимание. Хэппи, как говорится, кодинга. Ну и дебаггинга.
