1. 先搞懂 document 对象在浏览器里的地位
1.1 document 对象到底是什么
我相信很多前端初学者第一次看到 document 对象,是在 console 里敲了一句 document.title。后来慢慢知道 document 是 window 下的一个属性,代表整个 HTML 文档,不管是读取页面标题也好、手动调整某个表单值也好,最后都绕不开它。
按 W3C 和 HTML 标准的说法,document 对象是 Document 接口的实例,它和我们常说的 DOM 树是绑定关系:只要浏览器把 HTML 文本解析完,就会生成一棵 DOM 树,而 document 就是这棵树的入口。你可以把它理解成整棵树的根节点,树的任何一层节点都可以从 document 出发找到。
在我带新人的时候,经常让新人先跑一遍这行代码:
console.dir(document);dir 打印出来的是对象结构,直接在浏览器的开发者工具里展开,能看到一大堆属性,从 all、body、head 到 title、URL、readyState 都有。这个动作最大的价值是把“属性列表”变成“可以动手翻的树”,看完之后至少不会觉得 document 很神秘。
为什么我们特别关注 document 对象的属性?因为属性就是它对外暴露的数据和状态。有些属性是只读的,比如 document.URL 和 document.readyState,你改不动它们,它们的值由浏览器内部维护;有些属性是可写的,比如 document.title,你把它改了,浏览器标签页的标题立刻会变。搞清楚哪些能写、哪些只能读,是使用这些属性的第一步。
1.2 属性盘点:先搭好知识框架
document 对象的属性数量非常多,官方英文文档列出来的加上兼容性属性,粗算下来有三四十个。一次性全部背下来不现实,而且大部分场景你真正用到的就是其中十几个。
我把它们分成几类,在脑海里先有个抽屉:
- 节点与结构属性:document.documentElement、body、head、title、activeElement、all、children 等。
- 地址与状态属性:URL、domain、referrer、readyState、visibilityState、hidden、lastModified。
- 集合与内容属性:forms、images、links、anchors、scripts、styleSheets、embeds、plugins。
- 文本与配置属性:characterSet、charset、dir、designMode、compatMode、cookie。
- 历史与兼容性属性:doctype、implementation、location、defaultView、documentURI。
这套分类不是官方标准,是我自己在长期调试里为了方便记忆整理的。核心思路是:一类属性解决一类问题,比如要判断页面有没有加载完,用 readyState;要知道当前页面扎根在哪个域,用 domain;要拿到页面上所有表单做统一校验,用 forms。
实际项目里,很多人习惯了直接写 document.querySelector 和 document.getElementById,反而对 document 的对象属性不太敏感。但这些属性有自己的优势:性能好,比如 document.forms[0] 拿第一个表单不需要走完整选择器匹配;语义化强,比如 document.activeElement 直接告诉你当前焦点在哪,可读性比一堆 getElementById 拼接强得多。这篇博客我就带着大家逐个拆开讲。
注意,我后面提到的每个属性,都会顺带说一下兼容性和实际坑,新手直接照着用就行,老手也可以帮你们查漏补缺。
2. 跟节点和文档结构打交道的属性
2.1 三个高频根节点:documentElement、body、head
先看一组最常见的属性:
document.documentElement // 整个 <html> 元素 document.body // <body> 元素 document.head // <head> 元素documentElement 返回的是文档的根元素,对 HTML 文档来说就是 标签。它有什么用?最常见的场景就是读取视口相关的信息,比如要拿页面整体高度来做滚动相关逻辑,很多人会用 document.body.clientHeight,但某些文件类型或某些浏览器里 body 的高度可能是不完整的,更稳妥的是 document.documentElement.clientHeight 或 offsetHeight。我写页面滚动监听、懒加载判断时,都习惯优先用 documentElement。
body 和 head 就更直白了。document.body 常用于往页面尾部插入节点,比如动态加载脚本的时候,你创建一个 script 元素然后 document.body.appendChild(script),简单粗暴。document.head 适合在头部插入样式、meta 标签,比如动态设置视口、插入第三方统计脚本。
这里有一个坑想提醒大家:document.body 在文档解析完成之前可能是 null。比如你在 里直接放一段内联脚本,脚本执行到这个位置时 还没开始解析;这时候访问 document.body 得到的是 null。要安全使用,要么把脚本放到 前面,要么等 DOMContentLoaded 事件触发后再操作。我就因为在 head 里的一段代码中直接用了 document.body,结果线上环境偶发报错,排查了半天才发现是加载时机的锅。
2.2 顺着文档树继续:title、activeElement、all
document.title 是可写属性,改它可以直接改变浏览器标签页上显示的标题。这个属性在单页面应用里特别常用,比如路由切换后要把标题改成对应页面名称。一个常见的操作模式是:
document.title = "首页 - 我的网站";SPA 里的标题显示,底层的实现基本都是改 document.title。需要注意,搜索引擎对 document.title 抓取也有依赖,你在 SPA 里如果只靠它改标题,对 SEO 帮助有限,但这是另外的工程问题。
document.activeElement 返回当前获得焦点的元素,没有焦点的时候返回 body。在 PC 端可以用它判断用户当前正在哪个输入框里输入,在移动端可以监听键盘弹起相关时机。
const el = document.activeElement; if (el && el.tagName === 'INPUT') { // 当前焦点在输入框 }使用 activeElement 有一点要留神:有些浏览器在失焦后返回 body,有些返回 null,判断时最好兼容一下。而且它对 shadow DOM 里的焦点元素返回的是宿主元素,不是真正的内部元素,做复杂组件时要注意。
document.all 是历史遗留属性,早年 IE 用它返回页面所有元素集合,现在它其实是个伪数组,具有 HTMLAllCollection 接口。在标准浏览器里,typeof document.all 是 undefined 这个特例非常出名,曾经被很多库用来做 IE 检测。日常开发中不建议再依赖它,用 querySelectorAll 代替更靠谱。这个细节我放在后面兼容性章节再说。
3. 跟 URL、标题、加载状态有关的属性
3.1 URL、domain、referrer 到底存了什么东西
接下来这三个属性极容易混:URL、domain、referrer。
document.URL 返回当前文档的完整 URL 字符串,是只读的。注意,document.URL 是经过浏览器处理后的地址,和 location.href 基本一致,但 document.URL 不能用来做跳转。有人会问什么时候用 document.URL?比如要在页面里记录来源地址用于统计,读取它就行。
document.referrer 返回来源页面的 URL,也就是用户是从哪个页面跳过来的。如果直接输入网址或新开标签页进来,referrer 通常是空字符串。它常用于站内引导来源统计,或者判断是否从外部广告落地。这个属性是只读的,不过受浏览器安全策略影响,如果你的页面设置了 referrer policy 为 no-referrer,那么什么都拿不到。
document.domain 返回当前文档所属域名。它曾经被用来解决同源跨域通信,比如主域相同但子域不同的两个页面,把 domain 都设为同一个主域,就可以互相访问。现在主流浏览器已经收紧了这个能力,很多用法失效了,跨域的问题建议通过 postMessage 或服务端代理解决,而不是继续依赖修改 document.domain。
一个容易忽略的细节:document.URL 在设置 base 标签或者页面被 iframe 嵌入的时候,返回的仍然是当前文档实际的 URL,不会因为
3.2 readyState、characterSet 与 lastModified
document.readyState 用来表示文档加载状态,值有三种:
- loading:文档还在加载中。
- interactive:文档解析完成,但图片等资源可能还没加载完,此时 DOM 可访问。
- complete:文档和所有资源都已加载。
在实际开发里,很多人已经用了 DOMContentLoaded 事件,但通过轮询 readyState 来判断文档状态也是一种经典做法,在脚本动态注入、插件初始化的场景下很实用。
if (document.readyState === 'complete') { init(); } else { document.addEventListener('readystatechange', handleStateChange); }document.characterSet 返回文档使用的字符编码,比如 "UTF-8"。为什么提这个属性?因为如果你在做文本抓取、字节长度计算、乱码排查,判断页面编码非常关键。早年很多网站是 gb2312/gbk,如果不按对应编码解码,抓到的文本就是乱码。现在虽然绝大多数是 UTF-8,但接手老项目时还是建议先看一眼。
document.lastModified 返回文档最后修改时间字符串,格式是 "MM/DD/YYYY HH:mm:ss"。它依赖于服务器返回的 Last-Modified 响应头,如果服务器没给,那这个值就是当前时间或者不稳定值。它不适用来做精确的版本控制,但如果你想在页脚显示一个简单的“最后更新于 xxx”,用它是很省事的方案,比自己去写时间戳维护逻辑简单。
4. 集合属性:一张图抓出页面里的所有表单、图片和链接
4.1 forms、images、links、anchors 的定位差异
document.forms、document.images、document.links、document.anchors 这四个属性是 DOM 的即时集合(live collection),意思是它们不是静态快照,而是像“活引用”一样,DOM 变化后集合会跟着变化。
document.forms 返回页面上所有
const forms = document.forms; for (let i = 0; i < forms.length; i++) { const form = forms[i]; const formData = new FormData(form); }document.images 返回所有 元素。做图片懒加载、图片统计、图片替换时,从 document.images 出发很方便。但要注意:CSS 背景图不包含在 document.images 里,它只关心 img 标签。
document.links 和 document.anchors 非常容易弄混。document.links 返回所有带 href 属性的 和元素;document.anchors 返回所有带 name 属性的 元素,这是老式锚点语法,现在基本很少用了。所以实际工作里,links 的价值远大于 anchors。比如我要检查页面所有外链是否带 rel="noopener",遍历 document.links 就能做到。
集合属性虽好用,但它们是 live collection,这也意味着每次访问都会有浏览器内部查询。如果你在循环里反复读取 document.forms.length,性能会比缓存变量差。建议先 const list = document.forms,再基于 list 操作。另外,如果你用 for...in 遍历这些集合,可能会遍历到一些自定义属性或方法,更推荐用 for...of 或 Array.from 转成数组:
Array.from(document.images).forEach(img => { ... });4.2 scripts、styleSheets、embeds 与 plugins
document.scripts 返回所有