Javascript: исходный код и его отображение при отладке

от автора

Программисты делятся на две категории: те, которые используют отладчик при разработке, и те, которые обходятся без него. В этом посте я попытался обобщить, какие типы сущностей можно выявить в исходном коде 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.anObjdog‘ — это код.

В отладчике не всё так однозначно:

Если у символа отсутствует описание, то непонятно, какое свойство является именем, а какое — кодом.

Прототип объекта

В свойстве 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/