Функции в JavaScript: секреты, о которых вы не слышали

Kate

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

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

  • Чистые функции
  • Функции высшего порядка
  • Кэширование функций
  • Ленивые функции
  • Каррирование
  • Композиция функций

Чистые функции​

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

Функция называется чистой, если соблюдаются оба следующих условия:

  • для одних и тех же аргументов она возвращает одно и то же значение;
  • во время выполнения функции не возникает побочных эффектов.
Пример 1

function circleArea(radius){
return radius * radius * 3.14
}
Если передавать в функцию одно и то же значение радиуса, она каждый раз будет возвращать одинаковый результат. Кроме того, при выполнении этой функции ничего не происходит за ее пределами. Поэтому эта функция является чистой.

Пример 2

let counter = (function(){
let initValue = 0
return function(){
initValue++;
return initValue
}
})()
bb27e4fe99a39b03a1e55b2669914f31


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

Пример 3

let femaleCounter = 0;
let maleCounter = 0;
function isMale(user){
if(user.sex = 'man'){
maleCounter++;
return true
}
return false
}
Если в функцию isMale из третьего примера передавать одно и то же значение аргумента, она будет возвращать один и тот же результат, но у нее есть побочные эффекты. В этом случае побочным эффектом будет изменение значения глобальной переменной maleCounter, то есть эта функция также не является чистой.

Зачем нужны чистые функции?​

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

1.Чистые функции более понятны, их легко читать.

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

2.Компилятору проще оптимизировать чистые функции.

Рассмотрим фрагмент кода.

for (int i = 0; i < 1000; i++){
console.log(fun(10));
}
Если функция fun не является чистой, то вызов fun(10) придется совершить 1000 раз.

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

let result = fun(10)
for (int i = 0; i < 1000; i++){
console.log(result);
}
3. Чистые функции проще тестировать.

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

Рассмотрим простой пример. Чистая функция принимает в качестве аргумента числовой массив и увеличивает каждый элемент массива на 1.

const incrementNumbers = function(numbers){
// ...
}
Нам достаточно написать всего лишь вот такой модульный тест:

let list = [1, 2, 3, 4, 5];
assert.equals(incrementNumbers(list), [2, 3, 4, 5, 6])
Если функция не является чистой, то нужно учитывать множество внешних факторов, а это задача не из легких.

Функции высшего порядка​

Что такое функция высшего порядка?

Функция высшего порядка отвечает хотя бы одному из следующих критериев:

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

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

Без функций высшего порядка мы бы написали такой код:

const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
arr2.push(arr1 * 2);
}
В JavaScript у объекта массива есть метод map().

С помощью метода map(callback) мы создаем новый массив, элементы которого являются результатами вызова заданной функции для каждого элемента первоначального массива.
const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
return item * 2;
});
console.log(arr2);
Функция map — это функция высшего порядка.

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

Кэширование функций​

Допустим, у нас есть следующая чистая функция:

function computed(str) {
// Suppose the calculation in the funtion is very time consuming
console.log('2000s have passed')

// Suppose it is the result of the function
return 'a result'
}
Для повышения быстродействия программы мы хотим кэшировать результат выполнения функции, чтобы при последующих вызовах функции с теми же параметрами она уже не выполнялась, а результат возвращался непосредственно из кэша. Как этого добиться?

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

function cached(fn){
// Create an object to store the results returned after each function execution.
const cache = Object.create(null);

// Returns the wrapped function
return function cachedFn (str) {

// If the cache is not hit, the function will be executed
if ( !cache[str] ) {
let result = fn(str);

// Store the result of the function execution in the cache
cache[str] = result;
}

return cache[str]
}
}
Рассмотрим пример:

776b0325ffaceba3c1f7297765e22391

Ленивые функции​

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

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

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

let fooFirstExecutedDate = null;
function foo() {
if ( fooFirstExecutedDate != null) {
return fooFirstExecutedDate;
} else {
fooFirstExecutedDate = new Date()
return fooFirstExecutedDate;
}
}
При каждом вызове этой функции необходимо проверять выполнение заданного условия. Если условие очень сложное, это может замедлить работу программы. Вот тут-то нам и пригодится концепция ленивой функции для оптимизации кода.

