← Frontend

浏览器相关 & 函数式编程

浏览器 DOM/BOM、事件模型、ajax、跨域、RESTful、渲染流程、性能优化、函数式编程原理

目录

模块 章节 核心内容
Part I 浏览器相关 浏览器处理模型、对象模型、事件模型、防抖与节流
Part II 函数式编程 函数式原理、流水线组装
Part III 网络层 ajax、跨域、RESTful、状态码
Part IV 浏览器底层原理 DOM/CSSOM、渲染流程、性能优化

Part I. 浏览器相关

一、前端主要舞台 —— 浏览器

1. 浏览器处理模型:DOM、BOM、BEM

浏览器是前端代码运行的宿主环境,理解它的处理模型是入门基础。

模型 全称 作用
DOM Document Object Model 文档对象模型,操作 HTML 结构
BOM Browser Object Model 浏览器对象模型,操作浏览器窗口
BEM Block Element Modifier CSS 命名规范(非浏览器模型,但常一并讨论)
// DOM 操作:修改文档结构
document.querySelector('#app').innerHTML = 'Hello'

// BOM 操作:控制浏览器行为
window.location.href = 'https://example.com' // 跳转页面
window.history.back()                         // 后退

// BEM 命名:CSS 类名规范
// .block__element--modifier
// .card__title--large

2. 从一道经典面试题开始

💡 经典面试题: 从输入 URL 到页面展示,中间发生了什么?

这道题串起了几乎所有前端知识点:

  • DNS 解析 → TCP 连接 → HTTP 请求
  • 服务器响应 → 浏览器解析 → 渲染页面
  • JS 执行 → 事件绑定 → 交互响应

二、对象模型和事件模型

3. 对象模型

3.1 全局管家,主入口 —— window

window 是浏览器中所有对象的根,全局变量和函数都挂载在它身上。

// 全局变量本质上是 window 的属性
var name = 'Shea'
console.log(window.name) // 'Shea'

// 常用 window API
window.alert('提示')
window.setTimeout(() => {}, 1000)
window.innerWidth  // 视口宽度

⚠️ 注意: let 和 const 声明的变量不会挂载到 window 上。

3.2 专业导航,确保不迷路 —— location & history

// location:当前页面的 URL 信息
location.href      // 完整 URL
location.protocol  // 协议(http:/https:)
location.host      // 主机名 + 端口
location.pathname  // 路径
location.search    // 查询字符串(?key=value)
location.hash      // 锚点(#section)

// history:浏览历史栈
history.back()              // 后退
history.forward()           // 前进
history.go(-2)              // 跳转到前 2 页
history.pushState({}, '', '/new-path')   // 添加历史记录(SPA 路由核心)
history.replaceState({}, '', '/new-path') // 替换当前记录

3.3 更多信息的备忘录 —— navigation、screen……

// navigator:浏览器和系统信息
navigator.userAgent     // 用户代理字符串
navigator.language      // 浏览器语言
navigator.onLine        // 是否在线

// screen:屏幕信息
screen.width            // 屏幕宽度
screen.height           // 屏幕高度
screen.availWidth       // 可用宽度(去除任务栏)

💡 面试中常见的对象 API 相关问题: 考察对 BOM 的整体认知,而不只是死记 API。

4. 事件模型

4.1 常规流:捕获 => 目标 => 冒泡 vs 人工阻断

事件传播三阶段:

阶段 方向 说明
捕获阶段 window → target 从外向内传递
目标阶段 target 到达目标元素
冒泡阶段 target → window 从内向外冒泡
// addEventListener 第三个参数控制捕获/冒泡
element.addEventListener('click', handler, true)  // 捕获阶段触发
element.addEventListener('click', handler, false) // 冒泡阶段触发(默认)

// 人工阻断
function handler(e) {
  e.stopPropagation()        // 阻止冒泡/捕获继续传播
  e.stopImmediatePropagation() // 阻止同元素其他监听器
  e.preventDefault()         // 阻止默认行为(如表单提交)
}

4.2 事件绑定

① 原理

