Полчаса на изучение JavaScript

Полчаса на изучение JavaScript

На момент написания этой статьи, я занимался изучением JavaScript уже длительное время. Я мог строить алгоритмы в собственной голове, но объяснить их кому-то из мира IT составляло для меня трудности.

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

Но когда на собеседовании меня попросили объяснить, что такое замыкание, я был ошеломлен. Я интуитивно понимал это, но не мог выразить свои мысли словами.

Но действительно ли вы что-то знаете что-то, если не можете это объяснить?

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

Область видимости

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

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

В JavaScript у нас есть три типа области видимости - глобальная область видимости, область действия функции и область видимости блока.

Область видимости – это некоторая сущность JavaScript, которая определяет границы действия переменных.

Создаются области видимости во время выполнения программы. Самая первая область, которая создаётся и которая включает в себя все остальные называется глобальной.

Именно в этой области определены такие переменные как window в веб-браузере и global в Node.js.

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

let num = 5; const fruits = ['apple', 'pears', 'banana'];

Переменные объявленные в глобальной области видимости называются глобальными переменными. Такие переменные могут быть доступны в любой точке программы.

Кроме глобальной области видимости в JavaScript имеются ещё локальные. Они, создаются, когда интерпретатор, например, выполняет код блочной конструкции:

// глобальная переменная let a = 5; { // локальная переменная let b = 17; }
Полчаса на изучение JavaScript

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

Переменные, объявленные внутри блока с помощью let и const имеют область видимости ограниченную этим блоком. Т.е. они привязаны к нему и будет действовать только в его рамках. Переменные, объявленные в локальной области видимости называются локальными.

