Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Korzyści z używania funkcji wyższego rzędu w JavaScript

Christopher Tran Team Lead / Front End Web Developer / AiTmed
Dowiedz się, jakie są zalety korzystania z funkcji wyższego rzędu w JavaScript oraz sprawdź, jak ich używać.
Korzyści z używania funkcji wyższego rzędu w JavaScript

JavaScript wykorzystuje funkcje w niemal każdej istniejącej aplikacji JS. Dzięki funkcjom język ten jest zdolny do wielu rzeczy, nawet takich jak budowanie sztucznej inteligencji do zastosowania w służbie zdrowia.

Właściwość, o której opowiem w tym artykule, wykorzystuje funkcje nazywane funkcją wyższego rzędu. Funkcja wyższego rzędu to funkcja, która albo przyjmuje inną funkcję jako argument, albo ją zwraca. Pokażę też kilka przykładów i przypadków ich użycia, czego brakuje w większości tutoriali.

W każdym razie ktoś, kto zetknął się wcześniej z JavaScript, prawdopodobnie słyszał o tym terminie. Funkcje wyższego rzędu są powszechnie używane w JS i istnieją w funkcjach, takich jak .map, .filter, .reduce i .forEach. Jeśli jednak nie masz dużo doświadczenia w JavaScript, możesz nie kojarzyć, gdzie one się znajdują. Pojawiają się, gdy deklarujesz callbacki funkcji jako argumenty dla metod tablicowych:

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)


Funkcja wyższego rzędu nie jest jednak funkcją przekazywaną do metod takich jak .map. Metody takie jak .map to właśnie funkcje wyższego rzędu.

Kiedy wspomnieliśmy, że funkcje wyższego rzędu mogą być funkcjami, które przyjmują inną funkcję jako argument, to tak właśnie było, kiedy funkcja została przekazana.

Oto implementacja, która działa dokładnie tak samo, jak metoda .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
}


W tym fragmencie kodu parametr wywołania zwrotnego jest dokładnie tą samą funkcją, którą przekazaliśmy jako argument do metody .map:

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


Zmieńmy nazwę tego fragmentu kodu na tę samą, którą ma nasza implementacja funkcji map, żeby było to lepiej widać:

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
}


Na początku może się to wydawać bezużytecznym sposobem pisania kodu w JavaScript. Po co wprowadzać funkcję i zawracać sobie głowę zwracaniem innej funkcji, skoro można tego uniknąć i zrobić wszystko od razu w jednej?

Największą korzyścią funkcji wyższego rzędu jest możliwość ponownego ich użycia i prostota. Ponadto, funkcje te również korzystają z pięknie napisanego kodu.

Możliwość ponownego użycia tych funkcji daje nam okazję do napisania dobrych kompozycji kodu.  


Przykłady kompozycji kodu

Teraz, gdy wiemy, jak wyglądają funkcje wyższego rzędu w kodzie, warto przyjrzeć się konkretnym przykładom ich użycia, zwłaszcza takim, gdzie mogą zabłysnąć. Załóżmy, że mamy listę żab:

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


Aby dopasować żaby do określonej płci bez funkcji wyższego rzędu, musielibyśmy zrobić coś takiego:

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

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


To wygląda zupełnie OK. Może to być jednak uciążliwe, jeśli zostanie użyte zbyt wiele razy w aplikacji. Gdybyśmy mieli gigantyczną aplikację o żabach, filterGender mógłby zostać użyty więcej niż raz.


Idziemy dwa kroki dalej

Jeśli chcesz utworzyć nową listę żab, musisz znowu wywołać filterGender i ponownie zadeklarować swoją płeć jako pierwszy argument do dopasowania w nowej liście:

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?


Jeśli nigdy nie słyszałeś o zasadzie DRY, to polecam się z nią zapoznać. Nasz fragment kodu narusza tę regułę z powodu pierwszego argumentu. Zdecydowanie stać nas na więcej.

Aby rozwiązać ten problem, możemy użyć funkcji wyższego rzędu.

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


A teraz możemy po prostu przypisać filterGender do zmiennej i nigdy nie musielibyśmy deklarować tej samej płci podczas filtrowania listy żab!

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


Teraz możemy filtrować samice z wielu list żab bez konieczności pisania dużej ilości kodu:

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