事件绑定本质是:把回调函数登记到 DOM 节点的事件监听列表中,当事件触发时按顺序执行。

// 三种绑定方式对比
// 方式 1:HTML 内联(不推荐)
// <button onclick="handleClick()">

// 方式 2:DOM 属性赋值(同一事件只能绑一个)
btn.onclick = function() { console.log('clicked') }

// 方式 3:addEventListener(推荐,可绑多个)
btn.addEventListener('click', handler1)
btn.addEventListener('click', handler2)
② 面试核心

兼容相关 —— 事件绑定的浏览器兼容

// 现代 vs 老 IE 兼容写法
function addEvent(el, type, handler) {
  if (el.addEventListener) {
    // 标准浏览器
    el.addEventListener(type, handler, false)
  } else if (el.attachEvent) {
    // 老 IE(IE8 及以下),事件名要加 'on'
    el.attachEvent('on' + type, handler)
  } else {
    // 最兜底
    el['on' + type] = handler
  }
}

性能相关 —— 多子节点的事件绑定优化

💡 核心思路: 事件委托(Event Delegation)—— 利用冒泡,把子节点的监听器统一挂在父节点上。

// ❌ 反例:每个 li 都绑一个事件,性能差
document.querySelectorAll('li').forEach(li => {
  li.addEventListener('click', () => console.log(li.textContent))
})

// ✅ 正例:事件委托,只绑一次
document.querySelector('ul').addEventListener('click', e => {
  // 通过 e.target 判断真正点击的元素
  if (e.target.tagName === 'LI') {
    console.log(e.target.textContent)
  }
})

🚨 委托的好处: ① 减少内存占用 ② 动态添加的子节点也能响应事件


5. 防抖(debounce)与节流(throttle)

两者都是限制函数执行频率的优化手段,专门应对 scroll、resize、input 等高频触发事件。

防抖(debounce)

核心思想: 事件持续触发时不执行,停止触发后等待 n 毫秒才执行一次。

电梯类比: 只要有人按按钮,就重新倒计时,直到无人再按才关门启动。

function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);          // 每次触发都重置计时器
    timer = setTimeout(() => {
      fn.apply(this, args);       // delay 后无新触发,才真正执行
    }, delay);
  };
}

// 使用:搜索框输入联想(用户停止输入 500ms 后才发请求)
const handleSearch = debounce((keyword) => {
  fetch(`/api/search?q=${keyword}`);
}, 500);

input.addEventListener('input', e => handleSearch(e.target.value));

节流(throttle)

核心思想: 持续触发时,每隔 n 毫秒只执行一次,保证规律性执行。

水龙头类比: 控制流量,每隔固定时间放一次水,不管旋钮拧多猛。

function throttle(fn, interval) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {  // 距上次执行超过间隔才执行
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// 使用:页面滚动加载更多(每 200ms 最多检查一次)
const handleScroll = throttle(() => {
  const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
  if (scrollTop + clientHeight >= scrollHeight - 100) {
    loadMore(); // 触底加载
  }
}, 200);

window.addEventListener('scroll', handleScroll);

立即执行版防抖(leading debounce)

有时需要第一次触发立即执行,冷却期内的高频触发被忽略:

function debounce(fn, delay, immediate = false) {
  let timer = null;
  return function (...args) {
    if (immediate && !timer) {
      fn.apply(this, args); // 第一次立即执行
    }
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;         // 冷却结束,重置标志
      if (!immediate) fn.apply(this, args);
    }, delay);
  };
}

对比总结

特性 防抖(debounce) 节流(throttle)
执行时机 停止触发后才执行(trailing) 每隔固定时间执行一次
高频中是否执行 ❌ 不执行,一直重置 ✅ 按频率执行
典型场景 搜索联想、表单验证、resize 结束后计算布局 滚动加载、mousemove、游戏帧更新
核心操作 清除并重新计时 比较时间差

Part II. 函数式编程

一、函数式编程的出现

发展历程

