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 不变。
  }
}

如何遍历对象

  1. 使用 for…in(遍历可枚举属性,包括继承的属性)
    const obj = { a: 1, b: 2, c: 3 };
    for (let key in obj) {
      console.log(key, obj[key]);
    }
    
  2. 使用 Object.keys()(获取自身可枚举的“键”)
    const obj = { a: 1, b: 2, c: 3 };
    const keys = Object.keys(obj);
      keys.forEach(key => {
      console.log(key, obj[key]);
    });
    
  3. 使用 Object.entries()(获取键值对数组)
    const obj = { a: 1, b: 2, c: 3 };
    const entries = Object.entries(obj);
      entries.forEach(([key, value]) => {
      console.log(key, value);
    });
  4. 使用 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 分钟后服务器更新了资源,客户端还在用旧的缓存内容。
  • 这时只有协商缓存能解决这个问题:它允许服务器来判断资源是否真的需要更新。
    所以强缓存 + 协商缓存是浏览器缓存的两道防线,优先走强缓存,失败后才协商。都失败才会下载新资源,减少带宽浪费,提高性能。

项目中有没有用过缓存策略?怎么设置的?

  1. 请求头缓存(服务端缓存策略)
    配合后台设置缓存头,比如:
    Cache-Control: max-age=3600
    前端可以利用浏览器强缓存,不重复请求资源

  2. 组件层面的缓存 —— 页面切换时保留状态,提高页面切换性能。

🧩 使用场景:
比如在一个“列表页 → 详情页”切换中,返回列表时希望保留滚动位置、搜索条件等状态。
🧩 设置方式:

<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>
  1. 请求接口层面的缓存 —— 减少重复请求、提高响应速度

🧩 使用场景:
做的管理端里,比如新建用户要选择部门,这个下拉框来源于接口 /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"
    }
    前端请求时带上这个标识:
    axios.get('/api/news/list', {
      headers: { 'If-Modified-Since': lastModified }
    })
    
    服务端会判断数据有没有变,如果没变就返回 304 Not Modified,这其实就是「协商缓存」的思想。

浏览器存储的方式有哪些

cookie保存状态 LocalStorage 存储东西

它的诞生是为了解决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种)

  1. 用 Set 只允许存储唯一的值
    const arr = [1, 2, 2, 3, 4, 4]
    const uniqueArr = [...new Set(arr)]
    console.log(uniqueArr) // [1, 2, 3, 4]
    
  2. 利用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]
    
  3. 使用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,发送和接收数据。

顺序

  1. 主线程创建 Worker 实例
  2. 主线程通过 postMessage() 向 Worker 发消息
  3. Worker 线程收到消息(onmessage),开始处理任务
  4. Worker 处理完成,通过 postMessage() 把结果发送回主线程
  5. 主线程通过 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 在组件内使用 访问组件前/离开时逻辑处理
  1. 全局前置守卫
    // 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
  2. 路由独享守卫
{
  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请求

如何解决跨域问题?

  1. CORS(推荐):服务端设置响应头 Access-Control-Allow-Origin 来允许跨域访问
  2. JSONP:兼容性好, 仅限 GET 请求,利用<script> 标签不受同源策略限制,存在XSS 安全隐患,不支持复杂请求/请求头
  3. 代理转发:通过开发服务器中间层(如 webpack devServer 的 proxy)转发请求
  4. nginx 反向代理:线上通过 nginx 转发接口请求到目标服务
  5. 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';