JavaScript 进阶笔记的最后一篇,内容最杂:异常处理、storage、正则表达式、四个手写题,还有网络请求。
这部分像是「收尾大礼包」——把前面没归类的知识点统统塞了进来。
异常处理方案#
开发中封装的工具函数是给别人用的,使用者可能传进来不符合预期的参数(比如想要数组却传了字符串),所以函数内部需要对参数做验证。
不严谨的做法是直接 return,弊端是调用者不知道是函数没正常执行,还是结果本来就是 undefined。
更好的做法是用 throw 关键字抛出异常:
throw语句用于抛出一个用户自定义的异常;- 遇到
throw时,当前函数执行会被停止(后面的语句不执行); - 执行代码时会报错,拿到错误信息就能及时修正。
throw 后面可以跟基本数据类型(number、string、Boolean),也可以跟对象类型——对象能包含更多信息。
class HYError {
constructor(message, code) {
this.errMessage = message;
this.errCode = code;
}
}
function foo() {
console.log("foo function1");
// throw "发生错误"
// throw { errMessage: "我是错误信息", errCode: -1001 }
// throw new HYError("错误信息", -1001)
throw new Error("我是错误信息");
console.log("foo function2");
}Error 类型:JavaScript 提供了 Error 类,可以直接创建它的对象。Error 包含三个属性:
message:创建 Error 对象时传入的信息;name:Error 的名称,通常和类名一致;stack:整个错误信息,包括函数的调用栈,直接打印 Error 对象时打印的就是它。
Error 还有一些子类:
RangeError:下标值越界时使用的错误类型;SyntaxError:解析语法错误时使用的错误类型;TypeError:出现类型错误时使用的错误类型。
异常的处理:函数抛出异常后,如果没有被处理,异常会继续传递到上一个函数调用中;如果到了最顶层的全局代码依然没有处理,就会报错并终止程序。
try...catch 可以在发现异常时避免程序终止,ES10 中 catch 后面绑定的 error 还可以省略。必须执行的代码放在 finally 里:
function test() {
try {
foo();
} catch (error) {
console.log("catch 中的代码");
} finally {
console.log("finally 代码");
}
}自己捕获了异常,异常就不会传给浏览器,后续代码可以正常执行。
storage#
WebStorage 提供了一种比 cookie 更直观的 key-value 存储方式:
localStorage:永久性存储,关闭网页重新打开后内容依然保留;sessionStorage:会话存储,关闭会话后内容会被清除。
区别:
- 关闭网页后重新打开,localStorage 保留,sessionStorage 被删除;
- 在页面内跳转,两者都保留;
- 跳到页面外(打开新的网页),localStorage 保留,sessionStorage 不保留。
属性与方法:
Storage.length:只读,表示存储中的数据项数量;Storage.key(index):返回第 n 个 key 名称;getItem()/setItem()/removeItem()/clear()。
简单封装一下:
class Cache {
constructor(isLocal = true) {
this.storage = isLocal ? localStorage : sessionStorage;
}
setCache(key, value) {
if (!value) {
throw new Error("value error: value 必须有值!");
}
this.storage.setItem(key, JSON.stringify(value));
}
getCache(key) {
const result = this.storage.getItem(key);
if (result) {
return JSON.parse(result);
}
}
removeCache(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
const localCache = new Cache();
const sessionCache = new Cache(false);正则表达式#
初步认识#
正则表达式是一种字符串匹配利器,可以帮助我们搜索、获取、替换字符串。它由两部分组成:模式(patterns)和修饰符(flags)。
const re1 = /aaa/gi;
const re2 = new RegExp("aaa", "gi");适用于正则的方法#
RegExp 上的方法:
exec:在字符串中执行查找匹配,返回一个数组,未匹配到返回null;test:测试是否匹配,返回 true / false。
String 上的方法:
match:查找匹配,返回数组,未匹配到返回null;matchAll:查找所有匹配,返回一个迭代器;search:测试匹配,返回匹配到的位置索引,失败返回 -1;replace:查找匹配并替换;split:按正则或固定字符串分隔,把结果存到数组里。
修饰符 flags#
g:匹配全部;i:忽略大小写;m:多行匹配。
字符类#
\d(digit):数字,0 到 9;\s(space):空格符号,包括空格、制表符\t、换行符\n,以及\v、\f、\r这些;\w(word):拉丁字母、数字或下划线;.:除换行符之外的任何字符;\D/\S/\W:分别是上面三个的「非」。
锚点#
^ 匹配文本开头,$ 匹配文本末尾。
词边界 \b:像 ^ 和 $ 一样,检查字符串中的位置是否处于词边界——它检查位置的一侧匹配 \w,另一侧不匹配。
转义字符#
把特殊字符当作常规字符使用时要转义,在它前面加一个反斜杠即可。常见的需要转义的字符:[] \ ^ $ . | ? * + ( )。斜杠 / 本身不是特殊符号,但在字面量正则表达式里也需要转义。
集合(Sets)和范围(Ranges)#
方括号里的几个字符或字符类表示「搜索其中的任意一个」:
- 集合:
[eao]表示查找a、e、o中的任意一个; - 范围:
[a-z]匹配 a 到 z 的字母,[0-5]表示 0 到 5 的数字,[0-9A-F]表示两个范围; \d和[0-9]相同,\w和[a-zA-Z0-9_]相同;- 排除范围:
[^…]表示「除了这些字符」。
量词(Quantifiers)#
想找一段长度不固定的数字时,就要用数量范围来描述:
{n}:确切的位数,比如{5};{3,5}:某个范围的位数。
缩写:
+:一个或多个,相当于{1,};?:零个或一个,相当于{0,1},也就是让符号变得可选;*:零个或多个,相当于{0,}。
贪婪(Greedy)和惰性(lazy)模式#
默认情况下,匹配到内容后还会继续向后查找,一直找到最后一个匹配项,这叫贪婪模式。惰性模式则相反:拿到对应的内容后就不再往后匹配,在量词后面再加一个 ? 就能启用,比如 *?、+?,甚至把 ? 变成 ??。
const message = "我最喜欢的两本书: 《黄金时代》和《沉默的大多数》、《一只特立独行的猪》";
// 默认 .+ 采用贪婪模式
const nameRe1 = /《.+》/gi;
console.log(message.match(nameRe1));
// ['《黄金时代》和《沉默的大多数》、《一只特立独行的猪》']
// 使用惰性模式
const nameRe2 = /《.+?》/gi;
console.log(message.match(nameRe2));
// ['《黄金时代》', '《沉默的大多数》', '《一只特立独行的猪》']捕获组(capturing group)#
用括号把模式的一部分括起来就叫捕获组,它有两个作用:允许把匹配的一部分作为结果数组中的单独项;把括号里的内容视为一个整体。
str.match(regexp) 在没有 g 标志时查找第一个匹配并作为数组返回,有 g 时返回所有匹配:
- 索引 0:完全匹配;
- 索引 1:第一个括号的内容;
- 索引 2:第二个括号的内容;
- 依此类推。
括号多了不好数,可以用命名组:在开始括号之后立即写 ?<name>。
有时只是需要用括号来正确应用量词,但不希望内容出现在结果里,可以用非捕获组:在开头添加 ?:。
「或」在正则里用竖线 | 表示,通常和捕获组一起使用,在组内表示多个可选值。
手写:防抖、节流、深拷贝、事件总线#
JavaScript 是事件驱动的,大量操作会触发事件并加入事件队列。对于频繁触发的事件,可以用防抖和节流来限制执行频率。
防抖 debounce#
事件触发时不会立即执行函数,而是等待一段时间;如果在这段时间内又触发了,就重新等待。只有安静了一段时间没有新触发,才真正执行。
function hydebounce(fn, delay) {
let timer = null;
const _debounce = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn();
timer = null;
}, delay);
};
return _debounce;
}节流 throttle#
事件触发时会执行函数,但按照固定的频率执行——不管中间触发了多少次,执行次数总是固定的。
function hythrottle(fn, interval) {
let startTime = 0;
const _throttle = function () {
const nowTime = new Date().getTime();
const waitTime = interval - (nowTime - startTime);
if (waitTime <= 0) {
fn();
startTime = nowTime;
}
};
return _throttle;
}深拷贝#
最简单的办法是 JSON.parse(JSON.stringify(info)),但它会丢掉函数、undefined、Symbol,也不能处理循环引用。
手写版本:
function deepCopy(originValue, map = new WeakMap()) {
// 0. Symbol 类型
if (typeof originValue === "symbol") {
return Symbol(originValue.description);
}
// 1. 原始类型直接返回
if (!isObject(originValue)) {
return originValue;
}
// 2. Set 类型
if (originValue instanceof Set) {
const newSet = new Set();
for (const setItem of originValue) {
newSet.add(deepCopy(setItem, map));
}
return newSet;
}
// 3. 函数不需要深拷贝
if (typeof originValue === "function") {
return originValue;
}
// 4. 对象类型,用 map 处理循环引用
if (map.get(originValue)) {
return map.get(originValue);
}
const newObj = Array.isArray(originValue) ? [] : {};
map.set(originValue, newObj);
for (const key in originValue) {
newObj[key] = deepCopy(originValue[key], map);
}
// 单独处理 Symbol 类型的 key
const symbolKeys = Object.getOwnPropertySymbols(originValue);
for (const symbolKey of symbolKeys) {
newObj[symbolKey] = deepCopy(originValue[symbolKey], map);
}
return newObj;
}事件总线#
事件总线(Event Bus)用于在系统中传递事件和消息,基于发布-订阅模式,让不同组件之间解耦。
class HYEventBus {
constructor() {
this.eventMap = {};
}
on(eventName, eventFn) {
let eventFns = this.eventMap[eventName];
if (!eventFns) {
eventFns = [];
this.eventMap[eventName] = eventFns;
}
eventFns.push(eventFn);
}
off(eventName, eventFn) {
let eventFns = this.eventMap[eventName];
if (!eventFns) return;
for (let i = 0; i < eventFns.length; i++) {
if (eventFns[i] === eventFn) {
eventFns.splice(i, 1);
break;
}
}
if (eventFns.length === 0) {
delete this.eventMap[eventName];
}
}
emit(eventName, ...args) {
let eventFns = this.eventMap[eventName];
if (!eventFns) return;
eventFns.forEach((fn) => fn(...args));
}
}网络请求#
数据请求方式#
服务器端渲染(SSR):客户端发出请求 → 服务端接收并返回对应的 HTML 文档 → 页面刷新,客户端加载新的 HTML 文档。

