← [ BACK_TO_NOTES ]

JavaScript DOM 操作

JavaScript 操作 DOM 的实践层:节点查询、创建、修改、事件挂接与动态脚本加载;DOM 模型本身由 canonical DOM 笔记维护。

#concept #growing #javascript #web #browser

[!info] related notes

JavaScript DOM 操作

本页聚焦“JavaScript 代码怎样操作 DOM”。DOM 的节点模型、树关系与平台级边界统一维护在 DOM(文档对象模型),这里不再重复把 DOM 当成本页别名。

边界:平台模型 vs JavaScript 使用层

它解决什么问题

  • 让 JavaScript 可以查询页面结构
  • 让 JavaScript 可以创建、插入、删除、替换节点
  • 让脚本可以读取和修改文本、属性、样式、类名
  • 让事件系统有明确的目标节点和传播路径

先抓住几个核心对象

  • document: 当前页面文档入口
  • Node: 所有 DOM 节点的共同基类
  • Element: 元素节点,最常见的 DOM 操作对象
  • Text: 文本节点
  • DocumentFragment: 批量拼装节点时常用的轻量容器

常见操作分组

查

  • getElementById()
  • querySelector() / querySelectorAll()
  • children / childNodes

改

  • textContent: 改纯文本,通常比 innerText 更直接
  • innerHTML: 改 HTML 片段,方便但要注意 XSS
  • setAttribute() / classList / style

增删

  • createElement()
  • append() / appendChild()
  • remove() / removeChild()
  • replaceWith()

动态加载 script 并在完成后执行回调

这是 DOM 操作里很常见的一道面试题,本质是:

  • 创建 script 元素
  • 设置 src
  • 挂载 onload / onerror
  • 再插入到文档里
function loadScript(src, callback) {
  const script = document.createElement('script')
  script.src = src

  script.onload = () => {
    callback(null)
  }

  script.onerror = () => {
    callback(new Error(`failed to load: ${src}`))
  }

  document.head.appendChild(script)
}

调用方式:

loadScript('/sdk.js', (error) => {
  if (error) {
    console.error(error)
    return
  }

  console.log('script loaded')
})

更现代的写法:Promise 包装

function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = src

    script.onload = () => resolve(script)
    script.onerror = () => reject(new Error(`failed to load: ${src}`))

    document.head.appendChild(script)
  })
}
loadScript('/sdk.js')
  .then(() => {
    console.log('loaded')
  })
  .catch((error) => {
    console.error(error)
  })

加上去重、记忆

const scriptCache = new Map<string, Promise<void>>();
const loadedSet = new Set<string>();

function loadScript(url: string): Promise<void> {
  if (loadedSet.has(url)) {
    return Promise.resolve();
  }

  const cached = scriptCache.get(url);
  if (cached) {
    return cached;
  }

  const p = new Promise<void>((resolve, reject) => {
    const existed = document.querySelector(`script[data-src="${url}"]`) as HTMLScriptElement | null;

    if (existed) {
      if ((existed as any).__loaded__) {
        loadedSet.add(url);
        resolve();
        return;
      }

      existed.addEventListener('load', () => {
        (existed as any).__loaded__ = true;
        loadedSet.add(url);
        resolve();
      }, { once: true });

      existed.addEventListener('error', () => {
        scriptCache.delete(url);
        reject(new Error(`Failed to load script: ${url}`));
      }, { once: true });

      return;
    }

    const script = document.createElement('script');
    script.src = url;
    script.async = true;
    script.setAttribute('data-src', url);

    script.onload = () => {
      (script as any).__loaded__ = true;
      loadedSet.add(url);
      resolve();
    };

    script.onerror = () => {
      scriptCache.delete(url);
      reject(new Error(`Failed to load script: ${url}`));
    };

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

  scriptCache.set(url, p);
  return p;
}

箭头函数和普通函数在这里怎么替换

如果这里只是把回调交给事件或 Promise,箭头函数和普通匿名函数在功能上是等价的。

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    const script = document.createElement('script')
    script.src = src

    script.onload = function () {
      resolve(script)
    }

    script.onerror = function () {
      reject(new Error(`failed to load: ${src}`))
    }

    document.head.appendChild(script)
  })
}

loadScript('/sdk.js')
  .then(function () {
    console.log('loaded')
  })
  .catch(function (error) {
    console.error(error)
  })

这里通常不需要 IIFE。

  • onload / onerror / .then() / .catch() 需要的是“先注册,等以后再调用”的回调函数
  • IIFE 是“定义完立刻执行”
  • 如果把这里写成立即执行,就会在绑定阶段直接运行,破坏异步时机

错误示意:

script.onload = (function () {
  resolve(script)
})()

上面会立刻执行 resolve(script),而不是等脚本真正加载完成。

面试里要顺手提到的边界

  • 要监听加载成功和失败
  • 如果脚本依赖执行顺序,要注意不要乱开 async
  • 如果脚本只该加载一次,要做重复加载保护
  • 如果是第三方 SDK,还要考虑超时、幂等和全局变量是否已注入

容易混淆的边界

学习检查

  1. 为什么 DOM 应由 DOM(文档对象模型) 作为 canonical 概念,而本页只保留 JavaScript 使用层?
  2. querySelectorAll() 返回的集合与 live HTMLCollection 在后续 DOM 变化时有什么行为差异?
  3. 动态加载第三方脚本时,为什么需要同时考虑成功、失败、重复加载和执行顺序?
  4. 为什么 React/Vue 这类框架减少直接 DOM 操作,却没有让 DOM 本身消失?
  5. 一个“点击事件为什么从子节点冒泡到父节点”的问题,应该在哪个知识节点继续追?

实践上的一句话

框架不会让 DOM 消失,只是把直接 DOM 操作收敛成更高层的数据更新与渲染过程。

创建于 2025/1/1 更新于 2026/10/1
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本