JavaScript DOM
浏览器中通过 JavaScript 读写文档树的核心接口。
#type / concept
#status / growing
#resource / javascript
#resource / web
#platform / browser
[!info] related notes
- 所属 MOC: javascript-in-browser-moc, javascript-moc
- 上位主题: javascript-in-browser
- 相邻主题: javascript-bom, javascript-events, window-object
- 渲染关联: browser-rendering-process, html-js-scripts
- 面试问法: 同花顺 AI 前端笔试+面试准备
JavaScript DOM
DOM 是浏览器把 HTML 或 XML 文档表示成一棵可被脚本访问和修改的节点树之后,对外暴露出的编程接口。
它解决什么问题
- 让 JavaScript 可以查询页面结构
- 让 JavaScript 可以创建、插入、删除、替换节点
- 让脚本可以读取和修改文本、属性、样式、类名
- 让事件系统有明确的目标节点和传播路径
先抓住几个核心对象
document: 当前页面文档入口Node: 所有 DOM 节点的共同基类Element: 元素节点,最常见的 DOM 操作对象Text: 文本节点DocumentFragment: 批量拼装节点时常用的轻量容器
常见操作分组
查
getElementById()querySelector()/querySelectorAll()children/childNodes
改
textContent: 改纯文本,通常比innerText更直接innerHTML: 改 HTML 片段,方便但要注意 XSSsetAttribute()/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 关注文档树;BOM 关注窗口、地址栏、历史记录等浏览器宿主对象,见 javascript-bom
- DOM 改动常常会影响布局与绘制,见 browser-rendering-process
- 事件绑定到 DOM 节点上之后,传播规则看 javascript-events
实践上的一句话
框架不会让 DOM 消失,只是把直接 DOM 操作收敛成更高层的数据更新与渲染过程。