JavaScript — один из самых популярных языков программирования в мире.

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

В основном мы используем JavaScript для создания

  • сайты
  • веб-приложения
  • серверные приложения, использующие Node.js

но JavaScript не ограничивается этими вещами, и его также можно использовать для

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

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

JavaScript — это язык программирования, который:

  • высокий уровень: он предоставляет абстракции, которые позволяют вам игнорировать детали машины, на которой он работает. Он автоматически управляет памятью с помощью сборщика мусора, поэтому вы можете сосредоточиться на коде, а не на управлении памятью, как в других языках, таких как C, и предоставляет множество конструкций, которые позволяют вам иметь дело с очень мощными переменными и объектами.
  • динамический: в отличие от статических языков программирования, динамический язык выполняет во время выполнения многие действия, которые статический язык выполняет во время компиляции. У этого есть плюсы и минусы, и он дает нам мощные функции, такие как динамическая типизация, позднее связывание, отражение, функциональное программирование, изменение объекта во время выполнения, замыкания и многое другое. Не волнуйтесь, если эти вещи вам неизвестны — вы будете знать их все к концу курса.
  • динамически типизированный: переменная не имеет обязательного типа. Вы можете переназначить любой тип переменной, например, присвоив целое число переменной, содержащей строку.
  • свободная типизация: в отличие от строгой типизации, слабо типизированные языки не навязывают тип объекта, обеспечивая большую гибкость, но лишая нас безопасности типов и проверки типов (то, что TypeScript — который создает поверх JavaScript — обеспечивает)
  • интерпретируемый: он широко известен как интерпретируемый язык, что означает, что ему не требуется этап компиляции перед запуском программы, в отличие, например, от C, Java или Go. На практике браузеры компилируют JavaScript перед его выполнением из соображений производительности, но это прозрачно для вас — никаких дополнительных действий не требуется.
  • мультипарадигма: язык не навязывает какую-либо конкретную парадигму программирования, в отличие, например, от Java, которая требует использования объектно-ориентированного программирования, или C, которая требует императивного программирования. Вы можете писать JavaScript, используя объектно-ориентированную парадигму, используя прототипы и новый (начиная с ES6) синтаксис классов. Вы можете писать JavaScript в стиле функционального программирования с его первоклассными функциями или даже в императивном стиле (подобном C).

Если вам интересно, JavaScript не имеет ничего общего с Java, это плохой выбор имени, но нам придется с этим смириться.

Резюме справочника

  1. Немного истории
  2. Просто JavaScript
  3. Краткое введение в синтаксис JavaScript
  4. Точки с запятой
  5. "Ценности"
  6. Переменные
  7. Типы
  8. Выражения
  9. Операторы
  10. Правила приоритета
  11. Операторы сравнения
  12. Условные
  13. Массивы
  14. Струны
  15. Петли
  16. Функции
  17. Стрелочные функции
  18. "Объекты"
  19. Свойства объекта
  20. Объектные методы
  21. Занятия
  22. "Наследование"
  23. Асинхронное программирование и обратные вызовы
  24. Обещания
  25. Асинхронно и жди
  26. Переменная область видимости
  27. "Вывод"

Обновление: Теперь вы можете получить PDF- и ePub-версию этого руководства для начинающих по JavaScript.

Немного истории

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

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

Существуют и другие языки, но все они должны компилироваться в JavaScript — или, в последнее время, в WebAssembly, но это уже другая история.

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

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

JavaScript также теперь широко используется вне браузера. Развитие Node.js в последние несколько лет открыло возможности для разработки серверных приложений, которые когда-то были областью Java, Ruby, Python, PHP и более традиционных серверных языков.

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

Просто JavaScript

Иногда бывает трудно отделить JavaScript от особенностей среды, в которой он используется.

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

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

Предоставляется ли конкретная функция React или Vue? Или это «обычный JavaScript» или «ванильный JavaScript», как его часто называют?

В этой книге я рассказываю о JavaScript, языке.

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

Краткое введение в синтаксис JavaScript

В этом небольшом вступлении я хочу рассказать вам о 5 концепциях:

  • белое пространство
  • чувствительность к регистру
  • литералы
  • идентификаторы
  • Комментарии

Белое пространство

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

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

Например, я всегда использую 2 пробела для каждого отступа.

Деликатный случай

JavaScript чувствителен к регистру. Переменная с именем something отличается от Something.

То же самое касается любого идентификатора.

Литералы

