Программисты делятся на две категории: те, которые используют отладчик при разработке, и те, которые обходятся без него. В этом посте я попытался обобщить, какие типы сущностей можно выявить в исходном коде JS-программы, и как эти типы выглядят под отладчиком. JS-программисты из первой категории могут дополнить, если я упустил какой-либо тип сущностей, а JS-программисты из второй категории могут посмотреть на то, чего, возможно, никогда не видели.
Типы сущностей в исходном коде
Сам я сталкивался со следующими типами:
примитивы: строка, число, логическое значение, null, undefined, символ;
области видимости (scopes)
(update) замыкания
объекты
массивы
функции
классы
модули
пакеты
Примитивы
С примитивами ничего интересного, на то они и примитивы. Вот код:
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';
Под отладчиком сущности, импортируемые из пакета, аналогичны импортируемым из модуля:
Резюме
Не знаю, увидели ли вы что-либо новое для себя в этой статье (если нет, то надеюсь, вы хотя бы не читали её внимательно, надеясь найти что-то новое), зато я обнаружил для себя много чего незнакомого, пока её писал. Что уже хорошо, пусть и не в масштабах Вселенной.
Всем спасибо за внимание. Хэппи, как говорится, кодинга. Ну и дебаггинга.
Zenitchik
В чём практическая ценность этой статьи?
Показать, что как выглядит в отладчике? А не проще просто открыть отладчик и увидеть?
flancer Автор
Конечно проще. Если знать на что смотреть. А в чём практическая ценность вашего вопроса?
Zenitchik
А это не очевидно что ли? Если не разворачивается — значит примитив. Если разворачивается — значит объект, причём у него и конструктор подписан.
Для того, кто знает JavaScript — представление переменных в отладчике не окажется чем-то новым даже отладчик открыт первый раз. Там всё нарисовано так же, как оно доступно из кода.
Очевидно, в том, чтобы получить на него ответ. О чём Вы хотели написать-то?
flancer Автор
Вот видите, вы выделяете всего 2 сущности в исходном коде — объект и примитив (причём Symbol, являясь примитивом, в отладчике IDEA имеет признак разворачиваемого), а я выделяю 8 сущностей. Плюс, у анонимных классов конструктор не подписан, а модули, хоть и разворачиваются, конструкторов не имеют в принципе. И если бы вы читали не только заголовок, но и резюме, то там я специально для Вас написал:
Zenitchik
Но не разворачивается же.
Которые различаются тем, что у них внутри. Либо не различаются.
Моя классификация — двухуровневая.
Специально проверил. Если развернуть, то у прототипа есть свойство constructor, как у всех.
Вообще не вижу причины как-то особо выделять классы — под капотом те же конструктор и прототип.
Даже человек, для которого новая вся статья, узнал бы то же самое и без неё, просто открыв отладчик и 20 минут с ним поигравшись.
flancer Автор
Развивайте дальше вашу мысль. Что вы хотите донести своими комментами? Мне уже даже интересно.
Zenitchik
Я по-прежнему пытаюсь понять генеральную мысль статьи.
Не могу поверить, что Вы просто описали то, что легко увидеть и без Вас. Очевидно, у Вас была цель сказать читателю что-то неочевидное. А я почему-то в упор ничего такого не вижу. Может, не туда смотрю?
flancer Автор
Очевидно, что человек, различающий всего два цвета, не сможет понять описание радуги, сделанное человеком, различающим 7 цветов (в нашем случае — 8). Попробую объяснить.
Вот здесь
sub
— это примитив или объект?Если примитив, то почему разворачивается, если объект, то где у него конструктор?
Zenitchik
Понятно, что человек, видевший только газон, не может себе представить дерево. Вы не задумывались, что классификация бывает не только одноуровневая?
Это самостоятельная сущность. Тут я ошибся.