JavaScript 应用示例

控制页面滚动

页面滚动分两种:窗口滚动,以及设了 overflow: scrollauto 的元素滚动。

JavaScript 里常用下面这些方法和属性。scroll()scrollTo()scrollBy() 既可以挂在 window 上,也可以挂在滚动元素上(Element.scrollTo 等)。

常用的滚动方法

scrollTo()

把滚动容器滚到指定坐标(相对该容器内容的左上角)。

window.scrollTo(x, y)
window.scrollTo(options)
element.scrollTo(x, y)
element.scrollTo(options)

scroll()

scrollTo() 相同,是同一个行为的别名。

window.scroll(x, y)
window.scroll(options)

scrollBy()

相对当前位置再滚一段距离。

window.scrollBy(x, y)
window.scrollBy(options)
  • xy:水平、竖直方向上的像素。对 scrollTo 是目标坐标,对 scrollBy 是增量。
  • optionsScrollToOptions
    • top:竖直方向的像素
    • left:水平方向的像素
    • behaviorsmooth 平滑滚;instant 立刻跳;auto(默认)跟随该容器 CSS 的 scroll-behavior,没有平滑声明时就是立刻跳。auto 并不等同于永远 instant

scrollIntoView()

让当前元素滚进其最近滚动祖先的可视区域。

element.scrollIntoView()
element.scrollIntoView(alignToTop)
element.scrollIntoView(scrollIntoViewOptions)

无参等同于 scrollIntoView(true)

  • alignToTop(布尔):true 时元素顶边对齐可视区域顶边;false 时元素底边对齐可视区域底边。
  • scrollIntoViewOptions
    • behaviorautosmooth,默认 auto
    • block:竖直对齐,startcenterendnearest,默认 start
    • inline:水平对齐,取值同上,默认 nearest

常用的滚动属性

scrollLeft

读取或设置元素滚动条到内容左边的偏移。

element.scrollLeft

注意: 内容方向为 rtl 时,有的引擎里滚动条在右侧、起点处 scrollLeft0,继续往内容末尾拖会变成负数。LTR 里设成小于 0 会被收成 0。实现并不完全统一,读写前要在目标引擎上确认。

其他边界:

  • 内容没有溢出、不能滚时,值为 0
  • LTR 下设成大于可滚最大值,会被收成最大值

窗口没有 scrollLeft。读窗口横向滚动用 window.scrollX(或 window.pageXOffset),也可以用 document.documentElement.scrollLeft

scrollTop

读取或设置内容在竖直方向上已经卷过的像素:内容顶部到可见区域顶部的距离。

element.scrollTop

设值规则与 scrollLeft 类似。窗口用 window.scrollY(或 window.pageYOffset),或 document.documentElement.scrollTop。怪异模式里有的页面还写在 document.body.scrollTop 上。

其他方法

  • scrollByLines() 按行数滚文档。非标准,目前主要是 Firefox。
  • scrollByPages() 按页数翻当前文档。非标准,目前主要是 Firefox。

注意问题

  • window.scrollTowindow.scrollwindow.scrollBy 很早就有。带 options 对象、以及元素上的 element.scrollTo / element.scrollBy,IE 都不支持。
  • 部分旧机(例如华为麦芒 4,Android 6.0.1)上 element.scrollTo() 无效,改设 element.scrollLeftelement.scrollTop
  • window 没有 scrollTopscrollLeft,用 document.documentElementwindow.scrollY / window.scrollX

蒙层底部页面跟随滚动

弹窗常用蒙层隔开页面交互。在蒙层里滑到尽头再继续滑,底层页面会跟着滚,这就是滚动穿透(scroll chaining)。

解决方案一

打开蒙层时给 body 加样式(有的机还要加在根元素上)。关掉时去掉这些样式。

body {
  height: 100%;
  overflow: hidden;
}

优点: 只改 CSS,没有脚本逻辑。

缺点: PC 上往往够用;部分 Android 和 Safari 仍挡不住底层滚动。

解决方案二

在蒙层上拦截触摸的默认滚动。

// node 为蒙层容器节点。必须 passive: false,否则 preventDefault 会被忽略
node.addEventListener("touchstart", (e) => {
  e.preventDefault()
}, { passive: false })

蒙层自己也不会滚。内容不需要内滚时,这样足够。蒙层内部还要滚动时,就要判断是否已经滚到尽头:到尽头再 preventDefault,否则放行。逻辑会明显变长。

