Функции умеет писать каждый программист. Их часто называют объектами первого класса, потому что это ключевая концепция JavaScript. Но умеете ли вы использовать их эффективно?
Сегодня я дам несколько советов для продвинутой работы с функциями. Надеюсь, они вам пригодятся. В статье несколько разделов:
function circleArea(radius){
return radius * radius * 3.14
}
Если передавать в функцию одно и то же значение радиуса, она каждый раз будет возвращать одинаковый результат. Кроме того, при выполнении этой функции ничего не происходит за ее пределами. Поэтому эта функция является чистой.
Пример 2
let counter = (function(){
let initValue = 0
return function(){
initValue++;
return initValue
}
})()
Эта функция-счетчик каждый раз будет возвращать разные результаты, поэтому чистой она не является.
Пример 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().
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]
}
}
Рассмотрим пример:
Мы можем повысить быстродействие программы, «удалив» такие операторы после первого выполнения, чтобы функции больше не приходилось выполнять их при последующих вызовах. В этом и заключается сущность ленивых функций.
Например, нам нужно написать функцию 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)
}
Одним словом, если внутри функции есть условие, которое нужно проверять только один раз, мы можем оптимизировать код, написав ленивую функцию. После первой проверки первоначальная функция заменяется новой функцией, которая пропускает этап проверки условия.
То есть вместо того, чтобы принять все аргументы одновременно, функция сначала принимает первый аргумент и возвращает другую функцию, которая принимает второй аргумент; вторая функция в свою очередь принимает третий аргумент — и так до тех пор, пока не будут обработаны все аргументы.
Для чего это нужно?
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
}
Пример:
let toUpperCase = function(x) { return x.toUpperCase(); };
let hello = function(x) { return 'HELLO, ' + x; };
let greet = function(x){
return hello(toUpperCase(x));
};
В этом примере всего два этапа, поэтому в функции приветствия (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 реализована следующим образом.
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/
Сегодня я дам несколько советов для продвинутой работы с функциями. Надеюсь, они вам пригодятся. В статье несколько разделов:
- Чистые функции
- Функции высшего порядка
- Кэширование функций
- Ленивые функции
- Каррирование
- Композиция функций
Чистые функции
Что такое чистая функция?
Функция называется чистой, если соблюдаются оба следующих условия:- для одних и тех же аргументов она возвращает одно и то же значение;
- во время выполнения функции не возникает побочных эффектов.
function circleArea(radius){
return radius * radius * 3.14
}
Если передавать в функцию одно и то же значение радиуса, она каждый раз будет возвращать одинаковый результат. Кроме того, при выполнении этой функции ничего не происходит за ее пределами. Поэтому эта функция является чистой.
Пример 2
let counter = (function(){
let initValue = 0
return function(){
initValue++;
return initValue
}
})()
Эта функция-счетчик каждый раз будет возвращать разные результаты, поэтому чистой она не является.
Пример 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().
const arr1 = [1, 2, 3];С помощью метода map(callback) мы создаем новый массив, элементы которого являются результатами вызова заданной функции для каждого элемента первоначального массива.
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]
}
}
Рассмотрим пример:
Ленивые функции
В теле функции иногда содержатся условные операторы, которые выполняются только один раз.Мы можем повысить быстродействие программы, «удалив» такие операторы после первого выполнения, чтобы функции больше не приходилось выполнять их при последующих вызовах. В этом и заключается сущность ленивых функций.
Например, нам нужно написать функцию 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)
}
Одним словом, если внутри функции есть условие, которое нужно проверять только один раз, мы можем оптимизировать код, написав ленивую функцию. После первой проверки первоначальная функция заменяется новой функцией, которая пропускает этап проверки условия.
Каррирование функций
Каррирование — это техника трансформации функции с несколькими аргументами в последовательность функций с одним аргументом.То есть вместо того, чтобы принять все аргументы одновременно, функция сначала принимает первый аргумент и возвращает другую функцию, которая принимает второй аргумент; вторая функция в свою очередь принимает третий аргумент — и так до тех пор, пока не будут обработаны все аргументы.
Для чего это нужно?
- Каррирование позволяет избежать многократной передачи одной и той же переменной.
- Оно позволяет создавать функции высшего порядка. Оно существенно упрощает обработку событий.
- Небольшие фрагменты кода легко изменять и использовать повторно.
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
}
Пример:
Композиция функций
Предположим, нам надо написать функцию, которая работает так:Как видите, функция выполняет две задачи:вводим 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));
};
В этом примере всего два этапа, поэтому в функции приветствия (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 реализована следующим образом.
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/