Структуры данных в JavaScript: стек и очередь. Стек, массив и организация данных

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

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

Если множество проектов плотно завязаны на JavaScript, значит, разработчикам необходимо как можно более эффективно использовать всё, что даёт им язык и его экосистема, стремясь, на пути разработки замечательных программ, к глубокому пониманию внутренних механизмов языка.

Как ни странно, существует множество разработчиков, которые регулярно пишут на JavaScript, но не знают, что происходит в его недрах. Пришло время это исправить: этот материал посвящён обзору JS-движка на примере V8, механизмов времени выполнения, и стека вызовов.

Обзор

Почти все слышали, в самых общих чертах, о JS-движке V8, и большинству разработчиков известно, что JavaScript - однопоточный язык, или то, что он использует очередь функций обратного вызова.

Здесь мы поговорим, на довольно высоком уровне, о выполнении JS-кода. Зная о том, что, на самом деле, происходит при выполнении JavaScript, вы сможете писать более качественные программы, которые выполняются без «подвисаний» и разумно используют имеющиеся API.

Если вы недавно начали писать на JavaScript, этот материал поможет вам понять, почему JS, в сравнении с другими языками, может показаться довольно-таки странным.

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

Движок JavaScript

V8 от Google - это широко известный JS-движок. Он используется, например, в браузере Chrome и в Node.js. Вот как его, очень упрощённо, можно представить:


Упрощённое представление движка V8

На нашей схеме движок представлен состоящим из двух основных компонентов:

  • Куча (Memory Heap) - то место, где происходит выделение памяти.
  • Стек вызовов (Call Stack) - то место, куда в процессе выполнения кода попадают так называемые стековые кадры.

Механизмы времени выполнения

Если говорить о применении JavaScript в браузере, то здесь существуют API, например, что-то вроде функции setTimeout , которые использует практически каждый JS-разработчик. Однако, эти API предоставляет не движок.

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


Движок, цикл событий, очередь функций обратного вызова и API, предоставляемые браузером

Итак, помимо движка у нас есть ещё очень много всего. Скажем - так называемые Web API, которые предоставляет нам браузер - средства для работы с DOM, инструменты для выполнения AJAX-запросов, нечто вроде функции setTimeout , и многое другое.

Стек вызовов

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

Стек вызовов - это структура данных, которая, говоря упрощённо, записывает сведения о месте в программе, где мы находимся. Если мы переходим в функцию, мы помещаем запись о ней в верхнюю часть стека. Когда мы из функции возвращаемся, мы вытаскиваем из стека самый верхний элемент и оказываемся там, откуда вызывали эту функцию. Это - всё, что умеет стек.

Рассмотрим пример. Взгляните на следующий код:

Function multiply(x, y) { return x * y; } function printSquare(x) { var s = multiply(x, x); console.log(s); } printSquare(5);
Когда движок только начинает выполнять этот код, стек вызовов пуст. После этого происходит следующее:


Стек вызовов в ходе выполнения программы

Каждая запись в стеке вызовов называется стековым кадром .

На механизме анализа стековых кадров основана информация о стеке вызовов, трассировка стека, выдаваемая при возникновении исключения. Трассировка стека представляет собой состояние стека в момент исключения. Взгляните на следующий код:

Function foo() { throw new Error("SessionStack will help you resolve crashes:)"); } function bar() { foo(); } function start() { bar(); } start();
Если выполнить это в Chrome (предполагается, что код находится в файле foo.js), мы увидим следующие сведения о стеке:

Трассировка стека после возникновения ошибки

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

Function foo() { foo(); } foo();
Когда движок приступает к выполнению этого кода, всё начинается с вызова функции foo . Это - рекурсивная функция, которая не содержит условия прекращения рекурсии. Она бесконтрольно вызывает сама себя. В результате на каждом шаге выполнения в стек вызовов снова и снова добавляется информация об одной и той же функции. Выглядит это примерно так:


Переполнение стека

В определённый момент, однако, объём данных о вызовах функции превысит размер стека вызовов и браузер решит вмешаться, выдав ошибку:

Превышение максимального размера стека вызовов

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

Однако, и у исполнения кода в однопоточном режиме тоже есть определённые ограничения. Учитывая то, что у JavaScript имеется один стек вызовов, поговорим о том, что происходит, когда программа «тормозит».

Параллельное выполнение кода и цикл событий