阶段 范式 核心思想
1 命令(脚本)式编程 一步步告诉计算机怎么做
2 面向对象编程 用对象封装数据和行为
3 函数式编程 用函数作为主角,组合而非继承
// 1. 命令式:关注"怎么做"
const nums = [1, 2, 3, 4, 5]
const doubled1 = []
for (let i = 0; i < nums.length; i++) {
  doubled1.push(nums[i] * 2)
}

// 2. 面向对象:用对象组织
class Doubler {
  constructor(arr) { this.arr = arr }
  run() { return this.arr.map(n => n * 2) }
}

// 3. 函数式:关注"做什么",声明意图
const double = n => n * 2
const doubled3 = nums.map(double)

💡 函数式的主角: 函数(Function)—— 一等公民,可传递、可返回、可组合。


二、函数式编程原理特点

1. 函数式原理

核心原则:

  • 纯函数: 相同输入永远得到相同输出,且无副作用
  • 不可变性: 不修改原数据,而是返回新数据
  • 函数组合: 小函数拼装成大功能
// ❌ 非纯函数:依赖外部变量,有副作用
let count = 0
function impureAdd(n) {
  count += n      // 修改了外部状态
  return count
}

// ✅ 纯函数:输入决定输出,无副作用
function pureAdd(a, b) {
  return a + b
}

// ✅ 不可变性:不改原数组
const arr = [1, 2, 3]
const newArr = [...arr, 4]  // 创建新数组,而非 arr.push(4)

2. 理论思想 —— 一个中心(函数)两个基本点(函数功能 + 流程串联)

💡 一句话概括: 用函数封装功能,用组合串联流程。

// 一个中心:函数
const trim = str => str.trim()
const toLower = str => str.toLowerCase()
const replaceSpace = str => str.replace(/\s+/g, '-')

// 两个基本点:
// ① 函数功能(单一职责)
// ② 流程串联(组合)
const slugify = str => replaceSpace(toLower(trim(str)))

console.log(slugify('  Hello World  ')) // 'hello-world'

3. 函数式编程的主角 —— 惰性执行

惰性执行(Lazy Evaluation):不立即计算结果,而是在真正需要时才执行。

// 惰性执行示例:链式调用直到 .value() 才真正计算
function lazy(value) {
  const operations = []
  return {
    map(fn) {
      operations.push(fn)  // 只是记录操作,不执行
      return this
    },
    value() {
      // 真正用到结果时才依次执行
      return operations.reduce((acc, fn) => fn(acc), value)
    }
  }
}

const result = lazy(5)
  .map(x => x * 2)   // 不执行
  .map(x => x + 1)   // 不执行
  .value()            // 此时才执行:5 -> 10 -> 11

console.log(result) // 11

⚠️ 注意: 惰性执行能避免不必要的计算,是函数式性能优化的重要手段。


三、流水线的组装

1. 流水线式编程

流水线编程:把数据像传送带上的产品一样,经过一道道函数加工,最后得到成品。

// 数组方法天然就是流水线
const result = [1, 2, 3, 4, 5]
  .filter(n => n % 2 === 0)   // 过滤偶数:[2, 4]
  .map(n => n * n)             // 平方:[4, 16]
  .reduce((sum, n) => sum + n, 0) // 求和:20

console.log(result) // 20

2. 加工与变化

a. 过程加工

每一步函数都是一道工序,对数据进行转换、过滤、聚合。

// 通用流水线工具:pipe
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)

const addOne = x => x + 1
const double = x => x * 2
const square = x => x * x

// 组装流水线:先 +1,再 ×2,再平方
const process = pipe(addOne, double, square)
console.log(process(3)) // ((3+1)*2)^2 = 64

b. 函子(Functor)

函子是函数式编程的高阶概念:一个包含值的容器,且能用 map 对值进行操作而不离开容器。

// 简单 Functor 实现
class Container {
  constructor(value) {
    this._value = value
  }
  // 静态方法,方便构造
  static of(value) {
    return new Container(value)
  }
  // map:在容器内对值进行操作,返回新容器
  map(fn) {
    return Container.of(fn(this._value))
  }
}

