JS BOM 浏览器对象:定时器与同步异步底层理解

写在前面

DOM 让我们操作页面元素,而 BOM(Browser Object Model,浏览器对象模型) 让我们操作浏览器本身:地址栏、历史记录、浏览器信息、弹窗,以及最重要的------定时器

BOM 的门面是顶级对象 window,它既是全局对象的宿主,也是浏览器各种能力的入口。但定时器真正的难点不在 API,而在它背后隐藏的 JS 运行机制:单线程、同步与异步、事件循环、宏任务与微任务 。不理解这套底层,"为什么 setTimeout(0) 也不是立刻执行""为什么 Promise 在 setTimeout 之前打印"这类问题就永远靠背。

本文分两层:

  1. BOM 对象层window / location / navigator / history / screen 及对话框;
  2. 运行机制层:同步与异步、调用栈、事件循环、宏任务与微任务的执行顺序------这是理解定时器与 Promise 的钥匙。

一、认识 BOM 与 window

1.1 BOM 是什么

浏览器把"浏览器自身"暴露为一组对象,总称 BOM。与 DOM 的关系:

模型 操作对象 入口
DOM 页面文档(元素、节点) document
BOM 浏览器(窗口、地址、历史、信息) window
js 复制代码
window.document === document;   // true:DOM 也挂在 window 下

1.2 window 是全局对象

在浏览器里,window 是全局对象:var 声明的全局变量和全局函数都会成为 window 的属性

js 复制代码
var g = 10;          // 会成为 window.g
function foo() {}    // 会成为 window.foo

window.g;            // 10
window.foo;          // function foo

let / const 声明的全局变量不会 挂在 window 上,但同样是全局作用域。

1.3 常用 window 方法与属性

js 复制代码
window.innerWidth;    // 视口宽度(不含滚动条)
window.innerHeight;   // 视口高度
window.scrollTo(0, 0);// 滚动到顶部
window.open(url);     // 新窗口打开
window.close();       // 关闭窗口

二、BOM 核心对象

2.1 location:地址栏对象

location 表示当前 URL,可读可改:

js 复制代码
location.href;        // 完整 URL,如 https://example.com/page?id=1#top
location.protocol;    // "https:"
location.host;        // "example.com"
location.pathname;    // "/page"
location.search;      // "?id=1"(查询参数)
location.hash;        // "#top"(锚点)

// 跳转的三种方式
location.href = "https://gitee.com";   // 跳转(会留下历史记录)
location.assign("https://gitee.com");  // 同上
location.replace("https://gitee.com"); // 跳转(替换当前记录,不能后退)

location.reload();    // 刷新页面
js 复制代码
navigator.userAgent;     // 浏览器 UA 字符串
navigator.platform;      // 操作系统平台
navigator.language;      // 语言,如 "zh-CN"
navigator.onLine;        // 是否在线

判断移动端的常见(粗略)写法:

js 复制代码
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);

UA 判断并不可靠(可伪造),生产环境更推荐用 CSS 媒体查询或 navigator.userAgentData(新 API)。

2.3 history:历史记录

js 复制代码
history.length;     // 历史记录条数
history.back();     // 后退
history.forward();  // 前进
history.go(-2);     // 后退 2 步
history.pushState(state, "", url);   // 无刷新改地址(SPA 路由核心)

SPA 前端路由就是靠 pushState / replaceState + popstate 事件实现的------在不刷新页面的情况下改变 URL。

2.4 screen:屏幕信息(了解)

js 复制代码
screen.width;       // 屏幕物理宽度
screen.height;      // 屏幕物理高度
screen.availWidth;  // 可用宽度(排除任务栏)

2.5 对话框:alert / confirm / prompt

js 复制代码
alert("提示");            // 警告框,只有确定
const ok = confirm("确定删除?");   // 确定/取消,返回布尔
const val = prompt("请输入", "默认值"); // 输入框,返回输入或 null

三个方法都会阻塞 JS 执行直到用户操作,正式产品里少用(尤其移动端体验差)。


三、定时器

3.1 setTimeout:延时执行一次

js 复制代码
const timerId = setTimeout(() => {
  console.log("1 秒后执行");
}, 1000);

// 取消
clearTimeout(timerId);
  • 返回一个定时器 id ,用于 clearTimeout 取消;
  • 第 3 个及以后参数会传给回调函数:
js 复制代码
setTimeout((a, b) => console.log(a + b), 500, 10, 20);   // 30

3.2 setInterval:间隔重复执行

js 复制代码
const timerId = setInterval(() => {
  console.log("每 1 秒执行一次");
}, 1000);

// 停止
clearInterval(timerId);

3.3 定时器回调里的 this

