Geheimnisse von JavaScript-Funktionen

Jeder Programmierer ist mit Funktionen vertraut. Funktionen in JavaScript haben viele Möglichkeiten, die es uns ermöglichen, sie "Funktionen höherer Ordnung" zu nennen. Aber selbst wenn Sie ständig JavaScript-Funktionen verwenden, können diese Sie überraschen. In diesem Beitrag werde ich einige der erweiterten Funktionen von JavaScript-Funktionen behandeln. Ich hoffe, Sie finden es nützlich, was Sie heute lernen.











Reine Funktionen



Eine Funktion, die die folgenden zwei Anforderungen erfüllt, heißt rein:



  • Wenn es mit denselben Argumenten aufgerufen wird, wird immer das gleiche Ergebnis zurückgegeben.
  • Bei Ausführung der Funktion treten keine Nebenwirkungen auf.


Betrachten wir ein Beispiel:



function circleArea(radius){
  return radius * radius * 3.14
}


Wenn dieser Funktion der gleiche Wert übergeben wird radius, wird immer das gleiche Ergebnis zurückgegeben. Gleichzeitig ändert sich während der Ausführung der Funktion nichts außerhalb der Funktion, dh sie hat keine Nebenwirkungen. All dies bedeutet, dass dies eine reine Funktion ist.



Hier ist ein weiteres Beispiel:



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


Versuchen wir diese Funktion in der Browserkonsole.





Testen der Funktion in der Browserkonsole



Wie Sie sehen, gibt die Funktioncounter, die den Zähler implementiert, bei jedem Aufruf unterschiedliche Ergebnisse zurück. Daher kann es nicht als rein bezeichnet werden.



Und hier ist ein weiteres Beispiel:



let femaleCounter = 0;
let maleCounter = 0;
function isMale(user){
  if(user.sex = 'man'){
    maleCounter++;
    return true
  }
  return false
}


Hier ist eine Funktion dargestellt isMale, die bei Übergabe des gleichen Arguments immer das gleiche Ergebnis zurückgibt. Aber es hat Nebenwirkungen. Wir sprechen nämlich über das Ändern einer globalen Variablen maleCounter. Infolgedessen kann diese Funktion nicht als rein bezeichnet werden.



▍Warum werden reine Funktionen benötigt?



Warum ziehen wir die Grenze zwischen regulären und reinen Funktionen? Der Punkt ist, dass reine Funktionen viele Stärken haben. Ihre Verwendung kann die Qualität des Codes verbessern. Lassen Sie uns darüber sprechen, was uns die Verwendung reiner Funktionen bringt.



1. Der Code der reinen Funktionen ist klarer als der Code der normalen Funktionen und leichter zu lesen



Jede reine Funktion zielt auf eine bestimmte Aufgabe ab. Es wird mit derselben Eingabe aufgerufen und gibt immer dasselbe Ergebnis zurück. Dies verbessert die Lesbarkeit des Codes erheblich und erleichtert die Dokumentation.



2. Reine Funktionen eignen sich besser zur Optimierung beim Kompilieren ihres Codes



Angenommen, Sie haben einen Code wie diesen:



for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}


Wenn fun- dies eine Funktion ist, die nicht rein ist, muss diese Funktion während der Ausführung dieses Codes fun(10)1000-mal aufgerufen werden .



Und wenn es sich funum eine reine Funktion handelt, kann der Compiler den Code optimieren. Es könnte ungefähr so ​​aussehen:



let result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}


3. Reine Funktionen sind einfacher zu testen



Reine Funktionstests sollten nicht kontextsensitiv sein. Beim Schreiben von Komponententests für reine Funktionen werden diesen Funktionen einfach einige Eingabewerte übergeben und überprüft, was sie gegen bestimmte Anforderungen zurückgeben.



Hier ist ein einfaches Beispiel. Die reine Funktion verwendet ein Array von Zahlen als Argument und fügt jedem Element dieses Arrays 1 hinzu, wodurch ein neues Array zurückgegeben wird. Hier ist eine Kurzdarstellung:



const incrementNumbers = function(numbers){
  // ...
}


