Skip to content

JavaScript-Laufzeitumgebung: Ein tiefgehender Leitfaden

Vorwort

Sie haben bereits die grundlegende JavaScript-Syntax gelernt, aber haben Sie sich jemals gefragt:

  • Wo genau wird der Code ausgefuehrt?
  • Warum verhaelt sich derselbe Code im Browser und in Node.js unterschiedlich?
  • Warum "haengt" der Code manchmal, kann aber manchmal "parallel" ausgefuehrt werden?

Dieser Artikel fuehrt Sie tief in die JavaScript-Laufzeitumgebung ein, einschliesslich Event Loop, Call Stack, Speicherverwaltung und mehr. Nach der Lektuere werden Sie verstehen, warum Code in einer bestimmten Reihenfolge ausgefuehrt wird, asynchrone Bugs schnell lokalisieren, Code-Performance optimieren und Speicherlecks vermeiden koennen.

Was Sie in diesem Artikel lernen werden

KapitelInhaltWas Sie danach koennen
Kapitel 1Laufzeit-UeberblickVerstehen, wo JavaScript-Code ausgefuehrt wird
Kapitel 2Browser-LaufzeitWissen, welche Web APIs der Browser bereitstellt
Kapitel 3Node.js-LaufzeitDie serverseitige JavaScript-Umgebung verstehen
Kapitel 4Event Loop vertieftAusfuehrungsreihenfolge von Makro- und Mikrotasks beherrschen
Kapitel 5Call Stack und SpeicherCode-Ausfuehrungsprozess und Speicherverwaltung verstehen
Kapitel 6PraxistippsPerformance optimieren, Speicherlecks debuggen

1. Laufzeit-Ueberblick

🤔 Kernfrage

Was ist eine "Laufzeitumgebung"? JavaScript ist nur eine Sprache — warum verhaelt sich derselbe Code in verschiedenen Umgebungen unterschiedlich?

1.1 Was ist eine Laufzeitumgebung

Laufzeit = JavaScript-Engine + Umgebungs-APIs

Wenn JavaScript eine "Programmiersprache" ist, dann ist die Laufzeitumgebung das "Betriebssystem" — sie bestimmt, was Ihr Code tun kann und was nicht.

┌─────────────────────────────────────┐
│         JavaScript-Code              │
├─────────────────────────────────────┤
│      JavaScript-Engine (V8)          │  ← Zustaendig fuer Parsen und Ausfuehren
├─────────────────────────────────────┤
│      Laufzeitumgebung (Browser/Node.js) │  ← Bietet zusaetzliche Faehigkeiten
└─────────────────────────────────────┘

Eine Analogie: JavaScript ist "Hochdeutsch", die Laufzeit ist die "Stadt"

  • Die JavaScript-Syntax (Hochdeutsch) ist ueberall gleich
  • Aber verschiedene Staedte bieten unterschiedliche Einrichtungen:
    • Browser = hat DOM, window, fetch (wie eine Stadt mit Einkaufszentren, Bibliotheken)
    • Node.js = hat fs, http, path (wie eine Stadt mit Fabriken, Autobahnen)

1.2 Die zwei wichtigsten Laufzeitumgebungen

EigenschaftBrowserNode.js
HauptzweckWebseiten-Interaktion, BenutzeroberflaecheServeranwendungen, Kommandozeilen-Tools
Globales Objektwindowglobal
DOM-API✅ Unterstuetzt❌ Nicht unterstuetzt
Dateisystem❌ Eingeschraenkt✅ Vollstaendig unterstuetzt
ModulsystemES ModulesCommonJS + ES Modules
TimersetTimeout, setIntervalsetTimeout, setInterval
Netzwerkanfragenfetch, XMLHttpRequesthttp, https-Module

👇 Probieren Sie es aus: Vergleichen Sie die Umgebungsunterschiede zwischen Browser und Node.js

运行时环境对比

浏览器环境

window
浏览器全局对象
window.location.href
document
DOM 操作
document.querySelector("h1")
localStorage
本地存储
localStorage.setItem("key", "value")
fetch
网络请求
fetch("/api/data")
setTimeout
定时器
setTimeout(() => {}, 1000)
特点:
  • ✅ 有 DOM 和 BOM API,可以操作网页
  • ✅ 有 Web Storage (localStorage, sessionStorage)
  • ✅ 有 fetch 和 XMLHttpRequest 进行网络请求
  • ❌ 没有文件系统访问权限
  • ❌ 不能直接创建 HTTP 服务器

代码演示:不同环境的差异

🌐浏览器结果
点击"在浏览器运行"查看结果
🟢Node.js 结果
需要在 Node.js 环境中运行

核心区别:

浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 API。

Node.js 运行时专注于服务器端开发,提供文件系统、HTTP 服务器、进程管理等后端专用 API。

同样的 JavaScript 语法,但能用的 API 完全不同——这就是"环境判断"的重要性。

💡 Kernerkenntnis

Die Laufzeit bestimmt, welche APIs Sie verwenden koennen. DOM-APIs, die im Browser funktionieren, funktionieren nicht in Node.js; Datei-APIs, die in Node.js funktionieren, funktionieren nicht im Browser. Deshalb benoetigt mancher Code eine "Umgebungspruefung".


2. Browser-Laufzeit

🤔 Kernfrage

Welche Faehigkeiten stellt der Browser bereit, damit JavaScript Webseiten manipulieren kann?

2.1 Aufbau der Browser-Laufzeit

┌─────────────────────────────────────────────┐
│            JavaScript-Engine                 │
│            (V8 / SpiderMonkey)               │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│              Web APIs                        │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐     │
│  │   DOM   │ │   BOM    │ │ Network  │     │
│  │ Webseite│ │ Browser  │ │ Netzwerk │     │
│  │ manip.  │ │ manip.   │ │ Anfragen │     │
│  └─────────┘ └──────────┘ └──────────┘     │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│           Event Loop (Ereignisschleife)      │
│     Koordiniert Codeausfuehrung, Event-      │
│     verarbeitung und Task-Scheduling          │
└─────────────────────────────────────────────┘

2.2 Die drei Kategorien der Web-APIs

1. DOM-API — Webseiten-Inhalte manipulieren

javascript
// Element finden
const title = document.querySelector('h1')

// Inhalt aendern
title.textContent = 'Neuer Titel'

// Stil hinzufuegen
title.style.color = 'red'

2. BOM-API — Den Browser steuern

javascript
// Seitennavigation
window.location.href = 'https://example.com'

// Browser-Speicher
localStorage.setItem('key', 'value')

// Browser-Verlauf
history.back()

3. Network-API — Netzwerkanfragen

javascript
// HTTP-Anfrage senden
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))

2.3 Das browser-spezifische Event-System

Eine der kraeftigsten Faehigkeiten der Browser-Laufzeit ist "Event-Driven" — Code wird nicht staendig ausgefuehrt, sondern wartet auf Benutzeraktionen.

javascript
button.addEventListener('click', () => {
  console.log('Button wurde geklickt')
})

Haeufige Event-Typen:

Event-TypAusloesungPraxisszenario
clickMausklickButton-Interaktion
inputEingabefeld-Inhalt aendert sichEchtzeit-Suche
scrollSeitenscrollingLazy Loading
loadRessource fertig geladenDaten initialisieren
errorFehler aufgetretenFehlerbehandlung

3. Node.js-Laufzeit

🤔 Kernfrage

Wodurch kann JavaScript auf dem Server ausgefuehrt werden?

3.1 Aufbau von Node.js

┌─────────────────────────────────────────────┐
│            JavaScript-Engine                 │
│                 (V8)                         │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│           Node.js eingebaute Module          │
│  ┌─────────┐ ┌──────────┐ ┌──────────┐     │
│  │   fs    │ │   http   │ │   path   │     │
│  │ Datei-  │ │ Web-     │ │ Pfad-    │     │
│  │ op.     │ │ server   │ │ verarb.  │     │
│  └─────────┘ └──────────┘ └──────────┘     │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│          libuv Event-Loop-Bibliothek         │
│      Plattformuebergreifende asynchrone      │
│      I/O-Unterstuetzung                     │
└─────────────────────────────────────────────┘

3.2 Node.js-spezifische Faehigkeiten

1. Dateisystem-Operationen

javascript
const fs = require('fs')

// Datei lesen
fs.readFile('./data.txt', 'utf8', (err, data) => {
  if (err) throw err
  console.log(data)
})

// Datei schreiben
fs.writeFile('./output.txt', 'Hello', (err) => {
  if (err) throw err
  console.log('Erfolgreich geschrieben')
})

2. HTTP-Server

javascript
const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' })
  res.end('<h1>Hello World</h1>')
})

server.listen(3000)

3. Modulsystem

javascript
// CommonJS (Node.js-Standard)
const fs = require('fs')
module.exports = { myFunction }

// ES Modules (modern)
import fs from 'fs'
export { myFunction }

3.3 Browser vs. Node.js im Vergleich

EigenschaftBrowserNode.js
EinstiegsdateiHTML-DateiJavaScript-Datei
Globale Objektewindow, documentglobal, process
Modulladen<script>-Tagrequire() / import
SicherheitSandbox-Umgebung, eingeschraenktKann auf Systemressourcen zugreifen
VerwendungBenutzeroberflaecheBackend-Services, Tools

4. Event Loop vertieft

🤔 Kernfrage

JavaScript ist Single-Threaded — warum kann es "nicht blockieren"?

4.1 Was ist der Event Loop

Event Loop = JavaScripts "Task-Scheduling-Zentrale"

JavaScript ist Single-Threaded und kann nur eine Sache gleichzeitig erledigen. Aber der Event Loop laesst es so aussehen, als koenne es "gleichzeitig" viele Dinge tun.

Kernmechanismus:

  1. Synchrone Codes ausfuehren (Call Stack)
  2. Asynchrone Tasks verarbeiten (Task Queue)
  3. Auf neue Tasks warten (Endlos-Wiederholung)
Call Stack                   Task Queue
┌─────────┐              ┌──────────┐
│ Task 1  │              │ Makro 1  │
│ Task 2  │ ←────────────  │ Makro 2  │
│ Task 3  │  einen fertig  │ Makro 3  │
└─────────┘  naechsten holen └──────────┘
      ↓                        ↑
      └────────────────────────┘
       Event Loop prueft staendig

4.2 Makro-Tasks vs. Mikro-Tasks

Dies ist das am haeufigsten verwechselte Konzept in Vorstellungsgespraechen und der Praxis!

Makro-Tasks (Macrotask):

  • setTimeout, setInterval
  • I/O-Operationen
  • UI-Rendering

Mikro-Tasks (Microtask):

  • Promise.then
  • MutationObserver
  • queueMicrotask

Ausfuehrungsreihenfolge: Synchroon → Mikro-Tasks → Makro-Tasks

👇 Probieren Sie es aus: Beobachten Sie die Ausfuehrungsreihenfolge von Makro- und Mikro-Tasks

任务队列:宏任务 vs 微任务

代码示例

1console.log("1")同步
2setTimeout(() => console.log("2"), 0)宏任务
3Promise.resolve().then(() => console.log("3"))微任务
4console.log("4")同步
5setTimeout(() => console.log("5"), 0)宏任务

调用栈 (正在执行)

执行 console.log("1")

微任务队列 Microtask

队列为空

宏任务队列 Macrotask

队列为空

输出日志 (执行顺序)

等待输出...

执行顺序规则

1执行所有同步代码
2执行微任务队列中的所有任务
3执行一个宏任务
4重复步骤 2-3

核心要点: 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。

4.3 Klassische Interview-Frage

javascript
console.log('1')

setTimeout(() => console.log('2'), 0)

Promise.resolve().then(() => console.log('3'))

console.log('4')

// Ausgabe: 1, 4, 3, 2

Warum diese Reihenfolge?

  1. Synchrone Codes ausfuehren: console.log('1'), console.log('4') → Ausgabe 1, 4
  2. Mikro-Task-Queue pruefen: Promise.then → Ausgabe 3
  3. Makro-Task-Queue pruefen: setTimeout → Ausgabe 2

💡 Praxistipp

  • Wenn Code so schnell wie moeglich ausgefuehrt werden soll: Mikro-Tasks (Promise.then) verwenden
  • Wenn die Ausfuehrung verzoegert werden soll: Makro-Tasks (setTimeout) verwenden
  • Nie zu viele asynchrone Operationen mischen — sonst landen Sie in der "Callback-Hoelle"

5. Call Stack und Speicher

🤔 Kernfrage

Wie wird der Code ausgefuehrt? Wo werden Variablen gespeichert? Wann werden sie freigegeben?

5.1 Call Stack: Die "Fussabdruecke" der Funktionsausfuehrung

Call Stack = Ein "Notizbuch" zur Aufzeichnung von Funktionsaufrufen

Jedes Mal, wenn eine Funktion aufgerufen wird, wird ein neuer Eintrag auf dem Stack abgelegt; wenn die Funktion beendet ist, wird der Eintrag entfernt.

javascript
function a() {
  b()
}

function b() {
  c()
}

function c() {
  console.log('Ausfuehrung abgeschlossen')
}

a()

Veraenderungen im Call Stack:

Schritt 1: a() aufrufen
┌─────────┐
│    a    │
└─────────┘

Schritt 2: a() ruft b() auf
┌─────────┐
│    b    │
│    a    │
└─────────┘

Schritt 3: b() ruft c() auf
┌─────────┐
│    c    │
│    b    │
│    a    │
└─────────┘

Schritt 4: c() beendet, nacheinander abgebaut
┌─────────┐
│    b    │
│    a    │
└─────────┘

👇 Probieren Sie es aus: Beobachten Sie die Veraenderungen im Call Stack

调用栈:函数执行的足迹

代码

1main()
2function a() {
3function b() {
4function c() {
5console.log("执行完毕")
6}
7}
8}
9}

调用栈

栈底
栈为空
栈顶

当前状态:

调用 main()

输出

等待输出...

调用栈工作原理:

  • 每次调用函数,就会在栈上"压入"一个新的"栈帧"
  • 栈帧记录了函数的执行状态、局部变量等信息
  • 函数执行完毕,栈帧就会从栈上"弹出"
  • 栈是"后进先出"(LIFO)的数据结构
  • 如果递归太深,会导致"栈溢出"错误

调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。

5.2 Speicherverwaltung: Wohin geht der Muell?

JavaScript hat einen "automatischen Garbage-Collection"-Mechanismus — Sie muessen Speicher nicht manuell freigeben, die Engine erledigt das fuer Sie.

Prinzip der Garbage Collection: Mark-and-Sweep-Algorithmus

  1. Mark-Phase: Von der "Wurzel" ausgehend alle erreichbaren Variablen finden
  2. Sweep-Phase: Nicht markierte Variablen sind "Muell" und werden freigegeben
javascript
// Garbage-Collection-Beispiel
let obj1 = { name: 'Objekt1' }
let obj2 = { name: 'Objekt2' }

// obj1 wird neu zugewiesen, das urspruengliche Objekt verliert die Referenz
obj1 = null  // Das urspruengliche { name: 'Objekt1' } wird freigegeben

// obj2 wird noch verwendet, wird nicht freigegeben
console.log(obj2.name)

👇 Probieren Sie es aus: Beobachten Sie den Garbage-Collection-Prozess

垃圾回收机制

标记阶段从根对象开始,标记所有可达对象
清除阶段回收未标记的对象

对象引用关系

未标记
已标记(可达)
已回收
🌳
Root
📦
obj1
📦
obj2
📦
obj3
📦
obj4
📦
obj5
📦
obj6
当前操作:从根对象开始标记

标记-清除算法 (Mark-and-Sweep)

1
标记阶段

从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"

2
清除阶段

遍历整个堆内存,回收所有未被标记的对象

3
重置标记

清除所有标记位,为下一次垃圾回收做准备

核心要点
  • 根对象(Root): 全局变量、栈上的变量等,总是被认为是可达的
  • 可达对象: 从根对象出发,通过引用链能访问到的对象
  • 垃圾对象: 无法从根对象访问到的对象,会被回收
  • 循环引用: 如果两个对象互相引用但都不可达,仍会被回收

实际应用技巧

💡
及时解除引用

对象不再使用时,将其设为 null

🔒
避免意外的全局变量

使用 const/let 代替 var

🧹
清理事件监听

组件销毁时移除所有监听器

📊
定期检查内存

用 DevTools Memory 面板监控

5.3 Speicherlecks: Die Folgen vergessenen Aufraeumens

Speicherleck = Speicher, der freigegeben werden sollte, nicht freigegeben wird und sich ansammelt

Haeufige Ursachen:

1. Zu viele globale Variablen

javascript
// ❌ Fehler: Globale Variablen werden nicht freigegeben
globalCache = []

function addItem(item) {
  globalCache.push(item)
}

2. Event-Listener nicht entfernt

javascript
// ❌ Fehler: Listener nicht entfernt
button.addEventListener('click', handleClick)

// ✅ Richtig: Listener entfernen, wenn nicht mehr benoetigt
button.removeEventListener('click', handleClick)

3. Closures referenzieren grosse Objekte

javascript
// ❌ Fehler: Closure haelt Referenz auf grosses Objekt, wird nicht freigegeben
function createHandler() {
  const bigData = new Array(1000000).fill('data')
  return function() {
    console.log('Verarbeitung laeuft')
  }
}

const handler = createHandler()  // bigData bleibt im Speicher

👇 Probieren Sie es aus: Beobachten Sie, wie Speicherlecks entstehen

内存泄漏演示

内存使用情况0%

全局变量泄漏

问题:全局变量不会被垃圾回收,会一直占用内存

示例:不断往全局数组添加数据,从不清理

全局变量 (0 项)
暂无全局变量
❌ 错误做法
// 全局变量不会被回收
globalCache = []
function addItem() {
  globalCache.push(largeData)
}

如何避免内存泄漏

  • 避免全局变量: 使用 const/let 代替 var,尽量使用局部变量
  • 及时清理监听器: 组件销毁时移除所有事件监听
  • 释放闭包引用: 不需要时将闭包变量设为 null
  • 使用 WeakMap/WeakSet: 自动清理不再被引用的对象
  • 定期检查: 用 DevTools Memory 面板检查内存泄漏

💡 Praxistipp

  • Regelmaessig pruefen: Browser DevTools → Memory → Take Heap Snapshot, Speicherverbrauch ansehen
  • Globale Variablen vermeiden: const und let anstelle von var verwenden
  • Sofort aufraeumen: Event-Listener und Timer nach Gebrauch entfernen
  • Schwache Referenzen: WeakMap und WeakSet fuer Objektreferenzen verwenden

6. Praxistipps

🤔 Kernfrage

Wie schreibt man hochperformanten JavaScript-Code? Wie debuggt man bei Problemen?

6.1 Performance-Optimierungstipps

1. Reflows und Repaints reduzieren

javascript
// ❌ Fehler: Jede Iteration loest einen Reflow aus
for (let i = 0; i < 1000; i++) {
  element.style.top = i + 'px'
}

// ✅ Richtig: Aenderungen bündeln
element.style.transform = `translateY(${position}px)`

2. Event-Delegation verwenden

javascript
// ❌ Fehler: Jedem Button einen Listener hinzufuegen
buttons.forEach(btn => {
  btn.addEventListener('click', handleClick)
})

// ✅ Richtig: Nur dem Elternelement einen Listener hinzufuegen
container.addEventListener('click', (e) => {
  if (e.target.matches('.button')) {
    handleClick(e)
  }
})

3. Debounce und Throttle

javascript
// Debounce: Erst ausfuehren, wenn der Benutzer aufhoert einzugeben
function debounce(fn, delay) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// Throttle: Ausfuehrungsfrequenz begrenzen
function throttle(fn, delay) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

6.2 Debugging-Tipps

1. Call Stack mit DevTools anzeigen

javascript
function a() {
  b()
}

function b() {
  c()
}

function c() {
  debugger  // Hier anhalten, Call Stack pruefen
}

a()

2. Ausfuehrungspfad mit console.trace() verfolgen

javascript
function trackExecution() {
  console.trace('Ausfuehrungspfad')
  // Gibt den vollstaendigen Call Stack aus
}

3. Performance mit der Performance-API analysieren

javascript
performance.mark('start')

// Etwas Code ausfuehren
for (let i = 0; i < 10000; i++) {
  // ...
}

performance.mark('end')
performance.measure('Schleifen-Performance', 'start', 'end')

const measure = performance.getEntriesByName('Schleifen-Performance')[0]
console.log(`Ausfuehrungszeit: ${measure.duration}ms`)

6.3 Schnellreferenz haeufiger Probleme

ProblemMoegliche UrsacheLoesung
Hoher SpeicherverbrauchSpeicherleck, zu viel CacheGlobale Variablen pruefen, Listener entfernen
Seite ruckeltLange Tasks blockieren den Main ThreadTasks aufteilen, Web Workers verwenden
Events werden nicht ausgeloestListener nicht gebunden, Element existiert nichtDOM-Ladezeitpunkt pruefen
Asynchrone Reihenfolge falschMakro- und Mikro-Tasks vermischtEinheitlich Promise oder async/await verwenden
Timer ungenauMain Thread blockiertWeb Workers oder requestAnimationFrame verwenden

Zusammenfassung

Sie sollten jetzt Folgendes verstehen:

  • Laufzeit = Engine + Umgebungs-APIs, verschiedene Laufzeiten bieten unterschiedliche Faehigkeiten
  • Event Loop koordiniert die Ausfuehrungsreihenfolge von synchronem Code, Mikro-Tasks und Makro-Tasks
  • Call Stack zeichnet den Funktionsausfuehrungsprozess auf, Stack Overflow entsteht durch zu tiefe Rekursion
  • Garbage Collection raeumt ungenutzte Variablen automatisch auf, aber achten Sie auf Speicherlecks
  • Performance-Optimierung besteht hauptsaechlich aus der Reduzierung von Reflows/Repaints und der sinnvollen Nutzung von Asynchronitaet

💡 So sprechen Sie mit der KI bei Problemen

  • "Diese Funktion ist zu langsam, hilf mir die Performance zu optimieren"
  • "Der Speicherverbrauch steigt staendig, das koennte ein Speicherleck sein, bitte pruefen"
  • "Die asynchrone Reihenfolge stimmt nicht — es sollte erst A dann B sein, aber A und B starten fast gleichzeitig"
  • "Der Event-Listener wird nicht ausgeloest, pruefe ob das Element bereits im DOM geladen ist"