11. Parameterbehandlung
Inhaltsverzeichnis
Bitte unterstützen Sie dieses Buch: kaufen Sie es (PDF, EPUB, MOBI) oder spenden Sie
(Werbung, bitte nicht blockieren.)

11. Parameterbehandlung



11.1 Überblick

Die Parameterbehandlung wurde in ECMAScript 6 erheblich erweitert. Sie unterstützt nun Standard-Parameterwerte, Rest-Parameter (Varargs) und Destrukturierung.

Zusätzlich hilft der Spread-Operator bei Funktions-/Methoden-/Konstruktoraufrufen und Array-Literalen.

11.1.1 Standard-Parameterwerte

Ein Standard-Parameterwert wird für einen Parameter mit einem Gleichheitszeichen (=) angegeben. Wenn ein Aufrufer keinen Wert für den Parameter angibt, wird der Standardwert verwendet. Im folgenden Beispiel ist der Standard-Parameterwert von y 0

function func(x, y=0) {
    return [x, y];
}
func(1, 2); // [1, 2]
func(1); // [1, 0]
func(); // [undefined, 0]

11.1.2 Rest-Parameter

Wenn Sie einem Parameternamen den Rest-Operator (...) voranstellen, erhält dieser Parameter alle verbleibenden Parameter als Array

function format(pattern, ...params) {
    return {pattern, params};
}
format(1, 2, 3);
    // { pattern: 1, params: [ 2, 3 ] }
format();
    // { pattern: undefined, params: [] }

11.1.3 Benannte Parameter mittels Destrukturierung

Sie können benannte Parameter simulieren, wenn Sie in der Parameterliste mit einem Objekt-Muster eine Destrukturierung durchführen

function selectEntries({ start=0, end=-1, step=1 } = {}) { // (A)
    // The object pattern is an abbreviation of:
    // { start: start=0, end: end=-1, step: step=1 }

    // Use the variables `start`, `end` and `step` here
    ···
}

selectEntries({ start: 10, end: 30, step: 2 });
selectEntries({ step: 3 });
selectEntries({});
selectEntries();

Das = {} in Zeile A ermöglicht es Ihnen, selectEntries() ohne Parameter aufzurufen.

11.1.4 Spread-Operator (...)

In Funktions- und Konstruktoraufrufen wandelt der Spread-Operator iterierbare Werte in Argumente um

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11
> Math.max(-1, ...[-5, 11], 3)
11

In Array-Literalen wandelt der Spread-Operator iterierbare Werte in Array-Elemente um

> [1, ...[2,3], 4]
[1, 2, 3, 4]

11.2 Parameterbehandlung als Destrukturierung

Der ES6-Weg der Parameterbehandlung ist äquivalent zur Destrukturierung der tatsächlichen Parameter über die formalen Parameter. Das heißt, der folgende Funktionsaufruf

function func(«FORMAL_PARAMETERS») {
    «CODE»
}
func(«ACTUAL_PARAMETERS»);

ist ungefähr äquivalent zu

{
    let [«FORMAL_PARAMETERS»] = [«ACTUAL_PARAMETERS»];
    {
        «CODE»
    }
}

Beispiel – der folgende Funktionsaufruf

function logSum(x=0, y=0) {
    console.log(x + y);
}
logSum(7, 8);

wird zu

{
    let [x=0, y=0] = [7, 8];
    {
        console.log(x + y);
    }
}

Schauen wir uns nun spezifische Features an.

11.3 Standard-Parameterwerte

ECMAScript 6 ermöglicht es Ihnen, Standardwerte für Parameter anzugeben

function f(x, y=0) {
  return [x, y];
}

Das Weglassen des zweiten Parameters löst den Standardwert aus

> f(1)
[1, 0]
> f()
[undefined, 0]

Vorsicht – undefined löst ebenfalls den Standardwert aus

> f(undefined, undefined)
[undefined, 0]

Der Standardwert wird bei Bedarf berechnet, nur wenn er tatsächlich benötigt wird

> const log = console.log.bind(console);
> function g(x=log('x'), y=log('y')) {return 'DONE'}
> g()
x
y
'DONE'
> g(1)
y
'DONE'
> g(1, 2)
'DONE'

11.3.1 Warum löst undefined Standardwerte aus?

Es ist nicht sofort ersichtlich, warum undefined als fehlender Parameter oder fehlender Teil eines Objekts oder Arrays interpretiert werden sollte. Der Grund dafür ist, dass es Ihnen ermöglicht, die Definition von Standardwerten zu delegieren. Betrachten wir zwei Beispiele.

Im ersten Beispiel (Quelle: Rick Waldrons TC39-Meeting-Notizen vom 24.07.2012) müssen wir keinen Standardwert in setOptions() definieren, wir können diese Aufgabe an setLevel() delegieren.

function setLevel(newLevel = 0) {
    light.intensity = newLevel;
}
function setOptions(options) {
    // Missing prop returns undefined => use default
    setLevel(options.dimmerLevel);
    setMotorSpeed(options.speed);
    ···
}
setOptions({speed:5});

Im zweiten Beispiel muss square() keinen Standardwert für x definieren, es kann diese Aufgabe an multiply() delegieren

function multiply(x=1, y=1) {
    return x * y;
}
function square(x) {
    return multiply(x, x);
}

Standardwerte verankern die Rolle von undefined weiter als Indikator für das Nichtvorhandensein, im Gegensatz zu null, das für Leere steht.

11.3.2 Bezugnahme auf andere Parameter in Standardwerten

Innerhalb eines Standard-Parameterwerts können Sie auf jede Variable, einschließlich anderer Parameter, verweisen

function foo(x=3, y=x) {}
foo();     // x=3; y=3
foo(7);    // x=7; y=7
foo(7, 2); // x=7; y=2

Die Reihenfolge ist jedoch wichtig. Parameter werden von links nach rechts deklariert. „Innerhalb“ eines Standardwerts erhalten Sie einen ReferenceError, wenn Sie auf einen Parameter zugreifen, der noch nicht deklariert wurde

function bar(x=y, y=4) {}
bar(3); // OK
bar(); // ReferenceError: y is not defined

11.3.3 Bezugnahme auf „innere“ Variablen in Standardwerten

Standardwerte existieren in ihrem eigenen Gültigkeitsbereich, der zwischen dem „äußeren“ Gültigkeitsbereich, der die Funktion umgibt, und dem „inneren“ Gültigkeitsbereich des Funktionskörpers liegt. Daher können Sie von den Standardwerten aus nicht auf „innere“ Variablen zugreifen

const x = 'outer';
function foo(a = x) {
    const x = 'inner';
    console.log(a); // outer
}

Wenn im vorherigen Beispiel kein äußeres x vorhanden wäre, würde der Standardwert x einen ReferenceError erzeugen (falls ausgelöst).

Diese Einschränkung ist wahrscheinlich am überraschendsten, wenn Standardwerte Closures sind

const QUX = 2;
function bar(callback = () => QUX) { // returns 2
    const QUX = 3;
    callback();
}
bar(); // ReferenceError

11.4 Rest-Parameter

Wenn Sie den Rest-Operator (...) vor den letzten formalen Parameter setzen, bedeutet dies, dass dieser alle verbleibenden tatsächlichen Parameter in einem Array erhält.

function f(x, ...y) {
    ···
}
f('a', 'b', 'c'); // x = 'a'; y = ['b', 'c']

Wenn keine verbleibenden Parameter vorhanden sind, wird der Rest-Parameter auf das leere Array gesetzt

f(); // x = undefined; y = []

11.4.1 Kein arguments mehr!

Rest-Parameter können die berüchtigte spezielle Variable arguments von JavaScript vollständig ersetzen. Sie haben den Vorteil, dass sie immer Arrays sind

// ECMAScript 5: arguments
function logAllArguments() {
    for (var i=0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}

// ECMAScript 6: rest parameter
function logAllArguments(...args) {
    for (const arg of args) {
        console.log(arg);
    }
}
11.4.1.1 Kombination von Destrukturierung und Zugriff auf den destrukturierten Wert

Ein interessantes Merkmal von arguments ist, dass Sie normale Parameter und ein Array aller Parameter gleichzeitig haben können

function foo(x=0, y=0) {
    console.log('Arity: '+arguments.length);
    ···
}

Sie können arguments in solchen Fällen vermeiden, wenn Sie einen Rest-Parameter mit Array-Destrukturierung kombinieren. Der resultierende Code ist länger, aber expliziter

function foo(...args) {
    let [x=0, y=0] = args;
    console.log('Arity: '+args.length);
    ···
}

Die gleiche Technik funktioniert für benannte Parameter (Optionsobjekte)

function bar(options = {}) {
    let { namedParam1, namedParam2 } = options;
    ···
    if ('extra' in options) {
        ···
    }
}
11.4.1.2 arguments ist iterierbar

arguments ist in ECMAScript 6 iterierbar5, was bedeutet, dass Sie for-of und den Spread-Operator verwenden können

> (function () { return typeof arguments[Symbol.iterator] }())
'function'
> (function () { return Array.isArray([...arguments]) }())
true

11.5 Simulation benannter Parameter

Beim Aufrufen einer Funktion (oder Methode) in einer Programmiersprache müssen Sie die tatsächlichen Parameter (vom Aufrufer bereitgestellt) den formalen Parametern (einer Funktionsdefinition) zuordnen. Es gibt zwei gängige Möglichkeiten, dies zu tun

Benannte Parameter haben zwei Hauptvorteile: Sie liefern Beschreibungen für Argumente in Funktionsaufrufen und sie eignen sich gut für optionale Parameter. Ich werde zuerst die Vorteile erklären und Ihnen dann zeigen, wie Sie benannte Parameter in JavaScript über Objektliterale simulieren können.

11.5.1 Benannte Parameter als Beschreibungen

Sobald eine Funktion mehr als einen Parameter hat, können Sie verwirrt sein, wofür jeder Parameter verwendet wird. Nehmen wir zum Beispiel eine Funktion, selectEntries(), die Einträge aus einer Datenbank zurückgibt. Angesichts des Funktionsaufrufs

selectEntries(3, 20, 2);

was bedeuten diese drei Zahlen? Python unterstützt benannte Parameter, und sie machen es einfach, zu verstehen, was vor sich geht

# Python syntax
selectEntries(start=3, end=20, step=2)

11.5.2 Optionale benannte Parameter

Optionale Positions-Parameter funktionieren nur dann gut, wenn sie am Ende weggelassen werden. Irgendwo anders müssen Sie Platzhalter wie null einfügen, damit die verbleibenden Parameter die richtigen Positionen haben.

Bei optionalen benannten Parametern ist das kein Problem. Sie können jeden davon leicht weglassen. Hier sind einige Beispiele

# Python syntax
selectEntries(step=2)
selectEntries(end=20, start=3)
selectEntries()

11.5.3 Simulation benannter Parameter in JavaScript

JavaScript hat im Gegensatz zu Python und vielen anderen Sprachen keine native Unterstützung für benannte Parameter. Aber es gibt eine recht elegante Simulation: Jeder tatsächliche Parameter ist eine Eigenschaft in einem Objektliteral, dessen Ergebnis als einzelner formaler Parameter an den Aufgerufenen übergeben wird. Wenn Sie diese Technik verwenden, sieht ein Aufruf von selectEntries() wie folgt aus.

selectEntries({ start: 3, end: 20, step: 2 });

Die Funktion empfängt ein Objekt mit den Eigenschaften start, end und step. Sie können jede davon weglassen

selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();

In ECMAScript 5 würden Sie selectEntries() wie folgt implementieren

function selectEntries(options) {
    options = options || {};
    var start = options.start || 0;
    var end = options.end || -1;
    var step = options.step || 1;
    ···
}

In ECMAScript 6 können Sie Destrukturierung verwenden, die wie folgt aussieht

function selectEntries({ start=0, end=-1, step=1 }) {
    ···
}

Wenn Sie selectEntries() mit null Argumenten aufrufen, schlägt die Destrukturierung fehl, da Sie kein Objektmuster gegen undefined abgleichen können. Das kann durch einen Standardwert behoben werden. Im folgenden Code wird das Objektmuster gegen {} abgeglichen, wenn der erste Parameter fehlt.

function selectEntries({ start=0, end=-1, step=1 } = {}) {
    ···
}

Sie können auch Positions-Parameter mit benannten Parametern kombinieren. Es ist üblich, dass letztere zuletzt kommen

someFunc(posArg1, { namedArg1: 7, namedArg2: true });

Im Prinzip könnten JavaScript-Engines dieses Muster optimieren, sodass kein Zwischenobjekt erstellt wird, da sowohl die Objektliterale an den Aufrufstellen als auch die Objektmuster in den Funktionsdefinitionen statisch sind.

11.6 Beispiele für Destrukturierung in der Parameterbehandlung

11.6.1 forEach() und Destrukturierung

Sie werden in ECMAScript 6 wahrscheinlich am häufigsten die for-of-Schleife verwenden, aber die Array-Methode forEach() profitiert ebenfalls von der Destrukturierung. Oder besser gesagt, ihr Callback tut dies.

Erstes Beispiel: Destrukturierung der Arrays in einem Array.

const items = [ ['foo', 3], ['bar', 9] ];
items.forEach(([word, count]) => {
    console.log(word+' '+count);
});

Zweites Beispiel: Destrukturierung der Objekte in einem Array.

const items = [
    { word:'foo', count:3 },
    { word:'bar', count:9 },
];
items.forEach(({word, count}) => {
    console.log(word+' '+count);
});

11.6.2 Transformation von Maps

Eine ECMAScript 6 Map hat keine Methode map() (wie Arrays). Daher muss man

Das sieht wie folgt aus.

const map0 = new Map([
    [1, 'a'],
    [2, 'b'],
    [3, 'c'],
]);

const map1 = new Map( // step 3
    [...map0] // step 1
    .map(([k, v]) => [k*2, '_'+v]) // step 2
);
// Resulting Map: {2 -> '_a', 4 -> '_b', 6 -> '_c'}

11.6.3 Verarbeitung eines Arrays, das über eine Promise zurückgegeben wird

Die Tool-Methode Promise.all() funktioniert wie folgt

Destrukturierung hilft bei der Verarbeitung des Arrays, mit dem das Ergebnis von Promise.all() erfüllt wird

const urls = [
    'http://example.com/foo.html',
    'http://example.com/bar.html',
    'http://example.com/baz.html',
];

Promise.all(urls.map(downloadUrl))
.then(([fooStr, barStr, bazStr]) => {
    ···
});

// This function returns a Promise that is fulfilled
// with a string (the text)
function downloadUrl(url) {
    return fetch(url).then(request => request.text());
}

fetch() ist eine Promise-basierte Version von XMLHttpRequest. Es ist Teil des Fetch-Standards.

11.7 Coding Style Tipps

Dieser Abschnitt erwähnt einige Tricks für aussagekräftige Parameterdefinitionen. Sie sind clever, haben aber auch Nachteile: Sie fügen visuelles Durcheinander hinzu und können Ihren Code schwerer verständlich machen.

11.7.1 Optionale Parameter

Einige Parameter haben keine Standardwerte, können aber weggelassen werden. In diesem Fall verwende ich gelegentlich den Standardwert undefined, um deutlich zu machen, dass der Parameter optional ist. Das ist redundant, aber beschreibend.

function foo(requiredParam, optionalParam = undefined) {
    ···
}

11.7.2 Erforderliche Parameter

In ECMAScript 5 haben Sie einige, aber eher umständliche Optionen, um sicherzustellen, dass ein erforderlicher Parameter übergeben wurde

function foo(mustBeProvided) {
    if (arguments.length < 1) {
        throw new Error();
    }
    if (! (0 in arguments)) {
        throw new Error();
    }
    if (mustBeProvided === undefined) {
        throw new Error();
    }
    ···
}

In ECMAScript 6 können Sie Standard-Parameterwerte (miss-)brauchen, um prägnanteren Code zu erhalten (Verdient: Idee von Allen Wirfs-Brock)

/**
 * Called if a parameter is missing and
 * the default value is evaluated.
 */
function mandatory() {
    throw new Error('Missing parameter');
}
function foo(mustBeProvided = mandatory()) {
    return mustBeProvided;
}

Interaktion

> foo()
Error: Missing parameter
> foo(123)
123

11.7.3 Erzwingen einer maximalen Anzahl von Argumenten (Arity)

Dieser Abschnitt präsentiert drei Ansätze, um eine maximale Arity zu erzwingen. Das laufende Beispiel ist eine Funktion f, deren maximale Arity 2 beträgt – wenn ein Aufrufer mehr als 2 Parameter übergibt, soll ein Fehler ausgelöst werden.

Der erste Ansatz sammelt alle tatsächlichen Parameter im formalen Rest-Parameter args und prüft dessen Länge.

function f(...args) {
    if (args.length > 2) {
        throw new Error();
    }
    // Extract the real parameters
    let [x, y] = args;
}

Der zweite Ansatz setzt darauf, dass unerwünschte tatsächliche Parameter im formalen Rest-Parameter empty erscheinen.

function f(x, y, ...empty) {
    if (empty.length > 0) {
        throw new Error();
    }
}

Der dritte Ansatz verwendet einen Sentinel-Wert, der verschwindet, wenn ein dritter Parameter vorhanden ist. Ein Vorbehalt ist, dass der Standardwert OK auch dann ausgelöst wird, wenn ein dritter Parameter mit dem Wert undefined vorhanden ist.

const OK = Symbol();
function f(x, y, arity=OK) {
    if (arity !== OK) {
        throw new Error();
    }
}

Leider führt jede dieser Methoden zu erheblichem visuellen und konzeptionellen Durcheinander. Ich bin versucht, die Überprüfung von arguments.length zu empfehlen, aber ich möchte auch, dass arguments verschwindet.

function f(x, y) {
    if (arguments.length > 2) {
        throw new Error();
    }
}

11.8 Der Spread-Operator (...)

Der Spread-Operator (...) sieht genauso aus wie der Rest-Operator, ist aber sein Gegenteil

11.8.1 Spreading in Funktions- und Methodenaufrufe

Math.max() ist ein gutes Beispiel, um zu demonstrieren, wie der Spread-Operator in Methodenaufrufen funktioniert. Math.max(x1, x2, ···) gibt das Argument mit dem größten Wert zurück. Es akzeptiert eine beliebige Anzahl von Argumenten, kann aber nicht auf Arrays angewendet werden. Der Spread-Operator behebt das

> Math.max(-1, 5, 11, 3)
11
> Math.max(...[-1, 5, 11, 3])
11

Im Gegensatz zum Rest-Operator können Sie den Spread-Operator überall in einer Sequenz von Teilen verwenden

> Math.max(-1, ...[5, 11], 3)
11

Ein weiteres Beispiel ist, dass JavaScript keine Möglichkeit hat, die Elemente eines Arrays destruktiv an ein anderes anzuhängen. Arrays haben jedoch die Methode push(x1, x2, ···), die alle ihre Argumente an ihren Empfänger anhängt. Der folgende Code zeigt, wie Sie push() verwenden können, um die Elemente von arr2 an arr1 anzuhängen.

const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];

arr1.push(...arr2);
// arr1 is now ['a', 'b', 'c', 'd']

11.8.2 Spreading in Konstruktoren

Zusätzlich zu Funktions- und Methodenaufrufen funktioniert der Spread-Operator auch für Konstruktoraufrufe

new Date(...[1912, 11, 24]) // Christmas Eve 1912

Das ist etwas, das in ECMAScript 5 schwierig zu erreichen ist.

11.8.3 Spreading in Arrays

Der Spread-Operator kann auch innerhalb von Array-Literalen verwendet werden

> [1, ...[2,3], 4]
[1, 2, 3, 4]

Dies bietet eine bequeme Möglichkeit, Arrays zu verketten

const x = ['a', 'b'];
const y = ['c'];
const z = ['d', 'e'];

const arr = [...x, ...y, ...z]; // ['a', 'b', 'c', 'd', 'e']

Ein Vorteil des Spread-Operators ist, dass sein Operand ein beliebiges iterierbares Wert sein kann (im Gegensatz zur Array-Methode concat(), die keine Iteration unterstützt).

11.8.3.1 Konvertieren von iterierbaren oder Array-ähnlichen Objekten in Arrays

Der Spread-Operator ermöglicht es Ihnen, jeden iterierbaren Wert in ein Array zu konvertieren

const arr = [...someIterableObject];

Konvertieren wir ein Set in ein Array

const set = new Set([11, -1, 6]);
const arr = [...set]; // [11, -1, 6]

Ihre eigenen iterierbaren Objekte können auf die gleiche Weise in Arrays konvertiert werden

const obj = {
    * [Symbol.iterator]() {
        yield 'a';
        yield 'b';
        yield 'c';
    }
};
const arr = [...obj]; // ['a', 'b', 'c']

Beachten Sie, dass der Spread-Operator, genau wie die for-of-Schleife, nur für iterierbare Werte funktioniert. Alle integrierten Datenstrukturen sind iterierbar: Arrays, Maps und Sets. Alle Array-ähnlichen DOM-Datenstrukturen sind ebenfalls iterierbar.

Sollten Sie jemals auf etwas stoßen, das nicht iterierbar, aber Array-ähnlich ist (indizierte Elemente plus eine length-Eigenschaft), können Sie Array.from()6 verwenden, um es in ein Array zu konvertieren

const arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ECMAScript 5:
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ECMAScript 6:
const arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

// TypeError: Cannot spread non-iterable value
const arr3 = [...arrayLike];
Weiter: III Modularität