定时器回调中的 thiswindow(非严格模式),不指向调用它的对象 。用箭头函数捕获外层 this

js 复制代码
const obj = {
  count: 0,
  start() {
    // ❌ 普通函数:this 是 window
    // setInterval(function () { this.count++; }, 1000);

    // ✅ 箭头函数:this 来自外层 start() 的 obj
    setInterval(() => {
      this.count++;
      console.log(this.count);
    }, 1000);
  },
};

3.4 递归 setTimeout 与 setInterval 的区别

定时器回调的执行时机不保证精确:事件循环繁忙时会被延后。累积误差是经典问题:

js 复制代码
// setInterval:即使回调执行很久,仍然按固定间隔排队(可能堆积)
setInterval(() => { /* 耗时的任务 */ }, 1000);

// 递归 setTimeout:等上一次回调执行完,再等 1000ms
function tick() {
  // 执行任务...
  setTimeout(tick, 1000);   // 保证"上次完成后再等 1000ms"
}
tick();

递归 setTimeout 更可控 :不会像 setInterval 那样在回调未完成时排队堆积。需要精确节奏时优先考虑。

定时器回调是异步宏任务,只在当前同步代码执行完后才轮到它------这是理解下一节的关键。


四、同步与异步底层

4.1 JS 是单线程的

JS 引擎一次只能执行一段代码(一个调用栈),没有真正的多线程并发。为什么?

  • 因为 JS 需要操作 DOM,多线程同时改 DOM 会导致状态错乱;
  • 浏览器用"单线程 + 事件循环"解决:耗时的任务(网络、定时器、文件)交给浏览器其他线程,完成后把回调放进队列,等主线程空闲再执行。

4.2 同步与异步的执行顺序

  • 同步代码:按书写顺序立即执行;
  • 异步代码 :注册后不立即执行,等条件满足后进入任务队列,同步代码全部执行完才轮到它们
js 复制代码
console.log("1");                    // 同步
setTimeout(() => console.log("2"), 0); // 异步(即使 0ms 也最后)
console.log("3");                    // 同步
// 输出顺序:1 → 3 → 2

setTimeout(..., 0) 也不是立刻执行:它只是把回调放到队列尾部,至少等当前同步代码跑完。

4.3 事件循环(Event Loop)

浏览器运行时持续执行一个循环:

erlang 复制代码
① 执行调用栈里的所有同步代码(宏任务)
② 执行所有微任务队列(清空)
③ 从宏任务队列取出一个任务执行
④ 回到 ②(执行新产生的微任务)
   ...

核心:调用栈空 → 先清空微任务 → 再取一个宏任务。微任务永远在下一个宏任务之前清空。

4.4 宏任务与微任务

类别 常见来源 特点
宏任务(Macro-task) setTimeoutsetInterval、I/O、事件回调 每次事件循环只取一个
微任务(Micro-task) Promise.thenqueueMicrotaskMutationObserver 每次执行完同步/宏任务后全部清空

执行顺序口诀:同步 → 微任务 → 宏任务(→ 新微任务 → 新宏任务...)

经典输出顺序题:

js 复制代码
console.log("A");                 // 同步

setTimeout(() => console.log("B"), 0);   // 宏任务

Promise.resolve().then(() => console.log("C"));  // 微任务

console.log("D");                 // 同步

// 输出:A → D → C → B

分析:

  1. 同步 AD 先打印;
  2. 同步执行完,清空微任务 → 打印 C
  3. 再从宏任务队列取 setTimeout → 打印 B

微任务(Promise)一定先于宏任务(setTimeout)执行------即使 setTimeout 的延迟是 0。

4.5 为什么 setTimeout 不阻塞 UI

因为定时器回调被丢进宏任务队列,主线程继续渲染;用户能立刻看到界面响应。反之,如果用一个 while 死循环占住主线程,页面会卡死------UI 渲染也是任务,永远轮不到它:

js 复制代码
// ⚠️ 会卡死页面:主线程被占死
while (true) { /* 死循环 */ }

五、实战案例

实战 1:倒计时(setInterval + clearInterval)

html 复制代码
<button id="btn">开始倒计时 10 秒</button>
<span id="display">10</span>
js 复制代码
let timer = null;

document.getElementById("btn").addEventListener("click", () => {
  let count = 10;
  const display = document.getElementById("display");

  if (timer) clearInterval(timer);          // 防止重复启动
  display.textContent = count;

  timer = setInterval(() => {
    count--;
    display.textContent = count;
    if (count <= 0) {
      clearInterval(timer);                 // 到 0 停止
      timer = null;
    }
  }, 1000);
});

实战 2:轮播图自动播放(setInterval)

js 复制代码
let current = 0;
const slides = document.querySelectorAll(".slide");

