普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月1日首页

既然有了 defer,我们还需要像以前那样把 <script>标签放到 <body>的最底部吗?

作者 Smilezyl
2026年2月1日 17:51

既然有了 defer,我们还需要像以前那样把 <script> 标签放到 <body> 的最底部吗?如果我把带 defer 的脚本放在 <head> 里,会有性能问题吗?

核心答案

不需要了。 使用 defer 属性后,把 <script> 放在 <head> 里不仅没有性能问题,反而是更优的做法

原因:

  1. defer 脚本会并行下载,不阻塞 HTML 解析
  2. 脚本执行会延迟到 DOM 解析完成后,但在 DOMContentLoaded 事件之前
  3. 放在 <head> 里可以让浏览器更早发现并开始下载脚本

深入解析

浏览器解析机制

传统 <script>(无 defer/async):
HTML 解析 ──▶ 遇到 script ──▶ 暂停解析 ──▶ 下载脚本 ──▶ 执行脚本 ──▶ 继续解析

defer 脚本:
HTML 解析 ────────────────────────────────────────────▶ DOM 解析完成 ──▶ 执行脚本
     └──▶ 并行下载脚本 ──────────────────────────────────────────────────┘

为什么 <head> 里的 defer 更好?

位置 发现脚本时机 开始下载时机
<head> 解析开始时 立即
<body> 底部 解析接近完成时 较晚

放在 <head> 里,浏览器可以在解析 HTML 的同时下载脚本,充分利用网络带宽

常见误区

误区 1: "defer 脚本放 <head> 会阻塞渲染"

  • 错误。defer 脚本的下载和 HTML 解析是并行的

误区 2: "放 <body> 底部更保险"

  • 这是 defer 出现之前的最佳实践,现在已过时
  • 放底部反而会延迟脚本的发现和下载

误区 3: "defer 和放底部效果一样"

  • 不一样。放底部时,脚本下载要等到 HTML 解析到那里才开始
  • defer 在 <head> 里可以更早开始下载

defer vs async vs 传统方式

                    下载时机        执行时机              执行顺序
传统 script         阻塞解析        下载完立即执行         按文档顺序
async              并行下载        下载完立即执行         不保证顺序
defer              并行下载        DOM 解析完成后        按文档顺序

代码示例

<!-- ✅ 推荐:defer 脚本放在 <head> -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>页面标题</title>
  <!-- 浏览器立即发现并开始下载,但不阻塞解析 -->
  <script defer src="vendor.js"></script>
  <script defer src="app.js"></script>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <!-- HTML 内容 -->
</body>
</html>

<!-- ❌ 过时做法:放在 body 底部 -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>页面标题</title>
</head>
<body>
  <!-- HTML 内容 -->

  <!-- 要等 HTML 解析到这里才开始下载 -->
  <script src="vendor.js"></script>
  <script src="app.js"></script>
</body>
</html>

验证下载时机的方法

打开 Chrome DevTools → Network 面板,观察脚本的下载开始时间:

  • <head> 里的 defer 脚本:在 HTML 下载初期就开始
  • <body> 底部的脚本:在 HTML 解析接近完成时才开始

面试技巧

可能的追问方向

  1. "defer 和 async 有什么区别?"

    • async 下载完立即执行,不保证顺序
    • defer 等 DOM 解析完才执行,保证顺序
  2. "多个 defer 脚本的执行顺序是怎样的?"

    • 按照在文档中出现的顺序执行
    • 即使后面的脚本先下载完,也会等前面的
  3. "defer 脚本和 DOMContentLoaded 的关系?"

    • defer 脚本在 DOM 解析完成后、DOMContentLoaded 触发前执行
  4. "什么情况下还是要放 body 底部?"

    • 需要兼容不支持 defer 的古老浏览器(IE9 以下)
    • 现代开发中基本不需要考虑

展示深度的回答方式

"defer 放 <head> 不仅没有性能问题,反而是更优的选择。因为浏览器的预加载扫描器(Preload Scanner)可以在解析 HTML 的早期就发现这些脚本并开始下载,充分利用网络带宽。而放在 <body> 底部的话,脚本的发现时机会延后,相当于浪费了并行下载的机会。"