Что происходит, когда в стеке вызовов имеется функция, на выполнение которой нужно очень много времени? Например, представьте, что вам надо выполнить какое-то сложно преобразование изображения с помощью JavaScript в браузере.

«А в чём тут проблема?», - спросите вы. Проблема заключается в том, что до тех пор, пока в стеке вызовов имеется выполняющаяся функция, браузер не может выполнять другие задачи - он оказывается заблокированным. Это означает, что браузер не может выводить ничего на экран, не может выполнять другой код. Он просто останавливается. Подобные эффекты, например, несовместимы с интерактивными интерфейсами.

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


Браузер предлагает завершить выполнение страницы

Пользователям подобные вещи точно не понравятся.

Итак, как же выполнять тяжёлые вычисления, не блокируя пользовательский интерфейс и не подвешивая браузер? Решение этой проблемы заключается в использовании асинхронных функций обратного вызова. Это - тема для отдельного разговора.

Итоги

Мы, в общих чертах, рассмотрели устройство JS-движка, механизмов времени выполнения и стека вызовов. Понимание изложенных здесь концепций позволяет улучшить качество кода.

Уважаемые читатели! Этот материал - первый в серии «How JavaScript Works» из блога SessionStack . Уже опубликован второй - посвящённый особенностям V8 и техникам оптимизации кода. Как по-вашему, стоит ли его переводить?

JavaScript Stack from Scratch

Это русскоязычная версия руководства Джонатана Верекии (@verekia). Оригинальное руководство расположено . Данное пособие постоянно развивается и дополняется автором, предоставляя читателям максимально свежую и качественную информацию. Текст оригинального пособия и прилагаемый код будут меняться с течением времени. Мы так же будем стараться поддерживать русскоязычную версию в актуальном состоянии. Данный перевод соответствует английской версии по состоянию на . Мы будем рады вашим

Добро пожаловать в мое современное руководство по стеку технологий JavaScript: Стек технологий JavaScript с нуля

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

Конечно, вам не нужны все эти технологии, если вы делаете простую веб страницу с парой JS функций (комбинации Browserify / Webpack + Babel + jQuery достаточно, чтобы написать ES6 код в нескольких файлах и скомпилировать все через командную строку), но если вы собираетесь создать масштабируемое веб приложение, и вам нужно все правильно настроить, то это руководство отлично вам подходит.

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

В большой части технологий, описываемых здесь, используется React. Если вы только начинаете использовать React и просто хотите изучить его, то поможет вам и кратко ознакомит с инфраструктурой React на основе предустановленной конфигурации. Я бы, например, порекомендовал такой подход для тех, кому нужно влиться в команду, использующую React, и на чем-то потренироваться, чтобы подтянуть свои знания. В этом руководстве мы не будем пользоваться предустановленными конфигурациями, поскольку я хочу, чтобы вы полностью понимали все, что происходит "под капотом".

Примеры кода имеются в каждой части, и вы можете запускать их через yarn && yarn start или npm install && npm start . Я рекомендую писать все с нуля самостоятельно, следуя пошаговым инструкциям каждого раздела.

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

Примечание: Порядок частей не всегда обязателен. К примеру, тестирование / типизация могут быть выполнены до введения в React. Довольно сложно перемещать или редактировать опубликованные разделы, поскольку приходится вносить изменения во все следующие за ними части. Возможно, когда все определится, я приведу всю документацию к более удобному виду.

Код, приведенный в примерах, работает под Linux, macOS и Windows.

Стек вызова функций (Call stack in programming languages)

Как рассматривать стек вызова функции (Call Stack) в контексте языков программирования. Хочу сказать, что стек вызова не совсем то, что демонстрирует нам большинство материалов. Чтобы окончательно понять что такое стек вызова, необходимо вернуться к азам и рассмотреть как работает стек вызова в операционной системе, почему именно так? Потому что нельзя рассматривать стек вызова функций оторванно от физической памяти и возможностью управления ею и тем самым не надо забывать, что стек вызова функций это не «виртуальная» операция, а конкретное место в памяти выделяемое операционной системой для выполнение программного кода функции, которая была запущена внутренним или внешним API (будь-то браузера, IDE или движка какого либо языка).

Хочу определить что такое стека вызова функции в низкоуровневых (low-level programming languages) языках (Assembler, C++):

На примере Ci++:

(В современных низкоуровневых языках есть современные библиотеки позволяющие поддерживать работу ООП и контролировать стек функции не на физическом уровне, а использованием вывода консоли: Poppy, Pantheios)