Мы определяем литерал как значение, записанное в исходном коде, например, число, строку, логическое значение или также более сложные конструкции, такие как литералы объектов или литералы массивов:

5
'Test'
true
['a', 'b']
{color: 'red', shape: 'Rectangle'}

Идентификаторы

Идентификатор – это последовательность символов, которую можно использовать для идентификации переменной, функции или объекта. Он может начинаться с буквы, знака доллара $ или символа подчеркивания _ и может содержать цифры. В Юникоде буквой может быть любой разрешенный символ, например, эмодзи 😄.

Test
test
TEST
_test
Test1
$test

Знак доллара обычно используется для ссылки на элементы DOM.

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

Комментарии

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

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

Нравится:

// a comment
true //another comment

Другой тип комментария — многострочный комментарий. Он начинается с /* и заканчивается на */.

Все, что между ними, не считается кодом:

/* some kind
of 
comment 
*/

Точки с запятой

Каждая строка в программе JavaScript необязательно завершается точкой с запятой.

Я сказал «необязательно», потому что интерпретатор JavaScript достаточно умен, чтобы вводить для вас точки с запятой.

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

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

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

Ценности

Строка hello является значением.
Число, подобное 12, является значением.

hello и 12 являются значениями. string и number — это типы этих значений.

Тип — это тип значения, его категория. У нас есть много разных типов в JavaScript, и мы поговорим о них позже. Каждый тип имеет свои особенности.

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

Переменные

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

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

Переменная должна быть объявлена ​​до того, как вы сможете ее использовать.

У нас есть 2 основных способа объявления переменных. Первый — использовать const:

const a = 0

Второй способ — использовать let:

let a = 0

Какая разница?

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

Используя let, вы можете присвоить ему новое значение.

Например, вы не можете сделать это:

const a = 0
a = 1

Потому что вы получите сообщение об ошибке: TypeError: Assignment to constant variable..

С другой стороны, вы можете сделать это с помощью let:

let a = 0
a = 1

const не означает "константа" в том смысле, в каком это означают некоторые другие языки, такие как C. В частности, это не означает, что значение нельзя изменить — это означает, что его нельзя переназначить. Если переменная указывает на объект или массив (позже мы увидим больше об объектах и ​​массивах), содержимое объекта или массива может свободно изменяться.

const переменные должны быть инициализированы во время объявления:

const a = 0

но значения let можно инициализировать позже:

let a
a = 0

Вы можете объявить несколько переменных одновременно в одном операторе:

const a = 1, b = 2
let c = 1, d = 2

Но вы не можете повторно объявить одну и ту же переменную более одного раза:

let a = 1
let a = 2

или вы получите ошибку «дубликат объявления».

Мой совет — всегда использовать const и использовать let только тогда, когда вы знаете, что вам нужно переназначить значение этой переменной. Почему? Потому что чем меньше мощности у нашего кода, тем лучше. Если мы знаем, что значение нельзя переназначить, это на один источник ошибок меньше.

Теперь, когда мы увидели, как работать с const и let, я хочу упомянуть var.

До 2015 года var был единственным способом объявить переменную в JavaScript. Сегодня современная кодовая база, скорее всего, будет использовать только const и let. Есть несколько принципиальных отличий, о которых я подробно рассказываю в этом посте, но если вы только начинаете, они могут вас не волновать. Просто используйте const и let.

Типы

Переменные в JavaScript не имеют прикрепленного типа.

Они не типизированы.

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

В JavaScript есть два основных вида типов: примитивные типы и объектные типы.

Примитивные типы

Примитивные типы

  • числа
  • струны
  • булевы значения
  • символы

И два специальных типа: null и undefined.

Типы объектов

Любое значение, не относящееся к примитивному типу (строка, число, логическое значение, null или неопределенное), является объектом.

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

Мы поговорим об объектах позже.

Выражения

Выражение — это отдельная единица кода JavaScript, которую движок JavaScript может оценить и вернуть значение.

Выражения могут различаться по сложности.

Начнем с самых простых, называемых первичными выражениями:

2
0.02
'something'
true
false
this //the current scope
undefined
i //where i is a variable or a constant

Арифметические выражения — это выражения, которые принимают переменную и оператор (подробнее об операторах позже) и приводят к числу:

1 / 2
i++
i -= 2
i * 2

Строковые выражения — это выражения, результатом которых является строка:

'A ' + 'string'

Логические выражения используют логические операторы и разрешаются в логическое значение:

a && b
a || b
!a

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

Операторы

Операторы позволяют получить два простых выражения и объединить их в более сложное выражение.

