С примерами и вариантами использования

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

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

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

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

Вы видите это, когда объявляете обратные вызовы функций в качестве аргументов для этих методов массива:

const arr = [1, 2, 3, 4, 5, 'six', 'seven', 'eight', 'nine', 'ten']

// Duplicate the array
arr.map(function(value) {
  return value
})

// Return only the number types
arr.filter(function(value) {
  return typeof value === 'number'
})

// Log each value to the console
arr.forEach(function(value) {
  console.log(value)
})

// Add the numbers together, avoiding the string types
arr.reduce(function(acc, value) {
  if (typeof value === 'number') {
    acc += value
  }
  return acc
}, 0)

Но функция высшего порядка - это не функция, которую вы передаете таким методам, как .map. Такие методы, как .map, являются функциями высшего порядка.

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

Вот реализация, которая работает точно так же, как метод .map:

function map(callback) {
  const result = []
  
  for (let index = 0; index < this.length; index++) {
    const currentItem = this[index]
    const returnValue = callback(currentItem, index, this)
    result.push(returnValue)
  }
  
  return result
}

Глядя на фрагмент кода, параметр callback - это та же самая функция, которую мы передали в качестве аргумента методу .map, который я показал ранее:

// Duplicate the array
arr.map(function(value) {
  return value
})

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

const callback = function(value) {
  return value
}

// Duplicate the array
arr.map(callback)

// is the same callback used in our .map implementation:
function map(callback) {
  const result = []
  for (let index = 0; index < this.length; index++) {
    const currentItem = this[index]
    const returnValue = callback(currentItem, index, this)
    result.push(returnValue)
  }
  return result
}

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

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

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

Составление кода и эффективные примеры

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

Допустим, у нас есть список лягушек:

const frogsList = [
  // Yes, these frogs are intelligent. They know how to use email
  {
    name: 'bobTheFrog',
    email: '[email protected]',
    age: 2,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'hippoTheFrog',
    email: '[email protected]',
    age: 10,
    gender: 'Male',
    widthOfTongue: 11,
  },
  {
    name: 'sally',
    email: '[email protected]',
    age: 5,
    gender: 'Female',
    widthOfTongue: 4,
  },
  {
    name: 'george',
    email: '[email protected]',
    age: 11,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'lisa',
    email: '[email protected]',
    age: 19,
    gender: 'Female',
    widthOfTongue: 15,
  },
  {
    name: 'kentucky',
    email: '[email protected]',
    age: 18,
    gender: 'Male',
    widthOfTongue: 13,
  },
]

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

function filterGender(gender, frogs) {
  return frogs.filter(function(frog) {
    return frog.gender ==== gender
  })
}

// filterGender in use
const maleFrogs = filterGender('Male', frogsList)

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

Шаг вперед

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

function getFrogs() {
  // some logic and returns a new list of frogs
}

const newFrogs = getFrogs()
const moreMaleFrogs = filterGender('Male', newFrogs) // Shucks, I have to write 'Male' again?

Если вы никогда не слышали о принципе СУХОЙ, настоятельно рекомендую разобраться в нем. Наш фрагмент кода нарушает это правило из-за первого аргумента. Мы можем сделать лучше, чем это.

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

function filterGender(gender) {
  return function(frogs) {
    return frogs.filter(function(frog) {
      return frog.gender === gender
    })
  }
}

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

const filterFemaleFrogs = filterGender('Female')
const femaleFrogs = filterFemaleFrogs(frogsList)

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

Теперь мы можем фильтровать самок из нескольких списков лягушек без необходимости писать столько кода:

const frogsList = [
  // Yes, these frogs are intelligent. They know how to use email
  {
    name: 'bobTheFrog',
    email: '[email protected]',
    age: 2,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'hippoTheFrog',
    email: '[email protected]',
    age: 10,
    gender: 'Male',
    widthOfTongue: 11,
  },
  {
    name: 'sally',
    email: '[email protected]',
    age: 5,
    gender: 'Female',
    widthOfTongue: 4,
  },
  {
    name: 'george',
    email: '[email protected]',
    age: 11,
    gender: 'Male',
    widthOfTongue: 3,
  },
  {
    name: 'lisa',
    email: '[email protected]',
    age: 19,
    gender: 'Female',
    widthOfTongue: 15,
  },
  {
    name: 'kentucky',
    email: '[email protected]',
    age: 18,
    gender: 'Male',
    widthOfTongue: 13,
  },
]

const frogsList2 = [
  {
    name: 'abc',
    email: '[email protected]',
    age: 2,
    gender: 'Male',
    widthOfTongue: 1,
  },
  {
    name: '123',
    email: '[email protected]',
    age: 10,
    gender: 'Male',
    widthOfTongue: 4,
  },
  {
    name: 'joe',
    email: '[email protected]',
    age: 5,
    gender: 'Female',
    widthOfTongue: 6,
  },
  {
    name: 'jennifer',
    email: '[email protected]',
    age: 11,
    gender: 'Female',
    widthOfTongue: 10,
  },
]