Можно записать функцию так:

var foo = function() {
var t = new Date();
foo = function() {
return t;
};
return foo();
}
После первого выполнения мы заменим первоначальную функцию новой. При последующих вызовах этой функции условие уже не будет проверяться — и код будет работать быстрее.

Теперь рассмотрим более практический пример.

При добавлении к элементу событий DOM необходимо обеспечить кросс-браузерную совместимость, в том числе с браузером IE. Для этого нужно определить среду браузера:

function addEvent (type, el, fn) {
if (window.addEventListener) {
el.addEventListener(type, fn, false);
}
else if(window.attachEvent){
el.attachEvent('on' + type, fn);
}
}
При каждом вызове функции addEvent нужно проверять выполнение условия. Используя ленивые функции, это можно сделать так:

function addEvent (type, el, fn) {
if (window.addEventListener) {
addEvent = function (type, el, fn) {
el.addEventListener(type, fn, false);
}
} else if(window.attachEvent){
addEvent = function (type, el, fn) {
el.attachEvent('on' + type, fn);
}
}
addEvent(type, el, fn)
}
Одним словом, если внутри функции есть условие, которое нужно проверять только один раз, мы можем оптимизировать код, написав ленивую функцию. После первой проверки первоначальная функция заменяется новой функцией, которая пропускает этап проверки условия.

Каррирование функций​

Каррирование — это техника трансформации функции с несколькими аргументами в последовательность функций с одним аргументом.

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

Для чего это нужно?

  • Каррирование позволяет избежать многократной передачи одной и той же переменной.
  • Оно позволяет создавать функции высшего порядка. Оно существенно упрощает обработку событий.
  • Небольшие фрагменты кода легко изменять и использовать повторно.
Рассмотрим простую функцию add. Она принимает три операнда в качестве аргументов и возвращает их сумму.

function add(a,b,c){
return a + b + c;
}
Вы можете передать в функцию слишком мало аргументов (и получить ошибочные результаты) или, наоборот, слишком много аргументов (тогда лишние аргументы будут проигнорированы).

add(1,2,3) --> 6
add(1,2) --> NaN
add(1,2,3,4) --> 6 //Extra parameters will be ignored.
Как каррировать эту функцию?

Код:

function curry(fn) {
if (fn.length <= 1) return fn;
const generator = (...args) => {
if (fn.length === args.length) {

return fn(...args)
} else {
return (...args2) => {

return generator(...args, ...args2)
}
}
}
return generator
}
Пример:

306e1b5d0c0b8dcbbfcc0dea384e4677

Композиция функций​

Предположим, нам надо написать функцию, которая работает так:

вводим bitfish, в результате получаем HELLO, BITFISH
Как видите, функция выполняет две задачи:

  • конкатенация строк;
  • перевод строки в верхний регистр.
Для этого можно написать следующий код:

let toUpperCase = function(x) { return x.toUpperCase(); };
let hello = function(x) { return 'HELLO, ' + x; };
let greet = function(x){
return hello(toUpperCase(x));
};
164a2e566c95c889e308abb49770a30a

В этом примере всего два этапа, поэтому в функции приветствия (greet) ничего сложного нет. Но если бы операций было больше, вызов функции greet содержал бы больше вложенных элементов и выглядел бы примерно так: fn3(fn2(fn1(fn0(x)))).

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

let compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
Таким образом, функцию greet можно реализовать через функцию compose:

let greet = compose(hello, toUpperCase);
greet('kevin');
При объединении двух функций в одну посредством compose код будет выполняться справа налево, а не изнутри наружу, что повышает его читаемость.

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

В известном проекте с открытым исходным кодом underscore функция compose реализована следующим образом.

ed1abbef4a472ff11373f29c7b7a8c98
function compose() {
var args = arguments;
var start = args.length - 1;
return function() {
var i = start;
var result = args[start].apply(this, arguments);
while (i--) result = args.call(this, result);
return result;
};
};
Используя композицию функций, можно оптимизировать логические связи между функциями, улучшить читаемость кода и упростить его дальнейшее расширение и рефакторинг.

Источник статьи: https://habr.com/ru/company/otus/blog/530214/
 
Сверху