const result = Container.of(5)
  .map(x => x + 1)     // Container(6)
  .map(x => x * 2)     // Container(12)

console.log(result)  // Container { _value: 12 }

💡 提示: Array、Promise 本质都是函子 —— 它们都有 map / then 方法,把值包在容器里安全处理。


Part III. 网络层

5. ajax 的原理与手写 ajax 请求

AJAX(Asynchronous JavaScript And XML):浏览器在不刷新页面的前提下,与服务器进行数据交换。

核心 API: XMLHttpRequest(老)/ fetch(新)

手写一个 ajax 请求(基于 XHR)

function ajax({ url, method = 'GET', data = null, headers = {} }) {
  return new Promise((resolve, reject) => {
    // 1. 创建 XHR 实例
    const xhr = new XMLHttpRequest()

    // 2. 初始化请求(第三个参数 true 表示异步)
    xhr.open(method, url, true)

    // 3. 设置请求头
    Object.keys(headers).forEach(key => {
      xhr.setRequestHeader(key, headers[key])
    })

    // 4. 监听状态变化
    xhr.onreadystatechange = function() {
      // readyState === 4 表示请求完成
      if (xhr.readyState !== 4) return
      // 状态码 2xx 或 304 表示成功
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        resolve(JSON.parse(xhr.responseText))
      } else {
        reject(new Error(`请求失败:${xhr.status}`))
      }
    }

    // 5. 发送请求
    xhr.send(data ? JSON.stringify(data) : null)
  })
}

// 使用
ajax({ url: '/api/user', method: 'GET' })
  .then(res => console.log(res))
  .catch(err => console.error(err))

XHR 的 readyState 五个阶段

值 状态 含义
0 UNSENT 未调用 open()
1 OPENED 已调用 open(),未发送
2 HEADERS_RECEIVED 已发送,响应头已接收
3 LOADING 响应体下载中
4 DONE 全部完成

6. 跨域问题

同源策略

🚨 同源策略: 协议、域名、端口三者完全相同才算同源,否则浏览器会拦截请求响应。

URL 对比 是否同源 原因
https://a.com/x vs https://a.com/y ✅ 完全相同
https://a.com vs http://a.com ❌ 协议不同
https://a.com vs https://b.com ❌ 域名不同
https://a.com:80 vs https://a.com:8080 ❌ 端口不同

常见跨域解决方案

方案 原理 适用场景
CORS 服务端设置 Access-Control-Allow-Origin 现代标准方案
JSONP 利用 <script> 标签不受同源限制 老项目,只支持 GET
代理服务器 同源服务器转发请求 开发环境最常用
postMessage 跨窗口通信 iframe 通信
// JSONP 原理简单实现
function jsonp(url, callback) {
  const cbName = 'jsonp_' + Date.now()
  // 把回调挂到全局
  window[cbName] = function(data) {
    callback(data)
    document.body.removeChild(script)
    delete window[cbName]
  }
  // 用 script 标签绕过同源策略
  const script = document.createElement('script')
  script.src = `${url}?callback=${cbName}`
  document.body.appendChild(script)
}
// CORS:服务端响应头(Node.js 示例)
res.setHeader('Access-Control-Allow-Origin', '*')         // 允许的源
res.setHeader('Access-Control-Allow-Methods', 'GET,POST') // 允许的方法
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

7. RESTful 与 ajax 的多种请求方式

RESTful 是一种 API 设计风格:用 HTTP 方法表达操作,用 URL 表达资源。

HTTP 方法 语义 示例
GET 查询资源 GET /posts 获取文章列表
POST 新建资源 POST /posts 创建文章
PUT 全量更新 PUT /posts/1 替换文章 1
PATCH 部分更新 PATCH /posts/1 修改部分字段
DELETE 删除资源 DELETE /posts/1 删除文章 1
// 用 fetch 实现 RESTful 风格的调用
// GET 查询
fetch('/api/posts').then(r => r.json())

// POST 新建
fetch('/api/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Hello', content: '...' })
})