Мы можем классифицировать операторы на основе операндов, с которыми они работают. Некоторые операторы работают с 1 операндом. Большинство работает с 2 операндами. Всего один оператор работает с 3 операндами.

В этом первом введении в операторы мы представим операторы, с которыми вы, скорее всего, знакомы: операторы с двумя операндами.

Я уже представил один, говоря о переменных: оператор присваивания =. Вы используете = для присвоения значения переменной:

let b = 2

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

Оператор сложения (+)

const three = 1 + 2
const four = three + 1

Оператор + также выполняет конкатенацию строк, если вы используете строки, поэтому обратите внимание:

const three = 1 + 2
three + 1 // 4
'three' + 1 // three1

Оператор вычитания (-)

const two = 4 - 2

Оператор деления (/)

Возвращает частное первого оператора и второго:

const result = 20 / 5 //result === 4
const result = 20 / 7 //result === 2.857142857142857

Если вы разделите на ноль, JavaScript не выдаст никакой ошибки, а вернет значение Infinity (или -Infinity, если значение отрицательное).

1 / 0 //Infinity
-1 / 0 //-Infinity

Остаточный оператор (%)

Остаток — очень полезный расчет во многих случаях использования:

const result = 20 % 5 //result === 0
const result = 20 % 7 //result === 6

Остаток до нуля всегда равен NaN, специальному значению, которое означает «не число»:

1 % 0 //NaN
-1 % 0 //NaN

Оператор умножения (*)

Умножить два числа

1 * 2 //2
-1 * 2 //-2

Оператор возведения в степень (**)

Возведение первого операнда в степень второго операнда

1 ** 2 //1
2 ** 1 //2
2 ** 2 //4
2 ** 8 //256
8 ** 2 //64

Правила приоритета

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

Возьмите этот пример:

let a = 1 * 2 + 5 / 2 % 2

Результат 2,5, но почему?

Какие операции выполняются первыми, а какие нужно подождать?

Некоторые операции имеют больший приоритет, чем другие. Правила приоритета перечислены в этой таблице:

ОПИСАНИЕ ОПЕРАТОРА* / %умножение/деление+ -сложение/вычитание=присваивание

Операции на одном уровне (например, + и -) выполняются в порядке их обнаружения, слева направо.

Следуя этим правилам, описанную выше операцию можно решить следующим образом:

let a = 1 * 2 + 5 / 2 % 2
let a = 2 + 5 / 2 % 2
let a = 2 + 2.5 % 2
let a = 2 + 0.5
let a = 2.5

Операторы сравнения

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

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

Операторы сравнения всегда возвращают логическое значение, то есть значение true или false).

Это операторы сравнения несоответствий:

  • < означает «меньше чем»
  • <= означает «меньше или равно»
  • > означает «больше чем»
  • >= означает «больше или равно»

Пример:

let a = 2
a >= 1 //true

В дополнение к ним у нас есть 4 оператора равенства. Они принимают два значения и возвращают логическое значение:

  • === проверяет равенство
  • !== проверяет на неравенство

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

Условные

Имея операторы сравнения, мы можем говорить об условных выражениях.

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

Это самый простой пример, который всегда выполняется:

if (true) {
  //do something
}

напротив, это никогда не выполняется:

if (false) {
  //do something (? never ?)
}

Условие проверяет выражение, которое вы ему передаете, на истинное или ложное значение. Если вы передаете число, оно всегда оценивается как true, если только оно не равно 0. Если вы передаете строку, оно всегда оценивается как true, если это не пустая строка. Это общие правила приведения типов к логическому значению.

Вы заметили фигурные скобки? Это называется блоком и используется для группировки списка различных операторов.

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

if (true) doSomething()

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

Вы можете указать вторую часть оператора if: else.

Вы прикрепляете оператор, который будет выполнен, если условие if ложно:

if (true) {
  //do something
} else {
  //do something else
}

Поскольку else принимает оператор, вы можете вложить в него еще один оператор if/else:

if (a === true) {
  //do something
} else if (b === true) {
  //do something else
} else {
  //fallback
}

Массивы

Массив — это набор элементов.

Массивы в JavaScript не являются типом сами по себе.

Массивы — это объекты.

Мы можем инициализировать пустой массив двумя разными способами:

const a = []
const a = Array()

Первый использует синтаксис литерала массива. Второй использует встроенную функцию Array.

Вы можете предварительно заполнить массив, используя этот синтаксис:

const a = [1, 2, 3]
const a = Array.of(1, 2, 3)