一句话总结

defer 脚本放 <head> 是现代最佳实践:更早发现、并行下载、不阻塞解析、按序执行。

昨天以前首页

什么是"阻塞渲染"?如何避免 JavaScript 代码阻塞页面渲染?

作者 Smilezyl
2026年1月31日 15:24

什么是"阻塞渲染"?如何避免 JavaScript 代码阻塞页面渲染?

核心答案

阻塞渲染是指浏览器在解析 HTML 构建 DOM 树的过程中,遇到 <script> 标签时会暂停 DOM 解析,等待脚本下载并执行完毕后才继续解析。这是因为 JavaScript 可能会修改 DOM 结构(如 document.write),浏览器必须确保 DOM 的正确性。

避免阻塞的核心方法:

  1. 使用 async 属性:脚本异步下载,下载完立即执行
  2. 使用 defer 属性:脚本异步下载,DOM 解析完成后按顺序执行
  3. 将脚本放在 </body>
  4. 动态创建 script 标签

深入解析

浏览器渲染流程

HTML → DOM Tree
                  → Render Tree → Layout → Paint
CSS  → CSSOM

阻塞机制详解

1. JavaScript 阻塞 DOM 解析

HTML解析 → 遇到<script> → 暂停解析 → 下载JS → 执行JS → 继续解析

2. CSS 也会间接阻塞

  • CSS 本身不阻塞 DOM 解析,但阻塞渲染
  • 如果 JS 在 CSS 之后,JS 会等待 CSSOM 构建完成(因为 JS 可能访问样式)

async vs defer 的区别

特性 async defer
下载 异步,不阻塞解析 异步,不阻塞解析
执行时机 下载完立即执行 DOM 解析完成后执行
执行顺序 不保证顺序 保证顺序
适用场景 独立脚本(统计、广告) 有依赖关系的脚本

常见误区

  1. 误区:async 和 defer 可以同时使用

    • 实际:同时存在时,现代浏览器优先使用 async
  2. 误区:内联脚本可以使用 async/defer

    • 实际:async/defer 只对外部脚本有效
  3. 误区:放在 body 底部就不会阻塞

    • 实际:仍会阻塞,只是此时 DOM 已基本解析完成,影响较小

代码示例

<!-- 1. 阻塞渲染(默认行为) -->
<script src="app.js"></script>

<!-- 2. async:异步下载,下载完立即执行 -->
<script async src="analytics.js"></script>

<!-- 3. defer:异步下载,DOM 解析后按顺序执行 -->
<script defer src="vendor.js"></script>
<script defer src="app.js"></script>  <!-- 保证在 vendor.js 之后执行 -->

<!-- 4. 动态加载脚本 -->
<script>
  const script = document.createElement('script');
  script.src = 'lazy-module.js';
  script.async = false; // 保证顺序执行
  document.body.appendChild(script);
</script>

<!-- 5. 模块脚本(默认 defer 行为) -->
<script type="module" src="app.mjs"></script>

<!-- 6. 预加载关键资源 -->
<link rel="preload" href="critical.js" as="script">

现代优化方案

// 使用 Intersection Observer 懒加载脚本
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const script = document.createElement('script');
      script.src = entry.target.dataset.src;
      document.body.appendChild(script);
      observer.unobserve(entry.target);
    }
  });
});

// 使用 requestIdleCallback 在空闲时加载
requestIdleCallback(() => {
  const script = document.createElement('script');
  script.src = 'non-critical.js';
  document.body.appendChild(script);
});

面试技巧

可能的追问方向

  1. "async 和 defer 的执行时机具体是什么?"

    • async:下载完成后立即执行,可能在 DOMContentLoaded 之前或之后
    • defer:在 DOMContentLoaded 事件之前执行
  2. "CSS 会阻塞 JS 执行吗?"

    • 会。如果 <script><link> 之后,JS 会等待 CSSOM 构建完成
  3. "如何检测和量化阻塞时间?"

    • Performance API、Lighthouse、Chrome DevTools Performance 面板
  4. "type="module" 的脚本有什么特点?"

    • 默认 defer 行为、严格模式、独立作用域、支持 import/export