setInterval(() => {
  slides[current].classList.remove("active");
  current = (current + 1) % slides.length;   // 循环索引
  slides[current].classList.add("active");
}, 3000);

实战 3:防抖与节流(定时器两大应用)

防抖(Debounce):停止触发 N 毫秒后才执行------适合搜索框。

js 复制代码
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
const search = debounce((val) => console.log("搜索:" + val), 300);

节流(Throttle):N 毫秒内最多执行一次------适合滚动、拖拽。

js 复制代码
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}
const onScroll = throttle(() => console.log("滚动处理"), 200);

实战 4:用定时器封装 Promise 延时

把"延时"变成可 await 的 Promise,配合 async/await 让代码线性化:

js 复制代码
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run() {
  console.log("开始");
  await sleep(1000);
  console.log("1 秒后");
  await sleep(500);
  console.log("再 0.5 秒后");
}
run();

实战 5:事件循环输出顺序综合题

js 复制代码
console.log("1");                                  // 同步

setTimeout(() => console.log("2"), 0);              // 宏任务

new Promise((resolve) => {
  console.log("3");                                 // Promise 执行器是同步的
  resolve();
}).then(() => console.log("4"));                   // 微任务

async function fn() {
  console.log("5");                                 // 同步
  await Promise.resolve();
  console.log("6");                                 // await 之后是微任务
}
fn();

console.log("7");                                   // 同步

// 输出:1 → 3 → 5 → 7 → 4 → 6 → 2

分析要点:

  • new Promise 的执行器函数立即同步执行 (所以 3 先于 5?注意顺序:1 → 执行器 3 → 注册 then 微任务 → 调用 fn() → 打印 5await 挂起 → 返回 → 同步 7 → 微任务 4 → 微任务 6 → 宏任务 2);
  • await 之后的代码属于微任务;
  • 宏任务 setTimeout 永远在微任务之后。

六、高频误区清单

误区 正解
setTimeout(0) 会立刻执行 至少等当前同步代码完成,是异步宏任务
定时器回调里 this 指向原对象 普通函数下是 window,用箭头函数捕获外层 this
setInterval 时间精确 不精确;回调耗时可能堆积,用递归 setTimeout
Promise 回调立即执行 执行器同步执行,但 .then 回调是微任务
微任务在宏任务之后 每次同步/宏任务后先清空全部微任务,再取宏任务
死循环只是"慢" 会永久卡死页面(UI 渲染排不上队)
location.href = urlreplace 等价 replace 不产生历史记录,不能后退
prompt 友好无副作用 阻塞 JS 执行,正式产品慎用

七、总结

用一张表收束本篇核心:

主题 一句话记住
BOM window 是全局宿主,location/navigator/history 都是它的属性
跳转 href/assign 留历史,replace 不留历史
定时器 setTimeout 一次、setInterval 重复,都能 clear
this 定时器回调用箭头函数保 this
单线程 JS 一次执行一段代码,耗时任务交给浏览器线程
事件循环 调用栈空 → 清空微任务 → 取一个宏任务
宏/微 setTimeout 是宏任务,Promise.then 是微任务,微任务先执行

三条实践心法收尾:

  1. 写定时器必配清理setInterval / setTimeout 存 id,页面卸载或任务结束 clear,防内存泄漏与重复触发;
  2. 理解顺序再写异步:背下"同步 → 微任务 → 宏任务",面试题和线上 bug 排查都好用;
  3. 交互密集用防抖节流:搜索防抖、滚动节流,定时器是它们的底层。
相关推荐
梨想橙汁40 分钟前
ES6+ 必用新特性:箭头函数、解构、模板字符串、扩展运算符
前端·javascript
梨想橙汁41 分钟前
DOM 与 JS 事件:页面元素操作、事件冒泡、事件委托实战
前端·javascript
Dreams°1231 小时前
【React+TS+ECharts可视化避坑:图表空白、不实时更新、数据池堆积卡顿完整复盘(附完整源码)】
前端·react.js·echarts
计算机魔术师1 小时前
三年前估值45亿,现在129亿——Hugging Face凭什么被老黄盯上?
前端
一个游离的指针1 小时前
浏览器的渲染原理
前端·javascript
zttbee1 小时前
vue2的API大白话讲解
前端·vue.js·前端框架
几何心凉1 小时前
ToDesk/向日葵/UU远程,谁才是真正的“口袋里的办公室”
前端
亿元程序员1 小时前
一个人,4个岗位,20天:我用Cursor+Codex上线了一款微信小游戏
前端
LabVIEW开发2 小时前
将自定义 DLL 与 INI 文件部署到 LabVIEW 实时目标
前端·labview·labview知识·labview功能·labview程序