Массив может содержать любое значение, даже значения разных типов:

const a = [1, 'Flavio', ['a', 'b']]

Поскольку мы можем добавлять массив в массив, мы можем создавать многомерные массивы, которые имеют очень полезные приложения (например, матрица):

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]
matrix[0][0] //1
matrix[2][0] //7

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

a[0] //1
a[1] //2
a[2] //3

Вы можете инициализировать новый массив набором значений, используя этот синтаксис, который сначала инициализирует массив из 12 элементов и заполняет каждый элемент числом 0:

Array(12).fill(0)

Вы можете получить количество элементов в массиве, проверив его свойство length:

const a = [1, 2, 3]
a.length //3

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

const a = [1, 2, 3]
a //[ 1, 2, 3 ]
a.length = 2
a //[ 1, 2 ]

Как добавить элемент в массив

Мы можем добавить элемент в конец массива, используя метод push():

a.push(4)

Мы можем добавить элемент в начало массива, используя метод unshift():

a.unshift(0)
a.unshift(-2, -1)

Как удалить элемент из массива

Мы можем удалить элемент с конца массива, используя метод pop():

a.pop()

Мы можем удалить элемент из начала массива, используя метод shift():

a.shift()

Как объединить два или более массива

Вы можете объединить несколько массивов, используя concat():

const a = [1, 2]
const b = [3, 4]
const c = a.concat(b) //[1,2,3,4]
a //[1,2]
b //[3,4]

Вы также можете использовать оператор spread (...) следующим образом:

const a = [1, 2]
const b = [3, 4]
const c = [...a, ...b]
c //[1,2,3,4]

Как найти конкретный элемент в массиве

Вы можете использовать метод find() массива:

a.find((element, index, array) => {
  //return true or false
})

Возвращает первый элемент, который возвращает значение true, и возвращает undefined, если элемент не найден.

Обычно используемый синтаксис:

a.find(x => x.id === my_id)

Приведенная выше строка вернет первый элемент в массиве, который имеет id === my_id.

findIndex() работает аналогично find(), но возвращает индекс первого элемента, который возвращает true, а если не найден, возвращает undefined:

a.findIndex((element, index, array) => {
  //return true or false
})

Другой метод — includes():

a.includes(value)

Возвращает true, если a содержит value.

a.includes(value, i)

Возвращает true, если a содержит value после позиции i.

Струны

Строка — это последовательность символов.

Его также можно определить как строковый литерал, заключенный в кавычки или двойные кавычки:

'A string'
"Another string"

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

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

const name = 'Flavio'

Вы можете определить длину строки, используя ее свойство length:

'Flavio'.length //6
const name = 'Flavio'
name.length //6

Это пустая строка: ''. Его свойство длины равно 0:

''.length //0

Две строки можно соединить с помощью оператора +:

"A " + "string"

Вы можете использовать оператор + для интерполяции переменных:

const name = 'Flavio'
"My name is " + name //My name is Flavio

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

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

const string = `Hey
this
string
is awesome!`

Шаблонные литералы также хороши, потому что они обеспечивают простой способ интерполяции переменных и выражений в строки.

Вы делаете это, используя синтаксис ${...}:

const var = 'test'
const string = `something ${var}` 
//something test

внутри ${} можно добавить что угодно, даже выражения:

const string = `something ${1 + 2 + 3}`
const string2 = `something 
  ${foo() ? 'x' : 'y'}`

Петли

Циклы — одна из основных управляющих структур JavaScript.

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

JavaScript предоставляет множество способов итерации циклов.

Я хочу сосредоточиться на 3 способах:

  • в то время как циклы
  • для петель
  • для..из петель

while

Цикл while — это простейшая структура циклов, которую предоставляет нам JavaScript.

Мы добавляем условие после ключевого слова while и предоставляем блок, который выполняется до тех пор, пока условие не будет оценено как true.

Пример:

const list = ['a', 'b', 'c']
let i = 0
while (i < list.length) {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
}

Вы можете прервать цикл while, используя ключевое слово break, например:

while (true) {
  if (somethingIsTrue) break
}

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

while (true) {
  if (somethingIsTrue) continue
  //do something else
}

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

Это означает, что блок всегда выполняется хотя бы один раз.

Пример:

const list = ['a', 'b', 'c']
let i = 0
do {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
} while (i < list.length)

for

Второй очень важной структурой циклов в JavaScript является цикл for.

Мы используем ключевое слово for и передаем набор из 3 инструкций: инициализация, условие и часть увеличения.