展示深度的回答技巧

  • 提及浏览器的预解析器(Preload Scanner)会提前扫描并下载资源
  • 讨论 Critical Rendering Path 优化策略
  • 结合实际项目经验,如 Webpack 的代码分割、动态 import

一句话总结

JS 阻塞 DOM 解析是因为可能修改 DOM;用 defer 保顺序、async 求速度、动态加载最灵活。

在 HTML 中引入 JavaScript 有哪几种方式?它们各自的优缺点是什么?

作者 Smilezyl
2026年1月31日 15:22

在 HTML 中引入 JavaScript 有哪几种方式?它们各自的优缺点是什么?

核心答案

在 HTML 中引入 JavaScript 有 3 种主要方式

方式 语法 主要场景
1. 行内式 <div onclick="alert('hi')"> 极少使用,不推荐
2. 内嵌式 <script>alert('hi')</script> 小型脚本、单页应用
3. 外链式 <script src="app.js"></script> 生产环境首选

核心原则:生产环境优先使用外链式,配合 deferasync 优化加载性能。


深入解析

1. 三种方式详解

方式一:行内式(Inline)
<!-- 直接在 HTML 属性中写 JS -->
<button onclick="alert('点击了!')">点击我</button>
<a href="javascript:void(0)" onmouseover="console.log('悬停')">链接</a>

优点:

  • 快速测试、简单直观

缺点:

  • ❌ HTML 和 JS 强耦合,难以维护
  • ❌ 无法复用逻辑
  • ❌ 代码混乱,可读性差
  • ❌ 存在 XSS 安全风险
  • ❌ 无法利用浏览器缓存

方式二:内嵌式(Internal / Embedded)
<!DOCTYPE html>
<html>
<head>
    <script>
        // JS 代码写在 <script> 标签内
        function init() {
            console.log('页面初始化');
        }
    </script>
</head>
<body>
    <h1>内嵌式示例</h1>
</body>
</html>

优点:

  • ✓ 适合单页应用或小型项目
  • ✓ HTML 和 JS 在同一文件,便于调试
  • ✓ 可以访问页面中的所有元素

缺点:

  • ❌ HTML 文件体积变大
  • ❌ 无法被浏览器缓存(每次加载 HTML 都要重新加载 JS)
  • ❌ 多个页面无法共享同一份 JS 代码
  • ❌ 不符合关注点分离原则

方式三:外链式(External)⭐ 推荐
<!-- 基础用法 -->
<script src="js/app.js"></script>

<!-- 推荐用法:配合 defer -->
<script src="js/app.js" defer></script>

<!-- 或者 async(取决于场景) -->
<script src="js/analytics.js" async></script>

优点:

  • HTML 与 JS 分离,结构清晰
  • 可复用:多个页面共享同一个 JS 文件
  • 可缓存:浏览器缓存 JS 文件,提升加载速度
  • 便于维护:代码独立管理
  • 支持模块化:方便团队协作

缺点:

  • ⚠️ 需要额外的 HTTP 请求(但可通过缓存和打包优化)

2. <script> 标签的关键属性

deferasync 的区别
页面解析流程对比:

无属性(默认):
HTML解析 → 遇到script → 停止解析 → 下载JS → 执行JS → 继续解析HTML
                ↑ 阻塞页面渲染 ↑

defer:
HTML解析 → 并行下载JSHTML解析完成 → 按顺序执行JSDOMContentLoaded
            ↓ 不阻塞解析 ↓

async:
HTML解析 → 并行下载JS → 下载完立即执行 → 继续解析HTML
            ↓ 执行时机不确定 ↓