// PUT 更新
fetch('/api/posts/1', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Updated' })
})

// DELETE 删除
fetch('/api/posts/1', { method: 'DELETE' })

💡 RESTful 设计原则: URL 用名词复数(/posts 而非 /getPosts),方法表达动作,响应用状态码表达结果。


8. 从状态码开始,到浏览器存储

HTTP 状态码分类

范围 类别 常见状态码
1xx 信息 100 Continue
2xx 成功 200 OK / 201 Created / 204 No Content
3xx 重定向 301 永久 / 302 临时 / 304 Not Modified(协商缓存)
4xx 客户端错误 400 请求错误 / 401 未授权 / 403 禁止 / 404 Not Found
5xx 服务端错误 500 内部错误 / 502 网关错误 / 503 不可用

浏览器存储对比

存储方式 容量 生命周期 与服务器通信 适用场景
cookie ~4KB 可设置过期 每次请求自动携带 登录态、SessionID
localStorage ~5MB 永久(除非手动清除) 不参与 长期偏好设置
sessionStorage ~5MB 标签页关闭即清除 不参与 表单临时数据
IndexedDB 几百 MB 永久 不参与 大量结构化数据、离线应用
// cookie:document.cookie 操作较繁琐
document.cookie = 'token=abc123; max-age=3600; path=/'

// localStorage / sessionStorage API 完全一致
localStorage.setItem('user', JSON.stringify({ name: 'Shea' }))
const user = JSON.parse(localStorage.getItem('user'))
localStorage.removeItem('user')
localStorage.clear()

sessionStorage.setItem('draft', '草稿内容')

⚠️ 注意: cookie 会被每个请求携带,过多会影响性能;敏感数据应配合 HttpOnly 和 Secure 标记。


Part IV. 浏览器底层原理

9. 概念:DOM、CSSOM、RenderTree、Layout、Painting

概念 全称 作用
DOM Document Object Model HTML 解析后的节点树
CSSOM CSS Object Model CSS 解析后的样式树
RenderTree 渲染树 DOM + CSSOM 合并(只含可见节点)
Layout 布局(回流/Reflow) 计算每个节点的位置和大小
Painting 绘制(重绘/Repaint) 把节点画到屏幕像素上

10. 页面渲染流程

浏览器渲染五步走:

HTML ──┐
       ├─→ DOM Tree ──┐
CSS  ──┴─→ CSSOM ─────┴─→ RenderTree ─→ Layout ─→ Painting ─→ 屏幕显示

详细流程:

  1. 解析 HTML: 字节流 → 字符 → Token → Node → DOM Tree
  2. 解析 CSS: 同理构建 CSSOM Tree
  3. 合成 RenderTree: 遍历 DOM,跳过不可见节点(display:none、script、meta),为每个可见节点匹配 CSSOM 规则
  4. Layout 布局: 计算节点的精确位置和大小(box model)
  5. Painting 绘制: 分层 → 栅格化 → 合成 → 显示

11. 结合渲染流程铺开 —— DOM / CSSOM 的创建过程

DOM 创建

<html>
  <body>
    <h1>Hello</h1>
    <p>World</p>
  </body>
</html>
// 上述 HTML 解析后形成的 DOM Tree(简化结构):
// html
//   └── body
//        ├── h1
//        │    └── "Hello"
//        └── p
//             └── "World"

CSSOM 创建

body { font-size: 16px; }
h1 { color: red; font-size: 32px; }
p { color: blue; }

⚠️ CSSOM 的特点: CSS 是"渲染阻塞"资源,必须等 CSSOM 构建完成才能合成 RenderTree。

🚨 重要警告: JS 既会阻塞 DOM 解析,又会等待 CSSOM(因为可能查询样式)。所以 JS 推荐放在 </body> 之前,或使用 defer/async。

