Номенклатура JavaScript (в контексте Node.js и Web API)

    I. Предыстория


    Я много лет использую UltraEdit как редактор на самые разные случаи жизни. Одна из основных причин — быстрая работа с гигабайтными файлами без загрузки их в память. Для программирования на JavaScript он тоже достаточно удобен, вот только с одним существенным недостатком: автодополнение в нём основывается на достаточно бедном, жёстко заданном списке ключевых слов и глобальных переменных, вдобавок отстающем от развития языка. Как-то я задался вопросом, можно ли пополнить этот список полным перечнем всех готовых свойств и методов, какие только можно ввести в контексте Node.js и Web API (браузера). Где бы такой список можно раздобыть? Мне приходили в голову такие варианты:


    1. Готовый перечень, кем-то составляемый и обновляемый для всеобщего пользования, вроде библиотеки globals, но полнее.


    2. Парсинг документации (спецификация ECMAScript, сайты MDN и Node.js и т.п.), вручную или программно.


    3. Получение списка метапрограммированием.

    Основным ответом на мои вопросы было предложение сменить редактор и не мучиться. Но так как удобных редакторов для больших файлов не так уж и много, а мой минимализм затруднял использование нескольких под разные нужды, да и программирование не было моим основным занятием, я не сдавался. В конце концов, это стало для меня не только практической задачкой, но и принципиальным интересом: как же так, вроде бы такая простая нужда, а лёгких решений нет.


    Поскольку готовых списков я не нашёл, а парсить документацию — путь долгий и ненадёжный, я решил попробовать третий способ.


    II. Код


    Воспользовавшись несколькими советами, я написал небольшой скрипт, который выводит основную часть языковой номенклатуры в разных контекстах несколькими способами.


    Вот что у меня получилось.


    Скрипт можно запустить в Node.js или в браузере (через консоль или вставку в страницу). В первом случае результат будет выведен в файлы, во втором — в прибавленные к текущему документу текстовые поля (можно открыть about:blank вместе с консолью).


    Постараюсь прокомментировать код.


    1. Сперва мы создаём основные переменные-контейнеры. В первых двух мы будем накапливать нашу номенклатуру: в nomenclatureTerms будет храниться простой список всех лексем, в nomenclatureChains — те же лексемы, но с полными цепочками, начиная от корневых объектов. В globs мы будем хранить наши отправные точки для разматывания клубка и построения дерева — глобальные (корневые) объекты. Чтобы избежать бесконечной рекурсии из-за циклических ссылок, все обработанные объекты мы будем складывать в processedObjects для последующей проверки.


    2. На втором этапе мы заполняем globs.


    Сначала скрипт пытается определить, в каком контексте он выполняется. Если это браузер, нам достаточно объекта window.


    Если это Node.js, всё немного сложнее. Сначала мы добавляем два основных глобальных объекта, а также require, поскольку иначе на эту функцию мы не выйдем. Потом мы добавляем объекты всех стандартных библиотек: основную часть — отталкиваясь от недокументированного списка require('repl')._builtinLibs, посоветованного одним из разработчиков Node.js, а затем несколько недостающих модулей. В завершение, поскольку несколько внутримодульных переменных (__dirname и __filename) не привязаны ни какому глобальному объекту, мы сразу же добавим их в наши номенклатурные контейнеры.


    3. Далее следует основная работа: при помощи рекурсивной функции processKeys мы обходим все глобальные объекты и все объекты, хранящиеся в их свойствах, до последней возможной глубины. Затем выводим результаты в зависимости от контекста и завершаем их итоговым выводом в консоль размеров наших номенклатур (скрипт работает ощутимое время, так что этот вывод может служить сигналом завершения работы — хотя в Chrome может потребоваться дополнительное время на обновление страницы даже после этого сигнала).


    4. Функция processKeys является основным двигателем процесса.


    Сперва мы проверяем, с корневым ли объектом мы имеем дело. Если да, мы сразу заносим его имя в номенклатуру. Если объект расположен в дочернем свойстве объекта, это занесение уже совершилось на предыдущем этапе рекурсии, поэтому мы его пропускаем.


    Затем мы заносим объект в список обработанных объектов, чтобы не попасть в дурную бесконечность.


    После этого начинаем обходить все свойства объекта. Для этого мы используем метод Reflect.ownKeys(), поскольку только он перечисляет и обычные строковые ключи объекта, и ключи типа Symbol. Каждое из свойств мы заносим в nomenclatureTerms (тип Set автоматически отбрасывает повторения), затем формируем цепочку из имени родительского объекта и текущего свойства и заносим её в nomenclatureChains; эта же цепочка станет именем объекта для следующего рекурсивного вызова, поэтому она будет постоянно расти с продвижением вглубь (я выбрал нотацию с квадратными скобками на все случаи для унификации сортировки: если использовать точку для обычных идентификаторов и скобки для сложных строковых, это ломает порядок при выводе списка; JSON.stringify употребляется для перестраховки — для экранирования возможных кавычек в составе имён свойств). Ключи типа Symbol перед занесением в базу приводятся к строкам (к сожалению, это делает элементы базы с такими ключами в цепочках свойств непригодными для непосредственной интерпретации, например, в REPL Node.js или в консолях браузеров — перед этим нужно опять приводить такие ключи к типу Symbol, убирая лишнее из строкового представления).


    На следующем этапе мы проверяем, что хранится в свойстве: если это объект, мы делаем новый рекурсивный вызов, если этого объекта ещё нет в списке обработанных. Проверка на объектность двойная, поскольку instanceof Object возвращает false для Object.prototype и для объектов, созданных при помощи Object.create(null).


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


    5. Функция output отвечает за вывод результатов в зависимости от контекста выполнения. Сначала она формирует список, отсортированный в более привычном словарном порядке (правда, параметр caseFirst в Firefox не работает). Затем проверяет контекст выполнения: в браузере списки выводятся в два текстовых поля, встраиваемые в текущую страницу (вверх списка добавляется для удобства имя файла, с которым список можно сохранить при помощи редактора); в Node.js создаются два файла в текущем каталоге.


    Следует учитывать, что к браузерному списку добавляются имена функций нашего скрипта, а к списку Node.js — разные переменные окружения; также в перечень включаются разные недокументированные свойства внутреннего употребления, индексы массивов и т.п. С другой стороны, в наш список не попадают многие строковые элементы номенклатуры (например, названия событий или стандартные строковые параметры функций).


    III. Результаты


    После прогона скрипта на последней бета-версии Node.js и на ночных сборках двух браузеров я получил следующие списки (данные обновлены по состоянию на 18.10.2016):


    Node.js 7.0.0-test201610107f7d1d385d
    Terms: 1 822
    Chains: 7 394


    Google Chrome Canary 56.0.2891.0
    Terms: 3 352
    Chains: 15 091


    Mozilla Firefox Nightly 52.0a1 (2016-10-17)
    Terms: 5 082
    Chains: 16 125


    Возможно, у результатов программы могут быть разные применения. Например, сравнение номенклатуры разных браузеров или разных версий одного браузера (во время тестирования я замечал, что ночные сборки соседних дней могут давать результаты, различающиеся десятками позиций — что-то вводится, что-то уходит в историю). Если автоматизировать процесс, можно, например, создать историю API Node.js на протяжении многих версий. А можно собрать разнообразную языковую статистику: глубина вложения свойств, длина идентификаторов, принципы их создания и т.д.


    Наверняка код можно оптимизировать по скорости, по удобству использования, по полноте результатов или их читабельности. Также я мог допустить какие-то глупые ошибки из-за незнания тонкостей языка или контекстов использования. Буду благодарен за поправки и добавления. Спасибо за внимание.


    P.S. Хороший пример: http://electron.atom.io/blog/2016/09/27/api-docs-json-schema

    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 18
    • +2
      Как-то я задался вопросом, можно ли пополнить этот список полным перечнем всех готовых свойств и методов, какие только можно ввести в контексте Node.js и Web API (браузера). Где бы такой список можно раздобыть?

      Есть же для Typescript .d.ts-файлы, где описывают сигнатуры для всяких библиотек, в том числе — стандартных. Многие редакторы/IDE их и используют.


      Вот тут список файлов с сигнатурами стандартных библиотек ES и работы с DOM: https://github.com/Microsoft/TypeScript/tree/master/lib


      Для ноды можно взять эти файлы из репозитория с кучей сигнатур для кучи библиотек: https://github.com/DefinitelyTyped/DefinitelyTyped


      Вот папка с сигнатурами ноды: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/node


      В общем, странно что вы ничего не слышали про это, раз пишите на JS.

      • 0

        Спасибо. Я пишу на непрофессиональном уровне и в Typescript никогда не углублялся. Постараюсь разобраться, как извлечь из этих файлов чистые списки.

        • +1

          можете посмотреть на проект Atom Typescript, посмотреть, как там реализован выбор списка для автодополнений.

          • 0

            Попробовал понять с наскока. Понял, что для использования этих файлов нужно сперва выучить Typescript, а потом опять-таки или самому парсить эти описания, или искать для этого инструменты, поскольку просто так любой редактор не заставишь с этим работать без плагинов, которых может и не быть. Для меня это довольно сложный путь, я как-то для своих нужд всегда обходился без этого направления в языке — строгая типизация, классы, все прелести ООП. Но всё равно спасибо, это, по крайней мере, надёжная стратегия для тех, кто уже в этом хорошо разбирается.

    • +1

      Есть готовый перечень вообще-то.

    • +1
      Есть ещё способ. Например воспользовавшись webstorm. Пишем в коде например `document`. И с щелчка переходим на его описание (в DHTML.js). Там описана структура почти всех элементов js'a.
      Это лишь пример :)
    • 0
      Не читал но осуждаю.
      Чем SublimeText Вам не угодил? Он же для людей писан. А ултраедит думаю уже давно не то что нужно. Как и Нотепад++
      • 0

        А в SublimeText можно открыть текстовый файл в 2 гигабайта и провести в нём замены по регулярным выражениям? Он не будет читать его в память, не будет подвисать? Я просто регулярно работаю с исходниками больших цифровых словарей, мне это важно. Чаще всего мне для их обработки и нужен JavaScript.

        • +1
          Эх нет :(. На текстовом файле в 2гб он наедается и умирает. vim и nano тоже умирают.
          Atom даже не пытался запускать :)
          • 0

            Жаль. Я периодически тестировал популярные редакторы общего профиля, это распространённая проблема. Можно, конечно, простые скрипты писать для таких файлов, чтобы потоками обрабатывали, но это не всегда удобно.

      • +2
        Возьмите готовый перечень у тех кто уже решил схожую проблему. например у Ternjs
        Так у вас отпадёт нужда в поддержке(правда есть риск, что они могут изменить формат. но это не проблема, всегда можно узнать это заранее через ишьюсы)
      • 0
        • 0

          Обновил код и описание: теперь скрипт учитывает в объектах ключи типа Symbol.

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