Funktionen
Funktionsargumente (idealerweise 2 oder weniger)
Die Begrenzung der Anzahl von Funktionsparametern ist äußerst wichtig, weil es das Testen der Funktion erleichtert. Mehr als drei Parameter führen zu einer kombinatorischen Explosion, bei der zahlreiche Testfälle für jede Argumentkombination benötigt werden.
Ein oder zwei Argumente sind ideal, drei sollten möglichst vermieden werden. Bei mehreren Argumenten sollten diese in einem Objekt gebündelt werden. Grundsätzlich deutet eine höhere Parameteranzahl oft darauf hin, dass die Funktion zu viele Aufgaben erfüllt. In Ausnahmefällen genügt meist ein höherwertiges Objekt als Parameter.
Da JavaScript die dynamische Erzeugung von Objekten ohne umfangreiche Klassendeklarationen ermöglicht, können Sie bei hohem Parameterbedarf ein Objekt verwenden.
Um die erwarteten Eigenschaften klar zu definieren, bietet sich die ES2015/ES6-Destrukturierungssyntax an. Dies hat mehrere Vorteile:
- Die benötigten Eigenschaften sind direkt an der Funktionssignatur erkennbar.
- Sie ermöglicht die Simulation benannter Parameter.
- Die Destrukturierung klont zudem die angegebenen primitiven Werte des übergebenen Objekts, was Nebeneffekte reduziert. Hinweis: Destrukturierte Objekte und Arrays werden NICHT geklont.
- Linter können vor ungenutzten Eigenschaften warnen – ohne Destrukturierung unmöglich.
Schlecht:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);Gut:
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});Funktionen sollten eine einzige Aufgabe erfüllen
Dies ist die wichtigste Regel in der Softwareentwicklung. Funktionen mit mehreren Aufgaben sind schwerer kombinierbar, testbar und nachvollziehbar. Fokussierte Funktionen lassen sich leichter refaktorieren und verbessern die Lesbarkeit. Wenn Sie nur eine Regel beachten, sollte es diese sein.
Schlecht:
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}Gut:
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}Funktionsnamen sollten ihre Funktion klar benennen
Schlecht:
function addToDate(date, month) {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);Gut:
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);Funktionen sollten nur eine Abstraktionsebene haben
Mehrere Abstraktionsebenen deuten auf überladene Funktionen hin. Getrennte Funktionen fördern Wiederverwendbarkeit und Testbarkeit.
Schlecht:
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
const statements = code.split(" ");
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
// ...
});
});
const ast = [];
tokens.forEach(token => {
// lex...
});
ast.forEach(node => {
// parse...
});
}Gut:
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach(node => {
// parse...
});
}
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(" ");
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
tokens.push(/* ... */);
});
});
return tokens;
}
function parse(tokens) {
const syntaxTree = [];
tokens.forEach(token => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}Doppelten Code vermeiden
Redundanzen sind kritisch zu vermeiden, da Änderungen an mehreren Stellen vorgenommen werden müssten.
Stellen Sie sich ein Restaurant-Inventarsystem vor: Mehrere Tomaten-Listen erfordern synchronisierte Updates. Eine einzige Liste vereinfacht dies.
Redundanz entsteht oft bei ähnlichen Funktionen mit leichten Unterschieden, deren Unterschiede es erfordern, zwei oder mehr separate Funktionen zu erstellen, die viele gleiche Aufgaben erfüllen. Eine abstrakte Lösung kann dies konsolidieren.
Gute Abstraktionen erfordern die Einhaltung der SOLID-Prinzipien (siehe Klassen-Abschnitt). Schlechte Abstraktionen sind schlimmer als doppelter Code, also seien Sie vorsichtig! Bei gelungener Abstraktion jedoch: Implementieren! So vermeiden Sie Multiplikation von Änderungspunkten.
Schlecht:
function showDeveloperList(developers) {
developers.forEach(developer => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}Gut:
function showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}Standardobjekte mit Object.assign setzen
Schlecht:
const menuConfig = {
title: null,
body: "Bar",
buttonText: null,
cancellable: true
};
function createMenu(config) {
config.title = config.title || "Foo";
config.body = config.body || "Bar";
config.buttonText = config.buttonText || "Baz";
config.cancellable =
config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);Gut:
const menuConfig = {
title: "Order",
// User did not include 'body' key
buttonText: "Send",
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
);
return finalConfig
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);Flags nicht als Funktionsparameter verwenden
Flags implizieren Mehrfachfunktionalität. Trennen Sie solche Funktionen in spezialisierte Varianten auf.
Schlecht:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}Gut:
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}Nebeneffekte vermeiden (Teil 1)
Nebeneffekte umfassen mehr als Input-Output-Operationen: Dateizugriffe, Modifikation globaler Variablen oder unerwünschte Zustandsänderungen.
Nebeneffekte sind manchmal nötig (z.B. Dateischreiben), sollten aber zentralisiert werden. Ein einziger Service pro Nebeneffekt-Typ verhindert Chaos.
Vermeiden Sie geteilten Zustand ohne Struktur, veränderbare Datentypen mit unkontrolliertem Zugriff und dezentrale Nebeneffekt-Quellen.
Schlecht:
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
let name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];Gut:
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];Nebeneffekte vermeiden (Teil 2)
JavaScript-Objekte und Arrays sind veränderbar. Bei der Übergabe als Parameter ist Vorsicht geboten, da unerwartete Seiteneffekte entstehen können.
Beispiel Warenkorb-Array: Eine Modifikation durch eine Funktion betrifft alle Nutzer dieses Arrays. Problemfall: Netzwerk-Wiederholungsversuch während gleichzeitiger Benutzerinteraktion.
Wenn der Nutzer versehentlich einen Artikel hinzufügt, bevor die Netzwerkanfrage beginnt, wird der modifizierte „cart“-Array gesendet, da die Änderung direkt vorgenommen wurde.
Lösung: „addItemToCart“ sollte den Warenkorb immer klonen, modifizieren und den Klon zurückgeben. So bleibt der Originalzustand erhalten.
Zwei Einschränkungen:
- Direkte Objektmodifikationen sind selten notwendig und meist vermeidbar.
- Tiefe Klone großer Objekte sind performancerelevant. Bibliotheken wie Immutable.js optimieren dies.
Schlecht:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};Gut:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};Globale Funktionen nicht manipulieren
Globale Erweiterungen (z.B. „Array.prototype.diff“) können zu Konflikten mit anderen Bibliotheken führen. Besser: ES2015/ES6-Klassen nutzen.
Schlecht:
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};Gut:
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}Funktionale Programmierung vorziehen
JavaScript ermöglicht funktionalen Programmierstil, der saubereren und testbareren Code fördert.
Schlecht:
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}Gut:
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
const totalOutput = programmerOutput.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);Bedingungen kapseln
Schlecht:
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}Gut:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}Negative Bedingungen vermeiden
Schlecht:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}Gut:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}Bedingungen vermeiden
Polymorphie kann viele „if“-Anweisungen ersetzen. Funktionen sollten nur eine Aufgabe erfüllen – Bedingungen widersprechen diesem Prinzip.
Schlecht:
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case "777":
return this.getMaxAltitude() - this.getPassengerCount();
case "Air Force One":
return this.getMaxAltitude();
case "Cessna":
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}Gut:
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}Typüberprüfung vermeiden (Teil 1)
JavaScripts Typenfreiheit verführt manchmal zu Typchecks. Konsistente APIs sind die bessere Lösung.
Schlecht:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location("texas"));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location("texas"));
}
}Gut:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location("texas"));
}Typüberprüfung vermeiden (Teil 2)
Für grundlegende Typensicherheit bietet sich TypeScript an. Manuelle Typchecks beeinträchtigen oft die Lesbarkeit.
Schlecht:
function combine(val1, val2) {
if (
(typeof val1 === "number" && typeof val2 === "number") ||
(typeof val1 === "string" && typeof val2 === "string")
) {
return val1 + val2;
}
throw new Error("Must be of type String or Number");
}Gut:
function combine(val1, val2) {
return val1 + val2;
}Nicht überoptimieren
Moderne Browser optimieren automatisch. Ressourcen wie Optimization Killers helfen bei echten Engpässen.
Schlecht:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}Gut:
for (let i = 0; i < list.length; i++) {
// ...
}Toten Code entfernen
Ungenutzter Code ist redundant und sollte gelöscht werden. Die Versionskontrolle bewahrt benötigte Historien.
Schlecht:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");Gut:
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker("apples", req, "www.inventory-awesome.io");