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

    Области видимости

    Проще всего организовать различные области видимости переменных при помощи блоков:

    {
        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';

    Под отладчиком сущности, импортируемые из пакета, аналогичны импортируемым из модуля:

    Резюме

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

    Всем спасибо за внимание. Хэппи, как говорится, кодинга. Ну и дебаггинга.

    Комментарии 11

      +3

      В чём практическая ценность этой статьи?
      Показать, что как выглядит в отладчике? А не проще просто открыть отладчик и увидеть?

        –3

        Конечно проще. Если знать на что смотреть. А в чём практическая ценность вашего вопроса?

          +1
          Если знать на что смотреть.

          А это не очевидно что ли? Если не разворачивается — значит примитив. Если разворачивается — значит объект, причём у него и конструктор подписан.
          Для того, кто знает JavaScript — представление переменных в отладчике не окажется чем-то новым даже отладчик открыт первый раз. Там всё нарисовано так же, как оно доступно из кода.


          А в чём практическая ценность вашего вопроса?

          Очевидно, в том, чтобы получить на него ответ. О чём Вы хотели написать-то?

            –2

            Вот видите, вы выделяете всего 2 сущности в исходном коде — объект и примитив (причём Symbol, являясь примитивом, в отладчике IDEA имеет признак разворачиваемого), а я выделяю 8 сущностей. Плюс, у анонимных классов конструктор не подписан, а модули, хоть и разворачиваются, конструкторов не имеют в принципе. И если бы вы читали не только заголовок, но и резюме, то там я специально для Вас написал:


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

              Но не разворачивается же.


              а я выделяю 8 сущностей

              Которые различаются тем, что у них внутри. Либо не различаются.
              Моя классификация — двухуровневая.


              у анонимных классов конструктор не подписан

              Специально проверил. Если развернуть, то у прототипа есть свойство constructor, как у всех.
              Вообще не вижу причины как-то особо выделять классы — под капотом те же конструктор и прототип.


              Не знаю, увидели ли вы что-либо новое для себя в этой статье

              Даже человек, для которого новая вся статья, узнал бы то же самое и без неё, просто открыв отладчик и 20 минут с ним поигравшись.

                –1

                Развивайте дальше вашу мысль. Что вы хотите донести своими комментами? Мне уже даже интересно.

                  +1

                  Я по-прежнему пытаюсь понять генеральную мысль статьи.
                  Не могу поверить, что Вы просто описали то, что легко увидеть и без Вас. Очевидно, у Вас была цель сказать читателю что-то неочевидное. А я почему-то в упор ничего такого не вижу. Может, не туда смотрю?

                    0

                    Очевидно, что человек, различающий всего два цвета, не сможет понять описание радуги, сделанное человеком, различающим 7 цветов (в нашем случае — 8). Попробую объяснить.


                    Если не разворачивается — значит примитив. Если разворачивается — значит объект, причём у него и конструктор подписан.

                    Вот здесь sub — это примитив или объект?
                    image


                    Если примитив, то почему разворачивается, если объект, то где у него конструктор?

                      +1

                      Понятно, что человек, видевший только газон, не может себе представить дерево. Вы не задумывались, что классификация бывает не только одноуровневая?


                      Вот здесь sub — это примитив или объект?

                      Это самостоятельная сущность. Тут я ошибся.

        –1

        Похоже, что в моей классификации типов сущностей исходного кода пропущено замыкание (closure). Я ошибочно относил замыкание к областям видимости (scopes), но, переспав с этой мыслью, я всё-таки вынес замыкание в отдельный тип.


        {
            function outer() {
                let nameOut = 'outer';
                return function middle() {
                    let nameMid = 'middle';
                    return function inner() {
                        let nameIn = 'inner';
                        debugger;   // breakpoint 2
                        return `${nameOut}/${nameMid}/${nameIn}`;
                    };
                };
            }
        
            const func = outer()();
            debugger;   // breakpoint 1
            func();
        }

        В первой точке останова у нас, путём запуска "outer()()" определена функция "func", что приводит к созданию двух замыканий:



        Я вынес замыкания в отдельный тип, т.к. они создаются в момент определения функции, а не в момент её запуска. В момент запуска замыкания превращаются в области видимости (вторая точка останова):



        В общем, т.к. в отладчике "замыкания" и "области видимости" показываются по-разному (и искать их при отладке нужно в разных местах), я также их различаю. Таким образом в моей классификации на данный момент 9 типов сущностей, которые можно обнаружить в исходном коде. Из них 8 видны через отладчик (пакеты в nodejs видны как модули, а в браузере вообще отсутствуют).

          0
          пакеты в nodejs видны как модули

          cjs — как объекты. Можно даже получить конструктор и вызвать его с new

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое