JavaScript 运行时:浏览器 vs Node.js

简介

JavaScript 运行时(Runtime)是 JavaScript 代码执行的环境。它提供了 JavaScript 引擎(用于解析和执行 JS 代码)以及一组内置的 API(用于与外部环境交互)。

最常见的两个 JavaScript 运行时是:

  • 浏览器运行时:运行在用户的浏览器中,用于网页交互
  • Node.js 运行时:运行在服务器或本地命令行中,用于后端开发和工具脚本

JavaScript 引擎

JavaScript 引擎是运行时的核心,负责将 JavaScript 代码转换为机器码并执行。

引擎 使用环境
V8 Chrome、Node.js、Edge(新版)
SpiderMonkey Firefox
JavaScriptCore Safari
Hermes React Native

V8 是最流行的 JavaScript 引擎,由 Google 开发,同时被 Chrome 和 Node.js 使用。


浏览器运行时

概述

浏览器运行时提供了运行 JavaScript 代码的环境,主要特点是:

  • 可以操作网页(DOM)
  • 可以与浏览器交互(BOM)
  • 运行在客户端(用户的电脑/手机)
  • 全局对象是 window

全局对象:window

在浏览器中,全局对象是 window。所有全局变量和函数都是 window 的属性。

// 在浏览器中
console.log(window);          // Window 对象
console.log(this === window);  // true(非严格模式下)

var globalVar = 'hello';
console.log(window.globalVar); // "hello"

// 浏览器特有的全局函数
window.alert('Hello!');
window.setTimeout(() => console.log('1秒后'), 1000);

浏览器提供的 API

DOM(文档对象模型):

// 操作网页元素
let title = document.getElementById('title');
title.textContent = '新标题';

let items = document.querySelectorAll('.item');
items.forEach(item => item.style.color = 'red');

// 创建新元素
let div = document.createElement('div');
div.innerHTML = '<p>Hello World</p>';
document.body.appendChild(div);

BOM(浏览器对象模型):

// 浏览器信息
console.log(navigator.userAgent);  // 浏览器 UA 字符串

// URL 信息
console.log(location.href);        // 当前页面 URL

// 历史记录
history.back();                   // 后退一页

// 弹窗
alert('提示信息');
confirm('确定吗?');
prompt('请输入:');

定时器:

// setTimeout:延迟执行一次
let timer1 = setTimeout(() => {
  console.log('1秒后执行');
}, 1000);

// setInterval:重复执行
let timer2 = setInterval(() => {
  console.log('每秒执行一次');
}, 1000);

// 清除定时器
clearTimeout(timer1);
clearInterval(timer2);

网络请求(fetch):

// 发送 HTTP 请求
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('请求失败:', error));

// 使用 async/await
async function getData() {
  try {
    let response = await fetch('https://api.example.com/data');
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('请求失败:', error);
  }
}

事件监听:

// 监听点击事件
document.getElementById('btn').addEventListener('click', () => {
  console.log('按钮被点击了!');
});

// 监听页面加载
window.addEventListener('load', () => {
  console.log('页面加载完成');
});

// 监听键盘事件
document.addEventListener('keydown', (event) => {
  console.log('按下了:', event.key);
});

浏览器的事件循环

浏览器使用事件循环(Event Loop)处理异步操作:

执行栈(Call Stack)
  ↓
任务队列(Task Queue)
  ↓
微任务队列(Microtask Queue)— Promise 回调、MutationObserver
  ↓
宏任务队列(Macrotask Queue)— setTimeout、setInterval、I/O
// 事件循环示例
console.log('1');  // 同步代码,立即执行

setTimeout(() => console.log('2'), 0);  // 宏任务,放入任务队列

Promise.resolve().then(() => console.log('3'));  // 微任务,优先于宏任务

console.log('4');  // 同步代码,立即执行

// 输出顺序:1 4 3 2
// 解释:同步代码 → 微任务 → 宏任务

浏览器中无法使用的 API

// 以下 API 在浏览器中不可用:
// require()          // CommonJS 模块系统(Node.js 特有)
// fs.readFile()      // 文件系统(浏览器出于安全原因不提供)
// process.cwd()      // 进程信息(浏览器没有进程概念)

Node.js 运行时

概述

Node.js 是一个基于 V8 引擎的 JavaScript 运行时,让 JavaScript 可以运行在服务器端。

特点:

  • 可以操作文件系统
  • 可以创建服务器和网络应用
  • 运行在服务器端(或本地命令行)
  • 全局对象是 global(不是 window

安装和运行

# 检查 Node.js 版本
node --version

# 运行 JavaScript 文件
node app.js

# 进入 REPL(交互式环境)
node
> 1 + 2
3
> .exit  # 退出

全局对象:global

在 Node.js 中,全局对象是 global,而不是 window

// 在 Node.js 中
console.log(global);         // Global 对象
console.log(this === global); // false(顶层 this 是空对象或 undefined)

// Node.js 特有的全局变量
console.log(__filename);     // 当前文件的完整路径
console.log(__dirname);      // 当前文件所在目录
console.log(process.version); // Node.js 版本

注意:在 Node.js 模块中,顶层的 this 不是 global,而是空对象(严格模式下是 undefined)。

CommonJS 模块系统

Node.js 传统使用 CommonJS 模块系统。

导出模块:

// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

// 导出
module.exports = { add, subtract };

// 或者逐个导出
exports.add = add;
exports.subtract = subtract;

导入模块:

// app.js
const math = require('./math.js');

console.log(math.add(5, 3));      // 8
console.log(math.subtract(5, 3)); // 2

// 导入内置模块
const fs = require('fs');
const path = require('path');

Node.js 提供的 API

文件系统(fs):

const fs = require('fs');

// 读取文件(异步)
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('文件内容:', data);
});

// 读取文件(同步)
try {
  let data = fs.readFileSync('file.txt', 'utf8');
  console.log('文件内容:', data);
} catch (err) {
  console.error('读取失败:', err);
}

// 写入文件
fs.writeFile('output.txt', 'Hello Node.js!', (err) => {
  if (err) console.error('写入失败:', err);
  else console.log('写入成功');
});

// 使用 promise 版本(推荐)
const fs = require('fs').promises;

async function readFile() {
  try {
    let data = await fs.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

路径处理(path):

const path = require('path');

let filePath = '/users/alice/documents/file.txt';

console.log(path.basename(filePath));  // "file.txt"
console.log(path.dirname(filePath));   // "/users/alice/documents"
console.log(path.extname(filePath));   // ".txt"
console.log(path.join(__dirname, 'data', 'file.json')); // 拼接路径

创建 HTTP 服务器:

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from Node.js!');
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000');
});

进程信息(process):

// 命令行参数
console.log(process.argv);  // ['node', 'script.js', ...args]

// 环境变量
console.log(process.env.NODE_ENV);

// 退出进程
process.exit(0);  // 0 表示正常退出

// 当前工作目录
console.log(process.cwd());

Node.js 的事件循环

Node.js 的事件循环比浏览器更复杂,有多个阶段:

┌───────────────────────────┐
│          timers           │ ← setTimeout、setInterval 回调
├───────────────────────────┤
│     pending callbacks    │ ← 某些系统操作的回调
├───────────────────────────┤
│       idle, prepare      │ ← 内部使用
├───────────────────────────┤
│           poll            │ ← 检索新的 I/O 事件
├───────────────────────────┤
│           check          │ ← setImmediate 回调
├───────────────────────────┤
│      close callbacks     │ ← 关闭事件的回调
└───────────────────────────┘
// Node.js 事件循环示例
console.log('1');  // 同步代码

setTimeout(() => console.log('2'), 0);  // timers 阶段

setImmediate(() => console.log('3'));    // check 阶段

Promise.resolve().then(() => console.log('4'));  // 微任务

process.nextTick(() => console.log('5'));  // 微任务(优先级最高)

console.log('6');  // 同步代码

// 输出顺序:1 6 5 4 2 3(或 1 6 5 4 3 2,2 和 3 顺序可能因情况而异)

浏览器 vs Node.js:详细对比

全局对象对比

特性 浏览器 Node.js
全局对象 window global
顶层 this window(非严格) undefined 或 空对象
模块化全局 无(传统) moduleexportsrequire
文件路径 __filename__dirname

API 对比