属性 执行时机 顺序保证 适用场景
无属性 立即执行,阻塞解析 ✅ 按顺序
defer HTML 解析完成后,DOMContentLoaded ✅ 按顺序 DOM 操作脚本
async 下载完成后立即执行 ❌ 无顺序保证 独立脚本(如统计、广告)
<!-- defer 推荐用法 -->
<script src="main.js" defer></script>
<script src="utils.js" defer></script>
<!-- 保证:utils.js 一定在 main.js 之前执行 -->

<!-- async 用法 -->
<script src="analytics.js" async></script>
<script src="ads.js" async></script>
<!-- 不保证执行顺序,谁先下载完谁先执行 -->
其他重要属性
属性 作用 示例
type 指定脚本类型 type="module"(ES 模块)
crossorigin CORS 配置 crossorigin="anonymous"
integrity SRI(子资源完整性校验) integrity="sha384-..."
nomodule 不支持模块的浏览器才执行 <script nomodule src="legacy.js"></script>

3. 底层机制:浏览器如何加载和执行脚本

┌─────────────────────────────────────────────────────────┐
│                    浏览器渲染流程                         │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  1. HTML Parser ──→ 构建 DOM 树                          │
│         ↓                                                 │
│  2. CSS Parser ──→ 构建 CSSOM 树                         │
│         ↓                                                 │
│  3. 合并 ──→ 渲染树(Render Tree)                        │
│         ↓                                                 │
│  4. Layout(布局)                                        │
│         ↓                                                 │
│  5. Paint(绘制)                                         │
│                                                          │
└─────────────────────────────────────────────────────────┘

遇到 <script> 时:

默认行为:
┌─────────┐
│ 停止解析 │ ← 阻塞 DOM 构建
└────┬────┘
     ↓
┌─────────┐
│ 下载 JS │ ← 如果是外链脚本
└────┬────┘
     ↓
┌─────────┐
│ 执行 JS │ ← 阻塞渲染
└────┬────┘
     ↓
┌─────────┐
│ 继续解析 │
└─────────┘

使用 defer/async:
┌─────────┐      ┌─────────┐
 │继续解析 │ ←→   │并行下载 │  ← 不阻塞
└─────────┘      └─────────┘

4. 最佳实践

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>最佳实践示例</title>

    <!-- CSS 放在 head 中 -->
    <link rel="stylesheet" href="styles.css">

    <!-- 预加载关键脚本 -->
    <link rel="preload" href="critical.js" as="script">
</head>
<body>
    <!-- 页面内容 -->

    <!-- 方案1:现代浏览器推荐 -->
    <script src="main.js" defer></script>
    <script src="app.js" defer></script>

    <!-- 方案2:需要立即执行的脚本(如 polyfill) -->
    <script>
        // 同步执行的小型脚本
    </script>

    <!-- 方案3:独立第三方脚本 -->
    <script src="analytics.js" async></script>

    <!-- 方案4:ES 模块 -->
    <script type="module" src="module.js"></script>

    <!-- 方案5:模块降级方案 -->
    <script type="module" src="modern.js"></script>
    <script nomodule src="legacy.js"></script>
</body>
</html>

5. 常见误区

误区1deferasync 功能一样

纠正defer 保证顺序且在 DOMContentLoaded 前执行,async 不保证顺序

误区2:把所有 <script> 都放在 <head>

纠正:传统放 </body> 前,现代用 defer 可放 head

误区3defer 的脚本一定在 DOMContentLoaded 前执行

纠正:大部分情况是的,但如果脚本很大或网络慢,可能在之后

误区4:多个 async 脚本按书写顺序执行

纠正async 脚本按下载完成顺序执行,顺序不可控


代码示例

示例1:三种引入方式对比

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>JS 引入方式对比</title>

    <!-- 方式1:行内式(不推荐) -->
    <button onclick="handleClick()">行内式按钮</button>

    <!-- 方式2:内嵌式 -->
    <script>
        function handleClick() {
            console.log('内嵌式函数被调用');
        }

        // 内嵌式可以直接操作页面
        document.addEventListener('DOMContentLoaded', function() {
            console.log('DOM 加载完成');
        });
    </script>

    <!-- 方式3:外链式(推荐) -->
    <script src="js/utils.js" defer></script>
</head>
<body>
    <h1>三种引入方式</h1>

    <!-- 行内式的完整示例 -->
    <div onmouseover="this.style.background='yellow'"
         onmouseout="this.style.background='white'">
        鼠标悬停变色
    </div>
</body>
</html>

示例2:defer vs async 实际效果

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>defer vs async</title>
</head>
<body>
    <h1>页面标题</h1>
    <p>内容...</p>

    <script>
        // 同步脚本:阻塞后续渲染
        console.log('1. 同步脚本开始');
        // 模拟耗时操作
        const start = Date.now();
        while (Date.now() - start < 2000) {}
        console.log('2. 同步脚本结束(阻塞了2秒)');
    </script>

    <p>这行内容被延迟显示了</p>

    <!-- defer 脚本 -->
    <script src="defer1.js" defer></script>
    <script src="defer2.js" defer></script>
    <!-- 保证:defer1.js 在 defer2.js 之前执行 -->

    <!-- async 脚本 -->
    <script src="async1.js" async></script>
    <script src="async2.js" async></script>
    <!-- 不保证:谁先下载完谁先执行 -->

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            console.log('3. DOMContentLoaded 触发');
        });

        window.addEventListener('load', function() {
            console.log('4. 页面完全加载完成');
        });
    </script>
</body>
</html>

示例3:现代项目的标准引入方式

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>现代项目</title>

    <!-- 预连接到 CDN -->
    <link rel="preconnect" href="https://cdn.example.com">

    <!-- 预加载关键资源 -->
    <link rel="preload" href="critical.css" as="style">
    <link rel="preload" href="critical.js" as="script">

    <!-- 关键 CSS -->
    <link rel="stylesheet" href="critical.css">

    <!-- Polyfill:需要立即执行且不依赖 DOM -->
    <script>
        // 检测和添加必要的 polyfill
        if (!window.Promise) {
            document.write('<script src="polyfills/promise.js"><\/script>');
        }
    </script>
</head>
<body>
    <div id="app"></div>

    <!-- 主要应用脚本:使用 defer -->
    <script src="vendors.js" defer></script>
    <script src="main.js" defer></script>

    <!-- 第三方统计:使用 async -->
    <script src="analytics.js" async></script>

    <!-- ES 模块 + 降级方案 -->
    <script type="module" src="modern-app.js"></script>
    <script nomodule src="legacy-app.js"></script>
</body>
</html>

示例4:动态加载脚本

// 动态创建 script 标签
function loadScript(url, options = {}) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = url;

        // 设置属性
        if (options.async) script.async = true;
        if (options.defer) script.defer = true;
        if (options.type) script.type = options.type;

        // 事件监听
        script.onload = () => resolve(script);
        script.onerror = () => reject(new Error(`Failed to load ${url}`));

        document.head.appendChild(script);
    });
}

// 使用示例
async function initApp() {
    try {
        await loadScript('/utils.js', { defer: true });
        await loadScript('/main.js', { defer: true });
        console.log('所有脚本加载完成');
    } catch (error) {
        console.error('脚本加载失败:', error);
    }
}

// 条件加载
if ('IntersectionObserver' in window) {
    // 支持,加载现代版本
    loadScript('/modern-image-lazy-load.js');
} else {
    // 不支持,加载 polyfill
    loadScript('/polyfills/intersection-observer.js')
        .then(() => loadScript('/legacy-image-lazy-load.js'));
}

面试技巧

面试官可能的追问

  1. "为什么传统建议把 <script> 放在 </body> 之前?"

    • 避免阻塞页面渲染,让用户先看到内容
  2. "现在有了 defer,还需要放 </body> 前吗?"

    • 不需要,defer 可放 head,效果相同且更早开始下载
  3. "什么情况下用 async?"

    • 独立脚本:统计代码、广告脚本、不依赖其他代码的库
  4. "多个 defer 脚本的执行顺序?"

    • 按在 HTML 中的出现顺序执行
  5. "deferDOMContentLoaded 的关系?"

    • defer 脚本在 DOMContentLoaded 之前执行
  6. "什么是脚本阻塞(render blocking)?"

    • 解释浏览器解析 HTML 时遇到 <script> 停止渲染的机制

如何展示深度理解

  1. 谈性能优化

    • 关键渲染路径优化
    • 资源预加载(preload/prefetch)
    • 代码分割(code splitting)
  2. 谈实际项目经验

    • 如何处理第三方脚本(如 Google Analytics)
    • 如何优化首屏加载时间
    • 使用过哪些构建工具的优化
  3. 谈浏览器兼容性

    • defer/async 的浏览器支持情况
    • 如何为老浏览器做降级处理
  4. 谈安全

    • SRI(Subresource Integrity)
    • nonce/CSP(Content Security Policy)

一句话总结

外链式 + defer 是现代网页引入 JavaScript 的最佳实践,它实现了代码分离、可缓存、不阻塞渲染的完美平衡。

为何 Node.js 环境中没有 DOM 和 BOM?

作者 Smilezyl
2026年1月30日 09:32

为何 Node.js 环境中没有 DOM 和 BOM?

核心答案

因为 DOM 和 BOM 不是 JavaScript 语言本身的一部分,而是浏览器提供的宿主环境 API。

  • DOM (Document Object Model):浏览器解析 HTML 后生成的文档对象模型
  • BOM (Browser Object Model):浏览器窗口相关的对象(window、navigator、location 等)

Node.js 是服务端运行时,没有浏览器窗口,没有 HTML 文档,自然不需要也无法提供这些 API。Node.js 提供的是服务端所需的 API(文件系统、网络、进程等)。

深入解析

底层机制

1. ECMAScript vs 宿主环境

类别 来源 示例
ECMAScript 标准 JS 语言规范 Array, Promise, class, async/await
浏览器宿主 API W3C/WHATWG 规范 document, window, fetch, localStorage
Node.js 宿主 API Node.js 项目 fs, http, process, Buffer

2. 为什么这样设计?

浏览器的职责:                    Node.js 的职责:
├── 渲染网页                      ├── 执行服务端逻辑
├── 处理用户交互                   ├── 文件读写
├── 管理页面导航                   ├── 网络服务
└── 多媒体播放                     └── 系统调用

不同的运行环境有不同的需求,提供不同的 API 是合理的设计。

3. V8 引擎的角色

V8 只负责执行 JavaScript 代码,它本身不包含 DOM/BOM:

V8 引擎提供:
├── JS 代码解析和编译
├── 执行字节码
├── 垃圾回收
└── ECMAScript 标准内置对象

V8 不提供:
├── DOM 操作
├── 网络请求
├── 文件系统
└── 任何 I/O 操作

常见误区

  1. "JavaScript 天生就有 document 和 window"

    • 错误!这些是浏览器注入的全局对象
  2. "Node.js 是阉割版的 JavaScript"

    • 错误!Node.js 完整实现了 ECMAScript,只是宿主 API 不同
  3. "console.log 是 JavaScript 的一部分"

    • 严格来说不是!console 是宿主环境提供的,只是浏览器和 Node.js 都实现了它

代码示例

环境检测

// 检测当前运行环境
function detectEnvironment() {
    // 浏览器环境
    if (typeof window !== 'undefined' && typeof document !== 'undefined') {
        console.log('浏览器环境');
        console.log('window:', typeof window);      // object
        console.log('document:', typeof document);  // object
        console.log('navigator:', typeof navigator); // object
    }

    // Node.js 环境
    if (typeof process !== 'undefined' && process.versions?.node) {
        console.log('Node.js 环境');
        console.log('process:', typeof process);    // object
        console.log('__dirname:', typeof __dirname); // string (CommonJS)
        console.log('window:', typeof window);      // undefined
        console.log('document:', typeof document);  // undefined
    }
}

detectEnvironment();

跨环境兼容代码

// 同构/通用 JavaScript 代码示例
const isNode = typeof process !== 'undefined'
    && process.versions != null
    && process.versions.node != null;

const isBrowser = typeof window !== 'undefined'
    && typeof window.document !== 'undefined';

// 根据环境使用不同的 API
async function fetchData(url) {
    if (isBrowser) {
        // 浏览器使用 fetch API
        return fetch(url).then(res => res.json());
    } else if (isNode) {
        // Node.js 18+ 也有 fetch,或使用 http 模块
        const { default: fetch } = await import('node-fetch');
        return fetch(url).then(res => res.json());
    }
}

Node.js 中模拟 DOM(jsdom)

// 在 Node.js 中使用 jsdom 模拟浏览器环境
const { JSDOM } = require('jsdom');

const dom = new JSDOM(`
    <!DOCTYPE html>
    <html>
        <body>
            <div id="app">Hello</div>
        </body>
    </html>
`);

// 现在可以使用 DOM API 了
const document = dom.window.document;
const app = document.getElementById('app');
console.log(app.textContent); // "Hello"

app.textContent = 'Hello from Node.js!';
console.log(dom.serialize()); // 输出修改后的 HTML

全局对象对比

// 浏览器中的全局对象
// window === globalThis === self (在主线程中)

// Node.js 中的全局对象
// global === globalThis

// 通用写法(ES2020+)
console.log(globalThis); // 在任何环境都能获取全局对象

面试技巧

面试官可能的追问方向

  1. "那 Node.js 怎么做服务端渲染 (SSR)?"

    • 回答:使用 jsdom、happy-dom 等库模拟 DOM 环境,或使用 React/Vue 的服务端渲染 API(renderToString)直接生成 HTML 字符串,不需要真正的 DOM
  2. "fetch 是 JavaScript 的一部分吗?"

    • 回答:不是,fetch 是 WHATWG 规范定义的 Web API。Node.js 18+ 才原生支持,之前需要 node-fetch 等 polyfill
  3. "为什么 setTimeout 在 Node.js 中也能用?"

    • 回答:因为 Node.js 选择实现了这个 API 以保持兼容性,但实现机制不同(Node.js 用 libuv,浏览器用事件循环)
  4. "globalThis 是什么?"

    • 回答:ES2020 引入的标准,统一获取全局对象的方式,解决了 window/global/self 在不同环境不一致的问题

如何展示深度理解

  • 区分 ECMAScript 规范宿主环境 API
  • 了解 V8 引擎 的职责边界
  • 知道 jsdomhappy-dom 等工具的存在和用途
  • 理解 同构 JavaScript 的概念和挑战
  • 提及 DenoBun 等新运行时对 Web API 的支持程度不同

一句话总结

DOM/BOM 是浏览器的"特产",不是 JavaScript 的"标配"——JS 只是一门语言,能做什么取决于宿主环境给它什么 API。

向完全不懂编程的产品经理解释 JavaScript 是什么。你会怎么说?

作者 Smilezyl
2026年1月30日 09:31

想象你正站在一个完全不懂编程的 产品经理 面前,试图向他解释 JavaScript 是什么。你会怎么说?请尝试用通俗易懂的语言(比如打比方)向他解释 ECMAScript、DOM 和 BOM 的关系。

核心答案

我会这样向产品经理解释:

JavaScript 就像是一个"万能工人",它能让网页从"死"的变成"活"的。

打个比方:

  • ECMAScript 是工人的"技能手册"——规定了他会哪些基本动作(比如说话、走路、数数)
  • DOM 是工人和"页面内容"打交道的方式——让他能修改页面上的文字、图片、按钮
  • BOM 是工人和"浏览器环境"打交道的方式——让他能控制浏览器窗口、历史记录、弹出提示框

简单关系图:ECMAScript(核心能力) + DOM(操作页面) + BOM(操作浏览器) = 完整的 JavaScript


深入解析

1. 用更形象的比喻

想象你在装修一个房子

组成部分 比喻 实际作用
ECMAScript 装修队的施工规范 定义语法、变量、循环、函数等基本规则
DOM 和家具、墙壁打交道 操作页面元素:修改文字、样式、添加删除元素
BOM 和房子本身打交道 操作浏览器:控制窗口大小、跳转页面、本地存储

2. 技术层面的解释

