Значения и ссылки
Примитивы (строки, числа, булевы значения, null/undefined) при присваивании переменных копируются целиком или, как говорят, по значению.
var message = "Привет!";
var phrase = message;
В результате получили две независимые переменные, каждая из которых хранит значение "Привет!". Изменение message
никак не повлияет на phrase
Объекты (в том числе массивы и функции) копируются по ссылке.
В переменной, которой присвоен объект, хранится не сам объект, а «адрес его места в памяти», иными словами – «ссылка» на него.
Если есть две переменные с одной и той же функцией - в них не лежит копия этой функции, а обе эти переменные ссылаются на одну и ту же функцию:
function func() {
alert('!');
});
var test = func; //И test и func указывают на одну и ту же функцию
Ссылки
- learn.javascript.ru
- TOП-12 JavaScript-концепций: от ссылок до асинхронных операций
- Передача параметров по значению и по ссылке
- habr - Функции в Javascript: ссылки и вызовы
Замыкания
- v1 - Функция, содержащая в себе ссылки на переменные из внешней области видимости
- v2 - Функция, получающая переменные из родительской области видимости.
- v3 - Функция вместе со всеми внешними переменными, которые ей доступны.
- v4 - Комбинация функции и лексического окружения, в котором эта функция была объявлена.
- v5 - Способность функции запоминать контекст (т.е. LexicalEnvironment), в которой она была создана.
Обычно под замыкание понимают приём, который позволяет вызывать несколько экземпляров одной функции, и в каждом запомнить собственное значение внутренних переменных. Например - разные счёчики. Получаем эдакую "фабрику функций"
Описание 1
- Внутри функции создадим переменную Х и вторую функцию (назовём её вложенной).
- Вложенная функция использует внутри себя переменную Х.
- Родительская функция возвращает вложенную функцию в качестве результата своей работы (через return)
- Теперь, создавая разные экземпляры родительской функции получаем вложенные функции, каждая из которых помнит своё собственное значение Х
Описание 2
Вложенная функция запоминила значение Х из области своего создания (родительской функции).
Её вызывают уже вне этой области, а она возвращает это Х.
Т.е. она "замыкает" внешние переменные в себе.
Описание 3
Ты вызвал функцию - в ней создаются переменные локальной области видимости (т.е. доступные только самой функции).
Под эти переменные движок JavaScript выделяет память.
Когда обычная функция завершает свое выполнение - она освобождает память, которую ей выделели.
Если, конечно, на переменные не осталось ссылок.
В случае с замыканием, ты возвращаешь функцию обратно (делаешь return), т.е. ссылки остаются.
Поэтому движок не может освободить память, и переменные остаются доступными функции (и более никому).
Эта штука и называется "замыкание" - переменные замкнуты на саму функцию.
Другими словами, чтобы создать замыкание, ты должен вложить функцию в функцию, обратиться из вложенной функции к переменным оборачивающей, и вложенную функцию вернуть наружу. До тех пор, пока возвращенная функция остается в доступе, замыкание существует.
Описание 4
Замыкание — это особый вид функции.
Она определена в теле другой функции и создаётся каждый раз во время её выполнения. Синтаксически это выглядит как функция, находящаяся целиком в теле другой функции.
При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции.
Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.
Описание 5
- есть функция fA
- внутри неё есть переменная X и другая функция fB
- функция fB использует эту переменную X
- функция fA возвращает в return функцию fB. (именно функцию, а не результат её работы.
return fB;
) - каждый запуск fA созадёт новую функция с замыканием, которая хранит своё значение X.
var one=fA(); var two=fA();
function fA() {
var currentCount = 1;
function fB() { // (**)
return currentCount++;
};
return fB;
}
var counter = fA(); // (*)
console.log( counter() ); // 1
console.log( counter() ); // 2
console.log( counter() ); // 3
// создаём другой счётчик, он будет независим от первого
var counter2 = fA();
console.log( counter2() ); // 1
Что происходит:
- В строке (*) запускается fA(). Создаётся LexicalEnvironment для переменных текущего вызова. В функции есть одна переменная var currentCount, которая станет свойством этого объекта. Она изначально инициализуется в undefined, затем, в процессе выполнения, получит значение 1.
- В процессе выполнения fA() создаёт функцию fB в строке (**). При создании эта функция получает внутреннее свойство Scope со ссылкой на текущий LexicalEnvironment.
- Далее вызов fA() завершается и функция (**) возвращается и сохраняется во внешней переменной counter (*). Итоговым значением, записанным в переменную counter, является функция
function() { return currentCount++; };
, а её Scope= currentCount: 1 - Возвращённая из fA() функция counter помнит (через Scope) о том, в каком окружении была создана. Это и используется для хранения текущего значения счётчика.
- Когда-нибудь функция counter будет вызвана. Эта функция состоит из одной строки:
return currentCount++
. Своих переменных и параметров в ней нет, поэтому её Lexical Environment пуст. - Но, у неё есть свойство Scope - оно указывает на внешнее окружение. Чтобы увеличить и вернуть currentCount, интерпретатор ищет в текущем объекте переменных Lexical Environment, не находит, затем идёт во внешний объект, там находит, изменяет и возвращает новое значение. Как изменяет? Переменную во внешней области видимости можно не только читать, но и изменять.
- В примере было создано несколько счётчиков. Все они взаимно независимы, потому что при каждом запуске fA() создаётся свой объект переменных LexicalEnvironment, со своим свойством currentCount, на который новый счётчик получит ссылку Scope.
Пример замыкания через анонимную самовыполняющуюся функцию
var fn = (
function() {
var numberOfCalls = 0;
return function() {
return ++ numberOfCalls;
}
}
)();
Пример замыкания через стрелочные функции
const add = x => y => {
const z = x + y;
console.log(x + '+' + y + '=' + z);
return z;
};
const res = add(3)(6); // вернёт 9 и выведет в консоль 3+6=9
console.log(res);
Пояснение:
- В переменную add помещается функция от аргумента x,
- результатом которой будет являться другая функция, а именно функция от аргумента y,
- результат которой вычисляется приведённым в фигурных скобках блоком кода.
- Этот блок кода опирается на аргумент y своей функции и на замыкание, создаваемое для аргумента x внешней функции.
При вызове add(3)(6):
- функция, хранящаяся в переменной add, вызывается с аргументом 3
- и возвращает функцию, завязанную на значение 3 в замыкании x.
- в рамках такого обращения, эта функция выполняется с аргументом y = 6 и возвращает 9.
Зачем используются замыкания?
- ограничение доступа к данным, их изоляция (ограничение их области видимости)
- автономное атомарное хранилища данных + доступ к этим данным. Т.е. привязать к функции данные, сохраняющиеся между ее вызовами. Создавать функции, имеющие своё изменяемое состояние.
- создание функций, в свою очередь тоже создающих функции. Благодаря замыканию возвращаемая внутренняя функция «запоминает» параметры, переданные внешней функции.
- обработка callback
- Эмуляция private методов функций. Языки вроде Java позволяют нам объявлять private методы - они могут быть вызваны только методами того же класса, в котором объявлены. Частные методы не ограничивают доступ к коду, это также мощное средство глобальной организации пространства имен, позволяющее не засорять публичный интерфейс вашего кода внутренними методами классов. JS так не умеет, но это можно эмулировать с помощью замыкания.
«Понимать замыкания» в JS означает:
- Все переменные и параметры функций = свойства объекта LexicalEnvironment. Каждый запуск функции создает новый такой объект. На верхнем уровне им является «глобальный объект» (в браузере это window).
- При создании функция получает свойство Scope, оно ссылается на LexicalEnvironment, в котором она была создана.
- При вызове функции – она будет искать переменные сначала внтури себя, а затем во внешних LexicalEnvironment (с места своего «рождения», которое записано в Scope в момент создания и не может быть изменено).
Lexical Environment
При запуске функции, в ней создаётся объект LexicalEnvironmen.
Все переменные внутри функции = свойства объекта LexicalEnvironment. А также аргументы функции и вложенные функции.
В конце выполнения функции объект LexicalEnvironmen обычно удаляется и память очищается. Это, если нет замыкания.
Если переменная не найдена в LexicalEnvironmen функции – она будет искаться снаружи, через ссылку в Scope.
Scope
При создании функции, в ней создаётся свойство Scope (анг. область видимости).
В нём хранится ссылка на внешний объект LexicalEnvironment, в котором создана функция. Это может быть глобальный объект (window) или другая функция.
Это свойство никогда не меняется, оно всюду следует за функцией, "привязывая" её к месту своего рождения.
Значение переменной из внешней области берётся всегда текущее. Оно может быть уже не то, что было на момент создания функции.
Функция по ссылке Scope обращается во внешний LexicalEnvironmen и берёт значение, которое там есть на момент обращения.
Разница между созданием замыкания и созданием scope-объекта:
- замыкание (функция + ссылка на текущую цепочку scope-объектов) создается при определении функции,
- новый scope-объект создается при каждом вызове функции. Используется для модификации цепочки scope-объектов замыкания
Gloabal object
Global object = частный случай объекта LexicalEnvironment
Глобальные переменные и функции - те, которые не находятся внутри какой-то функции. На "верхнем уровне".
В JS все глобальные переменные и функции = свойства объекта "global object". В браузере этот объект доступен под именем window. Присваивая или читая глобальную переменную, мы, фактически, работаем со свойствами window.
Альтернатива замыканиям
Вместо замыканий можно использовать функцию как объект.
Создать в объекте функции свойство и привязать к нему значение. Это значение будет сохраняться между вызовами функции, также как в замыкании.
Свойства функции не стоит путать с переменными и параметрами. Они совершенно никак не связаны.
Пример:
function makeCounter() {
function counter() {
return counter.currentCount++;
};
counter.currentCount = 1;
return counter;
}
var counter = makeCounter();
alert( counter() ); // 1
alert( counter() ); // 2
Принципиальная разница: – во внутренней механике
- свойство функции общедоступно, в отличие от переменной из замыкания. К свойству имеет доступ любой, у кого есть объект функции.
Разное
- Все функции в JS = замыкания. Когда создается функция — всегда создается замыкание. Хотя, чаще оно пустое - обычно функции ничего не используют из внешнего LexicalEnvironmen.
- "Замкнутые" данных сохраняются в не в "стеке" а в "куче" (такая структура в памяти JS-движка). Это позволяет сохранять данные после вызова функции - то есть даже после того, как контекст выполнения удаляется из стека выполнения вызова.
- Производительность. Не нужно без необходимости создавать функции внутри функций. Использование этой техники снижает производительность - и в скорости, и в потребления памяти.
- Замыкания позволяют связать данные (LexicalEnvironmen) с функцией, которая работает с этими данными. Также в ООП - объекты позволяют связать некоторые данные (свойства объекта) с одним или несколькими методами. Поэтому, замыкания можно использовать везде, где вы обычно использовали объект с одним единственным методом.
- Из внутренней функции мы можем доставать/менять переменные внешней функции. Это называют "брать из замыкания".
- Вроде бы, в функциональном программировании такой подход (замыкания) не приветствуется. Но знать и уметь надо, а в JS - обязательно.
- До того, как в ES6 ввели ключевое слово let, часто возникала проблема при создании замыканий внутри цикла. В паре слов: будьте внимательны с использованием var/let в циклах. Либо используем let (
for (let i = 0; i < someVariable; i++){...}
), либо реализуем замыкание в отдельной функции снаружи цикла, а в цикле просто вызываем эут функцию при каждой итерации. Или сделать ещё один уровень вложенного замыкания. Подробнее: MDN, habr - Замыкания в JavaScript
Ссылки
- learn.javascript.ru
- habr - Замыкания в JavaScript
- htmlacademy - Замыкания в JavaScript
- MDN - Замыкания
- Wikipedia
- Hexlet - Возврат функций из функций
- code.mu - Продвинутая работа с функциями
- proglib - Пора понять замыкания в JavaScript! Часть 1. Готовим фундамент
- proglib - Пора понять замыкания в JavaScript! Часть 2. Переходим к делу
Как работают движки JavaScript? //ToDo - доработать
Прежде всего, исходный код (текст на JS) проходит через парсер, в результате возникает внутреннее представление кода — абстрактное синтаксическое дерево.
Дальше работает интерпретатор. Отдельные функции при исполнении преобразуются в байт-код — по сути, последовательность вызовов внутренних функций интерпретатора. При этом накапливается статистика использования JS-функций. Если для какой-то отдельной функции преоделён порог вызовов, то принимается решение о том, что её нужно оптимизировать и она передаётся компилятору. Компилятор генерирует машинный код, который сильно завязан на типы входных значений.
Допустим, у нас есть функция с двумя аргументами: foo(a, b), и мы вызываем её много раз с числовыми значениями параметров. В некоторый момент функция будет передана компилятору и станет выполняться быстрее. Допустим, мы вызовем её со строковым аргументом. В результате, движок выполнит «деоптимизацию»: передаст функцию от компилятора обратно интерпретатору, а готовый машинный код будет выброшен.
Движок JavaScript похож на мясорубку, бесконечно перемалывающую операции, которые последовательно берутся из стека вызовов (1). Код выполняется линейно и последовательно. Удалить операцию из стека нельзя, можно только прервать поток выполнения. Поток выполнения прерывается, если вызвать что-то типа alert или «исключение».
image
Каждая операция содержит контекст — некую область памяти, из которой доступны данные. Контексты расположены в памяти в виде дерева. Каждому листу в дереве доступны области видимости, которые определены в родительских ветках и в корне (глобальной области видимости). Функции в JavaScript — это данные, они хранятся в памяти именно как данные и поэтому передаются как переменные или возвращаются из других функций.
Асинхронные операции выполняются не в движке, а в окружении (5,6). (Как подсказал forgotten это не совсем так: мы можем из стека вызовов сразу же положить функцию в очередь вызовов и таким образом чистый движок тоже будет работать асинхронно) Окружение — надстройка на движком. NodeJS и Chrome для движка V8 и Firefox для Gecko. Иногда окружение еще называют web API. Чтобы создать асинхронный вызов, в web API передается ссылка на функцию, которая выполнится позже или не выполнится вовсе.
У функции есть свой контекст или своя область памяти (3), в которой она определена. Функция имеет доступ к этой области памяти и ко всем родителям этой области памяти. Такие функции называются замыканиями. С этой точки зрения, все функции в JavaScript — замыкания, так как все они имеют контекст.
Web API и JavaScrtipt движок работают независимо. Web API решает, в какой момент функция двигается дальше, в очередь вызовов (2).
Функции в очереди вызовов попадают в JavaScript-движок, где выполняются по одной. Выполнение происходит в том же порядке, в котором функции попадают в очередь.
Окружение самостоятельно решает, когда добавить переданный ей код в очередь вызовов. Функции из очереди добавляются в стек выполнения (выполняются) не раньше, чем стек вызовов закончит работу над текущей функцией. Таким образом, стек вызовов работает синхронно, а web API асинхронно.
Это очень важно! Разработчику не нужно самому контролировать параллельный доступ к ресурсам, асинхронную работу за него выполняет окружение. Окружения определяют различия между браузером и node.js, ведь на node.js мы пишем сетевые приложения или обращаемся напрямую к жесткому диску, а из Chrome перехватываем клики по кнопкам, используя один и тот же движок.
В очереди вызовов нельзя отменять отдельные операции. Это делается в окружении (removeEventListener — в качестве примера).
Давайте проясним основную концепцию и разницу между JavaScript Engine (движок) и JavaScript Run-time Enviroment.
- Движок JavaScript - это программа, которая используется для обработки заданного кода и конвертирования его в конкретные команды для выполнения.
- JavaScript Run-time Enviroment - это среда отвечающая за создание экосистемы с возможностями, сервисами и поддержкой, такими как массивы, функции, ключевые библиотеки и тп, которые необходимы для того, чтобы код запустился верно.
Почти все бразуеры имеют JavaScript движок. Самые популярные это V8 в Google Chrome и Node.js, SpiderMonkey Мазилы, Chakra для IE и т.д. Хоть все эти браузеры и выполняют JavaScript по-разному, но под капотом, они все работают по одной модели. Стэк вызова, "Куча", event loop, Web API
Ссылки
- habr - Как работает JS (19 статей)
- habr - Знакомство с WebAssembly
- habr - Асинхронность в JavaScript: Пособие для тех, кто хочет разобраться
Асинхронность в JS //ToDo - упростить
Однопоточность
JS - однопоточный язык.
Это означает, что только один блок кода может запускаться за раз. Делает одну задачу в один момент времени
С DOM-деревом работают в одном потоке, чтобы гарантировать целостность и непротиворечивость данных. Представьте себе - два параллельных потока пытаются наперегонки поменять один и тот же узел в DOM... Плохая идея.
Синхронность
Что означает синхронность?
Например: есть 2 строки кода. Первая идет за второй.
Синхронность означает то, что строка 2 не может запуститься до тех пор, пока строка 1 не закончит своё выполнение.
Схема такая:
- функция из очереди попадает в стэк
- выполняется
- стэк очищается
- в стэк попадает следующая функция из очереди
Вариант немного сложнее:
- функция 1 из очереди попадает в стэк
- внутри этой функции 1 находится вызов функции 2.
- в стэк попадает функция 2
- она вполняется и удаляется
- результат её выполнения записывается в функцию 1
- функция 1 выполняется
- стэк очищается
- в стэк попадает следующая функция из очереди
Асинхронность
В JS есть возможности асинхронного выполнения кода.
Как программировать интерфейс с одним потоком? Ведь сама суть интерфейса - в асинхронности. Для этого и придуманы асинхронные функции.
Асинхронные функции выполняются не сразу, а после наступления события.
Асинхронное выполнение кода позволяет пользовательскому интерфейсу веб-приложений нормально функционировать, реагировать на команды пользователя. Система «планирует» нагрузку на цикл событий таким образом, чтобы в первую очередь выполнялись операции, связанные с пользовательским интерфейсом.
Мы ещё можем использовать вебворкеры, но из них нельзя менять DOM или вызывать методы объекта window. ПолезноЮ но не для всех случаев.
Что такое вообще — асинхронность? Асинхронность это модель поведения.
Например: есть две строчки кода, первая за второй. Первая строка это инструкция, для которой нужно время.
Первая строка начинает запуск этой инструкции в фоновом режиме, позволяя второй строке запуститься без ожидания завершения первой строки.
Нам нужно такое поведение в случае, когда что-то тормозит. Синхронность может казаться прямолинейной и незатейливой, но всё же может быть медленной. Такие задачи, как обработка изображений, операции с файлами, создание запросов сети и ожидание ответа — всё это может тормозить и быть долгим, производя огромные расчеты в 100 миллионов циклов итераций. Так что такие вещи в стеке запросов превращаются в “задержку”, ну или “blocking” по-английски. Когда стек запросов заблокирован, браузер препятствует вмешательству пользователя и выполнению другого кода до тех пор, пока “задержка” не выполнится и не освободит стек запросов. В таких ситуациях используют асинхронные колбэки (callback)
Пусть даже JavaScript и однопоточный, мы можем достичь согласованности действий через асинхронное исполнение задач.
Пример асинхронности
Пример асинхронной работы - выполнение AJAX-запросов.
Так как ожидание ответа способно занять много времени, запросы можно делать асинхронно, при этом, пока клиент ожидает ответа, может выполняться код, не относящийся к запросу.
Также функция setTimeout() - простейший способ продемонстрировать основы асинхронного поведения.
JS движок и Web API
Асинхронные функции - не часть JavaScript-движков. Вызов setTimeout на чистом V8 приводит к ошибке, так как в V8 нет такой функции. Асинхронные операции выполняются не в движке, а в окружении. Например, в Web API браузера.
В принципе, мы можем из стека вызовов сразу же положить функцию в очередь вызовов и таким образом чистый движок тоже будет работать асинхронно. Но это редкий фокус.
Web API и JS движок работают независимо. Web API решает, в какой момент функция двигается дальше, в очередь вызовов JS движка. Т.е. окружение самостоятельно решает, когда добавить переданный ей код в очередь вызовов.
Функции в очереди вызовов попадают в JavaScript-движок, где выполняются по одной. Выполнение происходит в том же порядке, в котором функции попадают в очередь. Функции из очереди добавляются в стек выполнения (выполняются) не раньше, чем стек вызовов закончит работу над текущей функцией.
Стек вызовов движка работает синхронно. Web API работает асинхронно.
В очереди вызовов нельзя отменять отдельные операции. Это делается в окружении (пример: removeEventListener).
Контекст выполнения функции
У каждого вызова функции есть свой «контекст выполнения» (execution context).
Контекст выполнения – это служебная информация, которая соответствует текущему запуску функции. Она включает в себя локальные переменные функции и конкретное место в коде, на котором находится интерпретатор.
При любом вложенном вызове JavaScript запоминает текущий контекст выполнения в специальной внутренней структуре данных – «стеке контекстов».
Очередь (queue)
Очередь — структура данных. Доступа к элементам организован по принципу FIFO (First In — First Out) «первый пришёл — первый вышел» .
Добавление элемента (обозначают словом enqueue — поставить в очередь) возможно лишь в конец очереди, выборка — только из начала очереди (называют словом dequeue — убрать из очереди), при этом выбранный элемент из очереди удаляется.
Стэк (stack)
Стек (анг. стопка) — структура данных (кусок памяти + опр правила работы с ним), представляющая из себя список элементов. Подобен стопке тарелок - последняя поступившая кладётся сверху, и должна быть обработана, прежде чем начнётся обработка "тарелки" под ней. LIFO (last in — first out) «последним пришёл — первым вышел»).
Вызов любой функции создает контекст выполнения.
При вызове вложенной функции создается новый контекст, а старый сохраняется в специальной структуре. Так формируется стек контекстов.
Максимальный размер стэка в в движке V8 16 000 (чего?). По достижению этого размера движок просто очищает стэк, чтоб всё не зависло.
Event Loop (Или цикл обработки событий) и Web API
Web API - часть браузера. Еслив стэк попадает какая-то асинхронная функция, например setTimeout - она передаётся в Web API. А стэк продолжает работать как обычно, как будто эта функция выполнилась.
Когда придёт время (таймер сработла, или пришёл ответ AJAX) - Web API выдаст функцию обратно. Но не в стэк (чтоб не нарушить то, над чем стэк работает сейчас) Web API поставит это в очередь выполнения задач.
Event Loop (цикл обработки событий) - смотрит состояние стэка и очереди колбэков. Как только стэк становится пуст - EventLopp берёт первый Элемент из очереди и передаёт его в стэк. Всё. Никакой магии
Цикл событий решает одну основную задачу: наблюдает за стеком вызовов и очередью коллбэков (callback queue). Если стек вызовов пуст, цикл берёт первое событие из очереди и помещает его в стек, что приводит к запуску этого события на выполнение.
Подобная итерация называется тиком (tick) цикла событий. Каждое событие — это просто коллбэк
Микро и макро-задачи (microtask и macrotask)
На самом деле, очередь задач устроена несколько сложнее, чем написано выше:
- Есть события по ре-рендеру экрана браузера. Происходят около 60 раз в секунду, автоматически встают в самое начало очереди событий. Именно для того, чтоб их пропустить, рекомендуется не занимать стэк большой синхронной задачей, а разбить её на ассинхронные (например через setInterval(callback,0))
- Начиная с EAS6 очередь ещё разбита на микрозадачи и макрозадачи. Это очередь колбеков (макрозадачи, callback queue) и очередью заданий (микрозадачи, job queue)
- В одной итерации Event loop обрабатывается одна макрозадача (т.е. классичесакая задача) из очереди колбэков. После этого в том же цикле обрабатываются все микрозадачи из очереди микрозадач. Они как-бы пристраиваются хвостом к первому колбэку. Очередь заданий — это очередь, которая присоединена к концу каждого тика в очереди цикла событий. Эти микрозадачи могут добавлять в очередь другие микрозадачи, и процесс будет продолжаться, пока очередь микрозадач не опустеет. Т.е. до запуска следующей макрозадачи может пройти довольно много времени. Это может привести к зависанию интерфейса пользователя или простою приложения.
В мире ECMAScript микрозадачи именуют заданиями («jobs»).
Очередность
- вначале в стэке выполнятся текущие маркозадачи (например, все console.log). Пока стек не очистится.
- потом из очереди миркрозадач выполнятся микрозадачи (например Promise и их then + те микроздачи, которые они вызывают внутри себя)
- потом выполнится задача рендера (это маркозадача, по приоритету ниже микрозадач. Вроде бы)
- потом из очереди макрозадач выполнятся макрозадачи (setTimeout 0, setInterval и т.д.)
Единственное, кажется, в некоторых случаях Promise можно вызваться раньше, чем обычный console.log(), который объявлен в коде ниже promise
setTimeout( function timeout() {
console.log('Timeout');
}, 0);
var p = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
//Прмяо в стэке сразу отрабатывает .then. Кажется так
p.then(function(){
// some code
});
console.log('End');
// Вывод:
Promise
End
Timeout
Второй пример, Promise выполняется позже console.log:
setTimeout( function timeout() {
console.log('Timeout');
}, 0);
//В стэке отрабатывает resolve, и отправляет .then в очередь микрозадач. Кажется так
Promise.resolve().then(function() {
console.log('Promise');
})
console.log('End');
// Вывод:
End
Promise
Timeout
Макрозадачи планируются с помощью
- setTimeout
- setInterval
- setImmediate
- ...
Микрозадачи
- process.nextTick,
- Promises,
- MutationObserver.
При исполнении любого файла JS-движок конвертирует содержимое в функцию и ассоциирует её с событием start или launch. Движок инициализирует стартовое событие и добавляет события в очередь как макрозадачи.
Начиная обработку, движок JS выбирает первую макрозадачу из очереди и выполняет обработчик обратного вызова:
Микрозадачи обычно планируются для вещей, который должны исполняться моментально после текущего исполняемого сценария. Например, реагирование на пачку действий или для того, чтобы сделать что-то асинхронно без необходимости терять производительность на пустом месте из-за полностью новой задачи. Очередь микрозадач развёртывается в конце каждой полной задачи, а также после колбеков в случае если никакой другой JavaScript не находится на стадии исполнения. Любые дополнительные микрозадачи, поставленные в очередь во время развёртывания очереди микрозадач, добавляются в конец очереди и тоже обрабатываются. Микрозадачи включают в себя колбеки Mutation observer и промисов, как в примере выше.
Как только промис решается или если он уже был решён, он ставит в очередь микрозадачу на исполнение колбека. Это даёт уверенность, что колбеки промисов исполняются асинхронно даже если они уже решены.
//ToDo - дополнить. Постоянно спрашивают.
- https://habr.com/ru/post/264993/
- https://refaq.ru/voprosy/raznicza_mezhdu_mikrozadachej_i_makrozadachej_v_kontekste_czikla_sobytij
Куча (heap)
Динамически распределяемая память. Часть JS-движка. Также как и стэк
Web API's
Расширения браузера. не входят в состав движка JS.
Но, в движке есть возможность взаимодействовать с этими API
setTimeout, DOM, AJAX (XMLHttpRequest), геолокация, аудио, видео...
setTimeout(callback(), 0)
Выполнить что-то как только стэк очистится. Т.е. в текущем потоке кода этот колбэк не запускается, и код идёт так, будто setTimeout и его содержимого просто нет (на самоо деле он вылетает из стэка в WebAPI, И оттуда сразу же встаёт в очередь). А когда этот кусок кода закончится и стэк станет пустым - вот тогда выполнится колбэк из SetTimeout
Позволяет запланировать что-то сразу после выполнения основного кода
console.log('1')
setTimeout(function foo {
console.log('2')
}, 0)
console.log('3')
//Выведет в консоль:
1
3
2
Unsorted
Движок браузера выполняет JavaScript в одном потоке. Он не может поставить обработку события на паузу, переключиться на другое событие, а после — возобновить выполнение первого. Все события обрабатываются последовательно и каждое — до победного конца.
Для вышеописанного потока выделяется область памяти — стэк, где хранятся фреймы (аргументы, локальные переменные) вызываемых функций.
Список событий, подлежащих обработке формируют очередь событий. Когда стек освобождается, движок может обрабатывать событие из очереди. Координирование этого процесса и происходит в event loop.
Browser events
Какие же события происходят в браузере? Их великое множество: - клики мышкой; - скроллинг; - ввод с клавиатуры; - загрузка скриптов; - CSS анимации; - и тд.
Браузер может реагировать на эти события. Для этого событию нужно назначить обработчик, то есть функцию, которая сработает, когда событие произошло. Функция выполнится не сразу, она станет в конец очереди событий и выполнится, когда придёт её время.
Любые данные от сервера запрашиваются асинхронно: отправляется запрос (XMLHttpRequest или XHR), и код не ждет его возвращения, продолжая выполняться. Когда же сервер, наконец, отвечает, объект XHR получает уведомление об этом и запускает функцию обратного вызова — callback, который передали в него перед отправкой запроса.
В любой момент времени выполняется только один контекст функции (тело функции). Вот почему JavaScript является однопотоковым, так как единовременно может выполняться только одна команда. Обычно браузеры поддерживают этот контекст с помощью стека — stack. Стек — структура данных, выполняемая в обратном порядке: LIFO — «последним пришёл — первым вышел». Последнее, что вы добавили в стек, будет удалено первым из него. Это происходит из-за того, что мы можем только добавить или удалить элементы из верхушки стека. Текущий или «выполняющийся» контекст исполнения — всегда верхний элемент стека. Он выскакивает из стека, когда код в текущем контексте полностью разобран, позволяя следующему верхнему элементу стека взять на себя контекст выполнения.
Кроме того, если контекст уже выполняется, это не означает, что ему нужно завершить своё выполнение, прежде чем другой контекст выполнения сможет начать работу. Бывают случаи, когда контекст приостанавливается и другой контекст начинает работу. Прерванный контекст может быть позже забран обратно наверх в том месте, где он был приостановлен. В любое время один контекст может быть заменён другим, и этот новый контекст поместится в стек, став текущим контекстом выполнения.
У динамических языков программирования существует стековая архитектура — stack-based implementations, локальные переменные и функции хранятся в стеке. Поэтому, во время выполнения стека, программа определяет какую переменную вы имеете в виду. С другой стороны, статическая область видимости — это когда переменные ссылаются на контекст и фиксируются на момент создания. Другими словами, структура исходного кода программы определяет к каким переменным вы обращаетесь.
Для лучшего понимания асинхронности неплохо разобраться с тем, как устроен рантайм (браузер или Node.js) JavaScript. JavaScript изначально появился в браузерах, и к нему предъявлялись особые требования, из-за которых он кардинально отличается от остальных языков программирования. Браузер работает по так называемой событийной модели. Он загружает страницу и ждёт действий от пользователя: клики, набор текста или движение мышкой. А код, загруженный на страницу, реагирует на эти события.
Такая организация взаимодействия невозможна в синхронном коде, у которого есть понятия "запуск" и "завершение" работы. Код в браузере не может завершиться совсем, он проходит стадию инициализации, а затем ждёт событий для реакции на них. Технически это выглядит, как колбек, который соединён с определённым типом события. Когда событие срабатывает, то колбек вызывается.
SetTimeout гарантирует минимальную задержку. Т.е. это значит, что код выполнится не раньше, чем через Х секунд. По прошествии Х он встанет в очередь и будет ждать пока: а) очистится стэк, б) освободится очередь перед кодом из таймера.
Про setInterval
Предположим, мы кликнули мышью и в процессе этого ещё запсутили setInterval.
Пока обработчик клика мышью выполняется, срабатывает первый interval-callback. Он будет поставлен в очередь. Когда снова сработает interval, то он будет удален из очереди. Если бы все interval-callback'и попадали в очередь пока исполняется большой кусок кода, это бы привело к тому, что образовалась бы куча функций, ожидающих вызова без периодов задержек между окончанием их выполнения. Вместо этого браузеры стремятся ждать пока не останется ни одной функции в очереди прежде чем добавить в очередь еще один setInterval.
Ссылки
- YouTube - Как на самом деле работает EventLoop (26 мин) - Очень просто и понятно. Рекомендую
- habr - Конструкция async/await в JavaScript
- habr - Async/Await в javascript. Взгляд со стороны
- habr - Знай свой инструмент: Event Loop
- learnjavascript - Управление памятью в JavaScript
- learn.javascript.ru (en)
- JavaScript event loop в картинках . Часть 1
- JavaScript event loop в картинках . Часть 2
- Стеки и очереди в JavaScript
- Полное понимание синхронного и асинхронного JavaScript с Async/Await
- MDN
- Hexlet
- habr - Асинхронность в JavaScript: Пособие для тех, кто хочет разобраться
- habr - Как работает JS: цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
- Как эмулировать многопоточность в JavaScript
- pythontutor - как работает JS код
- learnjavascript - Про события и ассинхронность
- Замыкания в JavaScript
- Ад обратных вызовов
- Асинхронное программирование: концепция, реализация, примеры
Promises
Способ организации асинхронного кода. Объект, который содержит своё состояние (ожидание, выполнен успешно, ошибка). Позволяет вызывать разные колбеки в зависимости от результата - одни для успеха, другие для ошибок.
Промисы можно объединять в длинные цепочки - это хорошая замена пирамиде вложенных колбеков. Т.е. мы делаем какую-то ассинхронную операцию, с результамаи её работы выполняем ещё какую-то асинхронную опрацию, её результаты передаём в следующую и т.д.
Способ использования:
- В основном коде пишем new Promise() и внутри запсукаем асинхронную функцию
- Асинхронная функция создаёт объект promise и возвращает его.
- В основном коде мы принимаем объект promise и навешиваем на него обработчики (одни - на успех, другие - на ошибку).
- Когда код асинхронной функции завершается, он переводит promise в состояние fulfilled (с результатом) или rejected (с ошибкой). При этом автоматически вызываются соответствующие обработчики в основном коде.
Пример
var promise = new Promise(function(resolve, reject) {
// Эта функция будет вызвана автоматически, в ней можно делать любые асинхронные операции,
// Когда они завершатся — нужно вызвать (resolve(результат) при успехе) или (reject(ошибка) при ошибке)
setTimeout(() => {
// переведёт промис в состояние fulfilled с результатом "result"
resolve("result");
}, 1000);
})
promise
.then(
// функция-обработчик №1 - запустится при вызове resolve
result => console.log("Fulfilled: " + result), // result - аргумент resolve
// функция-обработчик №2 - запустится при вызове reject
// сработала бы, если б в SetTimeout вместо resolve("result") был вызов reject("error")
error => console.log("Rejected: " + error), // error - аргумент reject
);
Состояния promises: - вначале pending («ожидание»), - затем либо fulfilled («выполнено успешно») - либо rejected («выполнено с ошибкой»).
На promise можно навешивать коллбэки двух типов:
- onFulfilled – срабатывают, когда promise в состоянии «выполнен успешно».
- onRejected – срабатывают, когда promise в состоянии «выполнен с ошибкой».
Обработчики назначаются вызовом then/catch
- .then = универсальный метод для навешивания обработчиков:
- promise.then(onFulfilled, onRejected) //(удачно, неудачно)
- .catch = чтобы поставить обработчик только на ошибку
- вместо .then(null, onRejected)
- можно .catch(onRejected) – это то же самое.
Метод .catch(onRejected) – всего лишь сокращённая запись .then(null, onRejected).
Сhaining (чейнинг)
Возможность строить асинхронные цепочки из промисов
Основная причина, из-за которой существуют и активно используются промисы.
Например, мы хотим по очереди:
- Загрузить данные посетителя с сервера (асинхронно).
- Затем отправить запрос о нём на github (асинхронно).
- Когда это будет готово, вывести его github-аватар на экран (асинхронно).
- …И сделать код расширяемым, чтобы цепочку можно было легко продолжить.
httpGet('/article/promise/user.json') //делаем запрос
.then(response => {
console.log(response);
let user = JSON.parse(response);
return user;
})
// 2. Получить информацию с github
.then(user => {
console.log(user);
let githubUser = httpGet(`https://api.github.com/users/${user.name}`)
return githubUser;
})
// 3. Вывести картинку юзера
.then(githubUser => {
console.log(githubUser);
githubUser = JSON.parse(githubUser);
img.src = githubUser.avatar_url;
document.body.appendChild(img);
});
При чейнинге .then…then…then, в каждый следующий then переходит результат от предыдущего.
Если очередной then вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.
Цепочки Promise работают напрямую через event loop и в общем случае могут содержать внутри достаточно большие и тяжелые вычисления. Разбивая их на атомарные операции, мы оставляем пространство для выполнения обработчиков пользовательских событий.
Промисификация
Это когда берут асинхронный функционал и делают для него обёртку, возвращающую промис. Использование функционала, зачастую, становится удобнее.
Promise.resolve() Такой код
new Promise(function (resolve, reject) {
resolve(someSynchronousValue);
}).then( ... );
Можно записать так:
Promise.resolve(someSynchronousValue).then(/* ... */);
Важное
- внутри then всегда использовать return или выдавать ошибку при помощи throw.
- в конец цепочки промисов (.then(...).then(...)) всегда добавлять метод catch() (
.catch(console.log.bind(console))
) - всегда добавлять обработку ошибок ниже в виде catch(), и никогда не использовать для этой цели вторую функцию в методе then(). Исключение только одно — асинхронные тесты в Mocha, в случаях, когда я намеренно жду ошибку:
Ссылки:
- learn.javascript.ru
- habr - У нас проблемы с промисами
- Полное понимание синхронного и асинхронного JavaScript с Async/Await
- habr - Асинхронность в JavaScript: Пособие для тех, кто хочет разобраться
- Полное понимание синхронного и асинхронного JavaScript с Async/Await
- habr - Как работает JS: цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
- Ад обратных вызовов
- [habr - Промисы в ES6: паттерны и анти-паттерны](https://m.habr.com/ru/company/ruvds/blog/339414/
Async/Await
Асинхронные функции на основе promises
Асинхронные функции позволяют избавиться от так называемого «ада коллбэков» и улучшить внешний вид и читаемость кода.
Добавлены в ES8
Ключевое слово async сообщает JavaScript-интерпретатору о том, что функцию, объявленную с этим ключевым словом, нужно воспринимать по-особому. Систем приостанавливается, достигая ключевого слова await в этой функции. Она считает, что выражение после await возвращает промис и ожидает разрешения или отклонения этого промиса перед продолжением.
В следующем примере функция getAmount() вызывает две асинхронные функции — getUser() и getBankBalance().
Сделать это можно и в промисе, но использование конструкции async/await позволяет решить эту задачу проще и элегантнее.
Полное понимание синхронного и асинхронного JavaScript с Async/Await
Конструкция async-await появилась в стандарте ES6, и само слово await четко дает понять, что мы должны дождаться выполнения асинхронной функции getData(), не помещать ее в event loop, а выполнить прямо здесь и записать результат в i-й элемент массива.
async function fillArray() {
const arr = [];
for (var i = 0; i < 10000; i++) {
arr[i] = await getData(i);
}
}
Это значит, что при вызове асинхронной функции fillArray() она попадет в очередь контекстов и будет исполнена. Но, как мы уже знаем, следующий контекст будет ждать, пока текущий завершится. Все пользовательские события, таймеры и прочие помещаемые в очередь контексты будут ждать, пока не пройдут десять тысяч запросов к серверу.
Async/Await пытается решить одну из главных головных болей языка со времен его появления - асинхронность.
До их появления, основная работа c асинхронностью шла через callbacks
setTimeout(function() {
callback
}, 2000);
Но, что если мы столкнемся с последовательностью callbacks?
Это иногда называется "Pyramid of Doom" и "Callback Hell".
setTimeout(function() {
doThingOne(function() {
doThingTwo(function() {
doThingThree(function() {
doThingFour(function() {
// Oh no!
});
});
});
});
}, 2000);
Async функции объявляются добавлением слова async. async function doAsyncStuff() { …code }
Ваш код может встать на паузу в ожидании Async функции с await
Await возвращает то, что асинхронная функция отдаёт при завершении.
Await может быть использовано только внутри async функции.
Конструкция async / await позволяет обрабатывать синхронные и асинхронные ошибки с использованием одних и тех же механизмов. А именно, речь идёт о широко известном выражении try / catch.
При использовании промисов нам приходится использовать блок .catch() для обработки асинхронных ошибок, и блок try / catch для обработки синхронных ошибок.
В отличие от async / await, стек ошибки, возвращённый из цепочки промисов, не содержит сведений о точном месте, в котором произошла ошибка.
Если вы пользовались промисами, то вы знаете, что отладка подобных конструкций — это кошмар. Например, если установить точку останова внутри блока .then и использовать команды отладки вроде «step-over», отладчик не перейдёт к следующему .then, так как он умеет «перешагивать» лишь через синхронный код. С использованием async / await можно переходить по вызовам, в которых используется ключевое слово await так, будто это — обычные синхронные операции.
Ссылки
- habr - Конструкция async/await в JavaScript
- habr - Async/Await в javascript. Взгляд со стороны
- learn.javascript.ru (en)
- Полное понимание синхронного и асинхронного JavaScript с Async/Await
- habr - Асинхронность в JavaScript: Пособие для тех, кто хочет разобраться
Атрибуты async и defer тега script
Аттрибуты тэга <script>
. Влияют на то, когда будет загружаться и выполняться этот скрипт. Будет ли заблокирован парсинг HTML на время загрузки/выполнения или нет.
Async - указает браузеру, что скрипт может быть выполнен асинхронно. Скрипт скачивается ассинхронно (параллельно с формированием документа). Как только скрипт загружен - он запускается, парсер на это время будет приостановлен. Атрибут доступен только для файлов, подключающихся внешне.
Defer (анг откладывать) - указывает браузеру, что скрипт должен быть выполнен после того, как HTML-документ будет полностью разобран. Скрипт скачивается ассинхронно (параллельно с формированием документа). И после получения - скрипт не запускается сразу, а ждёт, пока документ будет полностью сформирован.
Где расположен элемент <script>
?
Асинхронное и отложенное выполнения наиболее важны, когда элемент <script>
не находится в самом конце документа. HTML-документы парсятся по порядку, с открытия <html>
до его закрытия. Если внешний JavaScript-файл размещается непосредственно перед закрывающим тегом </body>
, то использование async и defer становится менее уместным, так как парсер к тому времени уже разберёт большую часть документа, и JavaScript-файлы уже не будут оказывать воздействие на него.
Async - скрипт самодостаточен
Для файлов, которые не зависят от других файлов и/или не имеют никаких зависимостей, атрибут async будет наиболее полезен. Поскольку нам не важно, когда файл будет исполнен, асинхронная загрузка — наиболее подходящий вариант.
Defer - скрипт полагается на полностью разобранный DOM
Во многих случаях файл скрипта содержит функции, взаимодействующие с DOM. Или, возможно, существует зависимость от другого файла на странице. В таких случаях DOM должен быть полностью разобран, прежде чем скрипт будет выполнен. Как правило, такой файл помещается в низ страницы, чтобы убедиться, что для его работы всё было разобрано. Однако, в ситуации, когда по каким-либо причинам файл должен быть размещён в другом месте — атрибут defer может быть полезен.
Синхронный inline - скрипт небольшой и зависим
Если скрипт является относительно небольшим и/или зависит от других файлов, то, возможно, стоит определить его инлайново. Несмотря на то, что встроенный код блокирует разбор HTML-документа, он не должен сильно помешать, если его размер небольшой. Кроме того, если он зависит от других файлов, может понадобиться незначительная блокировка.
Ссылки
- learnjavascript - Внешние скрипты, порядок исполнения
- Асинхронный JavaScript против отложенного
- Разница между async и defer у тега script
- Атрибут defer
Стрелочные функции
Не имеют своего this. Внутри стрелочных функций тот же this, что и снаружи. Удобно в обработчиках событий и коллбэках.
Не имеют своего arguments. Используются аргументы внешней «обычной» функции.
Появились в ES6.
Ссылки
Самовыполняющиеся функции. Модули ( function(){} )()
Про анонимные функции и функциональыне выражения Анонимная функция — вид функций, которые объявляются в месте использования и не получают уникального имени для доступа к ним.
Есть «Function Expression» (функциональное выражение) - синтаксис объявления функций:
var f = function(параметры) {
// тело функции
};
Функциональное выражение, которое не записывается в переменную, называют анонимной функцией.
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}
ask(
"Вы согласны?",
function() { alert("Вы согласились."); },
function() { alert("Вы отменили выполнение."); }
);
Важное отличие Function Expression (и анонимных функций, в частности) - они должны объявляться до их вызова.
Такая функция создается в момент ее запуска в скрипте, а не во время парсинга. Поэтому в коде её надо прописать до её вызова.
//Плохо:
sayHi("Вася");
var sayHi = function(name) {
alert( "Привет, " + name );
}
//Хорошо:
var sayHi = function(name) {
alert( "Привет, " + name );
}
sayHi("Вася");
См. также Function Expression
Анонимные функции короче, их легче писать. Это удобно (если не надо ссылаться на них в коде). Например в обработчиках.
Про самовыполняющиеся функции
В определенной записи анонимные функции могут вызывать сами себя.
(function() {
// код выполняется автоматически
})();
Эффект создается пустыми скобками в конце функции.
Главная фишка этого приёма – изоляция области видимости функции.
Переменная, объявленная внутри функции, может быть вызвана только внутри этой функции. В остальном коде данная переменная не видна. Поэтому переменная внутри самовыполняющейся функции замыкается внутри этой функции. Такую переменную нельзя случайно вызвать из внешнего кода или переписать.
Эта техника аккуратно инкапсулирует переменные и код, пряча их от глобального пространства имен, чтобы они не вступили в конфликт с другим кодом. Поэтому полифилы и плагины часто пишутся в виде самовыполняющихся функций.
Ссылки
- Анонимные и самовыполняющиеся функции в JavaScript
- learn.javascript.ru - Модули через замыкания
- learn.javascript.ru - Функциональные выражения
- Wikipedia - Анонимная функция
- code.mu - Продвинутая работа с функциями
Function Declaration, Function Expression
Function Declaration – функция, объявленная в основном потоке кода.
function sayHi() {...}
- создаются интерпретатором до выполнения кода. Поэтому их можно вызвать в коде до объявления. Т.е. вверху вызов функции, а ниже - её код
Function Expression – объявление функции в контексте какого-либо выражения, например присваивания.
var f = function sayHi() {...}
- создаются интерпретатором в процессе выполнения выражения, в котором созданы. В данном случае – функция будет создана при операции присваивания var f = function...
- анонимные функции - частный случай Function Expression
В результате инициализации, к началу выполнения кода:
- Функции, объявленные как Function Declaration, создаются полностью и готовы к использованию.
- Переменные объявлены, но равны undefined. Присваивания выполнятся позже, когда выполнение дойдет до них.
Callback
Функция, которая должна быть выполнена после того, как другая функция завершила выполнение (отсюда и название: callback – функция обратного вызова).
Сами не вызываем функцию. Отдаём её как аругмент в другую функции, и та вызывает, когда сочтёт нужным.
Чуть сложнее: В JavaScript функции – это объекты. Поэтому функции могут принимать другие функции в качестве аргументов, а также функции могут возвращать функции в качестве результата. Функции, которые это умеют, называются функциями высшего порядка. А любая функция, которая передается как аргумент, называется callback-функцией.
Пример:
<button onClik={function}>txt</button>
Когда произойдёт событие onClick, кнопка вызовет эту функцию.
Нет скобок после function - мы не вызываем функцию сейчас, а передаём кому-то. И он вызовет, когда будет надо. От своего имени
Неправильно:
<button onClik={function()}>txt</button>
Здесь функция не передаётся, а сразу вызывается.
Зачем?
JS - событийно-ориентированный язык. Если функция не отвечает немедленно (например выполянет AJAX-запрос или Timeout) - JS не будет останавливать работу, ожидая ответа. Он продолжит выполнение других функций, одновременно ожидая ответа от нашей функции. Вывод: нельзя просто вызывать функции в нужном порядке и надеяться, что они в обязательно выполнятся в том же порядке.
Пример:
function first(){
// Как будто бы запрос к API
setTimeout( function(){
console.log(1);
}, 500 );
}
function second(){
console.log(2);
}
first();
second();
//Выдаст ответ:
// 2
// 1
Коллбэки позволяют нам быть уверенными в том, что определенный код не начнет исполнение до того момента, пока другой код не завершит исполнение.
При вызове callback может нарушиться контекст вызова this. Т.е. отвалиться привязка this к родительскому объекту.
<App
addQuote={store.addQuote}
/>
В таком случае, при создании callback надо сделать привязку контекста - bind
<App
addQuote={store.addQuote.bind(store)}
/>
Функции call() и apply() - ещё один способ вызова callback-функции. Здесь мы сами устанавливаем контекст, в котором выполняется функция. Это означает, что когда мы используем ключевое слово this внутри нашей callback-функции, оно ссылается на то, что мы передаём первым аргументом в call()/apply. (см. ниже)
Пример:
function showFullName() {alert(что-нибудь)}
var user = {что-то}
function_name.call(user); // вызываем колбек и в качестве контекста this передаём ему user
Коллбэки являются самым распространённым средством выражения и выполнения асинхронных действий в программах на JavaScript. Более того, коллбэк является наиболее фундаментальным асинхронным шаблоном языка. Бесчисленное множество JS-приложений, даже весьма хитроумных и сложных, основано исключительно на коллбэках.
Обратные вызовы — фундаментальная часть JavaScript (поскольку являются просто функциями), и вы должны научиться читать и писать их прежде, чем переходить к более продвинутым функциям языка, так как все они зависят от понимания обратных вызовов. Если вы пока не можете написать удобный для поддержки код обратных вызовов, то продолжайте работать над этим.
Ссылки:
- habr - Понимание callback-функций (колбеков)
- hexlet
- habr - Как работает JS: цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
- Ад обратных вызовов
"Call" & "apply" //ToDo - доработать
Явное указание this
Мы сами устанавливаем контекст, в котором выполняется функция. Это означает, что когда мы используем ключевое слово this внутри нашей callback-функции, оно ссылается на то, что мы передаём первым аргументом в call()/apply
Ссылки:
Bind
Метод. Позволяет привязать контекст к функции. Важно при callback
Метод bind() создаёт новую функцию, которая при вызове устанавливает в качестве контекста выполнения this предоставленное значение. В метод также передаётся набор аргументов, которые будут установлены перед переданными в привязанную функцию аргументами при её вызове.
При вызове callback может нарушиться контекст вызова this. Т.е. отвалиться привязка this к родительскому объекту.
<App
addQuote={store.addQuote} //нет скобок после addQuote
/>
Не вызываем функцию сейчас, а передаём кому-то. И он вызовет, когда будет надо, от своего имени.
В таком случае, при создании callback надо сделать привязку контекста - bind
<App
addQuote={store.addQuote.bind(store)}
/>
Ссылки:
Обработчики событий (events handlers)
Блоки кода (обычно функции), которые позволяют обрабатывать события (щелчок мыши...) и реагировать на них.
Когда такой блок кода определяют для запуска в ответ на некое событие, говорят "мы регистрируем обработчик событий". Иногда обработчики называют прослушивателями событий (event listeners). Термины часто взаимозаменяемы, но вообще: прослушиватель слушает событие, а обработчик — это код, который запускается в ответ на событие.
Ссылки:
Объекты {} и массивы []
{ width: 300, height: 200, }
- объект. Структура для хранения данных в формате ключ-значение.[ 300, 200, ]
- массив. Для хранения пронумерованных значений. Особый тип объектов.
Объект - ассоциативный массив: структура, пригодная для хранения любых данных. В других языках программирования такую структуру данных также называют «словарь» и «хэш». В JS объекты также используются как элементы ООП, это немного отдельно.
Массивы обычно используются для хранения упорядоченных коллекций данных, например – списка товаров на странице, студентов в группе и т.п. Предлагает дополнительные методы для удобного манипулирования такой коллекцией. Элементы в массиве должны идти подряд, иначе теряется большая часть преимуществ этой структуры.
Ссылки:
- learn.javascript.ru - Массивы
- learn.javascript.ru - Объекты
- habr - Несколько полезных кейсов при работе с массивами в JavaScript
- Козлова О - JS Interview Questions. Массивы
- Хватит использовать массивы! Как JavaScript Set ускоряет код
Декораторы
Ссылки:
- habr - Разбираем декораторы ES2016
- learn.javascript.ru - Декораторы и переадресация вызова, сall/apply
var = х
Способ объявления переменной. Используем в обычных случаях
Область видимости переменной var – функция.
var существуют и до объявления. Они равны undefined.
При использовании в цикле у нас будет одна var на все итерации цикла. Не создаётся заново в каждой итерации.
Не надо объявлять переменные без указания директивы (например var или let).
//Т.е. так писать не стоит
a = 1
//Надо так
var a = 1
//Или так
let a = 1
//Или так
const a = 1
Ссылки:
let = х
Способ объявления переменной. Используем если будем переопределять значение переменной. Видна в блоке
Область видимости переменной let – блок {...}, в котором объявлена.
Это, в частности, влияет на объявления внутри if, while или for.
let видна только после объявления. До тех пор их просто нет.
При использовании в цикле, для каждой итерации создаётся своя переменная.
Введена в язык в ES6 (ES-2015)
Ссылки:
const = х
Способ объявления переменной. Используем для констант
Объявление const задаёт константу, то есть переменную, которую нельзя менять. При попытке изменения выдаст ошибку.
Eсли в константу присвоен объект, то от изменения защищена сама константа, но не свойства внутри неё.
В остальном - аналогичная let.
Функции обычно лучше создавать через const.
Вообще хороший вариант объявления чего-то, что мы не собираемся менять.
Константы, которые жёстко заданы всегда, во время всей программы, обычно пишутся в верхнем регистре. Например: const ORANGE = "#ffa500".
Большинство переменных – константы в другом смысле: они не меняются после присвоения. Но при разных запусках функции это значение может быть разным. Для таких переменных можно использовать const и обычные строчные буквы в имени.
Использование const вместо var или let не говорит от том, что значение является константой или что оно иммутабельно (неизменяемо). Ключевое слово const просто указывает компилятору следить за тем, что переменной больше не будет присвоено никаких других значений.
В случае использования const современные JavaScript-движки могут выполнить ряд дополнительных оптимизаций.
Введена в язык в ES6 (ES-2015)
Ссылки:
Proxy-объекты
Особые объекты, позволяют перехватывать и изменять действия, выполняемые над другими объектами.
В частности, речь идёт о вызове функций, об операциях присваивания, о работе со свойствами, о создании новых объектов, и так далее.
Эту технологию используют для блокирования прямого доступа к целевому объекту или целевой функции и организации взаимодействия с объектом или функцией через прокси-объект.
Так выглядит объявление простого прокси-объекта, которому передаётся целевой объект и обработчик:
let proxy = new Proxy(target, handler);
Стандартное поведение объектов
Объявим объект, а затем попробуем обратиться к несуществующему свойству этого объекта.
let obj = {
c: "car",
b: "bike"
};
document.write(obj.b, ""); //Результат -> "bike"
document.write(obj.c, ""); //Результат -> "car"
document.write(obj.l); //Результат -> "undefined"
Использование прокси для объекта
Используем обработчик с перехватчиком get. Обработчик передаст целевой объект и запрошенный ключ перехватчику.
let handler = {
get: function(target, name) {
return name in target ? target[name] : "Key does not exist";
}
}
let obj = {
c: "car",
b: "bike"
};
let proxyObj = new Proxy(obj, handler);
document.write(proxyObj.b, ""); //Результат -> "bike"
document.write(proxyObj.c, ""); //Результат -> "car"
document.write(proxyObj.l); //Результат -> "Key does not exist"
Ссылки:
Функции-генераторы - function* ()
Могут приостанавливать своё выполнение, возвращать промежуточный результат и возобновляться позже.
Код такой функции не выполняется. Вместо этого она возвращает специальный объект, который как раз и называют «генератором»
Генератор связан с итераторами. В частности, он является итерируемым объектом.
Один генератор может включать в себя другие. Это называется композицией.
Плоский асинхронный код
Одна из основных областей применения генераторов – написание «плоского» асинхронного кода.
Общий принцип такой:
- Генератор yield'ит не просто значения, а промисы.
- Есть специальная «функция-чернорабочий» execute(generator) которая запускает генератор, последовательными вызовами next получает из него промисы – один за другим, и, когда очередной промис выполнится, возвращает его результат в генератор следующим next.
- Последнее значение генератора (done:true) execute уже обрабатывает как окончательный результат – например, возвращает через промис куда-то ещё, во внешний код или просто использует, как в примере ниже.
Появились в ES6.
Ссылки:
Итераторы
Тип объектов, содержимое которых можно перебрать в цикле
По сути - объект, предназначенный для перебора другого объекта.
Например массив, функция-генератор, список DOM-узлов, строка..
Для перебора таких объектов добавлен новый синтаксис цикла: for..of.
Итераторы дают возможность сделать «перебираемыми» любые объекты.
Ссылки:
function()()
Зачем при вызове функции ставят две двойные скобки?
Функция getFunc() возвращает другую функцию (та что в переменной func).
Вторые скобки нужны чтобы вызвать функцию, которую вернула getFunc().
Если скобки опустить, то return внешней функции вернет Вам не результат а саму функцию.
Пример:
var a = 1;
function getFunc() {
var a = 2;
var func = function() { alert(a); };
return func;
}
getFunc()(); // 2, из LexicalEnvironment функции getFunc
Деструктуризация массивов
const [fruit, setFruit] = useState('банан');
Такой синтаксис в JS называется «деструктуризацией массивов (array destructuring)». Он означает, что мы создаём две новые переменные, fruit и setFruit. Во fruit будет записано первое значение, вернувшееся из useState, а в setFruit — второе.
Это равносильно такому коду:
var fruitStateVariable = useState('банан'); // Возвращает пару значений
var fruit = fruitStateVariable[0]; // Извлекаем первое значение
var setFruit = fruitStateVariable[1]; // Извлекаем второе значение
Когда мы объявляем переменную состояния с помощью функции useState, мы получаем от неё пару, то есть массив из двух элементов. Первый элемент обозначает текущее значение, а второй является функцией, позволяющей менять это значение.
Ссылки:
Лексическое всплытие
Организация процесса обработки события (например клика по div), при котором вначале срабатывают обработчики на целевом объекте (сам div), потом на его родителе, потом выше...
Отсюда различия между event.target (куда кликнули) и this (где сейчас включился обработчик).
Всплытие можно искуственно прервать (не желательно без чёткой необходимости) - event.stopPropagation() и event.stopImmediatePropagation().
Обратный процесс называетя Погружение - вообще-то вначале идёт погружение и только потом всплытие.
Бывают особые случаии - например событие focus не всплывает и др.
Ссылки:
ООП в JS
На самом деле, поддержка JS-классов браузерами — не более чем «синтаксический сахар». Эти конструкции преобразуются в те же базовые структуры, которые уже поддерживаются языком. В результате, даже если пользоваться новым синтаксисом, на более низком уровне всё будет выглядеть как создание конструкторов и манипуляции с прототипами объектов.
Поддержка классов ES6 в JS-движке V8
При подготовке JS-кода к выполнению система производит его синтаксический анализ и формирует на его основе абстрактное синтаксическое дерево. При разборе конструкций объявления классов в абстрактное синтаксическое дерево попадают узлы типа ClassLiteral.
В подобных узлах хранится пара интересных вещей. Во-первых — это конструктор в виде отдельной функции, во-вторых — это список свойств класса. Это могут быть методы, геттеры, сеттеры, общедоступные или закрытые поля. Такой узел, кроме того, хранит ссылку на родительский класс, который расширяет класс, для которого сформирован узел, который, опять же, хранит конструктор, список свойств и ссылку на собственный родительский класс.
После того, как новый узел ClassLiteral трансформируется в код, он преобразуется в конструкции, состоящие из функций и прототипов.
Ссылки:
- learn.javascript.ru - ООП в функциональном стиле
- learn.javascript.ru - ООП в прототипном стиле
- Как работает JS: классы и наследование, транспиляция в Babel и TypeScript
ООП в JS
На самом деле, поддержка JS-классов браузерами — не более чем «синтаксический сахар». Эти конструкции преобразуются в те же базовые структуры, которые уже поддерживаются языком. В результате, даже если пользоваться новым синтаксисом, на более низком уровне всё будет выглядеть как создание конструкторов и манипуляции с прототипами объектов.
Поддержка классов ES6 в JS-движке V8
При подготовке JS-кода к выполнению система производит его синтаксический анализ и формирует на его основе абстрактное синтаксическое дерево. При разборе конструкций объявления классов в абстрактное синтаксическое дерево попадают узлы типа ClassLiteral.
В подобных узлах хранится пара интересных вещей. Во-первых — это конструктор в виде отдельной функции, во-вторых — это список свойств класса. Это могут быть методы, геттеры, сеттеры, общедоступные или закрытые поля. Такой узел, кроме того, хранит ссылку на родительский класс, который расширяет класс, для которого сформирован узел, который, опять же, хранит конструктор, список свойств и ссылку на собственный родительский класс.
После того, как новый узел ClassLiteral трансформируется в код, он преобразуется в конструкции, состоящие из функций и прототипов.
Ссылки:
- learn.javascript.ru - ООП в функциональном стиле
- learn.javascript.ru - ООП в прототипном стиле
- Как работает JS: классы и наследование, транспиляция в Babel и TypeScript
Утечки памяти в JS
В двух словах, утечки памяти можно определить как фрагменты памяти, которые больше не нужны приложению, но по какой-то причине не возвращённые операционной системе или в пул свободной памяти.
Языки программирования используют разные способы управления памятью. Однако, проблема точного определения того, используется ли на самом деле некий участок памяти или нет, как уже было сказано, неразрешима. Другими словами, только разработчик знает, можно или нет вернуть операционной системе некую область памяти.
-
Глобальные переменные - неявное объявление
Пример:function foo(arg) { bar = "скрытая глобальная переменная"; // это то же: window.bar = "явно объявленная глобальная переменная"; }
Решение: добавляйте 'use strict'; в начало JavaScript-файлов -
Глобальные переменные - явно объявленные, не вычищенные (кэши и т.д.)
Это касается глобальных переменных, использующихся для временного хранения и обработки больших блоков данных. Если вам нужна глобальная переменная, чтобы записать в неё большое количество информации, убедитесь, что в конце работы с данными её значение будет установлено в null или переопределено.
Примером увеличенного расхода памяти, связанным с глобальными переменными, являются кэши — объекты, которые сохраняют повторно используемые данные. Для эффективной работы их следует ограничивать по размеру. Если кэш увеличивается без ограничений, он может привести к высокому расходу памяти, поскольку его содержимое не может быть очищено сборщиком мусора.
В JavaScript используется интересный подход к работе с необъявленными переменными. Обращение к такой переменной создаёт новую переменную в глобальном объекте. В случае с браузерами, глобальным объектом является window. Рассмотрим такую конструкцию:
function foo(arg) {
bar = "some text";
}
Она эквивалентна следующему коду:
function foo(arg) {
window.bar = "some text";
}
Если переменную bar планируется использовать только внутри области видимости функции foo, и при её объявлении забыли о ключевом слове var, будет случайно создана глобальная переменная.
В этом примере утечка памяти, выделенной под простую строку, большого вреда не принесёт, но всё может быть гораздо хуже.
Другая ситуация, в которой может появиться случайно созданная глобальная переменная, может возникнуть при неправильной работе с ключевым словом this:
function foo() {
this.var1 = "potential accidental global";
}
// Функция вызывается сама по себе, при этом this указывает на глобальный объект (window),
// this не равно undefined, или, как при вызове конструктора, не указывает на новый объект
foo();
Для того, чтобы избежать подобных ошибок, можно добавить оператор "use strict"; в начало JS-файла. Это включит так называемый строгий режим, в котором запрещено создание глобальных переменных вышеописанными способами. Подробнее о строгом режиме можно почитать здесь.
Даже если говорить о вполне безобидных глобальных переменных, созданных осознанно, во многих программах их слишком много. Они, по определению, не подвергаются сборке мусора (если только в такую переменную не записать null или какое-то другое значение). В частности, стоит обратить пристальное внимание на глобальные переменные, которые используются для временного хранения и обработки больших объёмов данных. Если вы вынуждены использовать глобальную переменную для хранения большого объёма данных, не забудьте записать в неё null или что-то другое, нужное для дальнейшей работы, после того, как она сыграет свою роль в обработке большого объёма данных.
- Таймеры или забытые коллбэки
Пример:var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { // Сделаем что-нибудь с node и someResource. node.innerHTML = JSON.stringify(someResource)); } }, 1000);
В JS-программах использование функции setInterval — обычное явление.
Большинство библиотек, которые дают возможность работать с обозревателями и другими механизмами, принимающими коллбэки, заботятся о том, чтобы сделать недоступными ссылки на эти коллбэки после того, как экземпляры объектов, которым они переданы, становятся недоступными. Однако, в случае с setInterval весьма распространён следующий шаблон:
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //Это будет вызываться примерно каждые 5 секунд.
В этом примере показано, что может происходить с таймерами, которые создают ссылки на узлы DOM или на данные, которые в определённый момент больше не нужны.
Объект, представленный переменной renderer, может быть, в будущем, удалён, что сделает весь блок кода внутри обработчика события срабатывания таймера ненужным. Однако, обработчик нельзя уничтожить, освободив занимаемую им память, так как таймер всё ещё активен. Таймер, для очистки памяти, надо остановить. Если сам таймер не может быть подвергнут операции сборки мусора, это будет касаться и зависимых от него объектов. Это означает, что память, занятую переменной serverData, которая, надо полагать, хранит немалый объём данных, так же нельзя очистить.
В случае с обозревателями, важно использовать явные команды для их удаления после того, как они больше не нужны (или после того, как окажутся недоступными связанные объекты).
Раньше это было особенно важно, так как определённые браузеры (старый добрый IE6, например) были неспособны нормально обрабатывать циклические ссылки. В наши дни большинство браузеров уничтожают обработчики обозревателей после того, как объекты обозревателей оказываются недоступными, даже если прослушиватели событий не были явным образом удалены. Однако, рекомендуется явно удалять эти обозреватели до уничтожения объекта. Например:
var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
counter++;
element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);
// Сделать что-нибудь
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Теперь, когда элемент выходит за пределы области видимости,
// память, занятая обоими элементами и обработчиком onClick будет освобождена даже в старых браузерах,
// которые не способны нормально обрабатывать ситуации с циклическими ссылками.
В наши дни браузеры (в том числе Internet Explorer и Microsoft Edge) используют современные алгоритмы сборки мусора, которые выявляют циклические ссылки и работают с соответствующими объектами правильно. Другими словами, сейчас нет острой необходимости в использовании метода removeEventListener перед тем, как узел будет сделан недоступным.
Фреймворки и библиотеки, такие, как jQuery, удаляют прослушиватели перед уничтожением узлов (при использовании для выполнения этой операции собственных API). Всё это поддерживается внутренними механизмами библиотек, которые, кроме того, контролируют отсутствие утечек памяти даже если код работает в не самых благополучных браузерах, таких как уже упомянутый выше IE 6.
-
Забытые обработчики событий
Обработчики следует удалять, когда они становятся не нужны, или ассоциированные с ними объекты становятся недоступны.
В прошлом это было критично, так как некоторые браузеры (Internet Explorer 6) не умели грамотно обрабатывать циклические ссылки.
Большинство современных браузеров удаляет обработчики событий, как только объекты становятся недостижимы.
Однако по-прежнему правилом хорошего тона остаётся явное удаление обработчиков событий перед удалением самого объекта.
Рекомендуется явно удалять обработчики событий (removeEventListener) до удаления DOM-узлов или обнулять ссылки внутри обработчиков. -
Замыкания
Одна из важных и широко используемых возможностей JavaScript — замыкания. Это — внутренняя функция, у которой есть доступ к переменным, объявленным во внешней по отношению к ней функции. Особенности реализации среды выполнения JavaScript делают возможной утечку памяти в следующем сценарии:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // ссылка на originalThing
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
Самое важное в этом фрагменте кода то, что каждый раз при вызове replaceThing, в theThing записывается ссылка на новый объект, который содержит большой массив и новое замыкание (someMethod). В то же время, переменная unused хранит замыкание, которое имеет ссылку на originalThing (она ссылается на то, на что ссылалась переменная theThing из предыдущего вызова replaceThing). Во всём этом уже можно запутаться, не так ли? Самое важное тут то, что когда создаётся область видимости для замыканий, которые находятся в одной и той же родительской области видимости, эта область видимости используется ими совместно.
В данном случае в области видимости, созданной для замыкания someMethod, имеется также и переменная unused. Эта переменная ссылается на originalThing. Несмотря на то, что unused не используется, someMethod может быть вызван через theThing за пределами области видимости replaceThing (то есть — из глобальной области видимости). И, так как someMethod и unused находятся в одной и той же области видимости, ссылка на originalThing, записанная в unused, приводит к тому, что эта переменная оказывается активной (это — общая для двух замыканий область видимости). Это не даёт нормально работать сборщику мусора.
Если вышеприведённый фрагмент кода некоторое время поработает, можно заметить постоянное увеличение потребления им памяти. При запуске сборщика мусора память не освобождается. В целом оказывается, что создаётся связанный список замыканий (корень которого представлен переменной theThing), и каждая из областей видимости этих замыканий имеет непрямую ссылку на большой массив, что приводит к значительной утечке памяти.
Эту проблему обнаружила команда Meteor, у них есть отличная статья, в которой всё это подробно описано.
- Ссылки на удалённые из DOM элементы Ссылки на объекты DOM за пределами дерева DOM
Иногда может оказаться полезным хранить ссылки на узлы DOM в неких структурах данных. Например, предположим, что нужно быстро обновить содержимое нескольких строк в таблице. В подобной ситуации имеет смысл сохранить ссылки на эти строки в словаре или в массиве. В подобных ситуациях система хранит две ссылки на элемент DOM: одну из них в дереве DOM, вторую — в словаре. Если настанет время, когда разработчик решит удалить эти строки, нужно позаботиться об обеих ссылках.
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// Изображение является прямым потомком элемента body.
document.body.removeChild(document.getElementById('image'));
// В данный момент у нас есть ссылка на #button в
// глобальном объекте elements. Другими словами, элемент button
// всё ещё хранится в памяти, она не может быть очищена сборщиком мусора.
}
Есть ещё одно соображение, которое нужно принимать во внимание при создании ссылок на внутренние элементы дерева DOM или на его концевые вершины.
Предположим, мы храним ссылку на конкретную ячейку таблицы (тег ) в JS-коде. Через некоторое время решено убрать таблицу из DOM, но сохранить ссылку на эту ячейку. Чисто интуитивно можно предположить, что сборщик мусора освободит всю память, выделенную под таблицу, за исключением памяти, выделенной под ячейку, на которую у нас есть ссылка В реальности же всё не так. Ячейка является узлом-потомком таблицы. Потомки хранят ссылки на родительские объекты. Таким образом, наличие ссылки на ячейку таблицы в коде приводит к тому, что в памяти остаётся вся таблица. Учитывайте эту особенность, храня ссылки на элементы DOM в программах.
Как оптимизизровать JS-часть сайта?
- Проверьте зависимости проекта. Избавьтесь от всего ненужного.
- Разделите код на небольшие фрагменты вместо того, чтобы складывать его в один большой файл.
- Откладывайте, в тех ситуациях, когда это возможно, загрузку JS-скриптов. При обработке текущего маршрута пользователю можно выдавать только тот код, который необходим для нормальной работы, и ничего лишнего.
- Используйте инструменты разработчика и средства вроде DeviceTiming для того, чтобы находить узкие места своих проектов.
- Используйте средства вроде Optimize.js для того, чтобы помочь парсерам определиться с тем, какие фрагменты кода им нужно обработать как можно скорее.
Ссылки
Примитив - Symbol
Символ (анг. Symbol) — это уникальный и неизменяемый тип данных, который может быть использован как идентификатор для свойств объектов. Символьный объект (анг. symbol object) — это объект-обёртка (англ. wrapper) для примитивного символьного типа.
Появился в ES6 (в 2015 году).
Ссылки:
- learn.javascript.ru
- https://developer.mozilla.org (ru)
- Habr - Особенности использования типа данных Symbol в JavaScript
_переменная
Общеприянтое соглашение - если название переменной начинается с _ , её не надо менять или читать снаружи объекта.
Это просто соглашение об именовании, которое напоминает разработчику о том, что переменная (свойство) или метод являются либо private, либо protected, и к ним нельзя получить доступ из-за пределов класса.
Чтоб делать это - используй специальные методы:
- сеттеры (set... - присвоить)
- геттеры (get... - получить)
Ссылки
ПЕРЕМЕННАЯ
Общеприянтое соглашение - если название переменной написано ЗАГЛАВНЫМИ, её не надо менять. Это константа.
Ссылки
Переменная
Общеприянтое соглашение - если название переменной начинается с заглавной, значит это не переменная а класс ООП.
У класса есть методы и всё такое...
Ссылки
true && expression
true && expression</b>
- всегда вычисляется как expression,
false && expression
- всегда вычисляется как false.
Console.log
Брайзер добавляет глобальную переменную с именем «console» к каждой загруженной веб-странице. Объект содержит много методов, которые возволят писать на консоль и показывать информацию, проходящую через скрипты.
Console.dir(object)
Позволяет смотреть в консоли свойства заданного javascript объекта.
Также когда нужно как-то указать в логах на DOM-узел - лучшего всего использовать методы console.dir() или console.dirxml(). Они могут перечислить свойства элемента или вывести HTML кода элемента.
При помощи метода console.dir() можно вывести список всех свойств объекта. Выглядит аналогично тому, что Вы бы увидели во вкладке DOM.
Группировка
Иногда бывает полезно сгруппировать логи для упрощения работы с ними.
- console.group()
- console.groupCollapsed()
- console.groupEnd()
console.group("Overlord");
console.log("Overlord stuff");
console.group("Lord");
console.log("Overlord stuff");
console.group("Minion");
console.log("Minion stuff");
console.groupEnd();
console.groupCollapsed("Servant");
console.log("Servant stuff");
Раскраска
Методы подобные log, но отличающихся внешне:
- console.info()
- console.warn()
- console.error()
Шаблонные строки
console.log(`Значение переменной = ${var_name}`)
Профилирование и замеры
Консоль позволяет точно замерять время, используя метод console.time() и console.timeEnd(). Расположите вызов первого из них перед кодом, время исполнения которого хотите замерить, а второго — после.
console.time("Execution time took");
// Some code to execute
console.timeEnd("Execution time took");
Таймеры связаны между собой метками (передаются первым аргументом и могут быть любой строкой), так что Вы можете запустить несколько таймеров одновременно. Когда сработает console.timeEnd(), будет выведено сообщение с меткой и прошедшим временем в миллисекундах.
Помимо замера времени можно профилировать Ваш код и вывести стек профилирования, который подробно показывает, где и сколько времени потратил браузер.
console.profile();
// Some code to execute
console.profileEnd();
Assert Полезно при работе с unit-тестами.
Assert'ы позволяют обеспечивать соблюдение правил в коде и быть уверенным, что результаты выполнения этого кода соответствуют ожиданиям. Метод console.assert() позволяет проводить элементарное тестирование кода: если что-то пойдет не так, будет выброшено исключение. Первым аргументом может быть все, что угодно: функция, проверка на равенство или проверка существования объекта.
var a = 1, b = "1";
console.assert(a === b, "A doesn't equal B");
Метод assert принимает условие, которое является обязательным к выполнению (в данном случае простая строгая проверка на равенство) и, вторым аргументом, сообщение, которое будет выведено в консоль вместе с выброшенным исключением, если первое условие не будет выполнено.
Console.trace()
Вывод стека вызовов до текущего момента. Скажет, какие функции есть в стеке, и какие аргументы были переданы каждой.
Ещё есть
- console.clear - очищает консоль
- console.count - выводит, сколько раз данный код был выполнен.
- console.dirxml - выводит XML код элемента
- console.exception - выводит ошибку и результат trace() для места, откуда она была вызвана
- console.table - выводит таблицу (Подробнее)
- console.timeStamp - выводит текущий timestamp с текстом, который был передан в name.
Ссылки:
- habr - Используем console на полную
- habr - FireBug* Console API
- learn.javascript.ru - строки-шаблоны
- MDN - console.dir
- MDN - console.trace
- Про console.table (en)
Чистота кода
Общее
- Форматирование кода направлено на передачу информации, а передача информации является первоочередной задачей профессионального разработчика.
- Фигурные скобки в одном стиле
- Кавычки в одном стиле
- Точка с запятой - ставить
- Длина строки - 120 символов
- Отступы горизонтальные - не нарушать структуру
- Отступы вертикальные - не более 9 строк кода подряд без вертикального отступа.
- Имя любой сущности должно отвечать на 3 вопроса - "Почему она существует?", "Какие функции выполняет?", "Как она используется?"
- Имя переменной – существительное.
- Имя функции – глагол или начинается с глагола.
- Уровней вложенности должно быть немного.
- Вначале код, под ним функции
- Большие функции дробить на мелкие
- Функции = Комментарии
- Разумные комментарии - не "Что делает?", а "Как устроено?", "Какие параметры принимает?", "Почему выбрано это решение?".
- Принцип единственной обязанности
- Разделение команд и запросов - не смешивать функции, выполняющие запросы (например, получить имя) и функции выполняющие команды (например, привести имя к нижнему регистру)
- Слабое связывание - это хорошо, сильное - плохо. Сильное связывание = сильная зависимость разных частей программы друг от друга.
- Высокий ровень связности - хорошо (не путать с сильным связыванием). Низкий - плохо. Высокий - сбор конструкций, объединённых общей идеей, в одном месте.
- Изоляция кода - выделять фрагменты кода в отдельные блоки, основываясь на их предназначении. В качестве таких блоков обычно выступают функции.
- Разбивка кода на модули - функции, которые используются похожим образом или выполняют похожие действия, можно сгруппировать в одном модуле (или, если хотите, в отдельном классе).
- Признак слаженности команды - читая код, ты не можешь понять, написал его ты, или коллега
Конкретика
- Вместо == использовать ===
- Избегать "магических чисел"
- Имя переменной должно раскрывать её сущность
- Чем меньше у функции аргументов — тем лучше (спорно). Например, её будет легче тестировать. С другой стороны: если у функции N параметров, по первой строчке её объявления сразу видно - что нужно ей передать. Но, больше 5-6 параметров - перебор
- если функции нужно более 5-6 парамтеров - стоит подумать об использовании объекта с параметрами.
- Используйте аргументы по умолчанию, отдавая им предпочтение перед условными конструкциями
- Используйте Object.assign для установки свойств объектов по умолчанию
- Не используйте флаги в качестве параметров (isOpen и т.д.). Их использование означает, что функция выполняет больше действий, чем следует.
- Не загрязняйте глобальную область видимости
- Не называть логические переменные так, чтобы в их именах присутствовало бы отрицание (notAdmin -> isAdmin)
- Избегайте логических конструкций везде, где возможно. Вместо них используйте полиморфизм и наследование
- ES-классы стоит предпочесть обычным функциям-конструкторам
- Организуйте методы так, чтобы их можно было бы объединять в цепочки - в конце каждой из функций класса нужно возвращать this
- Удаляйте неиспользуемый код
- Если описывая, что должна делать функция, вы используете союз «и» - эта функция слишком сложна
- Функция должна решать одну задачу
- Большие функции стоит перерабатывать в классы. Если функция решает много задач, сильно связаных друг с другом, в которых используются одни и те же данные - имеет смысл переделать её в объект с методами
- Если имя функции отвечает на некий вопрос - она должна возвращать значение, а не менять состояние данных.
- Если имя функция "что-то делает" - она должна менять данные и не должна ничего возвращать
- Жёстко заданные ID в функциях - признак сильного связывания
- Несколько сильно завиясщих друг от друга функций, по сути = одна большая, просто разделённая на части. Избегай этого
- Если возникла необходимость модифицировать класс из-за изменений другого класса - это признак сильного связывания
- Если несколько функций используют одни и те же переменные - они должны быть сгруппированы. Хороший повод объединить их в объект.
- Одна и та же строка кода не должна повторяться дважды. Повторяющийся код — это надёжный признак низкого уровня связности. Плохо.
- Одни и те же данные не должны храниться в более чем одной переменной. Если определяете переменные с одинаковыми данными в разных местах программы - используйте класс.
- Если вы передаёте ссылку на один HTML-элемент в несколько функций - можно сделать ссылку частью экземпляра некоего класса.
- Не стоит собирать в одном классе сущности, не имеющие друг к другу никакого отношения.
- Если свойства не используются несколькими методами класса, это может быть признаком низкого уровня связности. Плохо.
- Если методы нельзя использовать в различных ситуациях (или метод вообще не используется) — признак плохой связности. Плохо.
- Возвращать что-либо из функций нужно с помощью ключевого слова return.
- Для экспорта самых важных сущностей, объявленных в модуле, используйте возможности экспорта по умолчанию. Для второстепенных сущностей можно применить именованный экспорт.
- Используйте деструктурирование
- Задавайте стандартные значения параметров функций
- Не передавайте функциям ненужные данные
- Ограничивайте размер файла. 100 строк - хорошо. 200-300 - приемлемо. Более 400 - не надо
- Вложенность кода не должна превышать четырёх уровней.
- Имена массивов. Массивы обычно содержат в себе наборы каких-то значений. В результате к имени переменной, хранящей массив, имеет смысл добавлять букву s. (students)
- Имен алогических значений - имеет смысл начинать с is или has.
- Имена параметров функций, передаваемых стандартным методам массивов - лучше называть с учётом данных, которые в них оказываются
- Использование коллбэков ухудшает читабельность кода. Особенно это касается вложенных коллбэков. Где возможно - используйте конструкцию async/await
- Подчищать за собой console.log. Лишние выводы захламляют консоль. Использование отладочного кода может негативно сказаться на производительности. Но, некоторые логи имеет смысл оставлять. Например — команды, выводящие сообщения об ошибках и предупреждения.
- Классы не должны быть длиннее 100 строк кода.
- Методы и функции не должны быть длиннее 5 строк кода.
- Методам следует передавать не более 4 параметров.
- Контроллеры могут инициализировать лишь один объект.
Книги
- Макконелл С - Совершенный код
Руководства по стилю
- Google JavaScript Style Guide (en)
- jQuery JavaScript Style Guide (en)
- Airbnb JavaScript Style Guide (en)
- Airbnb JavaScript Style Guide (ru)
- Idiomatic.JS (en)
- Idiomatic.JS (ru)
- Dojo Style Guide (en)
- JSLint style (en)
Автоматизированные средства проверки (линтеры)
Ссылки
- learnjavascript - Как писать неподдерживаемый код?
- learnjavascript - Советы по стилю кода
- habr - Рекомендации по написанию чистого кода на JavaScript
- habr - JavaScript: путь к ясности кода
- habr - 7 рекомендаций по оформлению кода на JavaScript
- habr - Как писать чистый и красивый код
- habr - Пишем чистый и масштабируемый JavaScript-код: 12 советов
- YouTube - Доклад Сэнди Метц о 4 правилах написания чистого кода в объектно-ориентированных языках (en)
История версий ES
JavaScript создавался как скриптовый язык для Netscape. Изначально разработкой занимались Брендан Эйх, Марк Андрессен и Билл Джой.
После чего он был отправлен в ECMA International для стандартизации (ECMA — это ассоциация, деятельность которой посвящена стандартизации информационных и коммуникационных технологий). Стандартизированная версия имеет название ECMAScript, описывается стандартом ECMA-262.
ECMAScript — стандарт, а JavaScript — самая популярная реализация этого стандарта.
Среди других реализаций можно отметить SpiderMonkey, V8 и ActionScript.
ECMAScript - стандарт, развивается и поддерживается ассоциацией ECMA International. Ecma International Technical Committee 39 (он же TC39) — комитет очень умных людей :) Задача TC39 - поддержка и обновление спецификации ECMAScript, после обсуждения и всеобщего согласия. Сюда относятся синтаксис языка, семантика, библиотеки и сопутствующие технологии, на которых основывается язык.
С 2015 года принято решение обновлять язык ежегодно.
ES.Next - термин является динамическим и автоматически ссылается на новую версию ECMAScript.
- ES1 - 1997
- ES2 - 1998
- ES3 - 1999
- ES4 - не выпущена
- ES5 - 2009
- ES6 - 2015
- ES7 - 2016
- ES8 - 2017
- ES9 - 2018
Ссылки
- ES6, ES8, ES2017: что такое ECMAScript и чем это отличается от JavaScript
- Официальная спецификация - актуальная (en)
- Официальная спецификация - архив (en)
- Разъяснения насчёт JavaScript, ECMA–262, TC39 и транскомпиляторов ECMAScript
- Обзор новшеств ECMAScript 2016, 2017, и 2018 с примерами
ES10 (ECMAScript 2019 )
Выход ожидается летом 2019
Предварительные ожидания:
- Изменения в классах JS - строковая декларация, приватные методы и поля, статические методы и поля.
- flat() и flatMap() - позволяет рекурсивно сгладить массивы до заданной глубины и вернуть новый массив. Т.е. многомерный массив сделать одномерным.
- Object.fromEntries - выполняет обратное Object.entries. Преобразует список пар ключ-значение в объект.
- Строковые методы trimStart() и trimEnd() - удаление пробелов в начале/конце строки
- Примитив BigInt - для целых чисел больших чем (253 - 1).
- Optional Catch Binding - теперь не обязательно иметь привязку переменной исключения к оператору catch.
- JSON - подкорректированы символы разделителя строк и абзацев
- JSON.stringify - представление кодов с помощью escape-последовательностей JSON
- .toString() - теперь возвращает точные фрагменты текста исходного кода, включая пробелы и комментарии.
- Symbol.prototype.description - свойство, возвращает необязательное описание объекта Symbol
- Стандартизированный объект globalThis
- Динамический import()
- import.meta
- Строковый метод matchAll()
- Стандартизированный Hashbang для приложений с интерфейсом командной строки (CLI)
Ссылки
ES9 (ECMAScript 2018 )
- Разделяемая память (shared memory) и атомарные операции (atomics) - касается ядра JS-движков. Позволяет писать высокопроизводительные параллельные приложения, дает возможность управлять памятью самостоятельно, не отдавая выполнение всех аспектов этой задачи JS-движку.
- Оператора rest - выглядит как три точки. Позволяет извлекать свойства объекта. Используется в левой части выражения.
- Оператор spread - тоже выглядит как три точки. Используется для создания новых объектов. Используется в правой части выражения со знаком присваивания.
- Асинхронная итерация, цикл for-await-of - позволяет создавать циклы, работающие с асинхронным кодом. Добавляется новый оператор цикла вида for-await-of, который позволяет вызывать асинхронные функции, возвращающие промисы (или обрабатывать массивы, содержащие промисы) в цикле.
- Метод finally() — это новый метод объектов Promise. Позволяет выполнять функцию обратного вызова после resolve() или reject(), чтобы корректно завершать операции (например, высвобождая ресурсы).
- Устранение ограничений тегированных шаблонных строк - больше свободы, что писать в шаблонных строках.
- Регулярки. Флаг dotAll - изменили настройки регулярок. Чтоб работать в новом формате - устанавливаем спец. флаг.
- Регулярки. Захват именованных групп - позволяет писать регулярные выражения с назначением имён (идентификаторов) для групп. Облегчает работу с группами.
- Регулярки. Ретроспективная проверка - позволяет узнать, существует ли некая строка сразу перед некоторой другой строкой.
- Регулярки. Улучшена поддержка Unicode - можно использовать спец. конструкцию для поиска символов не-латинских языков(хинди, греческий...)
Ссылки
- Официальная спецификация (en)
- Обзор новшеств ECMAScript 2016, 2017, и 2018 с примерами
- Что нового в ES2018 JavaScript
ES8 (ECMAScript 2017)
- Конструкция Async/Await - асинхронные функции, работают на основе promise
- Метод Object.values() - возвращает все значения собственных свойств объекта, исключая любые значения в цепочке прототипов.
- Меотд Object.entries() - похож на метод Object.keys(), но вместо того, чтобы возвращать лишь ключи, он возвращает, в виде массива, и ключи, и значения. Упрощает выполнение операций c объектами в циклах, или преобразование обычных объектов в объекты типа Map.
- Методы дополнения строк до заданной длины - String.prototype.padStart() и String.prototype.padEnd().
- Метод Object.getOwnPropertyDescriptors() - возвращает все сведения (включая данные о геттерах и сеттерах) для всех свойств заданного объекта. Позволяет создавать мелкие копии объектов и клонировать объекты, создавая новые объекты, при этом копируя, помимо прочего, геттеры и сеттеры.
- Теперь можно ставить завершающие запятые после последнего параметра функции
Ссылки
ES7 (ECMAScript 2016)
- Метод Array.prototype.includes() - метод объектов типа Array, который позволяет выяснить, имеется ли в массиве некий элемен.
- Оператор возведения в степень - **. Заменяет Math.pow().
Ссылки
ES6 (ECMAScript 2015)
- Переменные: let и const
- Деструктуризация
- Функции
- Строки - введены шаблоны, улучшена поддержка Unicode, добавлены методы
- Объекты и прототипы
- Классы
- Тип данных Symbol - для создания уникальных идентификаторов
- Итераторы - можно сделать "перебираемым любой" объект
- Set, Map, WeakSet и WeakMap - новые типы коллекций
- Promise - способ организации асинхронного кода
- Генераторы - новый вид функций. Могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять выполнение позже.
- Модули - введён официальный стандрат поддержки модулей в JS
- Proxy - особый объект, перехватывает обращения к другому объекту и, при необходимости, модифицирует их.
Ссылки
ES5 (ECMAScript 2009)
Среди изменений:
- поддержка строгого режима (strict mode);
- аксессоры getters и setters;
- возможность использовать зарезервированные слова в качестве ключей свойств и ставить запятые в конце массива;
- многострочные строковые литералы;
- поддержка JSON
- и ещё очень много всего - 10 лет готовили...
Ссылки
- Официальная спецификация (en)
- ES6, ES8, ES2017: что такое ECMAScript и чем это отличается от JavaScript
- ES5 руководство по JavaScript
- Перевод спецификации EcmaScript 5 с аннотациями
Языки поверх JavaScript
Синтаксис JavaScript устраивает не всех - одним он кажется слишком свободным, другим слишком ограниченным, третьи хотят добавить дополнительные возможности…
Появилось много языков, которые добавляют различные возможности «поверх» JavaScript. Для запуска в браузере они превращаются в обычный JS-код (при помощи специальных инструментов «трансляторов»).
Это преобразование происходит автоматически и совершенно прозрачно, при этом неудобств в разработке и отладке практически нет.
Разные языки выглядят по-разному и добавляют разные вещи:
- CoffeeScript – «синтаксический сахар» поверх JavaScript. Сосредоточен на большей ясности и краткости кода. Часто его любят программисты на Ruby.
- TypeScript - сосредоточен на добавлении строгой типизации данных. Предназначен для упрощения разработки и поддержки больших систем. Разрабатывается Microsoft.
- Dart - не только транслируется в JS, но имеет и свою независимую среду выполнения, которая даёт ему ряд возможностей и доступна для встраивания в приложения (вне браузера). Разрабатывается компанией Google.
Ссылки: