То, что вы никогда не знали о функциях JavaScript

JavaScript

Функции странные. Рассмотрим следующий код:

function sayHello() {
  console.log("Hello");
}

sayHello();

Кажется достаточно простым, не так ли? Мы создаем функцию sayHello, а затем сразу ее вызываем.

А как насчет следующего кода:

function sayHello() {
  console.log("Hello");
}
const greeting = sayHello;
greeting();

Этот «интуитивный» код содержит множество предположений и процессов, которые мы обычно принимаем как должное:

  • Почему мы можем присвоить функцию переменной?
  • Что это делает под капотом?
  • Можем ли мы использовать функции потенциально неожиданными способами?

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

Например, знаете ли вы, что такое «каррирование функций» и почему оно полезно? Или знаете ли вы, как [].map() и [].filter реализованы?

Не волнуйтесь, дорогой читатель, сейчас мы рассмотрим все эти вопросы.

Почему мы можем присвоить функцию переменной?

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

Как работает память

Внутри вашего компьютера есть так называемая «память», или ОЗУ, которая позволяет компьютеру хранить кратковременную память, к которой он может быстро обратиться позже.

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

Например, возьмем следующий код:

const helloMessage = "HELLO";
const byeMessage = "SEEYA";

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

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

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

Вы можете свободно думать об этом стеке памяти как о массиве, который компилятор просматривает, чтобы получить данные на основе индекса. Это число может быть огромным, поскольку ваш компьютер, вероятно, имеет несколько гигабайт оперативной памяти. Даже 16 ГБ эквивалентны 1,28e+11 байт. Из-за этого адреса памяти часто в разговорной речи сокращаются до шестнадцатеричных представлений.

Это означает, что наш адрес памяти 0x7de35306 связан с битом номер 2112049926, или чуть больше отметки 0,2 ГБ.

Когда ваш браузер компилирует следующий код:

const helloMessage = "HELLO";
const byeMessage = "SEEYA";

console.log(helloMessage);
console.log(byeMessage);

Компилятор браузера заменит имена переменных адресами памяти:

memoryBlocks[0x7de35306] = "HELLO";
memoryBlocks[0x7de35307] = "SEEYA";

console.log(memoryBlocks[0x7de35306]);
console.log(memoryBlocks[0x7de35307]);

Этот код — просто псевдокод, и на самом деле он не будет работать. Вместо этого ваш компьютер скомпилирует его в «машинный код» или «код ассемблера», который в свою очередь будет работать на «голом железе». Более того, это радикальное упрощение того, как JIT-компилятор вашего браузера и управление памятью вашей системы на самом деле работают под капотом.

Как это связано с хранением функций?

Помните, что функции в JavaScript имеют два разных синтаксиса:

function sayHello() {
  console.log("Hello");
}

sayHello();

Примерно эквивалентно:

const sayHello = () => {
  console.log("Hello");
}

sayHello();

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

Используя наш псевдокод снова, это может выглядеть так:

memoryBlocks[0x9de12807] = () => {
    console.log("Hello");
}

memoryBlocks[0x9de12807]();

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

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

console.log(1+2);

Без необходимости присваивать каждое число переменной:

const one = 1;
const two = 2;
console.log(one+two);

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

Это означает следующую sayHelloфункцию:

const sayHello = () => {
    console.log("Hello");
}
sayHello();

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

(() => console.log("Hello"))();

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

Можно ли передать функцию другой функции?

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

function sayThis(message) {
    console.log(message);
}

sayThis("Hello");

Здесь мы передаем строку как свойство функции sayThis.

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

function doThis(callback) {
    callback();
}

function sayHello() {
    console.log("Hello");
}

doThis(sayHello);

Это выведет то же самое «Hello», что и в предыдущем sayThisслучае.

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

function callThisFn(callback) {
    // Remember, `callback` is a function we're padding
    // `console.log` specifically
    return callback('Hello, world');
}

callThisFn(console.log);

Чтобы пройти этот процесс шаг за шагом, мы:

  • Пройти console.logчерез callThisFnаргумент
  • callThisFnприсваивает этому свойству значение callback, которое остается функцией
  • Затем мы вызываем функцию callbackс собственным параметром: «Привет, мир»

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

(callback => callback('Hello, world!'))(console.log);

А как насчет возврата функции из другой функции?

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

function getMessage() {
  return "Hello";
}

const message = getMessage();
console.log(message);
// Equivalent to
console.log(getMessage());

Если вы много кодировали на JavaScript, это покажется вам знакомым. Мы «вызываем» getMessageи сохраняем возвращаемое значение в messageпеременной. Затем мы можем сделать с этой переменной все, что только можно ожидать message, включая передачу ее в другие функции в качестве параметра.

Это также возможно с функцией в качестве возвращаемого значения:

function getMessageFn() {
    return () => {
        console.log("Hello");
    }
}

const messageFn = getMessageFn();
messageFn();
// This can be simplified to
getMessageFn()();

Этот блок кода является расширением идеи «возвращаемого значения». Здесь мы возвращаем другую функцию из getMessageFn. Затем эта функция назначается, messageFnи мы можем затем, в свою очередь, вызвать ее саму.

Мета, да?

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

function getMessageFn() {
    return () => {
        return "Hello";
    }
}

const messageFn = getMessageFn();
const message = messageFn();
console.log(message);
// This can be simplified to
console.log(getMessageFn()());

Давайте объединим концепции, приняв и вернув функцию из другой функции.

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

function passFunctionAndReturnFunction(callback) {
    return () => {
        callback("Hello, world");
    }
}

const sayHello = passFunctionAndReturnFunction(console.log);
sayHello(); // Will log "Hello, world"

Как передать данные из одной функции в другую? Функция конвейера!

Концепции, о которых мы говорили сегодня, обычно используются при программировании в стиле, называемом «функциональное программирование». Функциональное программирование — это стиль программирования, аналогичный «объектно-ориентированному программированию» (ООП), который использует функции как метод передачи, изменения и структурирования данных.

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

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

Традиционно pipeфункция принимает список других функций для их вызова и возвращает конечное значение.

Например, вы можете запустить:

const finalVal = pipe([
  () => 1,
  // Pass `1` to `v`
  v => v+1
]);

console.log(finalVal); // 2

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

К счастью, pipeэто простая в реализации функция:

function pipe(fns) {
    let val = undefined;
    for (let fn of fns) {
        val = fn(val)
    }
    return val;
}

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

clamp({min: 0, max: 10, val: 5}); // 5
clamp({min: 0, max: 10, val: 15}); // 10
clamp({min: 0, max: 10, val: -10}); // 0

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

  1. Проверьте, меньше ли минимума
  2. Проверьте, больше ли максимума
  3. Вернуть окончательное значение

Мы можем реализовать это, используя отдельные функции и наш новый pipeметод:

function pipe(fns) {
    let val = undefined;
    for (let fn of fns) {
        val = fn(val)
    }
    return val;
}

const min = (val, min) => val < min? min: val;
const max = (val, max) => val > max? max: val;

function clamp(props) {
    return pipe([
        // Step 1: Check if smaller than minimum
        () => min(props.val, props.min),
        // Step 2: Check if larger than maximum
        val => max(val, props.max)
    ])
}

clamp({min: 0, max: 10, val: 5}); // 5
clamp({min: 0, max: 10, val: 15}); // 10
clamp({min: 0, max: 10, val: -10}); // 0

Хотя на первый взгляд это может показаться немного запутанным, преимущество в том, что теперь мы можем использовать метод minи maxнезависимо от clamp.

min(10, 0); // 10
min(0, 10); // 10
max(10, 0); // 0
max(0, 10); // 0

Что такое встроенные функциональные парадигмы в JavaScript?

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

Например, хотите запустить функцию над каждым элементом массива? Array.forEach

[1, 2, 3].forEach(val => console.log(val));

// Will output
1
2
3

Array.forEach не просто передает одно значение внутренней функции отображения, а также индекс элемента и исходный массив:

[1,2,3].forEach((val, i, arr) => console.log({val, i, length: arr.length}));

// Will output
{val: 1, i: 0, length: 3}
{val: 2, i: 1, length: 3}
{val: 3, i: 2, length: 3}

Сопоставление элементов массива с новым значением

Не используете «forEach«? Неважно! Также есть Array.map, который позволяет вам иметь список и хотите изменить каждый элемент в списке каким-либо образом.

const listAddedByOne = [1, 2, 3].map(val => val+1);

Array.map принимает функцию, которая при возврате нового значения обновит этот элемент списка.

Так же, как и Array.forEach, Array.mapпередает индекс элемента и исходный массив также внутренней функции:

const newList = ["Eat", "Sleep", "Play Elden Ring"].map((val, i, arr) => {
    return `${i+1}/${arr.length} - ${val}`;
});

// This will return:
"1/3 - Eat"
"2/3 - Sleep"
"3/3 - Play Elden Ring"

Фильтрация списка на основе возвращаемого значения функции

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

const numbers = [10, 20, 30, 40, 50, 60, 70, 80, 90];

И хотим отфильтровать этот список, чтобы включить только «маленькие» числа — то есть числа меньше 50. Вот где мы можем использовать Array.fiter:

const smallNumbers = numbers.filter(val => val < 50);

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

Сократить массив до одного значения

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

const numbers = [1, 2, 3];
// This will return "6"
const sum = numbers.reduce((acc, curr) => acc+prev, 0);

Уменьшение передается двумя элементами:

  1. Функция, которая возвращает уменьшенное значение
  2. Начальное значение, которое нужно установитьacc

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

Хотя forEach, map, filter, и reduceвсе встроены в JavaScript, основы функционального программирования означают, что мы можем реализовать их самостоятельно. Это не имеет никаких практических вариантов использования, но позволяет нам немного лучше понять, как JavaScript работает под капотом.

Например, a forEachможно реализовать с помощью базового forцикла:

const forEach = (arr, callback) {
    for (let i = 0; i < arr.length; i++) {
        callback(arr[i], i, arr);
    }
};

const cars = ["Ford", "Volvo", "BMW"];
forEach(cars, car => console.log(car));

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

const map = (arr, callback) {
    const returnedVal = [];
    for (let i = 0; i < arr.length; i++) {
        const newVal = callback(arr[i], i, arr);
        returnedVal.push(newVal);
    }

    return returnedVal;
};

const cars = ["Ford", "Volvo", "BMW"];
const carNameLengths = map(cars, car => car.length);
console.log(carNameLengths); // [4, 5, 3]

Реализация filter так же проста, как добавление одного if оператора в нашу map реализацию.

const filter = (arr, callback) {
    const returnedVal = [];
    for (let i = 0; i < arr.length; i++) {
        const exist = callback(arr[i], i, arr);
        if (exist) {
            returnedVal.push(arr[i]);
        }
    }

    return returnedVal;
};
const cars = ["Ford", "Volvo", "BMW"];
const onlyBMW = filter(cars, car => car === "BMW");
console.log(onlyBMW); // ["BMW"]

Наконец, реализация reduceаналогична нашей map реализации, но вместо pushing новых значений в массиве мы просто заменяем старое значение между итерациями цикла.

const reduce = (arr, callback, init) => {
    let returnedVal = init;
    for (let i = 0; i < arr.length; i++) {
            returnedVal = callback(returnedVal, arr[i], i, arr);
    }

    return returnedVal;
};

const numbers = [1,2,3];
const sum = reduce(numbers, (acc, curr) => acc+curr, 0);
console.log(sum); // 6

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

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

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

Эти концепции не являются уникальными для JavaScript! Python использует похожие идеи в своей функциональности «понимания списков».

Оцените статью
Adblock
detector