缺点:
- 用户点击按钮只是数据变化,服务器却要把整个页面重新返回给浏览器加载,违背 DRY(Don't repeat yourself)原则;
- 只是数据变化却要返回整个 HTML 文档,给网络带宽带来不必要的开销。
前后端分离:只向服务器请求新的数据,在不让页面刷新的情况下动态替换页面中展示的数据,用的就是 AJAX。

AJAX 是 Asynchronous JavaScript And XML(异步的 JavaScript 和 XML)的缩写,是一种实现「无页面刷新获取服务器数据」的技术:不重新加载页面就能和服务器通信,发送请求、接收并使用返回的数据。
HTTP#
定义:超文本传输协议(HyperText Transfer Protocol),一种用于分布式、协作式和超媒体信息系统的应用层协议。
- HTTP 是万维网数据通信的基础,最初的设计目的是提供一种发布和接收 HTML 页面的方法;
- 通过 HTTP 或 HTTPS 请求的资源由统一资源标识符(URI)标识;
- HTTP 是客户端(用户)和服务端(网站)之间请求和响应的标准;
- 客户端可以是浏览器、爬虫或其他工具,称为用户代理程序(user agent);
- 响应方是存储资源的源服务器(origin server)。
组成与版本:一次 HTTP 请求主要包括请求(Request)和响应(Response)。

- HTTP/0.9(1991):只支持 GET 请求获取文本数据,主要为了获取 HTML 页面内容;
- HTTP/1.0(1996):支持 POST、HEAD 等方法,支持请求头、响应头和更多数据类型,但每次请求都要建立一次 TCP 连接,完成后立即断开;
- HTTP/1.1(1997,使用最广泛):增加 PUT、DELETE 等方法,采用持久连接(
Connection: keep-alive),多个请求可以共用一个 TCP 连接; - 2015 年发布 HTTP/2.0,2018 年发布 HTTP/3.0。
请求方式:
GET:获取指定资源,应该只用于获取数据;HEAD:与 GET 的响应相同,但没有响应体,比如下载文件前先获取文件大小;POST:把实体提交到指定资源;PUT:用请求载荷替换目标资源的当前表示;DELETE:删除指定资源;PATCH:对资源做部分修改;CONNECT:建立到目标服务器的隧道,通常用于代理服务器,网页开发很少用;TRACE:沿路径执行消息环回测试。
Request Header:

content-type:本次请求携带的数据类型。application/x-www-form-urlencoded:数据被编码成以&分隔、以=连接键值的键值对;application/json:JSON 类型;text/plain:文本类型;application/xml:XML 类型;multipart/form-data:上传文件。
content-length:文件的大小长度。keep-alive:HTTP 基于 TCP,通常一次请求响应结束后会断开。HTTP/1.0 中想保持连接,需要浏览器在请求头加Connection: keep-alive、服务器在响应头也加同样的字段;HTTP/1.1 中所有连接默认就是 keep-alive,不同服务器保持的时间不同,Node 中默认 5 秒。accept-encoding:告知服务器客户端支持的文件压缩格式,比如 js 文件可以用 gzip 编码。accept:告知服务器客户端可接受的文件格式类型。user-agent:客户端相关信息。
Response 响应状态码:
200 OK:请求成功;201 Created:POST 请求创建了新资源;301 Moved Permanently:资源的 URL 已修改,响应中会给出新的 URL;400 Bad Request:客户端错误,服务器无法或不进行处理;401 Unauthorized:未授权,必须携带身份信息;403 Forbidden:没有权限访问,被拒绝;404 Not Found:服务器找不到请求的资源;500 Internal Server Error:服务器遇到了不知道如何处理的情况;503 Service Unavailable:服务器不可用,可能在维护或重载。

XMLHttpRequest(XHR)#
AJAX 发送请求的步骤:
- 创建 XMLHttpRequest 对象;
- 监听对象状态的变化,或者监听
onload事件(请求完成时触发); - 通过
open方法配置请求(默认异步,第三个参数传false就是同步请求,会阻塞后续代码); - 通过
send发送请求。
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
const resJSON = JSON.parse(xhr.response);
console.log(resJSON.data.banner.list);
};
xhr.open("get", "url");
xhr.send();事件监听(宏任务):
readystatechange:HTTP 状态改变;loadstart:请求开始;progress:一个响应数据包到达;abort:调用xhr.abort()取消了请求;error:连接错误(比如域错误,404 这类 HTTP 错误不会触发它);load:请求成功完成;timeout:请求超时被取消(仅发生在设置了 timeout 时);loadend:在 load、error、timeout 或 abort 之后触发。
state 状态:
0 UNSENT:代理被创建,但还没调用open();1 OPENED:open()已经调用;2 HEADERS_RECEIVED:send()已经调用,头部和状态可获取;3 LOADING:下载中,responseText已包含部分数据;4 DONE:下载完成。
响应数据和响应类型:response 属性返回响应的正文内容,类型取决于 responseType。早期服务器返回的是文本和 XML,所以常用 responseText、responseXML,再手动转成 JS 对象;现在基本都返回 JSON,直接设置即可。
xhr.responseType = "json";
console.log(xhr.response); // 自动转成对象注意:XHR 的 state 记录的是对象本身的状态变化,不是 HTTP 请求的状态;要拿 HTTP 响应状态,用 status 和 statusText。
GET / POST 传递参数:用 xhr.setRequestHeader("Content-type", "类型") 设置传递类型,常见的四种方式:
const xhr = new XMLHttpRequest();
xhr.onload = function () {
console.log(xhr.response);
};
xhr.responseType = "json";
// 1. GET:query 参数
xhr.open("get", "https://example.com/api?name=why&age=18");
// 2. POST:urlencoded
xhr.open("post", "https://example.com/api");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("name=why&age=18");
// 3. POST:FormData
const formEl = document.querySelector(".info");
xhr.open("post", "https://example.com/api");
const formData = new FormData(formEl);
xhr.send(formData);
// 4. POST:JSON
xhr.open("post", "https://example.com/api");
xhr.setRequestHeader("Content-type", "application/json");
xhr.send(JSON.stringify({ name: "why", age: 18, height: 1.88 }));封装一个请求工具:
function hyajax({ url, method = "get", data = {}, success, failure } = {}) {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
success && success(xhr.response);
} else {
failure && failure({ status: xhr.status, message: xhr.statusText });
}
};
xhr.responseType = "json";
if (method.toUpperCase() === "GET") {
const queryStrings = [];
for (const key in data) {
queryStrings.push(`${key}=${data[key]}`);
}
xhr.open(method, url + "?" + queryStrings.join("&"));
xhr.send();
} else {
xhr.open(method, url);
xhr.setRequestHeader("Content-type", "application/json");
xhr.send(JSON.stringify(data));
}
return xhr;
}hyajax({
url: "https://example.com/api",
method: "GET",
data: { name: "why", age: 18 },
success: function (res) {
console.log("res:", res);
},
failure: function (err) {
console.log(err);
},
});超时与取消:为避免服务器长时间不返回数据,可以给请求设置 timeout。到达超时时间后请求会被自动取消;默认值为 0,表示不设置超时。
xhr.timeout = 3000; // 3s 超时
xhr.abort(); // 手动取消请求Fetch 函数使用#
json() 是 Fetch API 中 Response 对象的方法,用于把响应体文本解析成 JSON 对象。
// 1. 未优化:嵌套的 then
fetch("https://example.com/api").then((res) => {
res.json().then((res) => {
console.log("res:", res);
});
}).catch((err) => {
console.log("err:", err);
});
// 2. 优化一:把 json() 的 Promise 返回出去
fetch("https://example.com/api")
.then((res) => res.json())
.then((res) => {
console.log("res:", res);
})
.catch((err) => {
console.log("err:", err);
});
// 3. 优化二:async/await
async function getData() {
const response = await fetch("https://example.com/api");
const res = await response.json();
console.log("res:", res);
}
getData();写在最后#
到这一篇,2024 年的 JavaScript 笔记就整理完了。回头看,进阶篇覆盖的东西比基础篇杂得多:原理、API、手写题、网络请求都有,像是把「想搞明白的东西」一次性都记了下来。
手写题那几个(防抖、节流、深拷贝、事件总线)算是这批笔记里最实用的部分,后来做项目遇到类似需求,基本都能直接翻回来改一改就用。
前端笔记到这里还剩 Vue 一篇,写完就收工。
