js学习笔记

js学习笔记
晨梦什么是闭包有什么用
闭包是内部函数引用外部函数局部变量的一个引用关系,这个引用关系是用一个对象去描述的,这个对象存在于内部函数对象身上closure,外部函数调用几次就有几套独立机制的闭包机制
产生条件:函数嵌套函数;内部函数引用外部函数的局部变量;调用外部函数时就产生闭包,闭包不是在调用函数内部时产生的,而是在创建内部函数对象时产生。
作用:延长局部变量的生命周期,让函数外部间接操作内部的局部变量, 保护变量不被销毁(持久化变量)比如计数器、节流防抖中,都需要保存状态变量。
缺点:闭包分为临时闭包和永久闭包,永久闭包不会被销毁就可能产生内存泄漏,所以闭包使用完成,记得手动释放,让内部函数对象成为垃圾对象,断开指向它的所有引用
用到过 闭包的场景:
防抖函数:这里 timer 变量在返回的函数中被引用,形成闭包,使得它不会被销毁。防抖函数通过闭包保存了 timer 变量,让每次触发时都可以取消上一次的定时器,只保留最后一次,从而实现“最后一次操作后才触发”的效果。
function debounce(fn, delay) {
let timer = null // 定义一个定时器变量
return function (...args) { //接收任意数量的参数,并把它们收集到一个数组里。
if (timer) clearTimeout(timer) // 每次调用时都清除前一个定时器
// 重新设置定时器,等 delay 毫秒后执行
timer = setTimeout(() => fn.apply(this, args), delay)
// 过 delay 毫秒后,调用 fn 函数,并把你传入的参数原封不动地传进去,同时保持 this 不变。
}
}
如何遍历对象
- 使用 for…in(遍历可枚举属性,包括继承的属性)
const obj = { a: 1, b: 2, c: 3 }; for (let key in obj) { console.log(key, obj[key]); } - 使用 Object.keys()(获取自身可枚举的“键”)
const obj = { a: 1, b: 2, c: 3 }; const keys = Object.keys(obj); keys.forEach(key => { console.log(key, obj[key]); }); - 使用 Object.entries()(获取键值对数组)
const obj = { a: 1, b: 2, c: 3 }; const entries = Object.entries(obj); entries.forEach(([key, value]) => { console.log(key, value); }); - 使用 Reflect.ownKeys()(获取所有自身属性,包括不可枚举和 symbol)
const obj = { a: 1, b: 2, c: 3 }; Reflect.ownKeys(obj).forEach(key => { console.log(key, obj[key]); });
如何判断JS的数据类型
首先想到的是 typeof 但是typeof在检验null、对象、或者数组是不能检测出精准的数据类型,都会返回object。
然后想到a instanceof b 专门判断对象的数据,检查a的隐式原型属性是与b的显示原型
null instanceof Object // false
({}) instanceof Object // true
([]) instanceof Object // true
({}) instanceof Array // false
([]) instanceof Array // true
结论: 任何对象的instanceof Object都是true无法帮我们判定死是否是对象或者数组
判断是否为数组Array.isArray()
最终办法: Object.prototype.toString.call(obj).slice(8,-1)
说说常见的数组方法
防抖和节流
防抖:在事件触发后一段时间只执行一次,如果在这段时间内事件被重新触发,则重新计时。搜索框输入联想、防止按钮连点、resize、scroll等高频操作优化
节流: 一定时间内只执行一次,即使事件持续触发,也只会每隔设定时间执行一次。页面滚动监听、拖拽事件、游戏中的按键响应、窗口 resize 监听
防抖代码:当事件触发后,会设置一个定时器,在指定的延迟时间后在执行相应的操作,如果在延迟时间内再次触发了同一事件,那么就会清除之前的定时器,并重新设置新的定时器直到事件触发完成
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
节流代码:当事件触发后,事件处理函数会在固定的时间间隔内执行,即使事件被频繁触发也是如此
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime > delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
GET 和 POST 请求的区别
🔢 背景:最初是浏览器与服务器之间的通讯协议,GET用于读取资源,POST用于提交表单,后来被扩充到接口格式的定义,GET和POST作为接口的请求方式
🧭 区别:
- 传参方式不同
GET:参数拼接在 URL 后面,格式为例:/api/user?id=123
POST:参数放在 请求体(body) 中,不会出现在 URL 中。例子:请求体中 {“id”: 123} - 安全性对比
GET:参数暴露在 URL 中,不适合传输敏感信息(如密码)。易被浏览器缓存,参数也可能被记录在浏览历史中。
POST:相对更安全(虽然本质上也不加密),因为参数藏在请求体中。 - 数据长度限制
GET: URL 有长度限制(不同浏览器/服务器限制不同,通常在 2KB~8KB)。
POST:理论上没有大小限制,适合上传大文件或大量数据。 - 浏览器行为
GET 请求可以被收藏、缓存、重复提交(刷新不弹窗)。能由浏览器自动发起(如img-src,资源加载)
POST 请求不能被浏览器缓存,刷新会弹出“是否重新提交表单”提示。
强缓存和协商缓存
🧱 强缓存(Strong Cache)
浏览器 先检查本地缓存是否可用,如果可用则直接使用,不发起请求。
常见字段
- Expires(HTTP/1.0):设置一个 绝对时间点(GMT格式)
- Cache-Control(HTTP/1.1):设置相对时间(如 max-age=3600 表示缓存 1 小时)
// 表示该资源在缓存中可以保存 3600 秒内不再请求服务器。
Cache-Control: max-age=3600
🧱 协商缓存(Negotiation Cache)
当强缓存失效时,浏览器会向服务器发送请求,并携带资源的 标识信息(如时间戳或文件哈希)进行协商。
- 如果资源没变,返回 304 Not Modified,浏览器使用缓存。
- 如果资源变了,返回 200 OK 和新资源。
协商机制:
ETag + If-None-Match
ETag 是资源内容的哈希值。浏览器下次请求带上 If-None-Match,服务器判断是否相同。
ETag: "abc123"
If-None-Match: "abc123"
❓ 为什么有了强缓存还需要协商缓存?
✅ 原因一:强缓存有时间限制
- 强缓存依赖 max-age 或 Expires,缓存一旦过期就会失效。
- 浏览器此时需要向服务器确认资源是否更新 —— 就用协商缓存来减少资源浪费。
✅ 原因二:资源可能在有效期内就更新了
- 比如 max-age=3600,但 10 分钟后服务器更新了资源,客户端还在用旧的缓存内容。
- 这时只有协商缓存能解决这个问题:它允许服务器来判断资源是否真的需要更新。
所以强缓存 + 协商缓存是浏览器缓存的两道防线,优先走强缓存,失败后才协商。都失败才会下载新资源,减少带宽浪费,提高性能。
项目中有没有用过缓存策略?怎么设置的?
请求头缓存(服务端缓存策略)
配合后台设置缓存头,比如:
Cache-Control: max-age=3600
前端可以利用浏览器强缓存,不重复请求资源组件层面的缓存 —— 页面切换时保留状态,提高页面切换性能。
🧩 使用场景:
比如在一个“列表页 → 详情页”切换中,返回列表时希望保留滚动位置、搜索条件等状态。
🧩 设置方式:
<template>
<keep-alive include="UserList">
<RouterView />
</keep-alive>
</template>
或使用 Vue Router 的 meta 字段 + 动态 include:
{
path: '/user/list',
name: 'UserList',
component: () => import('@/views/UserList.vue'),
meta: { keepAlive: true }
}
<keep-alive>
<router-view v-slot="{ Component, route }">
<component :is="Component" v-if="route.meta.keepAlive" />
</router-view>
</keep-alive>
- 请求接口层面的缓存 —— 减少重复请求、提高响应速度
🧩 使用场景:
做的管理端里,比如新建用户要选择部门,这个下拉框来源于接口 /api/dept/list 这个部门列表基本不会变,但每次打开这个页面都发一次请求?多页面还会重复请求?
请求一次之后,缓存起来,下次直接使用缓存:
// 缓存在 Pinia 或 memory 中
if (!deptList.length) {
const res = await axios.get('/api/dept/list')
deptList.value = res.data
}
如果我缓存了数据,那怎么知道它已经变化、该重新请求了?
- 用户主动触发(最常见方式)await getExamList() // 重新拉取,更新缓存
- 设置过期时间(自动判断)比如你把数据存在 localStorage 里,可以加个时间戳判断是否“过期”:
- 后台返回版本号 / 时间戳 / 哈希(高级策略)
这个常用于大公司大型项目,服务端接口会返回一个 version 或 lastModified 字段:
前端请求时带上这个标识:{ "data": [...], "version": "v3.2", "lastModified": "2025-04-14T10:32:00" }
服务端会判断数据有没有变,如果没变就返回 304 Not Modified,这其实就是「协商缓存」的思想。axios.get('/api/news/list', { headers: { 'If-Modified-Since': lastModified } })
浏览器存储的方式有哪些
cookie保存状态 LocalStorage 存储东西
Cookie
它的诞生是为了解决HTTP协议的无状态问题,服务器无法识别不同用户或跟踪用户的状态,但也导致了一些问题比如无法保持用户的登陆状态等
主要用于会话管理,如登录状态(token),也可存储少量数据。
每次请求都会自动携带到服务器,适合服务器识别用户。会过期
document.cookie = "username=Tom; expires=Fri, 31 Dec 2025 23:59:59 GMT"
LocalStorage
数据长期保留,除非手动删除
适合存储:用户设置、主题偏好、缓存列表等。
localStorage.setItem('token', 'abc123')
const token = localStorage.getItem('token')
SessionStorage
仅当前“标签页”有效,关闭页面就清空。
适合存储:表单填写中间状态、当前页状态。
sessionStorage.setItem('step', '2')
const step = sessionStorage.getItem('step')
IndexedDB
特点:浏览器内建的 MySQL 数据库,支持结构化数据、事务、索引。
适合场景:大数据缓存、本地搜索数据、本地聊天记录、离线应用。
登陆的token放cookie还是放localstrorage
🎯 结论先说:
如果你对安全要求较高,推荐放在 Cookie 中,并设置 HttpOnly + Secure。否则,放在 localStorage 也可以,简单易用但安全性略差。容易被 XSS 攻击读出 token → 导致用户身份泄露。
❗ Cookie 的坑:如果设置了跨域请求,需要加上:axios.defaults.withCredentials = true
并配合后端 Access-Control-Allow-Credentials: true
👇 那么什么时候用 Cookie?什么时候用 localStorage?
✅ 选择 Cookie 的场景(安全优先):
- 登录态需要被服务器自动识别(比如传统服务端渲染项目)
- 对安全性要求较高的项目(如后台管理系统)
- 想防止 XSS 窃取 token(使用 HttpOnly)
Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict
🔐 Cookie + HttpOnly 方案中,token 过期怎么办?
短效 token + 刷新 token blue
服务端设置两个 Cookie:
access_token:短效,5-15分钟过期,HttpOnly
refresh_token:长效,7天-30天过期,HttpOnly
流程如下:
1.前端发请求,access_token 自动随 Cookie 发送
2.如果接口返回 401(token 过期),前端自动调用「刷新 token 接口」
3.服务端验证 refresh_token,返回新的 access_token
4.前端重试原请求
后端设置 Cookie 的时候可以指定 Expires 或 Max-Age:
Set-Cookie: access_token=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=900
Set-Cookie: refresh_token=xyz456; HttpOnly; Secure; SameSite=Strict; Max-Age=604800
access_token 15 分钟过期 refresh_token 7 天过期
✅ 选择 localStorage 的场景(灵活易用):
- 单页应用(SPA)前后端完全分离
- 使用 JWT + 手动加请求头(如 axios 拦截器)
- 对安全要求不是极高,但追求开发便利
// 请求拦截器:在每个请求发送之前都添加 token 到请求头
instance.interceptors.request.use(
(config) => {
// 获取 token,假设存储在 localStorage 中
const token = localStorage.getItem('token')
// 如果 token 存在,则在请求头中添加
if (token) {
config.headers['Authorization '] = `${token}`
}
return config
},
(error) => {
return Promise.reject(error)
},
)
浅拷贝和深拷贝
浅拷贝
拷贝对象的第一层属性值,如果属性是引用类型(如对象、数组),只拷贝地址,不复制内容。修改嵌套对象,会影响原对象。
总结一句话: 第一层复制,内部引用地址共享
const obj1 = { a: 1, b: { c: 2 } }
const obj2 = { ...obj1 } // 浅拷贝
obj2.b.c = 999
console.log(obj1.b.c) // 999
常见方法:
Object.assign({}, obj)
{ …obj }
深拷贝
拷贝对象及其所有层级的值,互不影响。总结一句话:层层复制,彻底独立
浅拷贝:只复制一层引用,适合不修改嵌套属性的场景;
深拷贝:彻底复制每一层,适合需要修改嵌套结构的场景。编辑场景/撤销功能,就必须保存独立的一份完整拷贝,这样以后回退时才不会影响原始数据 —— 所以要用 深拷贝!
const obj1 = { a: 1, b: { c: 2 } }
const obj2 = JSON.parse(JSON.stringify(obj1)) // 简单深拷贝
obj2.b.c = 999
console.log(obj1.b.c) // 2
常见方法:
const newObj = JSON.parse(JSON.stringify(obj)) ❌ 会丢失:函数、undefined、Symbol、循环引用等
const deepCopy = structuredClone(obj) (现代浏览器支持)
手写深拷贝(递归)
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj
const result = Array.isArray(obj) ? [] : {}
for (const key in obj) {
result[key] = deepClone(obj[key])
}
return result
}
🚨 浅拷贝带来的常见 bug
const state = {
user: {
name: 'Tom',
age: 18
}
}
const copy = { ...state }
copy.user.age = 20
console.log(state.user.age) // 变成了 20,浅拷贝坑
数组去重的方法(3种)
- 用 Set 只允许存储唯一的值
const arr = [1, 2, 2, 3, 4, 4] const uniqueArr = [...new Set(arr)] console.log(uniqueArr) // [1, 2, 3, 4] - 利用filter方法来遍历数组,只保留第一次出现的元素
const arr = [1, 2, 2, 3, 4, 4] const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index) console.log(uniqueArr) // [1, 2, 3, 4] - 使用reduce方法逐个遍历数组元素,构建一个新的数组,只添加第一次出现的元素
const arr = [1, 2, 2, 3, 4, 4] const uniqueArr = arr.reduce((acc, cur) => { if (!acc.includes(cur)) acc.push(cur) return acc }, []) console.log(uniqueArr) // [1, 2, 3, 4]
介绍下promise.all
工作中经常用到它来并发请求多个接口、等待所有结果比如同时加载多个接口数据(页面初始化),并行上传多个文件,并发处理多个异步任务(比如图片、资源等)等
✅ 一句话介绍:
Promise.all 用于并行执行多个 Promise,等全部成功才返回,其中任何一个失败都会进入 .catch。
Promise.all([promise1, promise2, promise3])
.then(([res1, res2, res3]) => {
// 所有成功,按顺序拿结果
})
.catch(error => {
// 只要有一个失败,就走这里
})
场景:
const getUser = () => axios.get('/api/user')
const getRoles = () => axios.get('/api/roles')
const getMenu = () => axios.get('/api/menu')
Promise.all([getUser(), getRoles(), getMenu()])
.then(([userRes, rolesRes, menuRes]) => {
console.log('用户信息:', userRes.data)
console.log('角色信息:', rolesRes.data)
console.log('菜单信息:', menuRes.data)
})
.catch(err => {
console.error('有一个接口失败了:', err)
})
🧠 进阶扩展:如果你想“部分失败不影响其他的成功”,可以用Promise.allSettled:
Promise.allSettled([p1, p2, p3]).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value)
} else {
console.log('失败:', result.reason)
}
})
})
lastIndexOf()
lastIndexOf() 是 JavaScript 中字符串(String)对象的一个方法,用来查找某个子字符串在原字符串中最后一次出现的位置(索引)。
例子
const str = 'path/to/middleware/custom/test.js';
// 查找最后一个斜杠的位置
console.log(str.lastIndexOf('/')); // 输出: 28(最后一个 '/' 在位置28)
// 查找 middleware 最后出现的位置
console.log(str.lastIndexOf('middleware')); // 输出: 9(首次出现位置)
// 截取 middleware 之后的部分
const result = str.substring(str.lastIndexOf('middleware/') + 'middleware/'.length);
console.log(result); // 输出: custom/test.js
substring()
substring() 是 JavaScript 中字符串(String)对象的一个方法,用来从字符串中提取子字符串(截取一部分字符串)。
const str = "hello world";
// 从索引0开始,到索引5之前结束(不包含5)
const part1 = str.substring(0, 5);
console.log(part1); // "hello"
// 从索引6开始截取到末尾
const part2 = str.substring(6);
console.log(part2); // "world"
Object.assign()
Object.assign() 用于将一个或多个源对象的可枚举属性复制到目标对象上,并返回目标对象。
Object.assign(target, …sources)
- target:目标对象,属性会被添加到它身上
- sources:一个或多个源对象,从中复制属性
例子:
应用场景:const a = { name: 'Tom' } const b = { age: 18 } const c = Object.assign({}, a, b) console.log(c) // { name: 'Tom', age: 18 } // 也可以覆盖 Object.assign({name: 'Tom'}, {name: 'Jerry'}) // 结果:{name: 'Jerry'} // 浅拷贝对象 const newObj = Object.assign({}, oldObj) // 不会复制原型链上的属性,只拷贝自身的可枚举属性 const obj1 = { nested: { a: 1 } } const obj2 = Object.assign({}, obj1) obj2.nested.a = 999 console.log(obj1.nested.a) // 999(浅拷贝)
1.对象合并(配置合并、状态合并)
2.浅拷贝对象
扩展 - 与扩展运算符区别:
都是浅拷贝,但扩展运算符不会复制 getter/setter
Object.assign() 会调用 getter 方法
原型链
代码示例:
首先定义了一个构造函数 Person, 然后在构造函数的 prototype 上定义了一个方法 sayHello。接着创造了一个 person1 实例对象,并访问它的属性和方法。最后, 验证了 person1 实例对象的 proto 属性确实指向构造函数的 prototype 对象, 建立了原型链关系
// 创建一个构造函数
function Person(name) {
this.name = name;
}
// 在构造函数的 prototype上定义一个方法
Person.prototype.sayHello = fuction() {
console.log(`Hello, my name is ${this.name}`)
}
// 创建一个实例对象
const person1 = new Person('Alice');
// 访问实例对象的属性和方法
console.log(person1.name); // 输出:"Alice"
person1.sayHello(); // 输出: "Hello, my name is Alice"
// 查看实例对象的 _proto_ 属性, 它指向构造函数的 prototype 对象
console.log(person1._proto_ === Person.prototype); // 输出 true
prototype
- 构造函数特有的属性, 每个函数对象都有一个prototype属性, 它是一个对象
- 常用于定义共享的属性和方法, 可以被构造函数创建的实例对象所继承。可以在构造函数的 prototype 上定义方法, 以便多个实例对象共享这些方法, 从而节省内存。
- 主要用于原型继承, 它是构造函数和实例对象之间的链接, 用于共享方法和属性。
_proto_
- 每个对象都具有的属性, 它指向对象的原型
- 用于实现原型链, 当访问一个对象的属性时, 如果对象本身没有这个属性, javaScript引擎会沿着原型链通过
_proto_属性向上查找, 直到找到属性或者到达原型链的顶部( 通常是Object.prototype ) - 主要是用于对象之间的继承, 它建立了对象之间的原型关系
总结:
构造函数的 prototype对象会被赋予给实例对象的 _proto_属性, 从而建立了原型链
Web Worker是什么
Web Worker 是一种在浏览器中开启独立线程运行脚本的方法,常用于处理耗时操作,以防止阻塞主线程(UI线程),提升页面响应性能。
适用场景
数据密集型计算(如大规模循环计算、加密、排序)
后台处理任务(如解析文件、大数据处理)
不需要操作 DOM(因为 Worker 无法访问 DOM)Worker 是运行在不同线程的,没有访问 DOM 的权限
主线程和 Worker 如何通信?通过 postMessage 和 onmessage,发送和接收数据。
顺序
- 主线程创建 Worker 实例
- 主线程通过 postMessage() 向 Worker 发消息
- Worker 线程收到消息(onmessage),开始处理任务
- Worker 处理完成,通过 postMessage() 把结果发送回主线程
- 主线程通过 onmessage 接收结果
基本用法
主线程中使用 Web Worker
// main.js
const worker = new Worker('worker.js');
// 发送数据给 Worker
worker.postMessage({ count: 100000 });
// 接收来自 Worker 的消息
worker.onmessage = function (event) {
console.log('收到 worker 的消息:', event.data);
};
// 监听错误
worker.onerror = function (error) {
console.error('Worker 错误:', error);
};
Worker 文件中(worker.js)
// worker.js
// 接收主线程发送的消息 self.onmessage
self.onmessage = function (event) {
const data = event.data;
let result = 0;
for (let i = 0; i < data.count; i++) {
result += i;
}
// 返回结果给主线程
self.postMessage(result);
};
终止 Worker
// 在主线程中终止
worker.terminate();
// 或在 worker 内部自杀
self.close();
使用 Blob 创建内联 Worker
const workerCode = `
self.onmessage = function (e) {
const num = e.data;
self.postMessage(num * 2);
}
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.postMessage(5); // 输出 10
worker.onmessage = e => console.log(e.data);
v8垃圾回收机制
分代回收: V8 将内存主要分为两部分, 通常包括新生代和老生代。新生代存放生命周期短的对象,空间小(几MB),垃圾回收频繁,速度快; 而老生代包括经过多次回收仍然存活的对象(新生代晋升的对象)或生命周期长 ,新生代对象通常比老生代对象更容易回收
标记清除: 它将内存中的对象分为 “可达”和“不可达” 两类,垃圾回收器首先标记所有可达对象,然后清除不可达的对象。这个过程涉及两个阶段,标记阶段和清除阶段。
标记阶段:从根元素开始,垃圾回收器递归遍历所有对象,并标记为可达对象, 标记表示对象是否被其他对象引用。
清除阶段:垃圾回收器遍历所有未标记的对象,即不可达的对象,并将其从内存中清除。
网络安全
XSS 攻击
指的是攻击者在网页中注入恶意脚本,当用户浏览网页时,这些脚本会在用户的浏览器中执行,通常是在用户评论、留言板或用户生成的内容中,当用户访问这个评论,恶意代码就会被执行,从而窃取Cookie、Token、账号密码等敏感信息,或进行恶意跳转、控制页面行为等。
XSS类型
存储型:恶意脚本被存入数据库, 用户访问时自动执行
反射型:恶意脚本通过URL参数传入并立即返回页面
DOM型:攻击脚本注入到页面后通过DOM操作执行
防范措施
- 对于用户输入的数据应该验证和过滤,仅允许预期的、安全的字符和内容通过,不要包含特殊字符。
- 将用户输入的数据插入到html,js之前,要进行转义,防止浏览器解释用户输入的内容为可执行代码
- 严格限制 innerHTML、document.write 等 API 使用
- 表单、接口层严格校验数据格式、长度、字符类型。
DDoS 攻击
是指攻击者通过控制大量被攻陷的计算机,向目标服务器发送大量请求,耗尽资源,使目标网站服务崩溃或无法响应正常用户请求。DDos表现
服务器CPU、内存飙升,响应极慢或直接宕机
网络带宽被刷满, 拒绝正常请求
大量无效请求造成数据库连接池耗尽
防范措施
- 开启 流量清洗服务,过滤异常 IP
- 设置 请求速率限制防止刷接口
- 对敏感操作增加验证(如验证码、人机验证)
- 配置 WAF(Web 应用防火墙)对流量行为分析和拦截
CSRF 攻击
是指攻击者诱导用户在登录状态下,向受信任的网站发送非法请求,从而执行未授权操作(如转账、改密码、删除数据等)。防范措施
- 使用 CSRF Token:服务端生成并校验,每个表单/请求中必须携带
- Referer 验证:判断请求来源是否合法
- SameSite Cookie 属性:设置 Cookie 的 SameSite=Lax 或 Strict,防止第三方请求携带 Cookie。
- 对敏感操作使用 POST + 验证码 / 二次确认。
SQL注入
假设一个网页上的登陆表单,该表单将用户提供的用户名和密码与数据库中的进行身份验证,攻击者可以绕开身份验证
假设你在网页中有一个登录表单,用户输入用户名和密码后,后台会执行类似这样的 SQL
SELECT * FROM users WHERE username = '用户输入' AND password = '用户输入';
如果攻击者在“用户名”处输入:’ OR ‘1’=’1
最终拼接的 SQL 语句变成了:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...';
这条语句永远为真,攻击者就可以绕过登录验证,直接登录系统
路由守卫是什么
是 Vue Router 提供的钩子函数, 用于在路由跳转前后进行拦截和控制, 比如用户权限校验、登陆状态判断、路由跳转前提示等
| 类型 | 说明 | 常见用途 |
|---|---|---|
全局前置守卫 router.beforeEach |
所有路由跳转前触发 | 登录校验、权限拦截 |
全局后置守卫 router.afterEach |
路由跳转后触发 | 页面埋点、清理操作 |
路由独享守卫 beforeEnter |
单个路由专属守卫 | 单页权限控制 |
组件内守卫 beforeRouteEnter |
在组件内使用 | 访问组件前/离开时逻辑处理 |
- 全局前置守卫
// router/index.ts import { createRouter, createWebHistory } from 'vue-router' import routes from './routes' const router = createRouter({ history: createWebHistory(), routes, }) // 常用于登录状态判断和重定向 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if( to.path == '/login' ){ return ; } if( !localStorage.getItem('TOKEN') ){ return '/login' } //动态添加路由 initRouter(); //当前路由没有匹配到任何路由记录 if( to.matched.length == 0){ router.push( to.fullPath ); } return true; }) export default router - 路由独享守卫
{
path: '/admin',
component: AdminView,
beforeEnter: (to, from, next) => {
const isAdmin = checkIsAdmin()
if (isAdmin) next()
else next('/403')
}
}
什么是跨域?
当你在前端页面 A(example.com)中,尝试向另一个接口 B(api.example.cn)发起请求时,浏览器发现两个地址不是同源,就会阻止请求或拒绝响应读取 —— 这就叫做“跨域”
定义:
两个URL的协议、域名、端口三者必须完全相同, 才属于同源
浏览器的同源策略限制了以下行为:
- Cookie、LocalStorage、SessionStorage 访问
- DOM节点访问
- AJAX请求
如何解决跨域问题?
- CORS(推荐):服务端设置响应头 Access-Control-Allow-Origin 来允许跨域访问
- JSONP:兼容性好, 仅限 GET 请求,利用
<script>标签不受同源策略限制,存在XSS 安全隐患,不支持复杂请求/请求头 - 代理转发:通过开发服务器中间层(如 webpack devServer 的 proxy)转发请求
- nginx 反向代理:线上通过 nginx 转发接口请求到目标服务
- WebSocket:不受同源策略限制
CORS 中浏览器会发送几次请求?分别是什么?
视请求类型而定:可能是 1 次,也可能是 2 次。
简单请求, 只发 1 次请求: 方法是:GET、POST; 请求头是:Accept、Content-Type(且值为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain); 没有使用自定义头(如 X-Token)
非简单请求,会发 2 次请求: 方法为:PUT、DELETE、PATCH; Content-Type 不是上述三种(如:application/json); 携带自定义请求头(如:Authorization、X-Token)
浏览器会先发送一个 OPTIONS 请求,称为 预检请求,目的是问服务器:我可以发这个跨域请求吗?
如果服务器响应头中包含:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers等,则浏览器才会继续发送实际请求
浏览器输入url按下回车键的过程?
1.url 解析
- 浏览器地址栏识别你输入的是有效的 URL还是关键词等,并且会纠错、补全和转换
- 浏览器开始解析域名(URL)并查找DNS缓存,以便查找域名对应的IP地址
2.DNS解析
- DNS解析的过程就是转换IP的过程,首先会去查找浏览器缓存有没有这样的一个ip映射, 如果没有它会查找系统缓存,再没有会查找host文件有没有这样的ip映射,(浏览器缓存 → 系统缓存 → hosts 文件)
- 若本地无命中,向运营商的 DNS 服务器发起请求
- 若仍未命中,向根域名服务器递归查询直到找到目标 IP 再层层进行缓存到本地的ip映射里面
3.建立TCP链接
- 浏览器使用解析后的IP地址,尝试与Web服务器建立TCP链接
- 通过TCP三次握手(客户端发送SYN包 —> 服务器响应SYN-ACK包 —> 客户端发送ACK包确认), 若是 HTTPS,还需要 TLS ( 协商加密算法 )握手过程 ,建立链接,确保浏览器和服务器之间可以互相通信
4.发起HTTP请求
- 建立好连接后,浏览器构造HTTP请求报文(请求行、请求头、请求体)
- 浏览器发送GET请求(或POST)
- 携带 Header、Cookie、Token 等信息
5.服务器处理请求
- 服务器接收到http请求,开始处理请求
- 服务器可能需要查询数据库,生成动态内容,或者从文件系统读取静态资源
6.服务器响应
- 服务器处理完成后,返回HTTP响应,其中包括HTTP状态码、响应头信息、响应数据等
- 如果请求资源不存在或者发生错误,服务器将会返回相应的状态码
7.渲染页面
- 解析HTML构建DOM树
- 解析CSS构建CSSOM树
- 合并DOM和CSSOM形成render树
- 布局render树(Layout/reflow),负责个元素尺寸、位置的计算
- 绘制render树(paint),将其转化为屏幕上的像素
- 完成页面加载: 当资源加载完成,浏览器会触发onload事件,表示页面加载完成
- 页面渲染完成:用户可以看到页面内容,页面渲染完成
es6新增了什么
1.变量声明
新增let、const块级作用域,代替var带来的函数作用域问题
2.箭头函数
const fn = (x) => x + 1
3.模版字符串
`Hello, ${name}`
4.解构赋值
对象解构:const { name, age } = obj
数组解构:const [a, b] = arr
5.引入Promise
解决回调地狱问题
三种状态:pending / fulfilled / rejected
支持 .then()、.catch() 链式调用
6.Symbol 类型(新基本数据类型)
独一无二的值,用于对象属性名,避免冲突
7. Set 和 Map 数据结构
Set:值唯一的集合 ,
Map:键值对集合,键可以是任意类型
8. Class 类支持
更清晰的面向对象写法
constructor、super()、继承(extends)
9. for…of 循环(可迭代对象)
10. 模块化语法
// a.js
export const x = 1;
// b.js
import { x } from './a.js';






