JavaScript DOM

浏览器中通过 JavaScript 读写文档树的核心接口。

#type / concept #status / growing #resource / javascript #resource / web #platform / browser

[!info] related notes

JavaScript DOM

DOM 是浏览器把 HTML 或 XML 文档表示成一棵可被脚本访问和修改的节点树之后,对外暴露出的编程接口。

它解决什么问题

  • 让 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,还要考虑超时、幂等和全局变量是否已注入

容易混淆的边界

实践上的一句话

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

创建于 2025/1/1 更新于 2026/5/27