Source code for javascript

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‘ — это код.

Читайте также:  Python наследование классов init super

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

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

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

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

Читайте также:  Css style text gradient

Объекты, созданные при помощи именованных классов, в отладчике видны под именами этих классов ( 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 (двойной щелчок мыши по свойству):

Читайте также:  Hex string to bytearray kotlin

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

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

Резюме

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

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

Источник

Оцените статью