if (true) { // локальная переменная let b = 17; // выведем значение переменной b в консоль console.log(b); // 17 } console.log(b); // Uncaught ReferenceError: b is not defined

Под блоком в JavaScript понимается любой код, который расположен в фигурных скобках { ... }. Блоки используются в конструкциях if, for, while и т.д. Даже тело функции является блоком, т.к. находится между фигурными скобками.

Кроме этого локальные области видимости также создаются вызовами функций и модулями. Они соответственно называются областью видимости функции и областью видимости модуля.

Пример, в котором создаётся две функциональные области видимости:

// глобальная область видимости function salute(welcomeText) { console.log(welcomeText); } salute('Привет'); // вызов функции salute salute('Здравствуйте'); // вызов функции salute

В этом примере в глобальной области видимости объявляется функция salute с помощью ключевого слова function. Затем эта функция вызывается два раза.

Область видимости функции создаётся для каждого вызова функции. Даже, когда мы вызываем одну и ту же функцию. При этом для каждого вызова создаётся своя отдельная область видимости.

В этом примере будут созданы две локальные области видимости уровня функции.

const name = 'Iymrith, the Giant' function logGlobalName() { console.log(name) } console.log(name) // Iymrith, the Giant logGlobalName() // Iymrith, the Giant

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

const name = 'Iymrith, the Giant' function logTrueName() { const name = 'Iymrith, the Blue Dragon' console.log(name) } console.log(name) // Iymrith, the Giant logTrueName() // Iymrith, the Blue Dragon

Цепочка областей видимости

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

// глобальная область видимости let a = 5; let b = 8; let c = 20; function fnA() { a = 7; b = 10; let b = 11; b = 13; function fnB() { let c = 25; console.log(a); // 7 console.log(b); // 13 console.log(c); // 25 } fnB(); } fnA();

В момент выполнения console.log(a) мы имеем следующую картину:

Полчаса на изучение JavaScript

Начинается этот код с создания переменных a, b, c и fnA с соответствующими значениями в глобальной области видимости.

После этого вызывается функция fnA(). При её вызове создаётся область видимости функции, которая имеет ссылку на внешнюю область. В данном случае ей является глобальная область видимости.

Далее переменной a присваивается значение 7.

a = 7;

Но перед тем, как присвоить ей значение, нам необходимо сначала её найти. Поиск переменной всегда начинается с текущей области видимости. Но переменной и параметра a в области видимости вызова функции fnA() нет. Поэтому мы переходим по ссылке, в данном случае ведущую в глобальную область видимости и ищем переменную там. В данном примере такая переменная здесь имеется, и мы присваиваем ей значение 7. Таким образом, на этом шаге мы внутри функции fnA присвоили новое значение глобальной переменной a.

На следующей строчке мы присваиваем переменной b значение 10:

b = 10;

На текущий момент у нас ещё нет объявленной переменной bb в текущей области видимости. Поэтому мы также переходим по ссылке в глобальную область видимости и находим эту переменную там. После этого задаём ей новое значение. На этом этапе выполнения кода у нас в глобальной области видимости переменные a и b имеют соответственно значения 7 и 10.

Затем мы объявляем переменную b в локальной области функции, созданной вызовом fnA() и в этом же выражении сразу же ей присваиваем число 11:

let b = 11;

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

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

Таким образом, на следующей строчке будет использоваться переменная b, объявленная в текущей области видимости:

b = 13;

После этого объявляется функция fnB. Затем она вызывается fnB() и интерпретатор создаёт новую область видимости внутри fnA. Эта область видимости в свою очередь тоже содержит ссылку на внешнюю по отношению к ней область видимости. В данном случае, на ту, которая была создана ранее при вызове fnA(). В итоге у нас получается цепочка областей видимости.

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

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

В этой области (в данном случае созданной в результате вызова fnA()) переменной a тоже нет. Но есть ссылка на следующую область, которая в данном случае является глобальной. Переходим по ней и пытаемся найти переменную там. В ней эта переменная есть. А, следовательно, берём эту переменную и выводим её значение в консоль. В данном случае, число 7.

Итак, поиск переменной интерпретатор JavaScript всегда начинает с текущей области видимости. Если она в ней имеется, то поиск прекращается и берётся эта переменная. В противном случае интерпретатор в поиске переменной переместится к следующей области, содержащейся в ссылке, и попробует отыскать её там. После этого действия повторяются, т.е. при отсутствии искомой переменной в просматриваемой области видимости, интерпретатор перемещается к следующей области посредством ссылки и пытается обнаружить её там.

В результате поиск всегда заканчивается одним из двух нижеприведённых сценариев:

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

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

Рассмотрим ещё один очень интересный пример:

let num = 10; function fnA() { console.log(num); } function fnB() { let num = 20; fnA(); } fnB();

В этом примере в глобальной области видимости объявлены переменные num, fnA и fnB. При вызове функции fnB() у нас создаётся локальная область видимости, которая будет содержать ссылку на внешнюю, в данном случае на глобальную область видимости. В этой области у нас создаётся переменная с таким же именем num, а затем вызывается функция fnA(). При вызове функции fnA() у нас создаётся локальная область видимости, которая будет иметь в качестве ссылки глобальную область. Почему так? Потому что внешняя область определяется в зависимости от того, где объявлена функция, а не вызвана. А так как функция объявлена в глобальной области видимости, то не зависимого того где она вызвана, она будет содержать в качестве ссылки - ссылку на внешнюю область видимости, в зависимости от того где она объявлена.

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

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

Область действия функции делает переменную доступной только внутри функции, в которой она была создана.

function logName() { const name = 'Iymrith, the Blue Dragon' } console.log(name) // ReferenceError: name is not defined

То же самое относится и к области действия блока.

if (true) { const name = 'Iymrith, the Blue Dragon' } console.log(name) // ReferenceError: name is not defined

Использование ключевого слова в области видимости

Но область видимости - это не просто вопрос того, где вы размещаете переменную. Она зависит от ключевого слова, которое вы используете для этого. Переменные, объявленные с помощью var, имеют функциональную область, в то время как переменные, объявленные с помощью let и const имеют блочную область.

Переменная, объявленная с помощью var, доступна даже вне блока, если она находится в той же функции.

function logName() { if (true) { var name = 'Iymrith, the Blue Dragon' } console.log(name) } logName() // Iymrith, the Blue Dragon

Но она не будет доступна, если условный оператор не выполняется.

function logName() { if (false) { var name = 'Iymrith, the Blue Dragon' } console.log(name) } logName() // undefined

Разбираемся с “поднятием” (hoisting) в JavaScript

Обратите внимание, что мы не получили ReferenceError, когда использовали var. Мы получили его, когда использовали const. Так произошло из-за явления, называющегося поднятием. Это механизм в JavaScript, в котором переменные и объявления функций, передвигаются вверх своей области видимости перед тем, как код будет выполнен.

Запомните и держите в уме одну важную деталь, JavaScript непреклонно сначала объявляет, а уже затем инициализирует наши переменные.

Поднятие позволяет переменным var быть доступными во всей функции, а функциям быть вызываемыми ещё до того, как они определены.

logName() // Iymrith, the Blue Dragon function logName() { const name = 'Iymrith, the Blue Dragon' console.log(name) }

Самый простой способ объяснить, как работает поднятие - это представить, что все переменные, объявленные с помощью var, и все функции, объявленные с помощью ключевого слова function, при выполнении перемещаются в верхнюю часть своей области видимости.

ogName() // Iymrith, the Blue Dragon function logName() { const name = 'Iymrith, the Blue Dragon' console.log(name) } // Turns into... function logName() { const name = 'Iymrith, the Blue Dragon' console.log(name) } logName() // Iymrith, the Blue Dragon

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

function logName() { if (false) { var name = 'Iymrith, the Blue Dragon' } console.log(name) } name() // undefined // Turns into... function logName() { var name if (false) { name = 'Iymrith, the Blue Dragon' } console.log(name) } logName() // undefined

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

logName() // TypeError: logName is not a function var logName = function () { const name = 'Iymrith, the Blue Dragon' console.log(name) } // Turns into... var logName logName() // TypeError: logName is not a function logName = function () { const name = 'Iymrith, the Blue Dragon' console.log(name) }

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

Мы использовали var для примеров поднятия, потому что let и const не подходят для реализации этого. Приведённый ниже код будет работать:

function logName() { console.log(name) var name = 'Iymrith, the Blue Dragon' } logName() // undefined

А этот выдаст ошибку:

function logName() { console.log(name) const name = 'Iymrith, the Blue Dragon' } logName() // ReferenceError

И то же самое относится к переменным, объявленным с помощью let.

Поднятие(Hoisting) - Правильное объяснение

Представление о том, что переменные, объявленные с помощью var, и определения функций выводятся в начало файла, является хорошей ментальной моделью, когда вы пытаетесь понять фрагмент кода. А теперь о том, как на самом деле работает поднятие в JavaScript.

Движок компилирует JavaScript в машинный код перед его выполнением. И в процессе этой компиляции он многократно прогоняет наш код. В одном из ранних запусков он объявит все функции и переменные.

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

Объявление переменной

В настоящее время больше нет объективных причин использовать var.

По умолчанию используется const, когда ожидается, что значение переменной не изменится, и let, когда вы собираетесь переназначить её или изменить. Одно из явлений, которое нам нужно запомнить, заключается в том, что массивы и объекты, определённые с помощью const, всё ещё могут быть изменены, если ссылка не была переназначена.

const character = { name: 'Drizzt', } character = { name: 'Drizzt', race: 'Drow', } // TypeError: Assignment to constant variable

Но если мы добавим свойство к существующему объекту, мы не получим ошибку:

const character = { name: 'Drizzt', } character.race = 'Drow'

Корректное именование переменных

Проще всего было бы использовать let везде и не думать дважды об области видимости и переназначениях. Но объявления переменных - это также способ общения с другими инженерами.

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

Значения и ссылки

Мы упоминали, что свойства в объекте, определённом с помощью const, уже не могут измениться, если были присвоены ранее в коде. Чтобы понять, что это означает, мы должны взглянуть на типы данных JavaScript.

Каждая переменная может быть одного из семи возможных типов - string, number, boolean, undefined, null, symbol и object.

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

const strength = 16 let dexterity = strength dexterity++ console.log(strength === dexterity) // false

Когда мы передаём примитивное значение функции, её значение копируется, и любые изменения, внесённые внутри функции, не будут отражены во внешней переменной.

const strength = 16 function increaseStat(strength) { strength++ } increaseStat(strength) console.log(strength) // 16

Из-за этого мы говорим, что примитивные типы передаются по значению. Содержимое переменной копируется, когда мы присваиваем её другой переменной или передаём функции.

Объекты и массивы, с другой стороны, работают по-другому.

const characterAStats = { strength: 16 } const characterBStats = { strength: 16 } console.log(characterAStats === characterBStats) // false

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

const characterAStats = { strength: 16 } const characterBStats = characterAStats console.log(characterAStats === characterBStats) // true

Это работает, потому что мы назначаем characterBStats для хранения той же ссылки, что и characterAStats. Но если мы что-то изменим в characterBStats, это также будет отражено в characterAStats.

const characterAStats = { strength: 16 } const characterBStats = characterAStats characterBStats.strength = 12 console.log(characterAStats.strength) // 12 console.log(characterBStats.strength) // 12

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

const characterAStats = { strength: 16 } const characterBStats = { ...characterAStats } characterBStats.strength = 12 console.log(characterAStats.strength) // 16 console.log(characterBStats.strength) // 12

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

const characterA = { stats: { strength: 16 } } const characterB = { ...characterA } characterB.stats.strength = 12 console.log(characterA.stats.strength) // 12 console.log(characterB.stats.strength) // 12

Приведение типов

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

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

42 + '' // "42"

Всякий раз, когда вы используете операторы + или -, значения должны быть одного типа. Очевидно, что это условие может не выполняться, тогда JS преобразует число в строку и объединит их.

42 + '0' // "420"

Но если мы используем оператор -, то вместо этого строка будет преобразована в число. Это происходит потому, что строки не имеют механизма вычитания, как это делают числа.

'42' - 7 // 35

Возвращаемое значение

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

const number = 42 const string = 'Drizzt' const empty = null a || b // 42 a && b // "Drizzt" a || c // 42 a && c // null b || c // "Drizzt" b && c // null

Когда вы используете оператор ||, если первый операнд принимает значение true, код вернёт его в результате. В противном случае вы всегда получите второй вариант. В случае && вы всегда получите второе значение, если оба операнда приведены к true. Если первый операнд будет равен false, то вы получите возвращённое значение.

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

function greet(name) { console.log(`Hello, ${name || 'visitor'}!`) } greet() // Hello, visitor!

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

function greet(name = 'visitor') { console.log(`Hello, ${name}!`) } greet() // Hello, visitor!

Равенство

В JavaScript есть два оператора равенства - == и ===. Разница между ними заключается в том, как они справляются с различием в типах между сравниваемыми значениями.

42 == '42' // true 42 === '42' // false

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

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

Замыкание (Closures)

Самое простое возможное объяснение замыкания - это экспортированная вложенная функция.

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

function createCharacter() { const stats = { strength: 16, dexterity: 14, } return { increaseLevel: function () { stats.strength += 2 stats.dexterity += 2 }, getStats: function () { return stats }, } } const character = createCharacter() character.increaseLevel() const stats = character.getStats() console.log(stats.strength) // 18 console.log(stats.dexterity) // 16

Открытые функции increaseLevel и getStats по-прежнему будут иметь доступ к переменной stats, даже если они выполняются в совершенно другой области.

Замыкания являются мощными, потому что они предоставляют нам метод инкапсуляции. Они позволяют нам скрывать данные и предоставлять только ту функциональность, которую мы считаем подходящей.

function createMerchantOrder(items) { function calculateItemTotal(items) { return items.reduce((acc, curr) => { return acc + curr.price }, 0) } function addCityTaxToPrice(price) { return price + price * 0.2 } return { calculateTotal: () => { return addCityTaxToPrice(calculateItemTotal(items)) }, } } const items = [ { name: 'Long Sword', price: 15 }, { name: 'Shield', price: 10 }, ] const order = createMerchantOrder(items) console.log(order.total) // undefined console.log(order.addTaxToPrice) // undefined console.log(order.calculateTotal()) // 30

В этом примере у нас есть несколько определений функций, которые обрабатывают детали расчета общей цены.

Ключевое слово "this"

В объектно-ориентированных языках, таких как Java, ключевое слово this относится к текущему объекту метода или конструктора. Если вы используете его внутри объекта, оно всегда будет ссылаться на него. Но реализация этого в JavaScript немного отличается.

Во-первых, вы можете использовать this в обычных функциях и объектах, а не только в классах.

function createCharacter(name) { return { name, greet: function () { console.log(`${this.name} says hello!`) }, } } const character = createCharacter('Drizzt') character.greet() // Drizzt says hello!

Приведённый выше код выдает результат, который вы ожидали, но значение этого параметра не зависит от функции, которая использует is.

function createCharacter(name) { return { name, greet: function () { console.log(`${this.name} says hello!`) }, } } const { greet } = createCharacter('Drizzt') greet() // " says hello !"

В этом случае мы получаем пустое значение для this.name. Оно не относится к функции, в которой используется, или к его области применения. Оно относится к объекту, на котором выполняется функция.

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

Простой способ запомнить это - посмотреть, какой объект находится в левой части функции.

Ключевое слово "key"

В объектно-ориентированных языках ключевое слово new используется для создания нового экземпляра класса. Несмотря на то, что в настоящее время в JavaScript существуют классы, поведение new немного отличается.

В OO языках ключевое слово new приведёт к вызову конструктора класса. Но в JS нам не нужен класс для создания нового объекта. Для этой цели мы можем использовать простую функцию.

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

function Character(name) { this.name = name } const character = new Character('Drizzt') console.log(character.name) // Drizzt

Когда вы используете new перед функцией, она создаст новый объект, и все привязки к нему внутри функции будут сделаны к этому вновь созданному объекту. Затем он будет возвращён, если функция не предусматривает возвращения другого аргумента.

Но если функция возвращает объект, то вышесказанное недопустимо.

Использование "this" и "new"

В настоящее время у вас будет мало причин использовать this и new вне работы с классами, поэтому вам не придётся помнить обо всех их особенностях. И когда дело доходит до объектно-ориентированного JavaScript, они ведут себя именно так, как вы ожидаете.

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

Прототипы

Есть два способа, с помощью которых вы можете построить иерархию объектов наследования в JavaScript: с помощью прототипов или классов (которые являются просто синтаксическим сахаром поверх прототипов).

Объекты в JS имеют свойство prototype, которое является ссылкой на другой объект.

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

Прототип объекта может быть задан непосредственно путем указания свойства __proto__.

const character = { attack: function () { console.log('Swing!') }, } const fighter = { characterClass: 'Fighter', __proto__: character, } fighter.attack() // Swing!

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

function Character(name) { this.name = name this.attack = function () { console.log(`${this.name} swings!`) } } function Fighter(name) { this.name = name } // Create a reference for the prototype Fighter.prototype = new Character() const fighter = new Fighter('Regdar') fighter.attack() // Regdar swings!

Устанавливая объект-прототип в функции конструктора, мы гарантируем, что все объекты, созданные путём вызова его с помощью new, будут настроены должным образом.

Классы

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

Используя классы, вы позволяете любому инженеру, прошедшему курс ООП, понять ваше приложение.

class Character { constructor(name) { this.name = name } attack() { console.log(`${this.name}: swings!`) } } class Fighter extends Character { constructor(name) { super(name) } } const fighter = new Fighter('Redgar') fighter.attack() // Redgar swings!

Композиция

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

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

function canAttack(name) { return { attack: () => console.log(`${name} swings!`), } } function createCharacter(name) { return { ...canAttack(name), } } const character = createDogEntity('Redgar') character.attack() // Redgar swings!

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

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

В JavaScript лучший способ добиться этого - использовать композиции.

Наследование и производительность

Важно отметить, что если производительность имеет решающее значение, то определение функции в прототипе может быть лучшим вариантом вместо определения её непосредственно в объекте.

Функции, прикреплённые к прототипу, создаются только один раз, в то время как, если они встроены в объект, каждая из них получит свою собственную копию.

Promises

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

При создании promise принимает функцию, которой будут переданы два аргумента - обратный вызов разрешения и обратный вызов отклонения.

const attackRoll = 18 const enemyArmorClass = 16 const canHitTarget = new Promise((resolve, reject) => { attackRoll > enemyArmorClass ? resolve() : reject() })

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

canHitTarget .then(() => { // Handle success }) .catch(() => { // Handle failure })

Метод then доступен для объекта Promise, и он будет выполняться со значением, переданным функции resolve. Catch выполняется, если была вызвана функция reject.

const attackRoll = 18 const enemyArmorClass = 16 const canHitTarget = new Promise((resolve, reject) => { const damage = Math.random() attackRoll > enemyArmorClass ? resolve(damage) : reject() }) canHitTarget .then((damage) => { // Do something with the total damage }) .catch(() => { // We don't pass anything on failure })

Если функция, которая выполняется внутри then, возвращает другое promise, несколько вызовов then могут быть объединены в цепочку.

const addBonus = (damage) => { return new Promise((resolve) => { resolve(damage + 2) }) } canHitTarget.then(addBonus).then((totalDamage) => { // Do something with the total damage })

Async/Await

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

async function getAttackDamage() { const attackDamage = await canHitTarget() const totalDamage = await addBonus(attackDamage) return totalDamage }

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

Вы все еще можете использовать .catch для реагирования на выданную ошибку, но для поддержания согласованности стиля было бы лучше полагаться на try-catch при работе с await.

async function getAttackDamage() { try { const attackDamage = await canHitTarget() const totalDamage = await addBonus(attackDamage) return totalDamage } catch (err) { // Attack was not successful return 0 } }

Цикл событий

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

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

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

setTimeout(() => { console.log('Later') }, 1000) console.log('Now') // Output: // Now // Later

Но даже если мы установим тайм-аут 0, код всё равно будет выполняться после второго входа в систему.

setTimeout(() => { console.log('Later') }, 0) console.log('Now') // Output: // Now // Later

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

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

setTimeout(() => { console.log('Later') }, 0) for (let i = 0; i < 100000; i++) { console.log(i) } // Output: // 0 // 1 // 2 // ... // 100000 // Later

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

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

setTimeout(() => { console.log('Later') }, 0) setTimeout(() => { console.log('Even Later') }, 0) // Output: // Later // Even Later

Но если мы поставим в очередь promise и вызовем setTimeout, promise будет запущено первым.

setTimeout(function () { console.log('Later') }, 0) Promise.resolve().then(function () { console.log('Also Later') }) // Output: // Also Later // Later

Это связано с тем, что promises и тайм-ауты помещаются в две отдельные очереди, которые имеют разные приоритеты. Вызов setTimeout отправляется в очередь макрозадач, в то время как promise отправляется в очередь микрозадач.

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

Сразу после выполнения каждой макрозадачи цикл событий выполнит все ожидающие микрозадачи.

Я надеюсь, что данная статья действительно оказалось полезной для вас!

Статья была взята из этого источника:

7878 показов
8080 открытий
Начать дискуссию