Пример:

const list = ['a', 'b', 'c']
for (let i = 0; i < list.length; i++) {
  console.log(list[i]) //value
  console.log(i) //index
}

Как и в случае с циклами while, вы можете прервать цикл for, используя break, и перемотать вперед к следующей итерации цикла for, используя continue.

for...of

Этот цикл появился относительно недавно (представлен в 2015 году) и представляет собой упрощенную версию цикла for:

const list = ['a', 'b', 'c']
for (const value of list) {
  console.log(value) //value
}

Функции

В любой умеренно сложной программе на JavaScript все происходит внутри функций.

Функции — это основная, неотъемлемая часть JavaScript.

Что такое функция?

Функция — это самодостаточный блок кода.

Вот объявление функции:

function getData() {
  // do something
}

Функцию можно запустить в любое время, вызвав ее, например:

getData()

Функция может иметь один или несколько аргументов:

function getData() {
  //do something
}
function getData(color) {
  //do something
}
function getData(color, age) {
  //do something
}

Когда мы можем передать аргумент, мы вызываем функцию передачи параметров:

function getData(color, age) {
  //do something
}
getData('green', 24)
getData('black')

Обратите внимание, что во втором вызове я передал строковый параметр black в качестве аргумента color, но не age. В этом случае age внутри функции равно undefined.

Мы можем проверить, не является ли значение неопределенным, используя это условное выражение:

function getData(color, age) {
  //do something
  if (typeof age !== 'undefined') {
    //...
  }
}

typeof — это унарный оператор, который позволяет нам проверять тип переменной.

Вы также можете проверить таким образом:

function getData(color, age) {
  //do something
  if (age) {
    //...
  }
}

Хотя условие также будет истинным, если age равно null, 0 или пустой строке.

У вас могут быть значения по умолчанию для параметров, если они не переданы:

function getData(color = 'black', age = 25) {
  //do something
}

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

Функция имеет возвращаемое значение. По умолчанию функция возвращает undefined, если вы не добавите ключевое слово return со значением:

function getData() {
  // do something
  return 'hi!'
}

Мы можем присвоить это возвращаемое значение переменной при вызове функции:

function getData() {
  // do something
  return 'hi!'
}
let result = getData()

result теперь содержит строку со значением hi!.

Вы можете вернуть только одно значение.

Чтобы вернуть несколько значений, вы можете вернуть объект или массив, например:

function getData() {
  return ['Flavio', 37]
}
let [name, age] = getData()

Функции могут быть определены внутри других функций:

const getData = () => {
  const dosomething = () => {}
  dosomething()
  return 'test'
}

Вложенная функция не может быть вызвана извне объемлющей функции.

Вы также можете вернуть функцию из функции.

Стрелочные функции

Стрелочные функции — это недавнее введение в JavaScript.

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

Визуально они позволяют писать функции с более коротким синтаксисом, из:

function getData() {
  //...
}

to

() => {
  //...
}

Но... обратите внимание, что здесь у нас нет имени.

Стрелочные функции анонимны. Мы должны присвоить их переменной.

Мы можем присвоить переменной обычную функцию, например:

let getData = function getData() {
  //...
}

Когда мы это сделаем, мы можем удалить имя из функции:

let getData = function() {
  //...
}

и вызовите функцию, используя имя переменной:

let getData = function() {
  //...
}
getData()

То же самое мы делаем со стрелочными функциями:

let getData = () => {
  //...
}
getData()

Если тело функции содержит только один оператор, вы можете опустить круглые скобки и написать все в одной строке:

const getData = () => console.log('hi!')

В скобках передаются параметры:

const getData = (param1, param2) => 
  console.log(param1, param2)

Если у вас есть один (и только один) параметр, вы можете полностью опустить круглые скобки:

const getData = param => console.log(param)

Стрелочные функции позволяют вам иметь неявный возврат — значения возвращаются без использования ключевого слова return.

Это работает, когда в теле функции есть однострочный оператор:

const getData = () => 'test'
getData() //'test'

Как и в случае с обычными функциями, у нас могут быть значения по умолчанию для параметров, если они не переданы:

const getData = (color = 'black', 
                 age = 2) => {
  //do something
}

Как и обычные функции, мы можем вернуть только одно значение.

Стрелочные функции также могут содержать другие стрелочные функции или даже обычные функции.

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

Объекты

Любое значение, не относящееся к примитивному типу (строка, число, логическое значение, символ, нуль или неопределенное значение), является объектом.

Вот как мы определяем объект:

const car = {
}

Это синтаксис объектного литерала, который является одной из самых приятных вещей в JavaScript.

Вы также можете использовать синтаксис new Object:

const car = new Object()

Другой синтаксис заключается в использовании Object.create():

const car = Object.create()

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

function Car(brand, model) {
  this.brand = brand
  this.model = model
}

Мы инициализируем новый объект, используя:

const myCar = new Car('Ford', 'Fiesta')
myCar.brand //'Ford'
myCar.model //'Fiesta'

Объекты всегда передаются по ссылке.

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

Возьмите этот пример:

let age = 36
let myAge = age
myAge = 37
age //36
const car = {
  color: 'blue'
}
const anotherCar = car
anotherCar.color = 'yellow'
car.color //'yellow'

Даже массивы или функции внутри являются объектами, поэтому очень важно понимать, как они работают.

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

Объекты имеют свойства, которые состоят из метки, связанной со значением.

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

Это синтаксис литерала объекта, который мы видели в предыдущей главе:

const car = {
}

Мы можем определить свойство color следующим образом:

const car = {
  color: 'blue'
}

Здесь у нас есть объект car со свойством color со значением blue.

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

const car = {
  color: 'blue',
  'the color': 'blue'
}

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

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

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

Во-первых, это точечная нотация:

car.color //'blue'

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

car['the color'] //'blue'

Если вы получите доступ к несуществующему свойству, вы получите значение undefined:

car.brand //undefined

Как упоминалось ранее, объекты могут иметь вложенные объекты в качестве свойств:

const car = {
  brand: {
    name: 'Ford'
  },
  color: 'blue'
}

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

car.brand.name

or

car['brand']['name']

Вы можете установить значение свойства при определении объекта.

Но вы всегда можете обновить его позже:

const car = {
  color: 'blue'
}
car.color = 'yellow'
car['color'] = 'red'

И вы также можете добавить новые свойства к объекту:

car.model = 'Fiesta'
car.model //'Fiesta'

Учитывая объект

const car = {
  color: 'blue',
  brand: 'Ford'
}

вы можете удалить свойство из этого объекта, используя

delete car.brand

Методы объекта

Я говорил о функциях в предыдущей главе.

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

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

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function() {
    console.log('Started')
  }
}
car.start()

Внутри метода, определенного с использованием синтаксиса function() {}, мы имеем доступ к экземпляру объекта, ссылаясь на this.

В следующем примере у нас есть доступ к значениям свойств brand и model с использованием this.brand и this.model:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function() {
    console.log(`Started 
      ${this.brand} ${this.model}`)
  }
}
car.start()

Важно отметить различие между обычными функциями и стрелочными функциями — у нас нет доступа к this, если мы используем стрелочную функцию:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: () => {
    console.log(`Started 
      ${this.brand} ${this.model}`) //not going to work
  }
}
car.start()

Это связано с тем, что функции стрелок не привязаны к объекту.

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

Методы могут принимать параметры, как обычные функции:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  goTo: function(destination) {
    console.log(`Going to ${destination}`)
  }
}
car.goTo('Rome')

Классы

Мы говорили об объектах, которые являются одной из самых интересных частей JavaScript.

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

Что такое классы? Это способ определить общий шаблон для нескольких объектов.

Возьмем объект человека:

const person = {
  name: 'Flavio'
}

Мы можем создать класс с именем Person (обратите внимание на заглавную P, соглашение при использовании классов), который имеет свойство name:

class Person {
  name
}

Теперь из этого класса мы инициализируем объект flavio следующим образом:

const flavio = new Person()

flavio называется экземпляром класса Person.

Мы можем установить значение свойства name:

flavio.name = 'Flavio'

и мы можем получить к нему доступ, используя

flavio.name

как мы делаем для свойств объекта.

Классы могут содержать свойства, такие как name, и методы.

Методы определяются следующим образом:

class Person {
  hello() {
    return 'Hello, I am Flavio'
  }
}

и мы можем вызывать методы для экземпляра класса:

class Person {
  hello() {
    return 'Hello, I am Flavio'
  }
}
const flavio = new Person()
flavio.hello()

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

Это работает следующим образом:

class Person {
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}

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

Теперь мы можем создать экземпляр нового объекта из класса, передать строку, и когда мы вызовем hello, мы получим персонализированное сообщение:

const flavio = new Person('flavio')
flavio.hello() //'Hello, I am flavio.'

Когда объект инициализируется, вызывается метод constructor с любыми переданными параметрами.