const frogsList3 = [
  {
    name: 'professorHammick',
    email: '[email protected]',
    age: 2,
    gender: 'Female',
    widthOfTongue: 1,
  },
  {
    name: 'macintosh',
    email: '[email protected]',
    age: 10,
    gender: 'Female',
    widthOfTongue: 6,
  },
  {
    name: 'frogger',
    email: '[email protected]',
    age: 5,
    gender: 'Female',
    widthOfTongue: 4,
  },
  {
    name: 'frogNation',
    email: '[email protected]',
    age: 11,
    gender: 'Female',
    widthOfTongue: 4,
  },
]

function gatherFemaleFrogsEverywhere(...frogLists) {
  const allFemaleFrogs = []
  const filterFemaleFrogs = filterGender('Female')
  frogLists.forEach(function(list) {
    allFemaleFrogs.push(...filterFemaleFrogs(list))
  })
  return allFemaleFrogs
}

const females = gatherFemaleFrogsEverywhere(frogsList, frogsList2, frogsList3)

Еще один шаг вперед

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

function filterFrogs(filter) {
  return function(frogs) {
    return frogs.filter(filter)
  }
}

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

const filterMaleFrogs = filterFrogs(function(frog) {
  return frog.gender === 'Male'
})

const filterAdultFrogs = filterFrogs(function(frog) {
  return frog.age >= 10
})

const filterFrogNamesThatStartWithHippo = filterFrogs(function(frog) {
  return frog.name.toLowerCase().startsWith('hippo')
})

const filterGmailEmails = filterFrogs(function(frog) {
  return /gmail.com/i.test(frog.email)
})

Ух ты!

Раньше у нас была удивительная возможность повторно использовать функцию фильтра пола без необходимости снова объявлять тот же тип пола. Теперь у нас есть дополнительная возможность создавать и повторно использовать функции того, как мы хотим фильтровать лягушек - потрясающе!

Мы даже можем использовать их все сразу:

function applyAllFilters(...filters) {
  return function(frogs) {
    let newFrogs = [...frogs]
    for (let index = 0; index < filters.length; index++) {
      const filter = filters[index]
      newFrogs = filter(newFrogs)
    }
    return newFrogs
  }
}

const applyFrogFilterers = applyAllFilters(
  filterMaleFrogs,
  filterAdultFrogs,
  filterFrogNamesThatStartWithHippo,
  filterGmailEmails,
)

const combinedFrogsList = [...frogsList, ...frogsList2, ...frogsList3]

const filteredFrogs = applyFrogFilterers(combinedFrogsList)

console.log(filteredFrogs)

/*
      result:
        {
          age: 10,
          email: "[email protected]",
          gender: "Male",
          name: "hippoTheFrog",
          widthOfTongue: 11
        }
*/

Сделаем еще один шаг вперед в последний раз

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

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

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

function applyAllFilters(...filters) {
  return function(frogs) {
    let newFrogs = [...frogs]
    for (let index = 0; index < filters.length; index++) {
      const filter = filters[index]
      newFrogs = filter(newFrogs)
    }
    return newFrogs
  }
}

Строка, на которую я хочу, чтобы вы посмотрели, следующая:

newFrogs = filter(newFrogs)

Эта строка кода является той же строкой кода, что и return frogs.filter(filter) в этой функции:

function filterFrogs(filter) {
  return function(frogs) {
    return frogs.filter(filter)
  }
}

Это проблема, потому что метод фильтра создает новый массив. Когда мы написали это:

const applyFrogFilterers = applyAllFilters(
  filterMaleFrogs,
  filterAdultFrogs,
  filterFrogNamesThatStartWithHippo,
  filterGmailEmails,
)

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

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

Ты угадал. Использование функций высшего порядка!

// NOTE: The filter functions are now individual functions (not wrapped with filterFrogs)
const filterMaleFrogs = function(frog) {
  return frog.gender === 'Male'
}
const filterAdultFrogs = function(frog) {
  return frog.age >= 10
}
const filterFrogNamesThatStartWithHippo = function(frog) {
  return frog.name.toLowerCase().startsWith('hippo')
}
const filterGmailEmails = function(frog) {
  return /gmail.com/i.test(frog.email)
}
function combineFilters(...fns) {
  return function(val) {
    for (let i = 0; i < fns.length; i++) {
      const filter = fns[i]
      const passes = filter(val)
      if (passes) {
        continue
      } else {
        return false
      }
    }
    return true
  }
}
function composeFrogFilterers(...fns) {
  return function(frogs) {
    return frogs.filter(combineFilters(...fns))
  }
}
const applyFrogFilterers = composeFrogFilterers(
  filterMaleFrogs,
  filterAdultFrogs,
  filterFrogNamesThatStartWithHippo,
  filterGmailEmails,
)
const combinedFrogsList = [...frogsList, ...frogsList2, ...frogsList3]
const allFilteredFrogs = applyFrogFilterers(combinedFrogsList)
console.log(allFilteredFrogs)
/*
      result:
        {
          age: 10,
          email: "[email protected]",
          gender: "Male",
          name: "hippoTheFrog",
          widthOfTongue: 11
        }
*/

Заключение

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

Ищите больше в будущем!