- Javascript: исходный код и его отображение при отладке
- Типы сущностей в исходном коде
- Примитивы
- Области видимости
- Объекты
- Прототип объекта
- Массивы
- Прототип массива
- Функции
- Стрелочные vs. Обычные
- Именованные vs. Анонимные
- Прототип функции
- Классы
- Именованные vs. Анонимные
- Класс — это функция
- Экземпляры класса
- Статические свойства и методы
- Приватные свойства и методы
- Акцессоры (get & set)
- Наследование
- Модули
- Пакеты
- Резюме
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 :
Таким образом 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 внутри соответствующей области видимости.
Класс — это функция
В отладчике видно, что класс 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';
Под отладчиком сущности, импортируемые из пакета, аналогичны импортируемым из модуля:
Резюме
Не знаю, увидели ли вы что-либо новое для себя в этой статье (если нет, то надеюсь, вы хотя бы не читали её внимательно, надеясь найти что-то новое), зато я обнаружил для себя много чего незнакомого, пока её писал. Что уже хорошо, пусть и не в масштабах Вселенной.
Всем спасибо за внимание. Хэппи, как говорится, кодинга. Ну и дебаггинга.