const frogsList2 = [
  {
    name: 'abc',
    email: 'froggy@gmail.com',
    age: 2,
    gender: 'Male',
    widthOfTongue: 1,
  },
  {
    name: '123',
    email: 'hippo@gmail.com',
    age: 10,
    gender: 'Male',
    widthOfTongue: 4,
  },
  {
    name: 'joe',
    email: 'sallyLipstick@aol.com',
    age: 5,
    gender: 'Female',
    widthOfTongue: 6,
  },
  {
    name: 'jennifer',
    email: 'georgeRoseBowl@aol.com',
    age: 11,
    gender: 'Female',
    widthOfTongue: 10,
  },
]

const frogsList3 = [
  {
    name: 'professorHammick',
    email: 'froggy@gmail.com',
    age: 2,
    gender: 'Female',
    widthOfTongue: 1,
  },
  {
    name: 'macintosh',
    email: 'hippo@gmail.com',
    age: 10,
    gender: 'Female',
    widthOfTongue: 6,
  },
  {
    name: 'frogger',
    email: 'sallyLipstick@gmail.com',
    age: 5,
    gender: 'Female',
    widthOfTongue: 4,
  },
  {
    name: 'frogNation',
    email: 'georgeRoseBowl@gmail.com',
    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)


Idziemy jeszcze o krok dalej

Jeśli to nadal za mało, aby przekonać Cię, jak bardzo korzystne jest używanie funkcji wyższego rzędu w JavaScript, stwórzmy jeszcze bardziej ogólną funkcję, w celu zapewnienia łatwiejszego ponownego używania:

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


Wcześniej mieliśmy możliwość wykonania funkcji wielokrotnego użycia dla danej płci danej żaby. Możemy jednak pójść o krok dalej i wyodrębnić logikę funkcji filtrowania, tak abyśmy mogli komponować i ponownie wykorzystywać różne funkcje filtrowania.

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)
})


Wow!

Wcześniej mieliśmy możliwość ponownego użycia funkcji filtrowania płci, bez konieczności ponownego zadeklarowania tej samej płci. Teraz możemy jednak stworzyć i ponownie wykorzystać funkcje do tego, w jaki sposób chcemy filtrować żaby. 

Możemy nawet korzystać ze wszystkich naraz:

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: "hippo@gmail.com",
          gender: "Male",
          name: "hippoTheFrog",
          widthOfTongue: 11
        }
*/


Idziemy o krok dalej ostatni raz

Nasza funkcja applyAllFilters całkiem dobrze wykonuje swoje zadanie. Jednak w przypadku ogromnych list żab, pójście jeszcze o krok dalej, może być trudne, ponieważ filtr zostanie uruchomiony wiele razy.

Możemy ponownie użyć funkcji wyższego rzędu, aby stworzyć prostą funkcję wielokrotnego użytku, która będzie w stanie wykonać jedno przejście przez całą listę żab, poprzez zastosowanie filtrów w tym samym czasie.

Dla jasności, spójrzmy na kod pętli for i spróbujmy zobaczyć, co się dzieje:

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
  }
}


Oto linijka kodu, którą chcę przeanalizować: 

newFrogs = filter(newFrogs)


Ten wiersz kodu jest tym samym, co return frogs.filter (filter) w następującej funkcji:

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


Jest to niestety problem, ponieważ metoda filtrowania tworzy nową tablicę. Tak to wtedy napisaliśmy:

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


Wywołujemy metodę filtrowania 4 razy. Innymi słowy, zmuszamy JavaScript do utworzenia 4 różnych tablic w pamięci, aby uzyskać końcowy wynik.

W jaki sposób możemy zatem zmusić JavaScript do utworzenia tylko jednej tablicy, tak aby uzyskać taki sam końcowy wynik?

Oczywiście możemy skorzystać z funkcji wyższego rzędu.

// 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)
}

// Credits to: SerjoA
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) {
    // Credits to: SerjoA
    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: "hippo@gmail.com",
          gender: "Male",
          name: "hippoTheFrog",
          widthOfTongue: 11
        }
*/


Mam nadzieję, że teraz widzisz, jak bardzo korzystne jest używanie funkcji wyższego rzędu oraz ile można z nimi zrobić.

Lubisz dzielić się wiedzą i chcesz zostać autorem?

Podziel się wiedzą z 130 tysiącami naszych czytelników

Dowiedz się więcej