JavaScript

Ereignisdelegation erklären

Ereignisdelegation ist eine Technik, bei der Ereignis-Listener an ein Elternelement angehängt werden, anstatt sie an die Nachkommenelemente anzuhängen. Der Listener wird ausgelöst, sobald das Ereignis auf den Nachkommenelementen aufgrund von Ereignisblasen im DOM ausgelöst wird. Die Vorteile dieser Technik sind:

  • Der Speicherbedarf sinkt, da nur ein einziger Handler auf dem Elternelement benötigt wird, anstatt Ereignis-Handler an jedem Nachkommen anzuhängen.
  • Es ist nicht notwendig, den Handler von entfernten Elementen zu lösen und das Ereignis für neue Elemente zu binden.

Erklären Sie, wie `this` in JavaScript funktioniert

Es gibt keine einfache Erklärung für this; es ist eines der verwirrendsten Konzepte in JavaScript. Eine vage Erklärung ist, dass der Wert von this davon abhängt, wie die Funktion aufgerufen wird. Ich habe viele Erklärungen zu this online gelesen, und ich fand [Arnav Aggrawals] Erklärung am klarsten. Die folgenden Regeln werden angewendet:

  1. Wenn das Schlüsselwort new beim Aufruf der Funktion verwendet wird, ist this innerhalb der Funktion ein brandneues Objekt.
  2. Wenn apply, call oder bind verwendet werden, um eine Funktion aufzurufen/zu erstellen, ist this innerhalb der Funktion das Objekt, das als Argument übergeben wird.
  3. Wenn eine Funktion als Methode aufgerufen wird, z.B. obj.method() – ist this das Objekt, dessen Eigenschaft die Funktion ist.
  4. Wenn eine Funktion als freier Funktionsaufruf aufgerufen wird, d.h. sie wurde ohne eine der oben genannten Bedingungen aufgerufen, ist this das globale Objekt. In einem Browser ist es das window-Objekt. Im Strict Mode ('use strict') ist this undefined statt des globalen Objekts.
  5. Wenn mehrere der oben genannten Regeln zutreffen, gewinnt die höhere Regel und setzt den this-Wert.
  6. Wenn die Funktion eine ES2015-Pfeilfunktion ist, ignoriert sie alle oben genannten Regeln und erhält den this-Wert ihres umgebenden Bereichs zum Zeitpunkt ihrer Erstellung.

Für eine detaillierte Erklärung lesen Sie seinen [Artikel auf Medium].

Können Sie ein Beispiel dafür geben, wie sich die Arbeit mit this in ES6 geändert hat?

ES6 ermöglicht die Verwendung von [Pfeilfunktionen], die den [einschließenden lexikalischen Bereich] verwenden. Dies ist in der Regel bequem, verhindert aber, dass der Aufrufer den Kontext über .call oder .apply steuern kann – die Folge ist, dass eine Bibliothek wie jQuery this in Ihren Ereignishandlerfunktionen nicht richtig bindet. Daher ist es wichtig, dies bei der Refaktorierung großer Legacy-Anwendungen zu berücksichtigen.

Erklären Sie, wie prototypische Vererbung funktioniert

Alle JavaScript-Objekte haben eine __proto__-Eigenschaft (mit Ausnahme von Objekten, die mit Object.create(null) erstellt wurden), die eine Referenz auf ein anderes Objekt ist, das als "Prototyp" des Objekts bezeichnet wird. Wenn eine Eigenschaft auf einem Objekt aufgerufen wird und die Eigenschaft auf diesem Objekt nicht gefunden wird, sucht die JavaScript-Engine im __proto__ des Objekts und im __proto__ des __proto__ usw., bis sie die Eigenschaft auf einem der __proto__s findet oder bis sie das Ende der Prototypenkette erreicht. Dieses Verhalten simuliert klassische Vererbung, ist aber eigentlich eher [Delegation als Vererbung].

Beispiel für prototypische Vererbung

// Konstruktor des Elternelements. function Animal(name) { this.name = name; } // Methode zum Prototyp des Elternelements hinzufügen. Animal.prototype.makeSound = function () { console.log('The ' + this.constructor.name + ' makes a sound.'); }; // Konstruktor des Kindobjekts. function Dog(name) { Animal.call(this, name); // Den Elternkonstruktor aufrufen. } // Den Prototyp des Kindobjekts auf den Prototyp des Elternelements setzen. Object.setPrototypeOf(Dog.prototype, Animal.prototype); // Methode zum Prototyp des Kindobjekts hinzufügen. Dog.prototype.bark = function () { console.log('Woof!'); }; // Eine neue Instanz von Dog erstellen. const bolt = new Dog('Bolt'); // Methoden für das Kindobjekt aufrufen. console.log(bolt.name); // "Bolt" bolt.makeSound(); // "The Dog makes a sound." bolt.bark(); // "Woof!"

Folgendes ist zu beachten:

  • .makeSound ist nicht auf Dog definiert, daher geht die Engine die Prototypenkette hinauf und findet .makeSound vom geerbten Animal.
  • Die Verwendung von Object.create zum Aufbau der Vererbungskette wird nicht mehr empfohlen. Verwenden Sie stattdessen Object.setPrototypeOf.

Was halten Sie von AMD vs. CommonJS?

Beides sind Möglichkeiten zur Implementierung eines Modulsystems, das in JavaScript bis zur Einführung von ES2015 nicht nativ vorhanden war. CommonJS ist synchron, während AMD (Asynchronous Module Definition) offensichtlich asynchron ist. CommonJS wurde für die serverseitige Entwicklung entwickelt, während AMD mit seiner Unterstützung für das asynchrone Laden von Modulen eher für Browser gedacht ist.

Ich finde die AMD-Syntax ziemlich ausführlich und CommonJS ähnelt eher dem Stil, wie man Import-Statements in anderen Sprachen schreiben würde. Meistens finde ich AMD unnötig, denn wenn Sie all Ihr JavaScript in einer einzigen zusammengefassten Bundledatei bereitstellen, würden Sie nicht von den asynchronen Ladeeigenschaften profitieren. Außerdem ähnelt die CommonJS-Syntax dem Node-Stil beim Schreiben von Modulen, und es gibt weniger Overhead beim Kontextwechsel, wenn man zwischen clientseitiger und serverseitiger JavaScript-Entwicklung wechselt.

Ich bin froh, dass wir mit ES2015-Modulen, die sowohl synchrone als auch asynchrone Ladung unterstützen, endlich bei einem Ansatz bleiben können. Obwohl es noch nicht vollständig in Browsern und in Node ausgerollt ist, können wir immer Transpiler verwenden, um unseren Code zu konvertieren.

Erklären Sie, warum Folgendes nicht als IIFE funktioniert: `function foo(){ }();`. Was muss geändert werden, damit es richtig zu einer IIFE wird?

IIFE steht für Immediately Invoked Function Expressions. Der JavaScript-Parser liest function foo(){ }(); als function foo(){ } und ();, wobei ersteres eine Funktionsdeklaration und letzteres (ein Klammerpaar) ein Versuch ist, eine Funktion aufzurufen, aber kein Name angegeben ist, daher wird ein Uncaught SyntaxError: Unexpected token ) ausgelöst.

Hier sind zwei Möglichkeiten, dies zu beheben, die das Hinzufügen weiterer Klammern beinhalten: (function foo(){ })() und (function foo(){ }()). Anweisungen, die mit function beginnen, werden als Funktionsdeklarationen betrachtet; indem diese Funktion in () eingeschlossen wird, wird sie zu einem Funktionsausdruck, der dann mit den folgenden () ausgeführt werden kann. Diese Funktionen werden nicht im globalen Gültigkeitsbereich exponiert, und Sie können ihren Namen sogar weglassen, wenn Sie sich im Rumpf nicht auf sich selbst beziehen müssen.

Sie könnten auch den void-Operator verwenden: void function foo(){ }();. Leider gibt es ein Problem mit diesem Ansatz. Die Auswertung des gegebenen Ausdrucks ist immer undefined, so dass Sie den Rückgabewert Ihrer IIFE-Funktion nicht verwenden können. Ein Beispiel:

const foo = void (function bar() { return 'foo'; })(); console.log(foo); // undefined

Was ist der Unterschied zwischen einer Variable, die: `null`, `undefined` oder undeklariert ist? Wie würden Sie diese Zustände überprüfen?

Undeklarierte Variablen werden erstellt, wenn Sie einem Bezeichner einen Wert zuweisen, der zuvor nicht mit var, let oder const erstellt wurde. Undeklarierte Variablen werden global definiert, außerhalb des aktuellen Gültigkeitsbereichs. Im Strict Mode wird ein ReferenceError ausgelöst, wenn Sie versuchen, einer undeklarierten Variable einen Wert zuzuweisen. Undeklarierte Variablen sind schlecht, genauso wie globale Variablen schlecht sind. Vermeiden Sie sie um jeden Preis! Um sie zu überprüfen, schließen Sie ihre Verwendung in einen try/catch-Block ein.

function foo() { x = 1; // Löst im Strict Mode einen ReferenceError aus } foo(); console.log(x); // 1

Eine Variable, die undefined ist, ist eine Variable, die deklariert, aber keinem Wert zugewiesen wurde. Sie ist vom Typ undefined. Wenn eine Funktion keinen Wert als Ergebnis ihrer Ausführung zurückgibt und dieser einer Variable zugewiesen wird, hat die Variable ebenfalls den Wert undefined. Um dies zu überprüfen, vergleichen Sie mit dem strikten Gleichheitsoperator (===) oder typeof, der den String 'undefined' zurückgibt. Beachten Sie, dass Sie den abstrakten Gleichheitsoperator nicht zur Überprüfung verwenden sollten, da er auch true zurückgibt, wenn der Wert null ist.

var foo; console.log(foo); // undefined console.log(foo === undefined); // true console.log(typeof foo === 'undefined'); // true console.log(foo == null); // true. Falsch, nicht zum Überprüfen verwenden! function bar() {} var baz = bar(); console.log(baz); // undefined

Eine Variable, die null ist, wurde explizit dem Wert null zugewiesen. Sie repräsentiert keinen Wert und unterscheidet sich von undefined in dem Sinne, dass sie explizit zugewiesen wurde. Um auf null zu prüfen, vergleichen Sie einfach mit dem strikten Gleichheitsoperator. Beachten Sie, dass Sie, wie oben, den abstrakten Gleichheitsoperator (==) nicht zur Überprüfung verwenden sollten, da er auch true zurückgibt, wenn der Wert undefined ist.

var foo = null; console.log(foo === null); // true console.log(typeof foo === 'object'); // true console.log(foo == undefined); // true. Falsch, nicht zum Überprüfen verwenden!

Als persönliche Gewohnheit deklariere oder weise ich meine Variablen niemals unausdrücklich zu. Ich weise ihnen nach der Deklaration explizit null zu, wenn ich sie noch nicht verwenden möchte. Wenn Sie einen Linter in Ihrem Workflow verwenden, kann dieser normalerweise auch überprüfen, ob Sie keine undeklarierten Variablen referenzieren.

Was ist eine Closure und wie/warum würden Sie eine verwenden?

Eine Closure ist die Kombination aus einer Funktion und der lexikalischen Umgebung, in der diese Funktion deklariert wurde. Das Wort "lexikalisch" bezieht sich auf die Tatsache, dass die lexikalische Bereichsdefinition den Ort verwendet, an dem eine Variable im Quellcode deklariert wird, um zu bestimmen, wo diese Variable verfügbar ist. Closures sind Funktionen, die Zugriff auf die Variablen der äußeren (umgebenden) Funktion – der Geltungsbereichskette – haben, auch nachdem die äußere Funktion zurückgegeben wurde.

Warum sollte man eine verwenden?

  • Datenprivatsphäre / Emulation privater Methoden mit Closures. Wird häufig im [Modul-Muster] verwendet.
  • [Partielle Anwendungen oder Currying].

Können Sie den Hauptunterschied zwischen einer `.forEach`-Schleife und einer `.map()`-Schleife beschreiben und warum Sie eine der beiden wählen würden?

Um die Unterschiede zwischen den beiden zu verstehen, schauen wir uns an, was jede Funktion tut.

forEach

  • Iteriert durch die Elemente eines Arrays.
  • Führt einen Callback für jedes Element aus.
  • Gibt keinen Wert zurück.
const a = [1, 2, 3]; const doubled = a.forEach((num, index) => { // Mach etwas mit num und/oder index. }); // doubled = undefined

map

  • Iteriert durch die Elemente eines Arrays.
  • "Mappt" jedes Element auf ein neues Element, indem die Funktion für jedes Element aufgerufen wird, wodurch ein neues Array entsteht.
const a = [1, 2, 3]; const doubled = a.map((num) => { return num * 2; }); // doubled = [2, 4, 6]

Der Hauptunterschied zwischen .forEach und .map() besteht darin, dass .map() ein neues Array zurückgibt. Wenn Sie das Ergebnis benötigen, aber das ursprüngliche Array nicht mutieren möchten, ist .map() die klare Wahl. Wenn Sie einfach über ein Array iterieren müssen, ist forEach eine gute Wahl.

Was ist ein typischer Anwendungsfall für anonyme Funktionen?

Sie können in IIFEs verwendet werden, um Code innerhalb eines lokalen Gültigkeitsbereichs zu kapseln, damit deklarierte Variablen nicht in den globalen Gültigkeitsbereich gelangen.

(function () { // Hier ist etwas Code. })();

Als Callback, der einmal verwendet wird und nirgendwo sonst benötigt wird. Der Code wirkt selbstständiger und lesbarer, wenn Handler direkt im aufrufenden Code definiert werden, anstatt die Funktionskörper anderswo suchen zu müssen.

setTimeout(function () { console.log('Hallo Welt!'); }, 1000);

Argumente für funktionale Programmkonstrukte oder Lodash (ähnlich wie Callbacks).

const arr = [1, 2, 3]; const double = arr.map(function (el) { return el * 2; }); console.log(double); // [2, 4, 6]

Wie organisieren Sie Ihren Code? (Modulmuster, klassische Vererbung?)

Früher habe ich Backbone für meine Modelle verwendet, was einen eher OOP-Ansatz fördert, indem Backbone-Modelle erstellt und Methoden an sie angehängt werden.

Das Modulmuster ist immer noch großartig, aber heutzutage verwende ich React/Redux, die einen unidirektionalen Datenfluss basierend auf der Flux-Architektur nutzen. Ich würde die Modelle meiner App mit einfachen Objekten darstellen und reine Utility-Funktionen schreiben, um diese Objekte zu manipulieren. Der Zustand wird mit Aktionen und Reducern wie in jeder anderen Redux-Anwendung manipuliert.

Ich vermeide klassische Vererbung, wo immer möglich. Wenn ich sie doch verwende, halte ich mich an [diese Regeln].

Was ist der Unterschied zwischen Host-Objekten und nativen Objekten?

Native Objekte sind Objekte, die Teil der JavaScript-Sprache sind, die durch die ECMAScript-Spezifikation definiert sind, wie String, Math, RegExp, Object, Function usw.

Host-Objekte werden von der Laufzeitumgebung (Browser oder Node) bereitgestellt, wie window, XMLHTTPRequest usw.

Unterschied zwischen: `function Person(){}`, `var person = Person()` und `var person = new Person()`?

Diese Frage ist ziemlich vage. Meine beste Vermutung, was sie beabsichtigt, ist, dass sie nach Konstruktoren in JavaScript fragt. Technisch gesehen ist function Person(){} nur eine normale Funktionsdeklaration. Die Konvention ist, PascalCase für Funktionen zu verwenden, die als Konstruktoren verwendet werden sollen.

var person = Person() ruft Person als Funktion auf und nicht als Konstruktor. Dies ist ein häufiger Fehler, wenn die Funktion als Konstruktor verwendet werden soll. Typischerweise gibt der Konstruktor nichts zurück, daher gibt das Aufrufen des Konstruktors wie eine normale Funktion undefined zurück, und das wird der Variablen zugewiesen, die als Instanz gedacht ist.

var person = new Person() erstellt eine Instanz des Person-Objekts mit dem new-Operator, die von Person.prototype erbt. Eine Alternative wäre die Verwendung von Object.create, wie z.B.: Object.create(Person.prototype).

function Person(name) { this.name = name; } var person = Person('John'); console.log(person); // undefined console.log(person.name); // Uncaught TypeError: Cannot read property 'name' of undefined var person = new Person('John'); console.log(person); // Person { name: "John" } console.log(person.name); // "john"

Was ist der Unterschied zwischen `.call` und `.apply`?

Sowohl .call als auch .apply werden verwendet, um Funktionen aufzurufen, und der erste Parameter wird als Wert von this innerhalb der Funktion verwendet. .call nimmt jedoch durch Kommas getrennte Argumente als nächste Argumente entgegen, während .apply ein Array von Argumenten als nächstes Argument entgegennimmt. Eine einfache Eselsbrücke ist C für call und durch Kommas getrennt und A für apply und ein Array von Argumenten.

function add(a, b) { return a + b; } console.log(add.call(null, 1, 2)); // 3 console.log(add.apply(null, [1, 2])); // 3

Erklären Sie `Function.prototype.bind`.

Wortwörtlich übernommen von [MDN]:

Die Methode bind() erstellt eine neue Funktion, die, wenn sie aufgerufen wird, ihr this-Schlüsselwort auf den angegebenen Wert gesetzt hat, wobei eine gegebene Reihenfolge von Argumenten allen Argumenten vorangeht, die beim Aufruf der neuen Funktion bereitgestellt werden.

Meiner Erfahrung nach ist es am nützlichsten, den Wert von this in Methoden von Klassen zu binden, die Sie an andere Funktionen übergeben möchten. Dies wird häufig in React-Komponenten getan.

Wann würden Sie `document.write()` verwenden?

document.write() schreibt eine Zeichenfolge in einen Dokumentstrom, der mit document.open() geöffnet wurde. Wenn document.write() nach dem Laden der Seite ausgeführt wird, ruft es document.open auf, wodurch das gesamte Dokument (<head> und <body> entfernt!) gelöscht und der Inhalt durch den angegebenen Parameterwert ersetzt wird. Daher wird es normalerweise als gefährlich und anfällig für Missbrauch angesehen.

Es gibt einige Antworten online, die erklären, dass document.write() in Analyse-Code oder [wenn Sie Stile einschließen möchten, die nur funktionieren sollen, wenn JavaScript aktiviert ist] verwendet wird. Es wird sogar in HTML5-Boilerplate verwendet, um [Skripte parallel zu laden und die Ausführungsreihenfolge beizubehalten]! Ich vermute jedoch, dass diese Gründe veraltet sein könnten und heutzutage ohne Verwendung von document.write() erreicht werden können. Bitte korrigieren Sie mich, wenn ich mich diesbezüglich irre.

Was ist der Unterschied zwischen Feature-Erkennung, Feature-Inferenz und der Verwendung des UA-Strings?

Feature-Erkennung

Die Feature-Erkennung beinhaltet die Überprüfung, ob ein Browser einen bestimmten Codeblock unterstützt, und die Ausführung von anderem Code, je nachdem, ob er dies tut (oder nicht), damit der Browser immer ein funktionierendes Erlebnis bieten kann, anstatt in einigen Browsern abzustürzen/Fehler zu verursachen. Zum Beispiel:

if ('geolocation' in navigator) { // Kann navigator.geolocation verwenden } else { // Umgang mit fehlender Funktion }

[Modernizr] ist eine großartige Bibliothek zur Handhabung der Feature-Erkennung.

Feature-Inferenz

Die Feature-Inferenz prüft eine Funktion genau wie die Feature-Erkennung, verwendet aber eine andere Funktion, weil sie davon ausgeht, dass diese ebenfalls existieren wird, z.B.:

if (document.getElementsByTagName) { element = document.getElementById(id); }

Dies ist nicht wirklich zu empfehlen. Die Feature-Erkennung ist narrensicherer.

UA-String

Dies ist ein vom Browser gemeldeter String, der es den Peers des Netzwerkprotokolls ermöglicht, den Anwendungstyp, das Betriebssystem, den Softwareanbieter oder die Softwareversion des anfragenden Software-Benutzeragenten zu identifizieren. Er kann über navigator.userAgent aufgerufen werden. Der String ist jedoch schwer zu parsen und kann gefälscht werden. Chrome meldet sich beispielsweise sowohl als Chrome als auch als Safari. Um Safari zu erkennen, müssen Sie also nach dem Safari-String und dem Fehlen des Chrome-Strings suchen. Vermeiden Sie diese Methode.

Erklären Sie Ajax so detailliert wie möglich.

Ajax (asynchrones JavaScript und XML) ist eine Sammlung von Webentwicklungstechniken, die viele Webtechnologien auf der Clientseite verwenden, um asynchrone Webanwendungen zu erstellen. Mit Ajax können Webanwendungen Daten asynchron (im Hintergrund) an einen Server senden und von dort abrufen, ohne die Anzeige und das Verhalten der vorhandenen Seite zu beeinträchtigen. Durch die Entkopplung der Datenaustauschschicht von der Präsentationsschicht ermöglicht Ajax, dass Webseiten und damit Webanwendungen den Inhalt dynamisch ändern können, ohne die gesamte Seite neu laden zu müssen. In der Praxis verwenden moderne Implementierungen häufig JSON anstelle von XML, da JSON nativ in JavaScript ist.

Die XMLHttpRequest-API wird häufig für die asynchrone Kommunikation oder heutzutage die fetch()-API verwendet.

Was sind die Vor- und Nachteile der Verwendung von Ajax?

Vorteile

  • Bessere Interaktivität. Neue Inhalte vom Server können dynamisch geändert werden, ohne dass die gesamte Seite neu geladen werden muss.
  • Reduzierung der Serververbindungen, da Skripte und Stylesheets nur einmal angefordert werden müssen.
  • Der Zustand kann auf einer Seite beibehalten werden. JavaScript-Variablen und DOM-Zustand bleiben erhalten, da die Hauptseite nicht neu geladen wurde.
  • Im Grunde die meisten Vorteile einer SPA.

Nachteile

  • Dynamische Webseiten sind schwieriger zu bookmarken.
  • Funktioniert nicht, wenn JavaScript im Browser deaktiviert wurde.
  • Einige Webcrawler führen JavaScript nicht aus und würden Inhalte, die von JavaScript geladen wurden, nicht sehen.
  • Webseiten, die Ajax zum Abrufen von Daten verwenden, müssen die abgerufenen Remotedaten wahrscheinlich mit clientseitigen Templates kombinieren, um das DOM zu aktualisieren. Damit dies geschieht, muss JavaScript im Browser geparst und ausgeführt werden, und Low-End-Mobilgeräte könnten damit Schwierigkeiten haben.
  • Im Grunde die meisten Nachteile einer SPA.

Erklären Sie, wie JSONP funktioniert (und wie es nicht wirklich Ajax ist).

JSONP (JSON mit Padding) ist eine Methode, die häufig verwendet wird, um die Cross-Domain-Richtlinien in Webbrowsern zu umgehen, da Ajax-Anfragen von der aktuellen Seite an eine Cross-Origin-Domain nicht erlaubt sind.

JSONP funktioniert, indem es eine Anfrage an eine Cross-Origin-Domain über ein <script>-Tag und normalerweise mit einem callback-Query-Parameter sendet, zum Beispiel: https://example.com?callback=printData. Der Server verpackt dann die Daten in eine Funktion namens printData und sendet sie an den Client zurück.

<script> function printData(data) { console.log(`My name is ${data.name}!`); } </script> <script src="[https://example.com?callback=printData](https://example.com?callback=printData)"></script>
printData({ name: 'Yang Shun' });

Der Client muss die Funktion printData in seinem globalen Gültigkeitsbereich haben, und die Funktion wird vom Client ausgeführt, wenn die Antwort von der Cross-Origin-Domain empfangen wird.

JSONP kann unsicher sein und hat einige Sicherheitsimplikationen. Da JSONP wirklich JavaScript ist, kann es alles andere tun, was JavaScript tun kann, daher müssen Sie dem Anbieter der JSONP-Daten vertrauen.

Heutzutage ist [CORS] der empfohlene Ansatz und JSONP wird als Hack angesehen.

Haben Sie jemals JavaScript-Templating verwendet? Wenn ja, welche Bibliotheken haben Sie verwendet?

Ja. Handlebars, Underscore, Lodash, AngularJS und JSX. Ich mochte das Templating in AngularJS nicht, weil es stark auf Strings in den Direktiven setzte und Tippfehler unbemerkt blieben. JSX ist mein neuer Favorit, da es näher an JavaScript ist und kaum Syntax zu lernen ist. Heutzutage können Sie sogar ES2015-Template-String-Literale als schnelle Möglichkeit zum Erstellen von Templates verwenden, ohne auf Drittanbieter-Code angewiesen zu sein.

const template = `<div>Mein Name ist: ${name}</div>`;

Seien Sie sich jedoch einer potenziellen XSS-Gefahr bei dem oben genannten Ansatz bewusst, da die Inhalte im Gegensatz zu Templating-Bibliotheken nicht für Sie escaped werden.

Erklären Sie "Hoisting".

Hoisting ist ein Begriff, der verwendet wird, um das Verhalten von Variablendeklarationen in Ihrem Code zu erklären. Variablen, die mit dem Schlüsselwort var deklariert oder initialisiert werden, haben ihre Deklaration an den Anfang ihres Modul-/Funktions-Gültigkeitsbereichs "verschoben", was wir als Hoisting bezeichnen. Es wird jedoch nur die Deklaration "hochgezogen", die Zuweisung (falls vorhanden) bleibt an ihrem ursprünglichen Ort.

Beachten Sie, dass die Deklaration tatsächlich nicht verschoben wird – die JavaScript-Engine parst die Deklarationen während der Kompilierung und wird sich der Deklarationen und ihrer Gültigkeitsbereiche bewusst. Es ist nur einfacher, dieses Verhalten zu verstehen, indem man sich die Deklarationen als an den Anfang ihres Gültigkeitsbereichs "hochgezogen" vorstellt. Erklären wir es anhand einiger Beispiele.

console.log(foo); // undefined var foo = 1; console.log(foo); // 1

Funktionsdeklarationen haben den Funktionskörper gehoistet, während Funktionsausdrücke (in Form von Variablendeklarationen geschrieben) nur die Variablendeklaration gehoistet haben.

// Funktionsdeklaration console.log(foo); // [Function: foo] foo(); // 'FOOOOO' function foo() { console.log('FOOOOO'); } console.log(foo); // [Function: foo] // Funktionsausdruck console.log(bar); // undefined bar(); // Uncaught TypeError: bar is not a function var bar = function () { console.log('BARRRR'); }; console.log(bar); // [Function: bar]

Variablen, die über let und const deklariert werden, werden ebenfalls gehoistet. Im Gegensatz zu var und function werden sie jedoch nicht initialisiert, und der Zugriff auf sie vor der Deklaration führt zu einer ReferenceError-Ausnahme. Die Variable befindet sich von Beginn des Blocks bis zur Verarbeitung der Deklaration in einer "temporalen Todeszone".

x; // undefined y; // Reference error: y is not defined var x = 'local'; let y = 'local';

Beschreiben Sie das Ereignis-Bubbling.

Wenn ein Ereignis auf einem DOM-Element ausgelöst wird, versucht es, das Ereignis zu behandeln, wenn ein Listener angehängt ist. Dann wird das Ereignis zu seinem Elternelement "hochgeblasen", und dasselbe geschieht. Dieses Bubbling tritt entlang der Ahnen des Elements bis zum document auf. Ereignis-Bubbling ist der Mechanismus hinter der Ereignisdelegation.

Was ist der Unterschied zwischen einem "Attribut" und einer "Eigenschaft"?

Attribute werden im HTML-Markup definiert, Eigenschaften jedoch im DOM. Um den Unterschied zu veranschaulichen, stellen Sie sich vor, wir haben dieses Textfeld in unserem HTML: <input type="text" value="Hello">.

const input = document.querySelector('input'); console.log(input.getAttribute('value')); // Hello console.log(input.value); // Hello

Aber nachdem Sie den Wert des Textfeldes geändert haben, indem Sie "World!" hinzugefügt haben, wird dies zu:

console.log(input.getAttribute('value')); // Hello console.log(input.value); // Hello World!

Warum ist das Erweitern von eingebauten JavaScript-Objekten keine gute Idee?

Das Erweitern eines eingebauten/nativen JavaScript-Objekts bedeutet das Hinzufügen von Eigenschaften/Funktionen zu seinem prototype. Obwohl dies auf den ersten Blick eine gute Idee erscheinen mag, ist es in der Praxis gefährlich. Stellen Sie sich vor, Ihr Code verwendet ein paar Bibliotheken, die beide das Array.prototype erweitern, indem sie dieselbe contains-Methode hinzufügen. Die Implementierungen werden sich gegenseitig überschreiben, und Ihr Code wird brechen, wenn das Verhalten dieser beiden Methoden nicht dasselbe ist.

Der einzige Fall, in dem Sie ein natives Objekt erweitern möchten, ist, wenn Sie ein Polyfill erstellen möchten, d.h. im Wesentlichen Ihre eigene Implementierung für eine Methode bereitstellen, die Teil der JavaScript-Spezifikation ist, aber möglicherweise im Browser des Benutzers aufgrund eines älteren Browsers nicht existiert.

Unterschied zwischen dem Dokument-`load`-Ereignis und dem Dokument-`DOMContentLoaded`-Ereignis?

Das DOMContentLoaded-Ereignis wird ausgelöst, wenn das anfängliche HTML-Dokument vollständig geladen und geparst wurde, ohne auf Stylesheets, Bilder und Subframes zu warten.

Das window's load-Ereignis wird erst ausgelöst, nachdem das DOM und alle abhängigen Ressourcen und Assets geladen wurden.

Was ist der Unterschied zwischen `==` und `===`?

== ist der abstrakte Gleichheitsoperator, während === der strikte Gleichheitsoperator ist. Der ==-Operator vergleicht auf Gleichheit, nachdem er notwendige Typkonvertierungen durchgeführt hat. Der ===-Operator führt keine Typkonvertierung durch, so dass, wenn zwei Werte nicht vom gleichen Typ sind, === einfach false zurückgibt. Bei der Verwendung von == können merkwürdige Dinge passieren, wie zum Beispiel:

1 == '1'; // true 1 == [1]; // true 1 == true; // true 0 == ''; // true 0 == '0'; // true 0 == false; // true

Mein Rat ist, den ==-Operator niemals zu verwenden, außer aus Bequemlichkeit beim Vergleich mit null oder undefined, wo a == null true zurückgibt, wenn a null oder undefined ist.

var a = null; console.log(a == null); // true console.log(a == undefined); // true

Erklären Sie die Same-Origin-Policy in Bezug auf JavaScript.

Die Same-Origin-Policy verhindert, dass JavaScript Anfragen über Domänengrenzen hinweg stellt. Eine Origin ist definiert als eine Kombination aus URI-Schema, Hostname und Portnummer. Diese Richtlinie verhindert, dass ein bösartiges Skript auf einer Seite über das Document Object Model dieser Seite Zugriff auf sensible Daten auf einer anderen Webseite erhält.

Machen Sie das hier funktionsfähig:

duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]
function duplicate(arr) { return arr.concat(arr); } duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]

Oder mit ES6:

const duplicate = (arr) => [...arr, ...arr]; duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]

Warum wird es ein ternärer Ausdruck genannt, was bedeutet das Wort "Ternär"?

"Ternär" bedeutet drei, und ein ternärer Ausdruck akzeptiert drei Operanden: die Testbedingung, den "dann"-Ausdruck und den "sonst"-Ausdruck. Ternäre Ausdrücke sind nicht spezifisch für JavaScript, und ich bin mir nicht sicher, warum sie überhaupt in dieser Liste stehen.

Was ist `"use strict";`? Was sind die Vor- und Nachteile der Verwendung?

'use strict' ist eine Anweisung, die verwendet wird, um den Strict Mode für ganze Skripte oder einzelne Funktionen zu aktivieren. Der Strict Mode ist eine Möglichkeit, eine eingeschränkte Variante von JavaScript zu verwenden.

Vorteile:

  • Macht es unmöglich, versehentlich globale Variablen zu erstellen.
  • Führt dazu, dass Zuweisungen, die sonst stillschweigend fehlschlagen würden, eine Ausnahme auslösen.
  • Führt dazu, dass Versuche, nicht löschbare Eigenschaften zu löschen, eine Ausnahme auslösen (wobei der Versuch zuvor einfach keine Wirkung hatte).
  • Erfordert, dass Funktionsparameternamen eindeutig sind.
  • this ist im globalen Kontext undefiniert.
  • Fängt einige häufige Programmierfehler ab und wirft Ausnahmen.
  • Deaktiviert Funktionen, die verwirrend oder schlecht durchdacht sind.

Nachteile:

  • Viele fehlende Funktionen, an die einige Entwickler vielleicht gewöhnt sind.
  • Kein Zugriff mehr auf function.caller und function.arguments.
  • Die Verkettung von Skripten, die in verschiedenen Strict Modes geschrieben wurden, kann zu Problemen führen.

Insgesamt denke ich, dass die Vorteile die Nachteile überwiegen, und ich musste mich nie auf die Funktionen verlassen, die der Strict Mode blockiert. Ich würde die Verwendung des Strict Mode empfehlen.

Erstellen Sie eine for-Schleife, die bis `100` iteriert und dabei **"fizz"** bei Vielfachen von `3`, **"buzz"** bei Vielfachen von `5` und **"fizzbuzz"** bei Vielfachen von `3` und `5` ausgibt.

Schauen Sie sich diese Version von FizzBuzz von [Paul Irish] an.

for (let i = 1; i <= 100; i++) { let f = i % 3 == 0, b = i % 5 == 0; console.log(f ? (b ? 'FizzBuzz' : 'Fizz') : b ? 'Buzz' : i); }

Ich würde Ihnen jedoch nicht raten, das oben Genannte während Interviews zu schreiben. Bleiben Sie einfach beim langen, aber klaren Ansatz. Für weitere verrückte Versionen von FizzBuzz, schauen Sie sich den untenstehenden Referenzlink an.

Warum ist es im Allgemeinen eine gute Idee, den globalen Geltungsbereich einer Website so zu belassen, wie er ist, und ihn niemals anzutasten?

Jedes Skript hat Zugriff auf den globalen Gültigkeitsbereich, und wenn jeder den globalen Namensraum verwendet, um seine Variablen zu definieren, werden wahrscheinlich Kollisionen auftreten. Verwenden Sie das Modulmuster (IIFEs), um Ihre Variablen in einem lokalen Namensraum zu kapseln.

Warum sollten Sie etwas wie das `load`-Ereignis verwenden? Hat dieses Ereignis Nachteile? Kennen Sie Alternativen und warum sollten Sie diese verwenden?

Das load-Ereignis wird am Ende des Dokumentladevorgangs ausgelöst. Zu diesem Zeitpunkt befinden sich alle Objekte im Dokument im DOM, und alle Bilder, Skripte, Links und Unterframes sind fertig geladen.

Das DOM-Ereignis DOMContentLoaded wird ausgelöst, nachdem das DOM für die Seite konstruiert wurde, wartet aber nicht darauf, dass andere Ressourcen vollständig geladen werden. Dies ist in bestimmten Fällen vorzuziehen, wenn Sie nicht die gesamte Seite geladen haben müssen, bevor Sie initialisieren.

Erklären Sie, was eine Single Page Application ist und wie man sie SEO-freundlich macht.

Das Folgende stammt aus dem großartigen [Grab Front End Guide], der zufällig von mir geschrieben wurde!

Webentwickler bezeichnen die Produkte, die sie heutzutage erstellen, als Web-Apps und nicht als Websites. Obwohl es keinen strikten Unterschied zwischen den beiden Begriffen gibt, sind Web-Apps tendenziell hochinteraktiv und dynamisch, sodass der Benutzer Aktionen ausführen und eine Antwort auf seine Aktion erhalten kann. Traditionell empfängt der Browser HTML vom Server und rendert es. Wenn der Benutzer zu einer anderen URL navigiert, ist ein vollständiges Seiten-Refresh erforderlich, und der Server sendet frisches neues HTML an die neue Seite. Dies wird als serverseitiges Rendering bezeichnet.

In modernen SPAs wird jedoch stattdessen clientseitiges Rendering verwendet. Der Browser lädt die anfängliche Seite vom Server zusammen mit den Skripten (Frameworks, Bibliotheken, App-Code) und Stylesheets, die für die gesamte App erforderlich sind. Wenn der Benutzer zu anderen Seiten navigiert, wird kein Seiten-Refresh ausgelöst. Die URL der Seite wird über die [HTML5 History API] aktualisiert. Neue Daten, die für die neue Seite benötigt werden, normalerweise im JSON-Format, werden vom Browser über [AJAX]-Anfragen an den Server abgerufen. Die SPA aktualisiert dann die Seite dynamisch mit den Daten über JavaScript, die sie bereits beim anfänglichen Seitenladevorgang heruntergeladen hat. Dieses Modell ähnelt der Funktionsweise nativer mobiler Apps.

Die Vorteile:

  • Die App fühlt sich reaktionsschneller an und Benutzer sehen das Blinken zwischen den Seitennavigationen aufgrund von vollständigen Seiten-Refreshes nicht.
  • Es werden weniger HTTP-Anfragen an den Server gestellt, da dieselben Assets nicht bei jedem Seitenladevorgang erneut heruntergeladen werden müssen.
  • Klare Trennung der Zuständigkeiten zwischen Client und Server; Sie können problemlos neue Clients für verschiedene Plattformen (z. B. Mobilgeräte, Chatbots, Smartwatches) erstellen, ohne den Servercode ändern zu müssen. Sie können auch den Technologie-Stack auf Client- und Serverseite unabhängig voneinander ändern, solange der API-Vertrag nicht verletzt wird.

Die Nachteile:

  • Höhere anfängliche Seitenladezeit aufgrund des Ladens von Framework, App-Code und Assets, die für mehrere Seiten benötigt werden.
  • Es ist ein zusätzlicher Schritt auf Ihrem Server erforderlich, nämlich ihn so zu konfigurieren, dass alle Anfragen an einen einzigen Einstiegspunkt geleitet werden und das clientseitige Routing von dort aus übernimmt.
  • SPAs sind auf JavaScript angewiesen, um Inhalte zu rendern, aber nicht alle Suchmaschinen führen JavaScript während des Crawlings aus, und sie sehen möglicherweise leere Inhalte auf Ihrer Seite. Dies beeinträchtigt unbeabsichtigt die Suchmaschinenoptimierung (SEO) Ihrer App. Meistens ist SEO jedoch nicht der wichtigste Faktor, wenn Sie Apps erstellen, da nicht alle Inhalte von Suchmaschinen indexiert werden müssen. Um dies zu überwinden, können Sie Ihre App entweder serverseitig rendern oder Dienste wie [Prerender] verwenden, um "Ihr JavaScript in einem Browser zu rendern, das statische HTML zu speichern und dieses an die Crawler zurückzugeben".

Wie umfangreich ist Ihre Erfahrung mit Promises und/oder deren Polyfills?

Verfüge über fundierte Kenntnisse darüber. Ein Promise ist ein Objekt, das irgendwann in der Zukunft einen einzelnen Wert erzeugen kann: entweder einen aufgelösten Wert oder einen Grund, warum es nicht aufgelöst wurde (z. B. ein Netzwerkfehler ist aufgetreten). Ein Promise kann sich in einem von drei möglichen Zuständen befinden: erfüllt, abgelehnt oder ausstehend. Promise-Benutzer können Callbacks anhängen, um den erfüllten Wert oder den Grund für die Ablehnung zu behandeln.

Einige gängige Polyfills sind $.deferred, Q und Bluebird, aber nicht alle von ihnen entsprechen der Spezifikation. ES2015 unterstützt Promises von Haus aus, und Polyfills werden heutzutage typischerweise nicht benötigt.

Was sind die Vor- und Nachteile der Verwendung von Promises anstelle von Callbacks?

Vorteile

  • Vermeidung des Callback-Höllen, die unleserlich sein kann.
  • Erleichtert das Schreiben von sequenziellem asynchronem Code, der mit .then() lesbar ist.
  • Erleichtert das Schreiben von parallelem asynchronem Code mit Promise.all().
  • Mit Promises treten die folgenden Szenarien, die bei rein Callback-basierter Codierung vorhanden sind, nicht auf:
    • Den Callback zu früh aufrufen
    • Den Callback zu spät (oder nie) aufrufen
    • Den Callback zu wenige oder zu viele Male aufrufen
    • Erforderliche Umgebung/Parameter nicht weitergeben
    • Fehler/Ausnahmen, die auftreten können, verschlucken

Nachteile

  • Etwas komplexerer Code (umstritten).
  • In älteren Browsern, in denen ES2015 nicht unterstützt wird, müssen Sie ein Polyfill laden, um es verwenden zu können.

Was sind einige der Vor-/Nachteile beim Schreiben von JavaScript-Code in einer Sprache, die zu JavaScript kompiliert wird?

Einige Beispiele für Sprachen, die zu JavaScript kompilieren, sind CoffeeScript, Elm, ClojureScript, PureScript und TypeScript.

Vorteile:

  • Behebt einige der langjährigen Probleme in JavaScript und entmutigt JavaScript-Anti-Patterns.
  • Ermöglicht das Schreiben kürzeren Codes, indem es syntaktischen Zucker über JavaScript bietet, den ES5 meiner Meinung nach vermisst, aber ES2015 ist großartig.
  • Statische Typen sind (im Fall von TypeScript) für große Projekte, die über längere Zeiträume gewartet werden müssen, fantastisch.

Nachteile:

  • Erfordert einen Build-/Kompilierungsprozess, da Browser nur JavaScript ausführen und Ihr Code in JavaScript kompiliert werden muss, bevor er den Browsern bereitgestellt wird.
  • Debugging kann mühsam sein, wenn Ihre Sourcemaps nicht gut zu Ihrem vorkompilierten Quellcode passen.
  • Die meisten Entwickler sind mit diesen Sprachen nicht vertraut und müssen sie lernen. Es entstehen Anlaufkosten für Ihr Team, wenn Sie sie für Ihre Projekte verwenden.
  • Kleinere Community (abhängig von der Sprache), was bedeutet, dass Ressourcen, Tutorials, Bibliotheken und Tools schwerer zu finden sind.
  • Unterstützung für IDE/Editor könnte fehlen.
  • Diese Sprachen werden immer hinter dem neuesten JavaScript-Standard zurückbleiben.
  • Entwickler sollten sich bewusst sein, woraus ihr Code kompiliert wird – denn das ist das, was tatsächlich ausgeführt wird, und das ist es, was letztendlich zählt.

Praktisch gesehen hat ES2015 JavaScript erheblich verbessert und das Schreiben viel angenehmer gemacht. Ich sehe heutzutage wirklich keine Notwendigkeit mehr für CoffeeScript.

Welche Tools und Techniken verwenden Sie zum Debuggen von JavaScript-Code?

  • React und Redux
    • [React Devtools]
    • [Redux Devtools]
  • Vue
    • [Vue Devtools]
  • JavaScript
    • [Chrome Devtools]
    • debugger-Anweisung
    • Gutes altes console.log-Debugging

Welche Sprachkonstrukte verwenden Sie zum Iterieren über Objekteigenschaften und Array-Elemente?

Für Objekte:

  • for-in-Schleifen – for (var property in obj) { console.log(property); }. Dies iteriert jedoch auch über seine geerbten Eigenschaften, und Sie würden eine obj.hasOwnProperty(property)-Prüfung hinzufügen, bevor Sie es verwenden.
  • Object.keys()Object.keys(obj).forEach(function (property) { ... }). Object.keys() ist eine statische Methode, die alle aufzählbaren Eigenschaften des Objekts auflistet, das Sie ihr übergeben.
  • Object.getOwnPropertyNames()Object.getOwnPropertyNames(obj).forEach(function (property) { ... }). Object.getOwnPropertyNames() ist eine statische Methode, die alle aufzählbaren und nicht aufzählbaren Eigenschaften des Objekts auflistet, das Sie ihr übergeben.

Für Arrays:

  • for-Schleifen – for (var i = 0; i < arr.length; i++). Der häufige Fallstrick hier ist, dass var im Funktionsbereich und nicht im Blockbereich liegt und meistens eine blockbezogene Iteratorvariable gewünscht wird. ES2015 führt let ein, das einen Blockbereich hat, und es wird empfohlen, dieses stattdessen zu verwenden. Dies wird also zu: for (let i = 0; i < arr.length; i++).
  • forEacharr.forEach(function (el, index) { ... }). Dieses Konstrukt kann manchmal bequemer sein, da Sie den index nicht verwenden müssen, wenn Sie nur die Array-Elemente benötigen. Es gibt auch die Methoden every und some, die es Ihnen ermöglichen, die Iteration frühzeitig zu beenden.
  • for-of-Schleifen – for (let elem of arr) { ... }. ES6 führt eine neue Schleife ein, die for-of-Schleife, die es Ihnen ermöglicht, über Objekte zu iterieren, die dem iterierbaren Protokoll entsprechen, wie String, Array, Map, Set usw. Sie kombiniert die Vorteile der for-Schleife und der forEach()-Methode. Der Vorteil der for-Schleife ist, dass Sie sie unterbrechen können, und der Vorteil von forEach() ist, dass sie prägnanter ist als die for-Schleife, da Sie keine Zählvariable benötigen. Mit der for-of-Schleife erhalten Sie sowohl die Möglichkeit, eine Schleife zu unterbrechen, als auch eine prägnantere Syntax.

Meistens würde ich die .forEach-Methode bevorzugen, aber es hängt wirklich davon ab, was Sie tun möchten. Vor ES6 verwendeten wir for-Schleifen, wenn wir die Schleife vorzeitig mit break beenden mussten. Aber jetzt mit ES6 können wir das mit for-of-Schleifen tun. Ich würde for-Schleifen verwenden, wenn ich noch mehr Flexibilität benötige, wie z.B. das Inkrementieren des Iterators mehr als einmal pro Schleife.

Beachten Sie auch, dass Sie bei Verwendung der for-of-Schleife, wenn Sie sowohl den Index als auch den Wert jedes Array-Elements zugreifen müssen, dies mit der ES6 Array entries()-Methode und Destrukturierung tun können:

const arr = ['a', 'b', 'c']; for (let [index, elem] of arr.entries()) { console.log(index, ': ', elem); }

Erklären Sie den Unterschied zwischen veränderlichen und unveränderlichen Objekten.

Unveränderlichkeit ist ein Kernprinzip in der funktionalen Programmierung und hat auch objektorientierten Programmen viel zu bieten. Ein veränderliches Objekt ist ein Objekt, dessen Zustand nach seiner Erstellung geändert werden kann. Ein unveränderliches Objekt ist ein Objekt, dessen Zustand nach seiner Erstellung nicht geändert werden kann.

Was ist ein Beispiel für ein unveränderliches Objekt in JavaScript?

In JavaScript sind einige eingebaute Typen (Zahlen, Strings) unveränderlich, aber benutzerdefinierte Objekte sind im Allgemeinen veränderlich.

Einige eingebaute unveränderliche JavaScript-Objekte sind Math, Date.

Hier sind ein paar Möglichkeiten, um Unveränderlichkeit bei einfachen JavaScript-Objekten hinzuzufügen/zu simulieren.

Objektkonstante Eigenschaften

Durch die Kombination von writable: false und configurable: false können Sie im Wesentlichen eine Konstante (kann nicht geändert, neu definiert oder gelöscht werden) als Objekteigenschaft erstellen, wie zum Beispiel:

let myObject = {}; Object.defineProperty(myObject, 'number', { value: 42, writable: false, configurable: false, }); console.log(myObject.number); // 42 myObject.number = 43; console.log(myObject.number); // 42

Erweiterungen verhindern

Wenn Sie verhindern möchten, dass einem Objekt neue Eigenschaften hinzugefügt werden, die restlichen Eigenschaften des Objekts jedoch unangetastet lassen möchten, rufen Sie Object.preventExtensions(...) auf:

var myObject = { a: 2, }; Object.preventExtensions(myObject); myObject.b = 3; myObject.b; // undefined

Im Nicht-Strict-Modus schlägt die Erstellung von b stillschweigend fehl. Im Strict-Modus wird ein TypeError ausgelöst.

Versiegeln (Seal)

Object.seal() erstellt ein "versiegeltes" Objekt, was bedeutet, dass es ein vorhandenes Objekt nimmt und im Wesentlichen Object.preventExtensions() darauf aufruft, aber auch alle seine vorhandenen Eigenschaften als configurable: false markiert.

Sie können also nicht nur keine weiteren Eigenschaften hinzufügen, sondern auch keine vorhandenen Eigenschaften neu konfigurieren oder löschen (obwohl Sie deren Werte noch ändern können).

Einfrieren (Freeze)

Object.freeze() erstellt ein eingefrorenes Objekt, was bedeutet, dass es ein vorhandenes Objekt nimmt und im Wesentlichen Object.seal() darauf aufruft, aber es markiert auch alle "Datenzugriffs"-Eigenschaften als writable: false, so dass ihre Werte nicht geändert werden können.

Dieser Ansatz ist die höchste Stufe der Unveränderlichkeit, die Sie für ein Objekt selbst erreichen können, da er jegliche Änderungen am Objekt oder an einer seiner direkten Eigenschaften verhindert (obwohl, wie oben erwähnt, der Inhalt aller referenzierten anderen Objekte unberührt bleibt).

var immutable = Object.freeze({});

Das Einfrieren eines Objekts erlaubt es nicht, neue Eigenschaften zu einem Objekt hinzuzufügen und verhindert das Entfernen oder Ändern bestehender Eigenschaften. Object.freeze() bewahrt die Aufzählbarkeit, Konfigurierbarkeit, Schreibbarkeit und den Prototyp des Objekts. Es gibt das übergebene Objekt zurück und erstellt keine eingefrorene Kopie.

Was sind die Vor- und Nachteile der Unveränderlichkeit?

Vorteile

  • Einfachere Änderungsdetektion - Die Objektgleichheit kann performant und einfach durch referentielle Gleichheit bestimmt werden. Dies ist nützlich für den Vergleich von Objektunterschieden in React und Redux.
  • Programme mit unveränderlichen Objekten sind einfacher zu durchdenken, da Sie sich keine Gedanken darüber machen müssen, wie sich ein Objekt im Laufe der Zeit entwickeln könnte.
  • Verteidigungskopien sind nicht mehr notwendig, wenn unveränderliche Objekte von oder an Funktionen zurückgegeben oder übergeben werden, da keine Möglichkeit besteht, dass ein unveränderliches Objekt dadurch geändert wird.
  • Einfaches Teilen über Referenzen - Eine Kopie eines Objekts ist genauso gut wie eine andere, so dass Sie Objekte zwischenspeichern oder dasselbe Objekt mehrmals wiederverwenden können.
  • Thread-sicher - Unveränderliche Objekte können in einer Multithread-Umgebung sicher zwischen Threads verwendet werden, da keine Gefahr besteht, dass sie in anderen gleichzeitig laufenden Threads geändert werden.
  • Durch die Verwendung von Bibliotheken wie ImmutableJS werden Objekte mittels struktureller Freigabe modifiziert, und es wird weniger Speicher für mehrere Objekte mit ähnlichen Strukturen benötigt.

Nachteile

  • Naive Implementierungen von unveränderlichen Datenstrukturen und ihren Operationen können zu extrem schlechter Leistung führen, da jedes Mal neue Objekte erstellt werden. Es wird empfohlen, Bibliotheken für effiziente unveränderliche Datenstrukturen und Operationen zu verwenden, die auf struktureller Freigabe basieren.
  • Die Zuweisung (und Freigabe) vieler kleiner Objekte anstatt der Modifikation bestehender kann sich auf die Leistung auswirken. Die Komplexität des Allocators oder des Garbage Collectors hängt in der Regel von der Anzahl der Objekte im Heap ab.
  • Zyklische Datenstrukturen wie Graphen sind schwierig zu erstellen. Wenn Sie zwei Objekte haben, die nach der Initialisierung nicht geändert werden können, wie können Sie sie dazu bringen, aufeinander zu zeigen?

Wie können Sie Unveränderlichkeit in Ihrem eigenen Code erreichen?

Die Alternative ist die Verwendung von const-Deklarationen in Kombination mit den oben genannten Techniken zur Erstellung. Zum "Mutieren" von Objekten verwenden Sie den Spread-Operator, Object.assign, Array.concat() usw., um neue Objekte zu erstellen, anstatt das ursprüngliche Objekt zu mutieren.

Beispiele:

// Array-Beispiel const arr = [1, 2, 3]; const newArr = [...arr, 4]; // [1, 2, 3, 4] // Objekt-Beispiel const human = Object.freeze({ race: 'human' }); const john = { ...human, name: 'John' }; // {race: "human", name: "John"} const alienJohn = { ...john, race: 'alien' }; // {race: "alien", name: "John"}

Erklären Sie den Unterschied zwischen synchronen und asynchronen Funktionen.

Synchrone Funktionen sind blockierend, während asynchrone Funktionen dies nicht sind. Bei synchronen Funktionen werden Anweisungen abgeschlossen, bevor die nächste Anweisung ausgeführt wird. In diesem Fall wird das Programm genau in der Reihenfolge der Anweisungen ausgewertet, und die Ausführung des Programms wird angehalten, wenn eine der Anweisungen sehr lange dauert.

Asynchrone Funktionen akzeptieren normalerweise einen Callback als Parameter, und die Ausführung wird unmittelbar nach dem Aufruf der asynchronen Funktion in der nächsten Zeile fortgesetzt. Der Callback wird erst aufgerufen, wenn der asynchrone Vorgang abgeschlossen ist und der Aufrufstack leer ist. Aufwendige Operationen wie das Laden von Daten von einem Webserver oder das Abfragen einer Datenbank sollten asynchron durchgeführt werden, damit der Hauptthread andere Operationen ausführen kann, anstatt zu blockieren, bis diese langwierige Operation abgeschlossen ist (im Falle von Browsern friert die Benutzeroberfläche ein).

Was ist der Event-Loop? Was ist der Unterschied zwischen Call Stack und Task Queue?

Der Event-Loop ist eine Single-Threaded-Schleife, die den Call Stack überwacht und prüft, ob in der Task Queue Arbeit erledigt werden muss. Wenn der Call Stack leer ist und sich Callback-Funktionen in der Task Queue befinden, wird eine Funktion aus der Warteschlange entfernt und auf den Call Stack gepusht, um ausgeführt zu werden.

Wenn Sie Philip Roberts Vortrag über den Event Loop noch nicht gesehen haben, sollten Sie das tun. Es ist eines der meistgesehenen Videos über JavaScript.

Erklären Sie die Unterschiede bei der Verwendung von `foo` zwischen `function foo() {}` und `var foo = function() {}`

Ersteres ist eine Funktionsdeklaration, letzteres ein Funktionsausdruck. Der Hauptunterschied besteht darin, dass bei Funktionsdeklarationen der Funktionskörper gehoistet wird, bei Funktionsausdrücken jedoch nicht (sie haben das gleiche Hoisting-Verhalten wie Variablen). Eine ausführlichere Erklärung zum Hoisting finden Sie in der oben genannten Frage zum Hoisting. Wenn Sie versuchen, einen Funktionsausdruck aufzurufen, bevor er definiert wurde, erhalten Sie einen Uncaught TypeError: XXX is not a function Fehler.

Funktionsdeklaration

foo(); // 'FOOOOO' function foo() { console.log('FOOOOO'); }

Funktionsausdruck

foo(); // Uncaught TypeError: foo is not a function var foo = function () { console.log('FOOOOO'); };

Was sind die Unterschiede zwischen Variablen, die mit `let`, `var` oder `const` erstellt wurden?

Variablen, die mit dem Schlüsselwort var deklariert werden, sind auf die Funktion beschränkt, in der sie erstellt wurden, oder, falls außerhalb einer Funktion erstellt, auf das globale Objekt. let und const sind blockbezogen, was bedeutet, dass sie nur innerhalb des nächsten Satzes von geschweiften Klammern (Funktion, If-Else-Block oder For-Schleife) zugänglich sind.

function foo() { // Alle Variablen sind innerhalb von Funktionen zugänglich. var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; console.log(bar); // bar console.log(baz); // baz console.log(qux); // qux } console.log(bar); // ReferenceError: bar is not defined console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined
if (true) { var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; } // Mit var deklarierte Variablen sind überall im Funktionsbereich zugänglich. console.log(bar); // bar // Mit let und const definierte Variablen sind außerhalb des Blocks, in dem sie definiert wurden, nicht zugänglich. console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined

var erlaubt das Hoisting von Variablen, was bedeutet, dass sie im Code referenziert werden können, bevor sie deklariert werden. let und const erlauben dies nicht und werfen stattdessen einen Fehler.

console.log(foo); // undefined var foo = 'foo'; console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization let baz = 'baz'; console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization const bar = 'bar';

Das erneute Deklarieren einer Variablen mit var löst keinen Fehler aus, let und const jedoch schon.

var foo = 'foo'; var foo = 'bar'; console.log(foo); // "bar" let baz = 'baz'; let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared

let und const unterscheiden sich darin, dass let die Neuzuweisung des Variablenwerts erlaubt, während const dies nicht tut.

// Das ist in Ordnung. let foo = 'foo'; foo = 'bar'; // Dies verursacht eine Ausnahme. const baz = 'baz'; baz = 'qux';

Was sind die Unterschiede zwischen ES6-Klassen und ES5-Funktionskonstruktoren?

Schauen wir uns zunächst ein Beispiel für jedes an:

// ES5 Funktionskonstruktor function Person(name) { this.name = name; } // ES6 Klasse class Person { constructor(name) { this.name = name; } }

Für einfache Konstruktoren sehen sie ziemlich ähnlich aus.

Der Hauptunterschied im Konstruktor ergibt sich bei der Verwendung von Vererbung. Wenn wir eine Student-Klasse erstellen wollen, die von Person erbt und ein studentId-Feld hinzufügt, müssen wir zusätzlich zu oben Folgendes tun.

// ES5 Funktionskonstruktor function Student(name, studentId) { // Aufruf des Konstruktors der Superklasse, um von der Superklasse abgeleitete Mitglieder zu initialisieren. Person.call(this, name); // Eigene Mitglieder der Unterklasse initialisieren. this.studentId = studentId; } Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; // ES6 Klasse class Student extends Person { constructor(name, studentId) { super(name); this.studentId = studentId; } }

Die Verwendung von Vererbung in ES5 ist viel ausführlicher, und die ES6-Version ist einfacher zu verstehen und zu merken.

Können Sie einen Anwendungsfall für die neue Pfeil-=>-Funktionssyntax nennen? Wie unterscheidet sich diese neue Syntax von anderen Funktionen?

Ein offensichtlicher Vorteil von Pfeilfunktionen ist die Vereinfachung der zum Erstellen von Funktionen benötigten Syntax, ohne dass das Schlüsselwort function erforderlich ist. Das this innerhalb von Pfeilfunktionen ist auch an den umgebenden Gültigkeitsbereich gebunden, was sich von regulären Funktionen unterscheidet, bei denen das this durch das aufrufende Objekt bestimmt wird. Lexikalisch gebundenes this ist nützlich beim Aufrufen von Callbacks, insbesondere in React-Komponenten.

Welchen Vorteil bietet die Verwendung der Pfeilsyntax für eine Methode in einem Konstruktor?

Der Hauptvorteil der Verwendung einer Pfeilfunktion als Methode innerhalb eines Konstruktors besteht darin, dass der Wert von this zum Zeitpunkt der Funktionserstellung festgelegt wird und sich danach nicht mehr ändern kann. Wenn also der Konstruktor zum Erstellen eines neuen Objekts verwendet wird, verweist this immer auf dieses Objekt. Nehmen wir zum Beispiel an, wir haben einen Person-Konstruktor, der einen Vornamen als Argument entgegennimmt und zwei Methoden zum console.log dieses Namens hat, eine als reguläre Funktion und eine als Pfeilfunktion:

const Person = function (firstName) { this.firstName = firstName; this.sayName1 = function () { console.log(this.firstName); }; this.sayName2 = () => { console.log(this.firstName); }; }; const john = new Person('John'); const dave = new Person('Dave'); john.sayName1(); // John john.sayName2(); // John // Der this-Wert der regulären Funktion kann geändert werden, aber der der Pfeilfunktion nicht john.sayName1.call(dave); // Dave (weil "this" jetzt das Dave-Objekt ist) john.sayName2.call(dave); // John john.sayName1.apply(dave); // Dave (weil 'this' jetzt das Dave-Objekt ist) john.sayName2.apply(dave); // John john.sayName1.bind(dave)(); // Dave (weil 'this' jetzt das Dave-Objekt ist) john.sayName2.bind(dave)(); // John var sayNameFromWindow1 = john.sayName1; sayNameFromWindow1(); // undefined (weil 'this' jetzt das Window-Objekt ist) var sayNameFromWindow2 = john.sayName2; sayNameFromWindow2(); // John

Die Hauptbotschaft hier ist, dass this für eine normale Funktion geändert werden kann, aber der Kontext für eine Pfeilfunktion immer gleich bleibt. Selbst wenn Sie Ihre Pfeilfunktion an verschiedene Teile Ihrer Anwendung weitergeben, müssten Sie sich keine Sorgen machen, dass sich der Kontext ändert.

Dies kann besonders in React-Klassenkomponenten hilfreich sein. Wenn Sie eine Klassenmethode für etwas wie einen Klick-Handler mit einer normalen Funktion definieren und diesen Klick-Handler dann als Prop an eine Kindkomponente weitergeben, müssen Sie this auch im Konstruktor der Elternkomponente binden. Wenn Sie stattdessen eine Pfeilfunktion verwenden, ist es nicht notwendig, "this" zu binden, da die Methode ihren "this"-Wert automatisch aus ihrem umgebenden lexikalischen Kontext erhält.

Was ist die Definition einer Higher-Order-Funktion?

Eine Higher-Order-Funktion ist jede Funktion, die eine oder mehrere Funktionen als Argumente entgegennimmt, die sie zur Bearbeitung von Daten verwendet, und/oder eine Funktion als Ergebnis zurückgibt. Higher-Order-Funktionen sollen eine wiederholt ausgeführte Operation abstrahieren. Das klassische Beispiel hierfür ist map, das ein Array und eine Funktion als Argumente entgegennimmt. map verwendet dann diese Funktion, um jedes Element im Array zu transformieren, und gibt ein neues Array mit den transformierten Daten zurück. Andere beliebte Beispiele in JavaScript sind forEach, filter und reduce. Eine Higher-Order-Funktion muss nicht nur Arrays manipulieren, da es viele Anwendungsfälle für das Zurückgeben einer Funktion aus einer anderen Funktion gibt. Function.prototype.bind ist ein solches Beispiel in JavaScript.

Map

Nehmen wir an, wir haben ein Array von Namen, die wir jeweils in Großbuchstaben umwandeln müssen.

const names = ['irish', 'daisy', 'anna'];

Der imperative Weg wäre wie folgt:

const transformNamesToUppercase = function (names) { const results = []; for (let i = 0; i < names.length; i++) { results.push(names[i].toUpperCase()); } return results; }; transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

Die Verwendung von .map(transformerFn) macht den Code kürzer und deklarativer.

const transformNamesToUppercase = function (names) { return names.map((name) => name.toUpperCase()); }; transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

Können Sie ein Beispiel für die Destrukturierung eines Objekts oder eines Arrays geben?

Destrukturierung ist ein in ES6 verfügbarer Ausdruck, der eine prägnante und bequeme Möglichkeit bietet, Werte von Objekten oder Arrays zu extrahieren und in separate Variablen zu platzieren.

Array-Destrukturierung

// Variablenzuweisung. const foo = ['one', 'two', 'three']; const [one, two, three] = foo; console.log(one); // "one" console.log(two); // "two" console.log(three); // "three"
// Variablen tauschen let a = 1; let b = 3; [a, b] = [b, a]; console.log(a); // 3 console.log(b); // 1

Objekt-Destrukturierung

// Variablenzuweisung. const o = { p: 42, q: true }; const { p, q } = o; console.log(p); // 42 console.log(q); // true

ES6 Template Literals bieten viel Flexibilität beim Generieren von Strings, können Sie ein Beispiel geben?

Template-Literale erleichtern die String-Interpolation oder das Einfügen von Variablen in einen String. Vor ES2015 war es üblich, etwas wie folgt zu tun:

var person = { name: 'Tyler', age: 28 }; console.log( 'Hallo, mein Name ist ' + person.name + ' und ich bin ' + person.age + ' Jahre alt!', ); // 'Hallo, mein Name ist Tyler und ich bin 28 Jahre alt!'

Mit Template-Literalen können Sie dieselbe Ausgabe jetzt stattdessen wie folgt erstellen:

const person = { name: 'Tyler', age: 28 }; console.log(`Hallo, mein Name ist ${person.name} und ich bin ${person.age} Jahre alt!`); // 'Hallo, mein Name ist Tyler und ich bin 28 Jahre alt!'

Beachten Sie, dass Sie Backticks und keine Anführungszeichen verwenden, um anzuzeigen, dass Sie ein Template-Literal verwenden, und dass Sie Ausdrücke innerhalb der ${}-Platzhalter einfügen können.

Ein zweiter hilfreicher Anwendungsfall ist das Erstellen von mehrzeiligen Strings. Vor ES2015 konnten Sie einen mehrzeiligen String wie folgt erstellen:

console.log('Dies ist Zeile eins.\nDies ist Zeile zwei.'); // Dies ist Zeile eins. // Dies ist Zeile zwei.

Oder wenn Sie es in Ihrem Code in mehrere Zeilen aufteilen wollten, damit Sie in Ihrem Texteditor nicht nach rechts scrollen mussten, um einen langen String zu lesen, konnten Sie es auch so schreiben:

console.log('Dies ist Zeile eins.\n' + 'Dies ist Zeile zwei.'); // Dies ist Zeile eins. // Dies ist Zeile zwei.

Template-Literale behalten jedoch den von Ihnen hinzugefügten Abstand bei. Um beispielsweise dieselbe mehrzeilige Ausgabe zu erstellen, die wir oben erstellt haben, können Sie einfach Folgendes tun:

console.log(`Dies ist Zeile eins. Dies ist Zeile zwei.`); // Dies ist Zeile eins. // Dies ist Zeile zwei.

Ein weiterer Anwendungsfall für Template-Literale wäre die Verwendung als Ersatz für Templating-Bibliotheken für einfache Variableninterpolation:

const person = { name: 'Tyler', age: 28 }; document.body.innerHTML = ` <div> <p>Name: ${person.name}</p> <p>Alter: ${person.age}</p> </div> `;

Beachten Sie, dass Ihr Code durch die Verwendung von .innerHTML anfällig für XSS sein kann. Bereinigen Sie Ihre Daten, bevor Sie sie anzeigen, wenn sie von einem Benutzer stammen!

Können Sie ein Beispiel für eine Curry-Funktion geben und erklären, warum diese Syntax einen Vorteil bietet?

Currying ist ein Muster, bei dem eine Funktion mit mehr als einem Parameter in mehrere Funktionen aufgeteilt wird, die, wenn sie nacheinander aufgerufen werden, alle erforderlichen Parameter einzeln ansammeln. Diese Technik kann nützlich sein, um Code, der im funktionalen Stil geschrieben wurde, leichter lesbar und zusammensetzbar zu machen. Es ist wichtig zu beachten, dass eine Funktion, um curried zu werden, als eine Funktion beginnen muss, dann in eine Sequenz von Funktionen aufgeteilt werden muss, die jeweils einen Parameter akzeptieren.

function curry(fn) { if (fn.length === 0) { return fn; } function _curried(depth, args) { return function (newArgument) { if (depth - 1 === 0) { return fn(...args, newArgument); } return _curried(depth - 1, [...args, newArgument]); }; } return _curried(fn.length, []); } function add(a, b) { return a + b; } var curriedAdd = curry(add); var addFive = curriedAdd(5); var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]

Was sind die Vorteile der Verwendung der Spread-Syntax und wie unterscheidet sie sich von der Rest-Syntax?

Die Spread-Syntax von ES6 ist beim Codieren im funktionalen Paradigma sehr nützlich, da wir problemlos Kopien von Arrays oder Objekten erstellen können, ohne auf Object.create, slice oder eine Bibliotheksfunktion zurückgreifen zu müssen. Diese Sprachfunktion wird häufig in Redux- und RxJS-Projekten verwendet.

function putDookieInAnyArray(arr) { return [...arr, 'dookie']; } const result = putDookieInAnyArray(['I', 'really', "don't", 'like']); // ["I", "really", "don't", "like", "dookie"] const person = { name: 'Todd', age: 29, }; const copyOfTodd = { ...person };

Die Rest-Syntax von ES6 bietet eine Kurzschreibweise, um eine beliebige Anzahl von Argumenten an eine Funktion zu übergeben. Sie ist wie eine Umkehrung der Spread-Syntax, indem sie Daten nimmt und sie in ein Array stopft, anstatt ein Array von Daten zu entpacken, und sie funktioniert in Funktionsargumenten sowie in Array- und Objekt-Destrukturierungszuweisungen.

function addFiveToABunchOfNumbers(...numbers) { return numbers.map((x) => x + 5); } const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15] const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4] const { e, f, ...others } = { e: 1, f: 2, g: 3, h: 4, }; // e: 1, f: 2, others: { g: 3, h: 4 }

Wie können Sie Code zwischen Dateien teilen?

Dies hängt von der JavaScript-Umgebung ab.

Auf der Clientseite (Browserumgebung) können alle Skripte auf Variablen/Funktionen zugreifen, solange sie im globalen Geltungsbereich (window) deklariert sind. Alternativ kann man das Asynchronous Module Definition (AMD) über RequireJS für einen modulareren Ansatz verwenden.

Auf der Serverseite (Node.js) wurde üblicherweise CommonJS verwendet. Jede Datei wird als Modul behandelt und kann Variablen und Funktionen exportieren, indem sie diese an das module.exports-Objekt anhängt.

ES2015 definiert eine Modulsyntax, die darauf abzielt, sowohl AMD als auch CommonJS zu ersetzen. Dies wird schließlich sowohl in Browser- als auch in Node-Umgebungen unterstützt werden.

Warum sollte man statische Klassenmitglieder erstellen wollen?

Statische Klassenmitglieder (Eigenschaften/Methoden) sind nicht an eine bestimmte Instanz einer Klasse gebunden und haben denselben Wert, unabhängig davon, welche Instanz darauf verweist. Statische Eigenschaften sind typischerweise Konfigurationsvariablen, und statische Methoden sind normalerweise reine Hilfsfunktionen, die nicht vom Zustand der Instanz abhängen.