监听若写在 documentwindow 上,touchstart / touchmove 默认是被动的,不写 { passive: false }preventDefault 无效。

解决方案三(锁住背景)

把页面用 position: fixed 钉在视口上,它就滚不了;关掉蒙层再松开,并回到原来的 scrollY。钉住之后内容会跳到视口顶部,所以要先记下滚动位置,写进 top

let top = 0
function stopBodyScroll(isFixed) {
  const bodyEl = document.body
  if (isFixed) {
    top = window.scrollY
    bodyEl.style.position = "fixed"
    bodyEl.style.top = -top + "px"
  } else {
    bodyEl.style.position = ""
    bodyEl.style.top = ""
    window.scrollTo(0, top)
  }
}

现行还可以给蒙层自己的滚动容器overscroll-behavior: contain(或 none):滚到尽头后不再把滚动链到背后的页面。新项目优先试这一条,再按机型补方案三。

iPhone 安全区域

全面屏(刘海、底部横条、Android 挖孔)会在可视窗口四周留出系统占用的安全区。顶部刘海一般由浏览器自己垫过;网页更常处理的是底部:吸底导航、回到顶部等 fixed 元素。

viewport-fit

viewportviewport-fit 控制网页如何铺进可视窗口(含圆角和安全区以外的区域):

  • contain:可视窗口完全包住网页内容(默认,与 auto 相同)
  • cover:网页内容铺满可视窗口,可以画进安全区
  • auto:与 contain 一致

env() 和 constant()

env() 读取环境变量。安全区四边距离:

  • safe-area-inset-left
  • safe-area-inset-right
  • safe-area-inset-top
  • safe-area-inset-bottom

viewport-fit 不是 cover 时,这四个值按 0 处理,写了 env() 也垫不出横条高度。浏览器不认识 env() 时会丢掉整条声明。

iOS 11 最初把这个函数做成 constant()。从 Safari Technology Preview 41 和 iOS 11.2 起改成 env()。要兼顾旧系统,两条都写,constantenv,顺序不能反。

padding-bottom: constant(safe-area-inset-bottom); /* iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom); /* iOS >= 11.2,以及现行引擎 */

如何适配

<meta name="viewport" content="width=device-width, viewport-fit=cover">
body {
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}

常见问题

  1. iPhone X 一类机型上,fixed 元素滑到页面底部时会往上挪一点。常见原因是页面高度不到一屏(或把撑高度的元素写成 fixed,脱离文档流)。给 body 或外层加 min-height: 100%,或用空元素、伪元素把文档撑高。
  2. height: 100% 不含底部安全区。若再用 padding-bottom: env(safe-area-inset-bottom) 去补横条高度,该元素需要 box-sizing: content-box,否则 padding 会吃进已经算死的 100% 高度里。

限制 input 输入

限制 input 只能进数字、中文、字母等。思路是:在输入过程中用正则丢掉非法字符,或在按键时取消默认插入。

三个键盘事件:keydownkeypresskeyup。粘贴走 paste 或随后的 input。下面内联写法能跑,但 keyCodereturnValueonafterpasteexecCommand 都已过时。新代码用 input 事件改 value,或直接用 typeinputmodepattern

只能输入或粘贴数字:

<input onkeyup="this.value = this.value.replace(/\D/g,'')" onafterpaste="this.value = this.value.replace(/\D/g,'')">

只能输入数字(按键阶段拦下,少一次先插入再删掉的闪动):

<input type="text" onkeypress="if (event.keyCode < 48 || event.keyCode > 57) event.returnValue = false">

keyCode 48-57 是主键盘数字。小键盘、中文输入法、粘贴绕得过这层,所以仍要配合 input 过滤。现行用 event.key(例如 "0""9"),并 event.preventDefault()

只能输入或粘贴数字和小数点:

<input onkeyup="if (isNaN(value)) execCommand('undo')" onafterpaste="if (isNaN(value)) execCommand('undo')">

document.execCommand('undo') 已废弃。同样情况改为在 input 里记下上次合法值,非法则回写。

只能输入或粘贴英文字母、数字、下划线、点、斜杠(\w 含下划线;正则里还放行了 ./),不能输入中文:

<input onkeyup="this.value = this.value.replace(/[^\w.\/]/g,'')" onafterpaste="this.value = this.value.replace(/[^\w.\/]/g,'')">

只能输入或粘贴基本汉字区的中文:

<input type="text" onkeyup="this.value = this.value.replace(/[^\u4e00-\u9fa5]/g,'')" onafterpaste="this.value = this.value.replace(/[^\u4e00-\u9fa5]/g,'')">

\u4e00-\u9fa5 盖不住所有汉字扩展区。

用 CSS 关输入法:

<input style="ime-mode: disabled">

ime-mode 是非标准属性。旧版 IE、部分 Firefox 认;Chrome 当无效。不要指望它在 WebKit 里禁中文。

点击的 300ms 延迟

移动端单击曾被故意推迟约 300ms:浏览器要等用户会不会再点一次,以区分单击和双击缩放(double tap to zoom)。

视口设成 width=device-width 之后,Chrome、Safari、Firefox 已经去掉这层等待。新项目不必默认引入 FastClick。

仍觉得点击发黏时,给可点元素关掉双击缩放手势即可,不必禁用整页缩放:

a,
button,
[role="button"] {
  -ms-touch-action: manipulation; /* IE10 */
  touch-action: manipulation; /* IE11 与现行引擎 */
}

下面这些属于旧方案,不要当首选:

<!-- 禁用缩放:损害无障碍,且不是现行去掉 300ms 的必要条件 -->
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">

<!-- Chrome 32 起:理想视口即可去掉延迟;不必把 maximum-scale 钉死 -->
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=3">

FastClick 在 touchend 时立刻派发模拟 click,并拦住大约 300ms 后浏览器自己的那次 click。只适合仍有延迟的老 WebView。部分机型上它会让 <input> 点不着;不要和 Zepto 的 tap(点击穿透)一起当通用方案。

文字无缝滚动

两份相同文案并排,容器持续 scrollLeft++;滚过第一份宽度时把 scrollLeft 往回减一份,看起来像无限循环。DEMO 里的类名如下。

<section class="box1">
    <div class="container1">
        <div class="item1 item11">【蒹葭】蒹葭苍苍,白露为霜。所谓伊人,在水一方,溯洄从之,道阻且长。溯游从之,宛在水中央。</div>
        <div class="item1 item12"></div>
    </div>
</section>
.box1 { overflow: hidden; border: 1px solid #0aa; }
.box1 .container1 { white-space: nowrap; }
.box1 .container1 .item1 { display: inline-block; color: #0aa; }
const boxDom01 = document.querySelector(".box1")
const itemDom01 = document.querySelector(".item11")
const itemDom02 = document.querySelector(".item12")
itemDom02.innerHTML = itemDom01.innerHTML

setInterval(() => {
    if (boxDom01.scrollLeft - itemDom02.offsetWidth >= 0) {
        boxDom01.scrollLeft -= itemDom01.offsetWidth
    } else {
        boxDom01.scrollLeft++
    }
}, 20)

文字无缝滚动 DEMO

防抖和节流

防抖: 事件触发后等 n 秒再执行;n 秒内又触发,则重新计时。适合输入联想、窗口 resize 结束再算布局。

function debounce(fn, delay) {
    var timer = null
    return function () {
        var context = this
        var args = arguments
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            fn.apply(context, args)
        }, delay)
    }
}

这是尾随防抖:停稳之后才跑。还有一种是先立刻执行,再在冷却期内忽略后续触发。

节流: 单位时间内最多执行一次。适合 scrollmousemove

function throttle(fn, delay) {
    var preTime = 0
    return function () {
        var context = this
        var args = arguments
        var nowTime = Date.now()
        if (nowTime - preTime >= delay) {
            preTime = nowTime
            return fn.apply(context, args)
        }
    }
}

preTime 初始为 0,第一次触发会立刻执行。若写成 Date.now(),创建后 delay 毫秒内的第一次触发会被丢掉。

滚动监听还可以改用 requestAnimationFrame 对齐帧,而不是死盯一个固定 delay

获取光标位置、聚焦、选择内容

获取 input 光标位置(插入点在值里的偏移):

function getCursor() {
    const input = document.getElementById("input")
    if (typeof input.selectionStart === "number") {
        return input.selectionStart
    }
    const range = document.selection.createRange()
    range.moveStart("character", -input.value.length)
    return range.text.length
}

selectionStart 是现行标准。document.selection 只存在于旧 IE。

选中 input 里一段文本:先聚焦,再 setSelectionRange(start, end)end 是结束位置的偏移(不含该下标之后的字符)。

function setSelection() {
    const input = document.getElementById("input")
    input.focus()
    input.setSelectionRange(1, 8)
}

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-26 20:53:49

results matching ""

    No results matching ""