Тройное C: Currying, Closure и Callback в JavaScript
Оглавление
- Функция как Объект первого класса
- Currying/Каррирование
- Closure/Замыкание
- Callback/Обратный вызов
- Заключение
Если хотите построить карьеру на языке JavaScript, вам необходимо хорошо разбираться в его ключевых концепциях. Тройное C: Currying, Closure, Callback в JavaScript — это то, что необходимо понять, для успешного прохождения собеседований. Это поможет не только получить знания по данным темам, но и даст дополнительное преимущество на собеседовании. Ведь чаще всего интервьюеры задают распространённые вопросы из этих тем.
Функция как Объект первого класса
Прежде чем перейти к рассмотрению концепций Тройного C, давайте вкратце познакомимся с функциями в JavaScript и посмотрим, насколько они мощны. В языках программирования существует понятие Объект первого класса, которое относится к передаче, возврату и присвоению типа. Если тип способен выполнять эти действия, то он будет считаться Объектом первого класса.
В языке JavaScript функция может выполнять все задачи, и поэтому она Объектом первого класса. Рассмотрим приведённый ниже пример кода функций JavaScript:
let addToNumbers = function(a, b) {
return a + b;
}
let sum = addToNumbers(10,10);
console.log(sum);
// Выводит: 20
Здесь вы видите, что мы объявили функцию, а затем сохранили её в переменной. Такое поведение известно как "Объект первого класса". Эта концепция поможет нам понять следующие темы. Давайте начнём с Currying в следующем разделе.
Currying/Каррирование
Термин "Каррирование/карринг" — это парадигма программирования или паттерн. Точнее, это паттерн функционального программирования, и придумал его Хаскелл Карри. Каррирование используется, когда мы можем разбить несколько параметров функции по одному.
Это процесс преобразования функций с несколькими параметрами в функцию с одним параметром. Мы разберёмся в концепции каррирования, решив самый популярный вопрос на собеседовании. Сначала давайте посмотрим на вопрос.
function multiply(x, y, z){
return x * y * z
}
console.log(multiply(5, 5, 5))
// Выводит: 125
На собеседовании вы можете получить вопрос такого типа, и задача, которую вам нужно решить, — вызвать функцию multiply(5)(5)(5)
вместо multiply(5, 5, 5)
. Как можно выполнить это действие? Ответ прост — это называется каррирование. Посмотрите приведённый ниже пример кода:
function multiply(x){
return function(y){
return function(z){
return x * y * z
}
}
}
console.log(multiply(5)(5)(5))
// Выводит: 125
В вопросе было три параметра в одной функции. Мы просто разбиваем эти параметры и преобразуем их в функцию с одним параметром.
Это возможно благодаря поведению функционального программирования, когда мы можем вызывать функцию внутри другой функции и создавать цепочку функций. Результат тот же, но теперь мы используем паттерн каррирования, чтобы получить этот результат.
Closure/Замыкание
Одной из самых важных концепций JavaScript являются замыкания. Проще говоря, замыкание — это акт закрывания. В JavaScript замыкание — это функция, которая ссылается на переменные во внешней области видимости из своей внутренней области видимости.
Прежде чем понять, что такое замыкание, нам нужно знать об области видимости переменной. У переменной есть три возможных области видимости, в зависимости от того, где она определена. Она может иметь локальную, глобальную или блочную область видимости (подробнее о лексическом окружении вы можете прочитать в статье Effective JavaScript - 10 JavaScript Concepts You Should Know).
Если переменная объявлена внутри функции, она будет относиться к локальной области видимости этой функции. Посмотрите фрагмент кода:
function demoFunc(){
let val = 10
return val + val
}
Здесь переменная val
находится в локальной области видимости, и её время жизни будет существовать пока выполняется вызов функции. С другой стороны, когда переменная объявляется вне функции, она будет находиться в глобальной области видимости и будет представлена Объектом window
браузера, если она объявлена с помощью var
. Посмотрите приведённый ниже фрагмент кода:
let val = 10
function demoFunc(){
return val + val
}
На этот раз val
находится в глобальной области видимости и будет существовать до тех пор, пока страница не будет удалена. Концепция замыкания используется для превращения глобальной переменной в локальную.
Давайте посмотрим на проблему, которую замыкание решает в следующей секции, и, сделав это, вы сможете понять, зачем нам нужно изучать концепцию замыкания в JavaScript.
function sayHello(){
let name = 'John'
function inner(){
console.log(`Hello ${name}`)
}
return inner
}
let abc = sayHello()
console.log(abc)
// Выводит:
// [Function: inner]
// Hello John
Здесь видно, что мы получаем ожидаемый результат. Но главный вопрос заключается в том, как мы это получаем? В чём причина этого? Можете ли вы догадаться, что на самом деле здесь происходит? Давайте разберём этот пример вместе.
Сначала мы объявили функцию sayHello()
и сохранили строку в переменной name
. Позже мы объявили ещё одну функцию и напечатали имя с сообщением Hello
, а затем вернули функцию inner()
. Наконец, мы сохранили возвращённую функцию в переменной abc
.
Теперь, когда JavaScript выполнит код, он не найдёт переменную name
, так как память функции sayHello()
была уничтожена. Но как мы получаем ожидаемый результат?
Основная причина этого — концепция замыканий. Здесь действительно память функции sayHello()
была уничтожена, но создаётся ссылка на эту функцию, и в результате при выполнении inner()
она может обращаться к переменной из ссылки функции sayHello()
. Чтобы выполнить это действие, не нужно писать ни строчки дополнительного кода. Потому что движок JavaScript достаточно умён, чтобы выполнить это действие. Всё, что вам нужно сделать, — это понять концепцию замыкания, которая создаётся по умолчанию каждый раз, когда в JavaScript создаётся функция.
Callback/Обратный вызов
Проще говоря, обратный вызов обозначает термин "вызвать позже". Это означает, что он ссылается на функцию, которая будет вызвана позже. Давайте представим себе реальный сценарий. Допустим, у нас есть массив, состоящий из названий фруктов. Массив сохранён в нашей базе данных. Нам нужно создать программу, которая будет добавлять новый фрукт и впоследствии выводить список фруктов с новым добавлением. Для выполнения этого действия мы написали следующий код:
let fruitNames = ['Apple','Banana','Orange']
function addNewFruit(name){
setTimeout(function(){
fruitNames.push(name)
},5000)
}
function printFruits(){
setTimeout(function(){
console.log(fruitNames)
},3000)
}
addNewFruit('Mango')
printFruits()
// Выводит:
// [ 'Apple', 'Banana', 'Orange' ]
Здесь мы создали новую функцию, добавляющую новые названия фруктов. Мы выполнили её, но новые названия фруктов не добавились. Основная причина этого — асинхронное поведение JavaScript.
В JavaScript, если задачи требуют много времени на выполнение, он автоматически сохраняет их в очереди и ищет те задачи, которые займут меньше времени. В нашем коде возникла такая ситуация. В результате мы не можем получить желаемый результат. Как же исправить эту ситуацию? Прежде чем переходить к решению, попробуйте сами подумать о том, как её решить.
Ответ на эту проблему прост. Всё, что нужно сделать, это использовать функцию обратного вызова, чтобы разрушить асинхронное поведение JavaScript, и он мог ждать нас, основываясь на определённой задаче. Посмотрите приведённый ниже пример кода:
let fruitNames = ['Apple','Banana','Orange']
function addNewFruit(name, cb){
setTimeout(function(){
fruitNames.push(name)
cb()
},5000)
}
function printFruits(){
setTimeout(function(){
console.log(fruitNames)
},3000)
}
addNewFruit('Mango',printFruits)
// Выводит:
// [ 'Apple', 'Banana', 'Orange', 'Mango' ]
В начале этой статьи мы уже узнали, что функция является Объектом первого класса в JavaScript. Здесь мы использовали только эту концепцию. Вы можете видеть, что мы передали функцию в качестве параметра другой функции.
Теперь, когда JavaScript будет выполнять код, он найдёт эту функцию обратного вызова и выполнит её только после того, как будут выполнены предыдущие функции. Эта концепция известна как обратный вызов, и вы можете видеть, что мы вызвали эту функцию после того, как предыдущая функция выполнила свои задачи. Именно по этой причине мы упомянули Обратный вызов как Вызов позже.
- JavaScript: Что такое функции обратного вызова/Callback
- JavaScript: Спасение из ада обратных вызовов
- JavaScript: Понимание асинхронных вызовов
Заключение
В этой статье мы рассмотрели три наиболее важные концепции JavaScript. Они также известны как Тройное C (Currying, Closure, Callback). Если вы зашли так далеко, то, возможно, вы уже знаете об этих понятиях. Цель этой статьи — рассказать об этих сложных вещах самым простым способом.
Существует множество продвинутых концепций, которые были построены на основе обратного вызова. Вы можете часто слышать о понятии Callback hell, являющееся своеобразным кошмаром для JavaScript разработчиков.