ECMAScript
  • 是 JavaScript 的语言标准,由 ECMA 国际组织制定
  • 只规定语言的语法和核心功能
  • 不涉及任何浏览器或环境相关的内容
  • 最新版本:ES6/ES2015、ES2024 等
DOM(Document Object Model)
  • 把 HTML 文档解析成树形结构
  • 提供了一组 API 让 JavaScript 能操作页面
  • 是 W3C 标准,不只是 JavaScript 专用
BOM(Browser Object Model)
  • 提供与浏览器交互的接口
  • 没有统一标准,不同浏览器实现有差异
  • 主要对象:windowlocationnavigatorhistorylocalStorage

3. 常见误区

误区1:ECMAScript 和 JavaScript 是同一个东西

纠正:ECMAScript 是标准规范,JavaScript 是这个规范的实现(类似接口和实现类的关系)

误区2:DOM 是 JavaScript 的一部分

纠正:DOM 是独立的标准,其他语言(如 Python)也能操作 DOM

误区3:BOM 有统一标准

纠正:BOM 长期缺乏标准,各浏览器实现不同,HTML5 规范后才逐步统一


代码示例

// ========== ECMAScript:核心语法 ==========
// 这些语法在任何符合 ES 标准的环境中都能运行

// 1. 基础语法
const name = 'JavaScript';
let count = 0;

// 2. 函数
function greet(user) {
    return `Hello, ${user}`;
}

// 3. 循环
for (let i = 0; i < 5; i++) {
    count++;
}

// 4. 对象和数组
const user = {
    name: 'Alice',
    age: 25,
    skills: ['js', 'html']
};


// ========== DOM:操作页面内容 ==========
// 只有在浏览器环境才可用

// 1. 获取元素
const button = document.querySelector('#myButton');
const title = document.getElementById('title');

// 2. 修改内容
title.textContent = '新的标题';

// 3. 修改样式
button.style.backgroundColor = 'blue';

// 4. 事件监听
button.addEventListener('click', () => {
    alert('按钮被点击了!');
});

// 5. 动态创建元素
const newDiv = document.createElement('div');
newDiv.className = 'box';
document.body.appendChild(newDiv);


// ========== BOM:操作浏览器 ==========
// 不同浏览器可能有差异

// 1. window 对象(BOM 的核心)
console.log(window.innerWidth);   // 浏览器窗口宽度
window.scrollTo(0, 500);          // 滚动页面

// 2. location(页面跳转)
console.log(location.href);       // 当前 URL
location.reload();                // 刷新页面

// 3. history(历史记录)
history.back();                   // 返回上一页
history.go(2);                    // 前进两页

// 4. navigator(浏览器信息)
console.log(navigator.userAgent);  // 浏览器标识

// 5. localStorage(本地存储)
localStorage.setItem('key', 'value');
const data = localStorage.getItem('key');

// 6. 定时器(window 的方法)
setTimeout(() => {
    console.log('1秒后执行');
}, 1000);

面试技巧

面试官可能的追问

  1. "ES6 新增了哪些特性?"

    • 回答:let/const、箭头函数、解构赋值、Promise、class、模块化等
  2. "DOM 操作为什么慢?"

    • 回答:会触发重排和重绘,建议用文档片段或虚拟 DOM
  3. "BOM 中有哪些常用的 API?"

    • 回答:localStorage、sessionStorage、history、location、navigator
  4. "JavaScript 只能在浏览器运行吗?"

    • 回答:不是,Node.js 让 JS 能在服务器运行,但没有 DOM/BOM

如何展示深度理解

  1. 提到标准的演进:说明 DOM 从混乱到标准化的历史
  2. 谈性能问题:DOM 操作的性能影响,如何优化
  3. 跨平台思考:Node.js、React Native 如何复用 ECMAScript
  4. 实际经验:举例说明处理过浏览器兼容性问题

一句话总结

ECMAScript 是语言规则,DOM 是操作页面的手,BOM 是操作浏览器的手——三者共同构成了我们在网页开发中使用的 JavaScript。

❌
❌