跳到主要内容

坐标与滚动定位

窗口与文档的宽高该量哪些属性?​

  • 可见区域 (内容区): document.documentElement.clientWidth/clientHeight, 已扣掉滚动条占用的宽度;
  • 快捷键: window.innerWidth/innerHeight 包含滚动条, 有滚动条时两者数值不同, 定位与绘制要用扣掉滚动条的那个;
window.innerWidth; // 完整窗口宽
document.documentElement.clientWidth; // 窗口宽减去滚动条
  • 无 <!DOCTYPE html> 时顶层几何属性行为会异常, 现代 HTML 必须写 DOCTYPE;
  • 整份文档高度 (含滚出部分): documentElement.scrollHeight 不可靠, Chrome/Safari/Opera 在无滚动时甚至小于 clientHeight, 要取多个属性的最大值;
const scrollHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight,
);
  • 元素自身的偏移/客户端/滚动尺寸: 见 样式与尺寸, 本节只补整页口径;

文档坐标与视口坐标如何换算?​

  • 两套坐标系: 视口坐标从窗口左上角算起 (对应 CSS position:fixed); 文档坐标从文档左上角算起 (对应锚定在文档上的 position:absolute);
  • 换算公式: pageY = clientY + 页面纵向已滚出高度, pageX 同理; 即文档坐标 = 视口坐标 + 当前滚动;
  • 元素视口坐标: elem.getBoundingClientRect(), 细节见 样式与尺寸; 注意 right/bottom 也是从左上角量起, 与 CSS 的 right/bottom 语义不同;
  • 派生属性: left = x、top = y、right = x + width、bottom = y + height; 之所以两组看似重复, 是因为 width/height 可为负 (表示从右下角往左上生长的有向矩形), 那时 left/top 不等于 x/y; 实践中 getBoundingClientRect() 的宽高恒为正;
  • 兼容: 老 IE 不支持 x/y, 直接用恒等的 left/top 即可;
  • 元素文档坐标: 没有标准方法, 自己用滚动量补齐;
// 文档坐标 = 视口坐标 + 当前滚动
function getCoords(elem) {
const box = elem.getBoundingClientRect();
return {
top: box.top + window.pageYOffset,
right: box.right + window.pageXOffset,
bottom: box.bottom + window.pageYOffset,
left: box.left + window.pageXOffset,
};
}
  • 定位选择: 想跟随窗口用 position:fixed + 视口坐标; 想跟着文档滚动用 position:absolute + 文档坐标;
// 提示框贴在元素下方
const message = document.createElement("div");
message.style.cssText = "position:absolute; color:red";
const coords = getCoords(elem);
message.style.left = coords.left + "px";
message.style.top = coords.bottom + "px";
  • 边界: getBoundingClientRect 可能返回小数与负数 (元素被滚到窗口上方时 top 为负), 不必取整;
  • offsetLeft/offsetTop: 相对 offsetParent (最近定位祖先), 不随页面滚动改变; 要视口坐标就用 getBoundingClientRect, 要文档坐标就再加滚动量;

如何读取与修改页面滚动位置?​

  • 读取: window.pageYOffset/pageXOffset, 只读, 是 window.scrollY/scrollX 的别名;
  • 元素级滚动状态: 元素的 scrollTop/scrollLeft, 页面对应 documentElement.scrollTop/scrollLeft (老 WebKit/Safari 要用 document.body), 用 pageYOffset 就不必记这些差异;
  • 修改: scrollBy 相对当前位置, scrollTo 到绝对坐标;
window.scrollBy(0, 10); // 从当前位置下移 10px
window.scrollTo(0, 0); // 回到页面开头
document.documentElement.scrollTop = 0; // 等价写法
  • 让元素可见: elem.scrollIntoView(top), top=true (默认) 与窗口顶部对齐, top=false 与窗口底部对齐;
  • 前提: 滚动前 DOM 必须已构建完, 放在 <head> 里的脚本滚不动页面;

如何禁止页面滚动, 滚动条消失怎么办?​

  • 做法: 设置 document.body.style.overflow = "hidden", 页面冻结在当前滚动位置; 同样可用于冻结任意元素;
  • 副作用: 滚动条消失, 原本被它占的宽度空出来, 内容会横向"跳"一下;
  • 补偿: 比较冻结前后的 clientWidth, 若变大说明滚动条消失了, 就给 document.body 补上等宽 padding, 保持内容宽度不变;
const before = document.documentElement.clientWidth;
document.body.style.overflow = "hidden";
const scrollbar = document.documentElement.clientWidth - before;
document.body.style.paddingRight = `${scrollbar}px`;

如何取某个视口坐标上的元素?​

  • document.elementFromPoint(x, y): 返回该视口坐标下最内层的元素, 坐标是视口坐标, 所以结果随滚动变化;
const elem = document.elementFromPoint(
document.documentElement.clientWidth / 2,
document.documentElement.clientHeight / 2,
);
elem.style.background = "red"; // elem 可能为 null!
  • 边界: 坐标越出可视区域 (为负或超过窗口宽高) 时返回 null, 直接访问属性会报错, 必须先判空;
  • 复数形式: document.elementsFromPoint(x, y) 返回该点的所有元素数组, 从最内层到最外层;