功能 浏览器 Node.js
DOM 操作 document ❌ 不可用
网络请求 fetchXMLHttpRequest httphttps 模块
文件系统 ❌ 不可用(安全限制) fs 模块
创建服务器 ❌ 不可用 http.createServer()
环境变量 window 下无 process.env
定时器 setTimeoutsetInterval setTimeoutsetInterval
控制台输出 console.log() console.log()

模块系统对比

特性 浏览器(传统) 浏览器(现代) Node.js(传统) Node.js(现代)
模块格式 <script> 标签 ES Modules CommonJS ES Modules
导入 全局变量 import require() import
导出 全局变量 export module.exports export
文件扩展名 .js .js .js .mjs.js(需配置)

代码示例对比

相同的 JavaScript 语法:

// 以下代码在浏览器和 Node.js 中都能运行
let name = 'Alice';
let greet = (name) => `Hello, ${name}!`;
console.log(greet(name));  // Hello, Alice!

// 数组和对象操作
let arr = [1, 2, 3];
let doubled = arr.map(x => x * 2);
console.log(doubled);  // [2, 4, 6]

// Promise 和 async/await
async function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve('data'), 1000);
  });
}

不同的 API 调用:

// ===== 浏览器中 =====
// 操作 DOM
document.getElementById('app').innerHTML = '<h1>Hello</h1>';

// fetch 请求
fetch('/api/data').then(res => res.json()).then(console.log);

// ===== Node.js 中 =====
// 读取文件
const fs = require('fs');
fs.readFile('data.txt', 'utf8', (err, data) => console.log(data));

// http 服务器
const http = require('http');
http.createServer((req, res) => {
  res.end('Hello from server!');
}).listen(3000);

ES Modules(现代模块系统)

ES Modules(ESM)是 JavaScript 官方的模块系统,现在浏览器和 Node.js 都支持。

在浏览器中使用 ESM

<!-- 使用 type="module" 启用 ES Modules -->
<script type="module">
  import { add } from './math.js';
  console.log(add(5, 3));  // 8
</script>
// math.js — 浏览器和 Node.js 都支持这种写法
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// 默认导出
export default function multiply(a, b) {
  return a * b;
}
<!-- 导入示例 -->
<script type="module">
  import { add, subtract } from './math.js';
  import multiply from './math.js';  // 默认导入

  console.log(add(5, 3));       // 8
  console.log(multiply(5, 3));  // 15
</script>

在 Node.js 中使用 ESM

方法 1:使用 .mjs 扩展名

// math.mjs
export function add(a, b) {
  return a + b;
}

// app.mjs
import { add } from './math.mjs';
console.log(add(5, 3));  // 8

方法 2:在 package.json 中设置 "type": "module"

// package.json
{
  "name": "my-app",
  "type": "module",
  "main": "app.js"
}
// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
console.log(add(5, 3));  // 8

CommonJS vs ES Modules

特性 CommonJS ES Modules
语法 require() / module.exports import / export
加载方式 同步加载 异步加载
静态分析 困难(动态) 容易(静态)
循环依赖 支持(部分) 支持(更好)
浏览器支持 ❌ 不支持 ✅ 支持(需 type=“module”)
Node.js 支持 ✅ 默认 ✅(需配置)
// CommonJS(Node.js 传统)
const { add } = require('./math');
module.exports = { add };

// ES Modules(现代,通用)
import { add } from './math.js';
export { add };

共享的 JavaScript 特性

无论在浏览器还是 Node.js 中,以下 JavaScript 特性都是相同的:

// 数据类型
let str = 'hello';
let num = 123;
let arr = [1, 2, 3];
let obj = { a: 1 };

// 函数和作用域
function greet(name) {
  return `Hello, ${name}!`;
}
const arrowFunc = (x) => x * 2;

// 内置对象
console.log(Math.max(1, 2, 3));       // 3
console.log(JSON.stringify({ a: 1 })); // '{"a":1}'
console.log(Promise.resolve(42));       // Promise { 42 }

// 数组方法
let nums = [1, 2, 3];
let doubled = nums.map(x => x * 2);
let sum = nums.reduce((a, b) => a + b, 0);

// 对象方法
let keys = Object.keys({ a: 1, b: 2 });
let entries = Object.entries({ a: 1 });

如何选择:浏览器 vs Node.js

使用浏览器的场景

  • 开发网页和 Web 应用
  • 需要与用户交互(点击、输入等)
  • 需要操作 DOM(显示/隐藏元素、修改内容)
  • 需要访问浏览器 API(地理位置、摄像头、本地存储等)
// 浏览器专属任务
document.querySelector('.btn').addEventListener('click', () => {
  alert('按钮被点击!');
});

localStorage.setItem('theme', 'dark');
let theme = localStorage.getItem('theme');

使用 Node.js 的场景

  • 开发后端服务器和 API
  • 开发命令行工具
  • 操作文件系统(读写文件)
  • 构建工具和自动化脚本
  • 数据库操作
// Node.js 专属任务
const fs = require('fs');
const files = fs.readdirSync('./');
console.log('目录中的文件:', files);

// 创建简单的 API 服务器
const express = require('express');
const app = express();

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'Alice' }]);
});

app.listen(3000);

可以共用代码的场景

有些代码既可以在浏览器运行,也可以在 Node.js 运行:

// utils.js — 可以在两端共用的工具函数
export function formatDate(date) {
  return new Date(date).toLocaleDateString('zh-CN');
}

export function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 数据验证、格式化等纯函数通常可以共用

综合示例

示例 1:读取文件(Node.js)

// Node.js:读取并分析文件
const fs = require('fs');

function countWords(filePath) {
  try {
    let content = fs.readFileSync(filePath, 'utf8');
    let words = content.trim().split(/\s+/).filter(w => w.length > 0);
    return {
      totalWords: words.length,
      uniqueWords: new Set(words).size
    };
  } catch (err) {
    console.error('读取文件失败:', err.message);
    return null;
  }
}

// console.log(countWords('article.txt'));

示例 2:DOM 操作(浏览器)

<!-- 浏览器:动态更新页面 -->
<!DOCTYPE html>
<html>
<body>
  <ul id="todo-list"></ul>
  <input type="text" id="todo-input">
  <button onclick="addTodo()">添加</button>

  <script>
    let todos = [];

    function addTodo() {
      let input = document.getElementById('todo-input');
      if (!input.value) return;

      todos.push(input.value);
      renderTodos();
      input.value = '';
    }

    function renderTodos() {
      let list = document.getElementById('todo-list');
      list.innerHTML = '';
      todos.forEach(todo => {
        let li = document.createElement('li');
        li.textContent = todo;
        list.appendChild(li);
      });
    }
  </script>
</body>
</html>

示例 3:HTTP 请求对比

// ===== 浏览器 =====
fetch('https://api.example.com/users')
  .then(res => res.json())
  .then(users => {
    users.forEach(user => {
      console.log(user.name);
    });
  });

// ===== Node.js =====
const https = require('https');

https.get('https://api.example.com/users', (res) => {
  let data = '';
  res.on('data', chunk => data += chunk);
  res.on('end', () => {
    let users = JSON.parse(data);
    users.forEach(user => console.log(user.name));
  });
});

// ===== 共用(使用 fetch 的 Node.js)=====
// Node.js 18+ 也内置了 fetch
// fetch('https://api.example.com/users')
//   .then(res => res.json())
//   .then(console.log);

示例 4:模块导出/导入对比

// ===== CommonJS(Node.js 传统)=====
// math.js
function add(a, b) { return a + b; }
module.exports = { add };

// app.js
const { add } = require('./math');
console.log(add(2, 3));

// ===== ES Modules(现代,通用)=====
// math.js
export function add(a, b) { return a + b; }

// app.js(或 browser <script type="module">)
import { add } from './math.js';
console.log(add(2, 3));

总结

对比项 浏览器 Node.js
运行位置 客户端(用户设备) 服务器端 / 本地
全局对象 window global
DOM 访问
文件系统
网络请求 fetch http / fetch(18+)
模块系统 ESM(需 type=“module”) CommonJS(传统)/ ESM
创建服务器
事件循环 较简单 更复杂(多阶段)
适用场景 网页、Web 应用 后端、CLI 工具、构建工具

记住:浏览器和 Node.js 都使用 JavaScript 语法,但提供的 API 不同。浏览器专注于与网页交互,Node.js 专注于服务器端能力。