Шаг 1: запуск лейаута кода.

В лейауте кроме самих инструкций находится дополнительная информация (переменные, декларативные данные, дополнительные технические данные и …)

Получив первую инструкцию о запуске ОС начинает строить стек. Сначала выделяет физическое место на кластерах памяти и заполняет его данными, но какими:

  1. Первый вызов какой либо программной инструкции происходит от модуля ОС, этот запуск так и получил название уровень модуля (это первая инструкция, которая начинает свою работу с модуля памяти ОС) (далее приведено стандартное название в языках main());

Инструкция получена, создается сам стек в котором основная часть - выделенное место для выполнения операций, а сегмент стека для хранения данных для возврата результата выполнения функции.

На рисунке 1 показаны стеки (участки памяти) предназначенные для выполнения кода для каждой функции, с правой стороны находится сегмент, куда отправляются инструкции возврата в порядке вызова функций.

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

Инструкция возврата вызова, это не место вызова этой функции, а следующая за ней инструкция.

Сегмент стека (который мы сейчас по простоте называем стеком вызова) это отдельный участок памяти, в котором храняться инструкции о возврате результатов вызванных функции. Используется метод: кто пришел последний - уйдет первый (LIFO).

В языках подобных С++ имплементированы два метода: Call и Ret, Call создает инструкцию с адресом(в бинарной нотации) и кладет на верхний уровень стека, Ret не содержит адреса, этот метод просто забирает верхнюю инструкцию и возвращает нас по указанному адресу. Сам стек, после возвращения данных и выполнения функций уничтожается и данные из него недоступны.

На рисунке 2 схематично нарисован сегмент стека вызова функций и указаны локальные переменные и инструкции по возврату.

Далее необходимо понять, как высокоуровневые языки реализовывают работу стека вызова. Давайте рассмотрим два случая: языки использующие многопоточность и однопоточные языки. Почему именно такие варианты? Потому что именно многопоточность (multithreading) и однопоточность определяет взаимодействие виртуальной и физической памяти ресурса.

Языки использующие многопоточность рассмотрим на примере Java.

На рис 3 приведена простая схема распределения динамической памяти. (Здесь тоже хочу сказать, что динамическая память также использует физические кластеры, то есть общее представление стека вызовов будет таким же как у низкоуровневых языков), на этом рисунке еще более схематично динамическая память при вызове функции.

В движок java имплементирована специальная коллекция Stack, у которой есть два метода push(добавит данные) и pop(удалить данные). Виртуальная машина Javaведет запись всех вызванных функций и с каждым вызовом функции создает StackTraceElement. После завершения выполнения функции StackTraceElement уничтожается, поэтому информация о стеке вызовов всегда актуальная. С Помощью метода getMethodName можно всегда получить информацию о методе верхнего элемента StackTraceElement, а значит о методе вызова функции.

На рисунке представлен стек вызова с переменными и методами объектов, реализованных в Java

Javascript: Javascript однопоточный скриптовый язык использующий очередь функций обратного вызова(callback functions queue) с методом FIFO (первый пришел - первый уйдешь).

Два варианта взаимодействия физической и виртуальной памяти в Javascript:

  1. Работа JS в браузере (цикл событий - event loop с очередью исполняемых колбэков). За переполнением стека (опять же того сегмента с информацией о адресе возврата) тут следит как ОС так и API браузера и V8 двигатель JS. У API Google Chrome есть метод, который после 16 тыс записей стек-кадров (stack frames) выдает сообщение о переполнении стека. после этого браузер выводит сообщение об ошибке или невозможности получить данные для возврата функции и очистки стека. (Источник: ;)

Сам стек как и базовые использует LIFO, а очередь колбэков - FIFO. Так же в V8 имплементированы методы push и pop, декларация идентична методам, реализованным в Java.

Таймеры в JS

Методы Javascript использующие таймеры использующие API OS и таймеры которые работают на API браузера. Таймеры всегда передвигают выполнение функции-колбэка в конец стека вызовов. Что это значит? Это значит, что в браузере выполняя методы setTimeout, setInterva сначала запускается таймер, а лишь потом функция-колбэк попадает в очередь ожидания. Если в момент попадания колбэка стек переполнен, колбэку приходится ждать своей очереди. Таймеры запущенные вне браузера реализованы на основании либо внутренних методов V8 либо при обращении к API окружения (API OS).