Обычно методы определяются в экземпляре объекта, а не в классе.

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

class Person {
  static genericHello() {
    return 'Hello'
  }
}
Person.genericHello() //Hello

Это очень полезно, временами.

Наследование

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

Предположим, у нас есть класс Person:

class Person {
  hello() {
    return 'Hello, I am a Person'
  }
}

Мы можем определить новый класс Programmer, который расширяет Person:

class Programmer extends Person {
}

Теперь, если мы создадим экземпляр нового объекта с классом Programmer, у него будет доступ к методу hello():

const flavio = new Programmer()
flavio.hello() //'Hello, I am a Person'

Внутри дочернего класса вы можете сослаться на родительский класс, вызвав super():

class Programmer extends Person {
  hello() {
    return super.hello() + 
      '. I am also a programmer.'
  }
}
const flavio = new Programmer()
flavio.hello()

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

Асинхронное программирование и обратные вызовы

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

Это означает, что выполняется строка кода, затем выполняется следующая и так далее.

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

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

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

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

JavaScript решает эту проблему с помощью обратных вызовов.

Один из самых простых примеров использования обратных вызовов — это таймеры. Таймеры не являются частью JavaScript, но они предоставляются браузером и Node.js. Позвольте мне рассказать об одном из имеющихся у нас таймеров: setTimeout().

Функция setTimeout() принимает 2 аргумента: функцию и число. Число — это миллисекунды, которые должны пройти до запуска функции.

Пример:

setTimeout(() => {
  // runs after 2 seconds
  console.log('inside the function')
}, 2000)

Функция, содержащая строку console.log('inside the function'), будет выполнена через 2 секунды.

Если вы добавите console.log('before') перед функцией и console.log('after') после нее:

console.log('before')
setTimeout(() => {
  // runs after 2 seconds
  console.log('inside the function')
}, 2000)
console.log('after')

Вы увидите, как это происходит в вашей консоли:

before
after
inside the function

Функция обратного вызова выполняется асинхронно.

Это очень распространенный шаблон при работе с файловой системой, сетью, событиями или DOM в браузере.

Все, что я упомянул, не является основным JavaScript, поэтому они не объясняются в этом руководстве, но вы найдете множество примеров в других моих руководствах, доступных на https://flaviocopes.com.

Вот как мы можем реализовать обратные вызовы в нашем коде.

Мы определяем функцию, которая принимает параметр callback, который является функцией.

Когда код готов вызвать обратный вызов, мы вызываем его, передавая результат:

const doSomething = callback => {
  //do things
  //do things
  const result = /* .. */
  callback(result)
}

Код, использующий эту функцию, будет использовать ее следующим образом:

doSomething(result => {
  console.log(result)
})

Обещания

Промисы — это альтернативный способ работы с асинхронным кодом.

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

Нравится:

doSomething(result => {
  console.log(result)
})

Когда код doSomething() заканчивается, он вызывает функцию, полученную в качестве параметра:

const doSomething = callback => {
  //do things
  //do things
  const result = /* .. */
  callback(result)
}

Основная проблема такого подхода заключается в том, что если нам нужно использовать результат этой функции в остальной части нашего кода, то весь наш код должен быть вложен внутри callback-функции, а если нам нужно сделать 2-3 callback-а, то мы вводим в то, что есть. обычно определяется «ад обратных вызовов» со многими уровнями функций, вложенными в другие функции:

doSomething(result => {
  doSomethingElse(anotherResult => {
    doSomethingElseAgain(yetAnotherResult => {
      console.log(result)
    })
  }) 
})

Обещания — один из способов справиться с этим.

Вместо того, чтобы делать:

doSomething(result => {
  console.log(result)
})

Мы вызываем функцию на основе промисов следующим образом:

doSomething()
  .then(result => {
    console.log(result)
  })

Сначала мы вызываем функцию, затем у нас есть метод then(), который вызывается, когда функция завершается.

Отступ не имеет значения, но вы часто будете использовать этот стиль для ясности.

Обычно для обнаружения ошибок используется метод catch():

doSomething()
  .then(result => {
    console.log(result)
  })
  .catch(error => {
    console.log(error)
  })

Теперь, чтобы иметь возможность использовать этот синтаксис, реализация функции doSomething() должна быть немного особенной. Он должен использовать Promises API.

Вместо того, чтобы объявлять это как обычную функцию:

const doSomething = () => {
  
}

Мы объявляем его как объект обещания:

const doSomething = new Promise()

и мы передаем функцию в конструктор Promise:

const doSomething = new Promise(() => {
})

Эта функция получает 2 параметра. Первая — это функция, которую мы вызываем для разрешения промиса, вторая — функция, которую мы вызываем, чтобы отклонить промис.

const doSomething = new Promise(
  (resolve, reject) => {
    
})

Разрешение промиса означает его успешное завершение (что приводит к вызову метода then() во всем, что его использует).

Отклонение обещания означает завершение его с ошибкой (что приводит к вызову метода catch() во всем, что его использует).

Вот как:

const doSomething = new Promise(
  (resolve, reject) => {
    //some code
    const success = /* ... */
    if (success) {
      resolve('ok')
    } else {
      reject('this error occurred')
    }
  }
)

Мы можем передать параметр функциям разрешения и отклонения любого типа.

Асинхронно и ждать

Асинхронные функции — это абстракция обещаний более высокого уровня.

Асинхронная функция возвращает обещание, как в этом примере:

const getData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => 
      resolve('some data'), 2000)
  })
}

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

const data = await getData()

и при этом любые данные, возвращаемые обещанием, будут присвоены переменной data.

В нашем случае данные — это строка «некоторые данные».

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

Нравится:

const doSomething = async () => {
  const data = await getData()
  console.log(data)
}

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

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

И это очень простой пример, основные преимущества возникают, когда код намного сложнее.

В качестве примера, вот как можно получить ресурс JSON с помощью Fetch API и проанализировать его с помощью промисов:

const getFirstUserData = () => {
  // get users list
  return fetch('/users.json') 
    // parse JSON
    .then(response => response.json()) 
    // pick first user
    .then(users => users[0]) 
    // get user data
    .then(user => 
      fetch(`/users/${user.name}`)) 
    // parse JSON
    .then(userResponse => response.json()) 
}
getFirstUserData()

А вот та же функциональность, предоставляемая с помощью await/async:

const getFirstUserData = async () => {
  // get users list
  const response = await fetch('/users.json') 
  // parse JSON
  const users = await response.json() 
  // pick first user
  const user = users[0] 
  // get user data
  const userResponse = 
    await fetch(`/users/${user.name}`)
  // parse JSON
  const userData = await user.json() 
  return userData
}
getFirstUserData()

Область видимости переменных

Когда я представил переменные, я говорил об использовании const, let и var.

Область видимости — это набор переменных, видимых части программы.

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

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

Между объявлениями var, let и const есть очень важное различие.

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

С другой стороны, переменная, определенная как const или let, видна только внутри блока, где она определена.

Блок — это набор инструкций, сгруппированных в пару фигурных скобок, подобных тем, которые мы можем найти внутри оператора if, цикла for или функции.

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

Это имеет очень практическое значение.

Предположим, вы определяете переменную var внутри условного выражения if в функции.

function getData() {
  if (true) {
    var data = 'some data'
    console.log(data) 
  }
}

Если вы вызовете эту функцию, вы получите some data на консоль.

Если вы попытаетесь переместить console.log(data) после if, он все равно будет работать:

function getData() {
  if (true) {
    var data = 'some data'
  }
  console.log(data) 
}

Но если вы переключите var data на let data:

function getData() {
  if (true) {
    let data = 'some data'
  }
  console.log(data) 
}

Вы получите сообщение об ошибке: ReferenceError: data is not defined.

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

function getData() {
  var data
  if (true) {
    data = 'some data'
  }
  console.log(data) 
}

Вот почему вы также можете использовать console.log(data) в начале функции, даже до того, как она будет объявлена, и вы получите undefined в качестве значения этой переменной:

function getData() {
  console.log(data) 
  if (true) {
    var data = 'some data'
  }
}

но если вы переключитесь на let, вы получите ошибку ReferenceError: data is not defined, потому что подъем не происходит с объявлениями let.

const следует тем же правилам, что и let: это блочная область.

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

function doLoop() {
  for (var i = 0; i < 10; i++) {
    console.log(i)
  }
  console.log(i)
}
doLoop()

Когда вы выйдете из цикла, i будет допустимой переменной со значением 10.

Если переключиться на let, то при попытке console.log(i) будет выдаваться ошибка ReferenceError: i is not defined.

Вывод

Большое спасибо за чтение этой книги.

Я надеюсь, что это вдохновит вас узнать больше о JavaScript.

Чтобы узнать больше о JavaScript, посетите мой блог flaviocopes.com.

Примечание. Вы можете получить PDF- и ePub-версию этого руководства для начинающих по JavaScript