Um eine solche Funktion zu testen, reicht es aus, einen Komponententest zu schreiben, der dem folgenden ähnelt:



let list = [1, 2, 3, 4, 5];
assert.equals(incrementNumbers(list), [2, 3, 4, 5, 6])


Wenn eine Funktion nicht sauber ist, müssen Sie zum Testen viele externe Faktoren berücksichtigen, die ihr Verhalten beeinflussen können. Infolgedessen ist das Testen einer solchen Funktion schwieriger als das Testen einer reinen Funktion.



Funktionen höherer Ordnung.



Eine Funktion höherer Ordnung ist eine Funktion mit mindestens einer der folgenden Funktionen:



  • Es kann andere Funktionen als Argumente akzeptieren.
  • Es kann eine Funktion als Ergebnis seiner Arbeit zurückgeben.


Durch die Verwendung von Funktionen höherer Ordnung können Sie die Flexibilität Ihres Codes erhöhen und kompaktere und effizientere Programme schreiben.



Angenommen, es gibt eine Reihe von Ganzzahlen. Es ist notwendig, auf seiner Basis ein neues Array mit der gleichen Länge zu erstellen, wobei jedes Element das Ergebnis der Multiplikation des entsprechenden Elements des ursprünglichen Arrays mit zwei darstellt.



Wenn Sie die Funktionen von Funktionen höherer Ordnung nicht nutzen, sieht die Lösung für dieses Problem möglicherweise folgendermaßen aus:



const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] * 2);
}


Wenn Sie über das Problem nachdenken, stellt sich heraus, dass Typobjekte Arrayin JavaScript eine Methode haben map(). Diese Methode heißt als map(callback). Es wird ein neues Array erstellt, das mit den Elementen des Arrays gefüllt ist, für das es aufgerufen wird, und mit der an es übergebenen Funktion verarbeitet wird callback.



So sieht die Lösung dieses Problems mit der Methode aus map():



const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item * 2;
});
console.log(arr2);


Die Methode map()ist ein Beispiel für eine Funktion höherer Ordnung.



Die korrekte Verwendung von Funktionen höherer Ordnung trägt zur Verbesserung der Qualität Ihres Codes bei. In den folgenden Abschnitten dieses Materials werden wir mehrmals auf solche Funktionen zurückkommen.



Ergebnisse der Caching-Funktion



Angenommen, Sie haben eine reine Funktion, die so aussieht:



function computed(str) {    
    // ,       
    console.log('2000s have passed')
      
    // ,     
    return 'a result'
}


Um die Leistung des Codes zu verbessern, schadet es uns nicht, die Ergebnisse der in der Funktion durchgeführten Berechnungen zwischenzuspeichern. Wenn Sie eine solche Funktion mit denselben Parametern aufrufen, mit denen sie bereits aufgerufen wurde, müssen Sie dieselben Berechnungen nicht erneut ausführen. Stattdessen werden ihre zuvor im Cache gespeicherten Ergebnisse sofort zurückgegeben.



Wie rüste ich eine Funktion mit einem Cache aus? Dazu können Sie eine spezielle Funktion schreiben, die als Wrapper für die Zielfunktion verwendet werden kann. Wir werden dieser speziellen Funktion einen Namen geben cached. Diese Funktion verwendet eine Zielfunktion als Argument und gibt eine neue Funktion zurück. In einer Funktion können cachedSie das Zwischenspeichern der Ergebnisse eines Aufrufs der Funktion, die sie umschließt, mithilfe eines regulären Objekts ( Object) oder eines Objekts, das eine Datenstruktur ist, organisierenMap...



So könnte der Funktionscode aussehen cached:



function cached(fn){
  //     ,      fn.
  const cache = Object.create(null);

  //   fn,    .
  return function cachedFn (str) {

    //       -   fn
    if ( !cache[str] ) {
        let result = fn(str);

        // ,   fn,   
        cache[str] = result;
    }

    return cache[str]
  }
}


Hier sind die Ergebnisse des Experimentierens mit dieser Funktion in der Browserkonsole.





Experimentieren mit einer Funktion, deren Ergebnisse zwischengespeichert werden



Faule Funktionen