Особенностью языка Javascript является технология ajax или иначе возможность отправки асинхронных запросов (XMLHTTPRequest) и выполнения каких либо операций без задержки выполнения основного кода. XMLHTTPRequest может быть отправлен синхронно, но при этом весь остальной код будет дожидаться либо результата отправки запроса, что порой нецелесообразно. Технология ajax реализована исключительно в браузерах, что делает выделяет Javascript из группы языков использующих async. Асинхронные запросы в JS используют цикл событий с очередью колбэков (event loop with callback queue). В остальных языках программирования тоже есть возможность запуска асинхронного кода (Python (asyncio), C#(async/await)), что позволяет выполнять какие-то операции без задержки выполнения основных функций, принципом тут тоже выступает цикл событий, где функция колбэк помещается в очередь ожидания. Принцип работы очереди - FIFO.

What You"ll Be Creating

Две из наиболее часто используемых структур данных в веб-разработке - это стеки и очереди. Многие пользователи Интернета, в том числе веб-разработчики, не знают об этом удивительном факте. Если вы один из этих разработчиков, тогда подготовьтесь к двум просветительским примерам: операция «отменить» текстового редактора использует стек для организации данных; Цикл событий веб-браузера, который обрабатывает события (клики и т.д.), использует очередь для обработки данных.

Теперь остановитесь на мгновение и представьте, сколько раз мы, как пользователь и разработчик, используем стеки и очереди. Это потрясающе, не так ли? Из-за их повсеместности и сходства в дизайне я решил использовать их для ознакомления со структурами данных.

Стек

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

Этот последовательный порядок обычно описывается как стопка блюд в столовой. Когда тарелку добавляют в стопку блюд, она сохраняет порядок, когда она была добавлена; кроме того, когда добавляется другая тарелка, ее подталкивают к нижней части стопки. Каждый раз, когда мы добавляем новую тарелку, тарелка подталкивается к нижней части стопки, но она также представляет собой верхнюю часть стопки тарелок.

Этот процесс добавления тарелок сохранит последовательный порядок, когда каждая тарелка была добавлена в стек. Удаление тарелок из стека также сохранит последовательный порядок всех тарелок. Если тарелка удаляется из верхней части стека, каждая другая тарелка в стеке по-прежнему сохраняет правильный порядок в стеке. То, что я описываю, возможно, слишком выглядит чересчур подробно, но смысл заключается в том, как тарелки добавляются и удаляются в большинстве кафетерий!

Чтобы предоставить более технический пример стека, вспомним операцию «отменить» текстового редактора. Каждый раз, когда текст добавляется в текстовый редактор, этот текст помещается в стек. Первое дополнение к текстовому редактору представляет собой нижнюю часть стека; Последнее изменение представляет собой верхнюю часть стека. Если пользователь хочет отменить последнее изменение, верхняя часть стека будет удалена. Этот процесс можно повторить до тех пор, пока в стек не будет добавлено больше дополнений, это пустой файл!

Операции стека

Поскольку теперь у нас есть концептуальная модель стека, определим две операции стека:

  • push(data) добавляет данные.
  • pop() удаляет последние добавленные данные.

Реализация стека

Теперь давайте напишем код для стека!

Свойства стека

Для нашей реализации мы создадим конструктор Stack . Каждый экземпляр Stack будет иметь два свойства: _size и _storage .

Function Stack() { this._size = 0; this._storage = {}; }

this._storage позволяет каждому экземпляру Stack иметь собственный контейнер для хранения данных; this._size отражает количество попыток передачи данных в текущую версию Stack . Если создается новый экземпляр Stack и данные помещаются в его хранилище, то this._size будет увеличиваться на 1. Если данные снова вставляются в стек, this._size будет увеличиваться до 2. Если данные удаляются из стека, то this._size будет уменьшаться до 1.

Методы стека

Нам нужно определить методы, которые могут добавлять (push) и удалять (pop) данные из стека. Начнем с добавления данных.

Метод 1 из 2: push(data)

(Этот метод можно использовать для всех экземпляров Stack , поэтому мы добавим его в прототип Stack .)

У нас есть два требования к этому методу:

  1. Каждый раз, когда мы добавляем данные, мы хотим увеличить размер нашего стека.
  2. Каждый раз, когда мы добавляем данные, мы хотим сохранить порядок, в котором он был добавлен.
Stack.prototype.push = function(data) { // increases the size of our storage var size = this._size++; // assigns size as a key of storage // assigns data as the value of this key this._storage = data; };

Наше реализация push(data) включает в себя следующую логику. Объявите переменную с именем size и присвойте ей значение this._size ++ . Определите size в качестве ключа this._storage . И определите data как значение соответствующего ключа.

Если бы наш стек вызывал push(data) пять раз, тогда размер нашего стека был бы 5. Первое добавление данных в стек присвоит этим данным ключ из 1 в этом._storage . Пятый вызов push(data) присваивает этим данным ключ из 5 в this._storage . Мы только что присвоили порядок нашим данным!

Метод 2 из 2: pop()

Теперь мы можем передавать данные в стек; Следующий логический шаг - выталкивание (удаление) данных из стека. Забор данных из стека - это не просто удаление данных; Он удаляет только самые последние добавленные данные.

Вот наши цели для этого метода:

  1. Используйте текущий размер стека, чтобы получить самые последние добавленные данные.
  2. Удалите последние добавленные данные.
  3. Уменьшение _this._size на единицу.
  4. Верните последние удаленные данные.
Stack.prototype.pop = function() { var size = this._size, deletedData; deletedData = this._storage; delete this._storage; this.size--; return deletedData; };

pop() соответствует каждой из наших четырех целей. Сначала мы объявляем две переменные: size инициализируется размером стека, а deletedData присваивается последним данным, добавленным в стек. Во-вторых, мы удаляем пару ключ-значение наших последних добавленных данных. В-третьих, мы уменьшаем размер стека на 1. В-четвертых, мы возвращаем данные, которые были удалены из стека.

Если мы протестируем нашу текущую реализацию pop() , мы обнаружим, что она работает для следующего прецедента. Если мы вставляем данные push(data) в стек, размер стека увеличивается на единицу. Если мы достаем pop() данные из нашего стека, размер нашего стека уменьшается на единицу.

Однако возникает проблема, когда мы меняем порядок вызова. Рассмотрим следующий сценарий: мы вызываем pop() , а затем делаем push(data) . Размер нашего стека меняется на -1, а затем на 0. Но правильный размер нашего стека равен 1!

Чтобы обработать этот вариант использования, мы добавим оператор if в pop() .

Stack.prototype.pop = function() { var size = this._size, deletedData; if (size) { deletedData = this._storage; delete this._storage; this._size--; return deletedData; } };

С добавлением нашего оператора if тело нашего кода выполняется только при наличии данных в нашем хранилище.

Полная реализация стека

Наша реализация Stack завершена. Независимо от порядка, в котором мы ссылаемся на любой из наших методов, наш код работает! Вот окончательная версия нашего кода:

Function Stack() { this._size = 0; this._storage = {}; } Stack.prototype.push = function(data) { var size = ++this._size; this._storage = data; }; Stack.prototype.pop = function() { var size = this._size, deletedData; if (size) { deletedData = this._storage; delete this._storage; this._size--; return deletedData; } };

От стека к очереди

Стек полезен, когда мы хотим добавить данные в последовательном порядке и удалить эти данные. Основываясь на своем определении, стек может удалить только самые последние добавленные данные. Что произойдет, если мы хотим удалить самые старые данные? Мы хотим использовать структуру данных под названием очередь.

Очередь

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

Чтобы помочь вам концептуализировать, как это будет работать, давайте возьмем минутку, чтобы использовать аналогию. Представьте себе, что очередь очень похожа на систему билетов в гастроном. Каждый клиент берет билет и обслуживается при вызове их номера. Сначала нужно обслуживать клиента, который берет первый билет.

Давайте далее предположим, что этот билет имеет номер «один», отображаемый на нем. Следующий билет имеет номер «два», отображаемый на нем. Клиент, который берет второй билет, будет обслуживаться вторым. (Если бы наша система билетов работала как стек, то клиент, который первым вступил в стек, был бы последним, который будет обслуживаться!)

Более практичным примером очереди является цикл событий веб-браузера. По мере запуска различных событий, таких как щелчок кнопки, они добавляются в очередь цикла событий и обрабатываются в том порядке, в котором они попали в очередь.

Операции очереди

Поскольку теперь у нас есть концептуальная модель очереди, определим ее операции. Как вы заметили, операции очереди очень похожи на стек. Разница заключается в том, где данные удаляются.

  • enqueue(data) добавляет данные в очередь.
  • dequeue удаляет самые старые добавленные данные в очередь.

Реализация очереди

Теперь давайте напишем код для очереди!

Свойства очереди

Для нашей реализации мы создадим конструктор с именем Queue . Затем мы добавим три свойства: _oldestIndex , _newestIndex и _storage . Потребность в _oldestIndex и _newestIndex станет более понятной в следующем разделе.

Function Queue() { this._oldestIndex = 1; this._newestIndex = 1; this._storage = {}; }

Методы очереди

Теперь мы создадим три метода, разделяемых между всеми экземплярами очереди: size() , enqueue(data) и dequeue(data) . Я опишу цели для каждого метода, покажу код для каждого метода, а затем объясню код для каждого метода.

Метод 1 из 3: size()

У нас есть две цели для этого метода:

  1. Вернуть правильный размер для очереди.
  2. Сохранить правильный диапазон ключей для очереди.
Queue.prototype.size = function() { return this._newestIndex - this._oldestIndex; };

Реализация size() может показаться тривиальной, но вы быстро обнаружите, что это не так. Чтобы понять, почему так, мы должны быстро вернуться к тому, как размер был реализован для size .

Используя нашу концептуальную модель стека, давайте представим, что мы добавляем пять тарелок в стек. Размер нашего стека равен пяти, и каждая тарелка имеет число от одного (первая добавленная тарелка) до пяти (последняя добавленная тарелка). Если мы удалим три тарелки, то у нас будет две тарелки. Мы можем просто вычесть три из пяти, чтобы получить правильный размер, который равен двум. Вот наиболее важный вопрос о размере стека: Текущий размер представляет собой правильный ключ, связанный с тарелкой наверху стека (2), а другой - в стеке (1). Другими словами, диапазон ключей всегда от текущего размера до 1.

Теперь давайте применим эту реализацию size стека к нашей очереди. Представьте, что пять клиентов берут билет из нашей системы билетов. У первого клиента есть билет, показывающий номер 1, а пятый клиент имеет билет с номером 5. С очередью сначала обслуживается клиент с первым билетом.

Давайте теперь представим, что первый клиент обслуживается и этот билет удаляется из очереди. Подобно стеку, мы можем получить правильный размер нашей очереди, вычитая 1 из 5. В нашей очереди в настоящее время есть четыре небронированных билета. Теперь возникает проблема: размер больше не соответствует правильным номерам билетов. Если мы просто вычтем один из пяти, мы будем иметь размер 4. Мы не можем использовать 4 для определения текущего диапазона оставшихся билетов в очереди. Есть ли у нас билеты в очереди с номерами от 1 до 4 или от 2 до 5? Ответ непонятен.

Это преимущество наличия следующих двух свойств в очереди: _oldestIndex и _newestIndex . Все это может показаться запутанным - меня все еще иногда они путают. Что помогает мне рационализировать все, это следующий пример, который я разработал.

Представьте, что в нашем гастрономе есть две системы продажи билетов:

  1. _newestIndex представляет собой билет из системы билетов клиентов.
  2. _oldestIndex представляет собой билет из системы билетов для сотрудников.

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

  1. Клиент берет билет. Номер билета клиента, который извлекается из _newestIndex , равен 1. Следующий билет, доступный в системе билета клиента, - 2.
  2. Сотрудник не берет билет, а текущий билет в системе билета сотрудника равен 1.
  3. Мы берем текущий номер билета в системе клиента (2) и вычитаем номер в системе сотрудников (1), чтобы получить номер 1. Число 1 представляет количество билетов, все еще находящихся в очереди, которые не были удалены.
  4. Сотрудник берет билет из своей системы продажи билетов. Этот билет представляет собой билет клиента, который обслуживается. Билет, который был получен, извлекается из _oldestIndex , который отображает номер 1.
  5. Мы повторяем шаг 4, и теперь разница равна нулю - в очереди больше нет билетов!

Теперь у нас есть свойство (_newestIndex), которое может указать нам наибольшее число (ключ), назначенное в очереди, и свойство (_oldestIndex), которое может рассказать нам самый старший номер индекса (ключа) в очереди.

Мы достаточно изучили size() , поэтому перейдем теперь к enqueue(data) .

Метод 2 из 3: enqueue (data)

Для enqueue у нас есть две цели:

  1. Используйте _newestIndex в качестве ключа this._storage и используйте любые данные, добавляемые в качестве значения этого ключа.
  2. Увеличьте значение _newestIndex на 1.

На основе этих двух целей мы создадим следующую реализацию enqueue(data) :

Queue.prototype.enqueue = function(data) { this._storage = data; this._newestIndex++; };

Тело этого метода содержит две строчки кода. В первой строке мы используем this._newestIndex для создания нового ключа для this._storage и назначения ему data . this._newestIndex всегда начинается с 1. На второй строке кода мы увеличиваем значение this._newestIndex на 1, которое обновляет его значение до 2.

Это весь код, который нам нужен для enqueue(data) . Давайте теперь реализуем dequeue() .

Метод 3 из 3: dequeue()

Вот цели для этого метода:

  1. Удалить самые старые данные в очереди.
  2. Увеличить _oldestIndex на 1.
Queue.prototype.dequeue = function() { var oldestIndex = this._oldestIndex, deletedData = this._storage; delete this._storage; this._oldestIndex++; return deletedData; };

В теле dequeue() мы объявляем две переменные. Первой переменной oldestIndex присваивается текущее значение очереди для this._oldestIndex . Второй переменной, deletedData , присваивается значение, содержащееся в this._storage .

Затем мы удаляем самый старый индекс в очереди. После его удаления мы увеличиваем значение this._oldestIndex на 1. Наконец, мы возвращаем данные, которые мы только что удалили.

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

Queue.prototype.dequeue = function() { var oldestIndex = this._oldestIndex, newestIndex = this._newestIndex, deletedData; if (oldestIndex !== newestIndex) { deletedData = this._storage; delete this._storage; this._oldestIndex++; return deletedData; } };

Всякий раз, когда значения oldestIndex и newestIndex не равны, мы выполняем ранее имевшуюся логику.

Полная реализация очереди

Наша реализация очереди завершена. Давайте рассмотрим весь код.

Function Queue() { this._oldestIndex = 1; this._newestIndex = 1; this._storage = {}; } Queue.prototype.size = function() { return this._newestIndex - this._oldestIndex; }; Queue.prototype.enqueue = function(data) { this._storage = data; this._newestIndex++; }; Queue.prototype.dequeue = function() { var oldestIndex = this._oldestIndex, newestIndex = this._newestIndex, deletedData; if (oldestIndex !== newestIndex) { deletedData = this._storage; delete this._storage; this._oldestIndex++; return deletedData; } };

Заключение

Cho is a full-stack web-application developer. He dislikes mean people but likes the MEAN stack (MongoDB, ExpressJS, AngularJS, Node.js). During a typical week, he"ll be coding in JavaScript, writing about JavaScript, or watching movies NOT about JavaScript.

Итак, у вас и у вашего партнера появилась замечательная бизнес-идея. Верно?

Вы постоянно добавляете в уме все новые и новые возможности.

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

Окей, значит людям это нужно. На этом можно даже заработать денег. И единственная причина, по которой люди до сих пор этим не пользуются: вы не реализовали свою идею. Пока не реализовали.

И наконец, в один прекрасный день вы решили: “Сделаем это!”. И вот вы уже пытаетесь разобраться как реализовать бизнес-логику своего приложения, ту киллер-фичу, которая будет двигать продукт вперед. У вас есть идея как это сделать, и вы знаете, что способны на это.

И вот вы говорите: “Готово! Работает!” У вас есть успешный прототип! Осталось только упаковать его в веб приложение.

“Окей, сделаем сайт,” говорите вы.

А только потом вы понимаете, что для этого нужно выбрать язык программирования; нужно выбрать (современную) платформу; нужно выбрать какие-то (современные) фреймворки; нужно настроить (и купить) хранилище, базы данных и хостинг; нужно обеспечить интерфейс для администрирования; нужно обеспечить контроль доступа и систему управления контентом. Вы хотите быть бережливым (lean) и гибким (agile). Вы хотите использовать технологии, которые помогут вам быть успешным как в краткосрочной, так и в долгосрочной перспективе. А выбрать их далеко не всегда так просто.

Перед вами десятки и десятки архитектурных решений, которые необходимо принять. И вы не хотите ошибиться: требуются технологии, которые позволят вести быструю разработку, поддерживают постоянные итерации, максимальную эффективность, скорость, устойчивость и многое другое. Вы хотите быть бережливым (lean) и гибким (agile). Вы хотите использовать технологии, которые помогут вам быть успешным как в краткосрочной, так и в долгосрочной перспективе. А выбрать их далеко не всегда так просто.

“Я перегружен”, говорите вы и чувствуете себя перегруженным. Энергия уже не та, что была в начале. Вы пытаетесь собраться с мыслями, но работы слишком много.

Прототип медленно блекнет и умирает.

Предложение

После того, как я забросил кучу идей по похожим причинам, я решил спроектировать решение для этой проблемы. Я назвал этот проект ‘Init ’ (или init.js).

Основная идея – использовать один проект для старта любого проекта, дать возможность разработчику или техническому руководителю принять все основные решения за раз и получить подходящий начальный шаблон, основанный на них. Я знаю, многие скажут “Нельзя применить одно решение ко всем проблемам” (haters gonna hate). И они, возможно, правы. Но мы можем постараться создать подходящее в целом решение, и, на мой взгляд, Init с этой задачей справился.

Чтобы достичь этой цели, необходимо принять во внимание несколько важных моментов. При разработке Init я сделал упор на следующем:

    Компоненты

    Компонентное представление – одна из ключевых характеристик любой системы, так как она позволяет повторно использовать компоненты программного обеспечения в нескольких проектах, что является основной целью Init. Но компонентное представление содержит в себе побочный эффект – заменяемость, которая станет нашим основным союзником в борьбе с различными проблемами, решение которых “почти” одинаково.

    Простота разработки

    Какая-то проблема где-нибудь имеет решение, лучше всего реализованное на Brainf*ck . будет практически невозможной для написания, не говоря уже о чтении. Это будет стоить вам времени и громадных усилий. В целом, вы должны использовать языки и платформы, которые упрощают, а не усложняют разработку для вас (и тех, кто будет делать ее позже).

    Сообщество

    Какую бы платформу вы не выбрали, убедитесь, что вокруг нее существует большое сообщество. Такое, которое может помочь вам с большинством стандартных и нестандартных проблем. Помните: jQuery, возможно, не самая быстрая , чистая , и элегантная библиотека, но она – победитель, благодаря своему сообществу .

Я покажу как принимал решения при создании Init, не забывая про эти цели.

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

Но эти идеи не были новыми, почему же они стали так популярны с приходом Node.js? Простое неблокирующее программирование достигается несколькими способами. Пожалуй, самый простой это использовать обратные вызовы (callbacks) и цикл событий –- event loop . В большинстве языков это непростая задача: если обратные вызовы это довольно распространенная функция, то цикл событий – нет, и в какой-то момент вы оказываетесь в схватке с внешними библиотеками (например: Python, с Tornado). Но в JavaScript обратные вызовы это часть языка, как и цикл событий, и каждый программист, который хотя бы пробовал JavaScript, знаком с ними (или как минимум использовал их, даже если не до конца понимал что такое event loop).

Внезапно, любой стартап на Земле может повторно использовать разработчиков (читай: ресурсы) и на клиентской и на серверной стороне, решая кадровую проблему , “Нам нужен гуру Питона”.

Да, альтернативы JavaScript’у рождаются каждый день, например, CoffeeScript , TypeScript и миллионы языков, которые компилируются в JavaScript . Эти альтернативы могут быть полезными на этапах разработки (), но им не удастся заменить JavaScript в долгосрочной перспективе по двум причинам: их сообщества никогда не станут больше, и их лучшие возможности будут реализованы в ECMA Script (читай: JavaScript). JavaScript это не язык ассемблера, это высокоуровневый язык программирования с исходных кодом, который вы можете понять, так что вы должны понять его.

К сожалению, вынужден признаться, что у меня очень мало опыта с Angular.js, так что я исключу его из этой дискуссии. Итак, Ember.js и Backbone.js представляют собой два разных пути для решения одной проблемы.

Минимален, прост и предлагает вам необходимый минимальный набор для написания простого SPA. Ember.js же - это полноценный и профессиональный фреймворк для создания одностраничных приложений. В нем больше возможностей, но и кривая обучаемости круче.

В зависимости от размера приложения, решение может быть таким же простым, как анализ отношения “используемые функции / доступные функции”. Оно даст вам хорошую подсказку.

В случае с Init, я хотел покрыть большинство сценариев, поэтому выбрал Backbone.js для простого создания SPA, с Backbone.Marionette.View для компонентизации. В такой схеме каждый компонент - это простое приложение, а конечный продукт может быть настолько комплексным насколько я захочу.

Стилизация – это тоже большая задача, но мы в очередной раз можем рассчитывать на фреймворки. Для CSS нет ничего лучше