Программисты делятся на две категории: те, которые используют отладчик при разработке, и те, которые обходятся без него. В этом посте я попытался обобщить, какие типы сущностей можно выявить в исходном коде JS-программы, и как эти типы выглядят под отладчиком. JS-программисты из первой категории могут дополнить, если я упустил какой-либо тип сущностей, а JS-программисты из второй категории могут посмотреть на то, чего, возможно, никогда не видели.
Типы сущностей в исходном коде
Сам я сталкивался со следующими типами:
-
примитивы: строка, число, логическое значение, null, undefined, символ;
-
области видимости (scopes)
-
объекты
-
массивы
-
функции
-
классы
-
модули
-
пакеты
Примитивы
С примитивами ничего интересного, на то они и примитивы. Вот код:
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 я развернул — нет там ничего.
Области видимости
Проще всего организовать различные области видимости переменных при помощи блоков:
{ const outer = 'outer scope'; { const medium = 'medium scope'; { const inner = 'inner scope'; debugger; } } }
При остановке в отладчике во внутреннем блоке видны переменные из всех трёх областей:

Также и в браузере, и в nodejs доступна глобальная область видимости (Global), а в nodejs ещё доступна область видимости исполняемого фрагмента кода (скрипта) — Local.
Объекты
В JavaScript’е всё, что не примитив, то объект (включая функции и массивы). В данном разделе я рассматриваю именно объекты (которые не функции и не массивы):
const id = Symbol('id'); 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 id = Symbol.for('id'); const arr = [1, 'str', null, undef, {[id]: 'ant'}, ['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проще было бы создать при помощи обычных фигурных скобок {name: 'Just Object'}, чем при помощи одноразовой конструкции new (class {name = 'Just Object'})().

В общем, объекты, созданные при помощи именованных классов, в отладчике маркируются именем соответствующего класса (именем конструктора), что очень сильно облегчает жизнь разработчику.
Отладчик Хрома выводит и классы, и объекты-переменные в едином списке, 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 {modFunc, ModClass, MOD_CONST};
а вызывающий скрипт выглядит так:
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';
Под отладчиком сущности, импортируемые из пакета, аналогичны импортируемым из модуля:

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