In Funktionskörpern gibt es normalerweise einige Anweisungen zum Überprüfen einiger Bedingungen. Manchmal müssen die ihnen entsprechenden Bedingungen nur einmal überprüft werden. Es macht keinen Sinn, sie bei jedem Aufruf der Funktion zu überprüfen.



Unter solchen Umständen können Sie die Leistung der Funktion verbessern, indem Sie diese Anweisungen nach ihrer ersten Ausführung "entfernen". Infolgedessen stellt sich heraus, dass die Funktion bei ihren nachfolgenden Aufrufen keine Überprüfungen durchführen muss, die nicht mehr erforderlich sind. Dies wird die "faule" Funktion sein.



Angenommen, wir müssen eine Funktion schreiben foo, die immer das Objekt zurückgibt, das Datebeim ersten Aufruf dieser Funktion erstellt wurde. Bitte beachten Sie, dass wir ein Objekt benötigen, das genau beim ersten Aufruf der Funktion erstellt wurde.



Der Code könnte folgendermaßen aussehen:



let fooFirstExecutedDate = null;
function foo() {
    if ( fooFirstExecutedDate != null) {
      return fooFirstExecutedDate;
    } else {
      fooFirstExecutedDate = new Date()
      return fooFirstExecutedDate;
    }
}


Bei jedem Aufruf dieser Funktion muss die Bedingung überprüft werden. Wenn dieser Zustand sehr schwierig ist, führen Aufrufe einer solchen Funktion zu einem Rückgang der Programmleistung. Hier können wir die Technik der Erstellung "fauler" Funktionen verwenden, um den Code zu optimieren.



Wir können die Funktion nämlich wie folgt umschreiben:



var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
}


Nach dem ersten Aufruf der Funktion ersetzen wir die ursprüngliche Funktion durch die neue. Diese neue Funktion gibt den Wert zurück, der tdurch das Objekt dargestellt wird, Datedas beim ersten Aufruf der Funktion erstellt wurde. Daher müssen beim Aufrufen einer solchen Funktion keine Bedingungen überprüft werden. Dieser Ansatz kann die Leistung Ihres Codes verbessern.



Dies war ein sehr einfaches bedingtes Beispiel. Schauen wir uns nun etwas an, das der Realität näher kommt.



Wenn Sie Ereignishandler an DOM-Elemente anhängen, müssen Sie Überprüfungen durchführen, um sicherzustellen, dass die Lösung mit modernen Browsern und mit dem IE kompatibel ist:



function addEvent (type, el, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}


Es stellt sich heraus, dass jedes Mal, wenn wir eine Funktion aufrufen addEvent, eine Bedingung darin überprüft wird, die ausreicht, um sie beim ersten Aufruf nur einmal zu überprüfen. Lassen Sie uns diese Funktion "faul" machen:



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


Infolgedessen können wir sagen, dass Sie den Code optimieren können, wenn eine bestimmte Bedingung in einer Funktion überprüft wird, die nur einmal ausgeführt werden muss, indem Sie die Technik zum Erstellen "fauler" Funktionen anwenden. Die Optimierung besteht nämlich darin, dass nach der ersten Überprüfung der Bedingung die ursprüngliche Funktion durch eine neue ersetzt wird, bei der keine Überprüfung der Bedingungen mehr erfolgt.



Currying-Funktionen



Currying ist eine solche Transformation einer Funktion, nach deren Anwendung eine Funktion, die zuvor durch Übergabe mehrerer Argumente gleichzeitig aufgerufen werden musste, in eine Funktion umgewandelt wird, die durch Übergabe der erforderlichen Argumente nacheinander aufgerufen werden kann.



Mit anderen Worten, wir sprechen über die Tatsache, dass eine Curry-Funktion, für deren korrekte Funktion mehrere Argumente erforderlich sind, das erste akzeptieren und eine Funktion zurückgeben kann, die ein zweites Argument annehmen kann. Diese zweite Funktion gibt wiederum eine neue Funktion zurück, die ein drittes Argument akzeptiert und eine neue Funktion zurückgibt. Dies wird fortgesetzt, bis die erforderliche Anzahl von Argumenten an die Funktion übergeben wurde.