<!-- 三种 script 加载方式对比 -->
<script src="a.js"></script>           <!-- 阻塞解析,立即下载执行 -->
<script src="b.js" async></script>     <!-- 异步下载,下载完立即执行(乱序) -->
<script src="c.js" defer></script>     <!-- 异步下载,DOM 解析完才执行(有序) -->
加载方式 下载 执行时机 顺序保证
默认 阻塞 立即 ✅
async 异步 下载完立即(可能在 DOMContentLoaded 前后) ❌
defer 异步 DOMContentLoaded 前按顺序 ✅

12. 回到最初的面试题

💡 经典面试题回归: 从输入 URL 到页面展示,中间发生了什么?

结合前面所学,完整答案应包含:

  1. URL 解析: 解析协议、域名、路径
  2. DNS 查询: 域名 → IP(浏览器缓存 → 系统缓存 → 路由器 → ISP → 根域名服务器)
  3. TCP 三次握手: 建立连接(HTTPS 还要 TLS 握手)
  4. HTTP 请求: 发送 Request
  5. 服务器处理 & 响应: 返回 Response
  6. 浏览器接收: 解析 HTML → 构建 DOM
  7. 加载子资源: CSS / JS / 图片(可能复用 TCP 连接)
  8. 构建 CSSOM、执行 JS
  9. 合成 RenderTree → Layout → Painting
  10. TCP 四次挥手: 断开连接

13. 了解浏览器机制的目的 => 性能优化

💡 核心目的: 了解原理,是为了知道在哪里"省"和怎么"省",从而做性能优化。

优化方向一:减少回流(Reflow)与重绘(Repaint)

回流一定引起重绘,重绘不一定引起回流。回流的代价远高于重绘。

操作 触发
修改宽高、位置、字体大小 回流 + 重绘
修改颜色、背景、可见性 仅重绘
transform、opacity 合成层,不触发回流重绘
// ❌ 反例:每次修改样式都触发回流
const el = document.getElementById('box')
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'

// ✅ 正例 1:合并样式修改(用 class)
el.className = 'new-style'

// ✅ 正例 2:使用 cssText 批量赋值
el.style.cssText = 'width:100px; height:100px; margin:10px'

// ✅ 正例 3:离线 DOM 操作
el.style.display = 'none'  // 脱离文档流
// ... 多次操作
el.style.display = 'block' // 一次性回流

优化方向二:资源加载优化

  • CSS: 放在 <head>,避免无样式闪烁(FOUC)
  • JS: 放在 </body> 前,或用 defer/async
  • 图片: 懒加载、压缩、使用 WebP/AVIF
  • 字体: 使用 font-display: swap 避免阻塞

优化方向三:利用缓存

// HTTP 缓存策略
// 强缓存:Cache-Control: max-age=3600
// 协商缓存:If-Modified-Since / ETag(对应 304 状态码)

// 业务层:Service Worker / IndexedDB 离线缓存
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
}

优化方向四:善用合成层

/* 启用 GPU 加速,绕过 Layout 和 Paint */
.smooth-anim {
  transform: translateZ(0);  /* 或 will-change: transform */
  transition: transform 0.3s;
}

🚨 警告: 合成层不是越多越好,过多合成层会占用大量内存,反而拖慢页面。


📌 总结思维导图

JS 核心知识
│
├── Part I 浏览器相关
│   ├── 处理模型:DOM / BOM / BEM
│   ├── 对象模型:window / location / history / navigator
│   └── 事件模型:三阶段 + 绑定 + 委托
│
├── Part II 函数式编程
│   ├── 演进:命令式 → 面向对象 → 函数式
│   ├── 三大原则:纯函数 / 不可变 / 组合
│   └── 流水线:pipe / Functor / 惰性执行
│
├── Part III 网络层
│   ├── ajax:XHR / fetch
│   ├── 跨域:CORS / JSONP / 代理
│   ├── RESTful:GET/POST/PUT/PATCH/DELETE
│   └── 状态码 + 浏览器存储
│
└── Part IV 浏览器底层
    ├── 五大概念:DOM/CSSOM/RenderTree/Layout/Paint
    ├── 渲染流程 + 阻塞机制(CSS/JS)
    └── 性能优化:减少回流 / 资源加载 / 缓存 / 合成层