Was nützt das?



  • Currying hilft, Situationen zu vermeiden, in denen eine Funktion aufgerufen werden muss, indem das gleiche Argument immer wieder übergeben wird.
  • Diese Technik hilft, Funktionen höherer Ordnung zu erzeugen. Es ist äußerst nützlich für die Behandlung von Ereignissen.
  • Dank Curry können Sie die vorläufige Vorbereitung von Funktionen für die Ausführung bestimmter Aktionen organisieren und diese Funktionen dann bequem in Ihrem Code wiederverwenden.


Stellen Sie sich eine einfache Funktion vor, die die übergebenen Zahlen hinzufügt. Nennen wir es add. Es werden drei Operanden als Argumente verwendet und ihre Summe zurückgegeben:



function add(a,b,c){
 return a + b + c;
}


Eine solche Funktion kann aufgerufen werden, indem weniger Argumente übergeben werden, als sie benötigt (obwohl dies dazu führt, dass sie etwas völlig anderes zurückgibt, als von ihr erwartet wird). Es kann auch mit mehr Argumenten aufgerufen werden, als bei der Erstellung vorgesehen waren. In einer solchen Situation werden "unnötige" Argumente einfach ignoriert. Das Experimentieren mit einer ähnlichen Funktion könnte folgendermaßen aussehen:



add(1,2,3) --> 6 
add(1,2) --> NaN
add(1,2,3,4) --> 6 //  .


Wie kann man eine solche Funktion curry?



Hier ist der Code der Funktion curry, die andere Funktionen curry soll:



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
}


Hier sind die Ergebnisse des Experimentierens mit dieser Funktion in der Browserkonsole.





Experimentieren mit Curry in der Browserkonsole



Funktionszusammensetzung



Angenommen, Sie müssen eine Funktion schreiben, die als Eingabe, z. B. eine Zeichenfolge bitfish, eine Zeichenfolge zurückgibt HELLO, BITFISH.



Wie Sie sehen können, dient diese Funktion zwei Zwecken:



  • Verkettung von Strings.
  • Konvertieren der Zeichen in der resultierenden Zeichenfolge in Großbuchstaben.


So könnte der Code für eine solche Funktion aussehen:



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


Experimentieren wir damit.





Testen einer Funktion in der Browserkonsole



Diese Aufgabe enthält zwei Unteraufgaben, die als separate Funktionen organisiert sind. Infolgedessen ist der Funktionscodegreetrecht einfach. Wenn es notwendig wäre, mehr Operationen an Strings auszuführen,greetwürdedie Funktioneine Konstruktion wie enthaltenfn3(fn2(fn1(fn0(x)))).



Vereinfachen wir die Lösung des Problems und schreiben eine Funktion, die andere Funktionen zusammensetzt. Nennen wir escompose. Hier ist der Code:



let compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};


Jetzt kann die Funktion greetmit folgender Funktion erstellt werden compose:



let greet = compose(hello, toUpperCase);
greet('kevin');


Wenn Sie eine Funktion verwenden compose, um eine neue Funktion basierend auf zwei vorhandenen zu erstellen, müssen Sie eine Funktion erstellen, die diese Funktionen von links nach rechts aufruft. Als Ergebnis erhalten wir kompakten Code, der leicht zu lesen ist.



Jetzt akzeptiert unsere Funktion composenur zwei Parameter. Und wir möchten, dass es eine beliebige Anzahl von Parametern akzeptieren kann.



Eine ähnliche Funktion, die eine beliebige Anzahl von Parametern akzeptieren kann, ist in der bekannten Open-Source- Unterstrichbibliothek verfügbar .



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[i].call(this, result);
        return result;
    };
};


Durch die Verwendung der Funktionszusammensetzung können Sie die logischen Beziehungen zwischen Funktionen verständlicher machen, die Lesbarkeit Ihres Codes verbessern und die Grundlage für zukünftige Erweiterungen und Refactorings legen.



Verwenden Sie eine spezielle Methode zum Arbeiten mit Funktionen in Ihren JavaScript-Projekten?










All Articles