【面试】整理了一些常考的前端面试题,以及实际被问到的问题

🪐背八股文最好结合实际项目,理解式的记下来

JavaScript

一、手写函数

1. 防抖
function debounce(func, delay, immediate) {
  let timer
  return function() { // 闭包
    let _this = this
    let arg = arguments
    if (timer) clearTimeout(timer) // 重复触发,重新开始计时
    if (immediate) { // 立即执行
      let callNow = !timer // 定时器是空的,说明可以执行
      timer = setTimeout(() => { // 开始计时
        timer = null // 计时结束后将timer置空,下一次立即执行
      }, delay);
      if (callNow) { func.apply(_this, arg) }
    } else { // 非立即执行
      timer = setTimeout(() => {
        func.apply(_this, arg) // 计时结束后执行
      }, delay);
    }
  }
}

// 第三个参数为true表示立即执行,为空或者false表示非立即执行
box.addEventListener('mouseover', debounce(function() {
  box.innerHTML = i++
}, 2000, true))
2.节流
// 定时器版
function throttle1(func, wait) {
  let pre = 0
  return function() {
    let _this = this
    let arg = arguments
    let now = new Date()
    if (now - pre > wait) {
      func.apply(_this, arg)
      pre = now
    }
  }
}


// 时间戳版
function throttle2(func, wait) {
  let timer
  return function() {
    let _this = this
    let arg = arguments
    if (!timer) {
      timer = setTimeout(() => {
        timer = null
        func.apply(_this, arg)
      }, wait);
    }
  }
}

box4.addEventListener('mouseover', throttle2(function() {
  box4.innerHTML = i4++
}, 1000))
3. 深拷贝
// 先自己写一个判断数据类型的方法
function typeOf(param) {
  const res = Object.prototype.toString.call(param).replace(/(\[|\]|object)/g, '').trim().toLowerCase()
  return res
}
// 手写深克隆
function deepClone(obj) {
  // 首先判断参数是基本数据类型还是引用数据类型
  if (typeOf(obj) === 'object' || typeOf(obj) === 'array') { // 引用数据类型
    // 进一步判断是对象还是数组
    let newObj = typeOf(obj) === 'object' ? {} : []
    for (let k in obj) {
      if (typeOf(obj[k]) === 'object' || typeOf(obj[k]) === 'array') {
        newObj[k] = deepClone(obj[k]) // 递归克隆
      } else {
        newObj[k] = obj[k]
      }
    }
    return newObj
  } else { // 基本数据类型
    // 直接返回原数据
    return obj
  }
}
const decoObj1 = deepClone(obj)
4. 数组去重/数组乱序
数组去重
// 第一种: 6ES的Set方法
res = [...new Set(arr)]
res = Array.from(new Set(arr)) // [12, 20, 13, 5]

// 第二种:循环比较(拿后一项与当前项比较)
for (let i = 0; i < arr.length - 1; i++) {
  let cur = arr[i] // 当前项
  let args = arr.slice(i +1) // 剩余的项
  const index = args.indexOf(cur)
  if (index !== -1) { // 说明有重复的
    // 遇到重复的,拿最后一项来替换,这样就不需要每次都改变index,结果 [5, 20, 12, 13]
    arr[i] = arr[arr.length - 1]
    i-- // 从当前项(原本的最后一项)开始继续比较
    arr.length-- // 去掉最后一项
  }
}

// 从最后一项开始循环, [12, 20, 13, 5]保证了顺序
for (let i = arr.length-1; i >= 0; i--) {
  let cur = arr[i]
  let args = arr.slice(0, i)
  if (args.indexOf(cur) !== -1) { // 重复项
    arr.splice(i, 1)
  }
}

// 第三种:对象法, 顺序不变[12, 20, 13, 5]
let obj = {}
for (let i = 0; i < arr.length; i++) {
  let cur = arr[i]
    if (obj[cur] === undefined) { // 如果为undefined,表示前面没有与该项相同的
      obj[cur] = cur
    } else { // 如果有相同的,就把当前项删掉
     arr.splice(i, 1)
     i--
  }
}

// 第四种:正则表达式方法,先将数组按升序或者降序排列
arr.sort((a, b) => a - b)
// 将数组转化为字符串
arr = arr.join('@') + '@'
// 定义正则表达式,找到数字后面带有@符号的,1*表示连续匹配0到n次、
const reg = /(\d+@)\1*/g
// 定义新数组用来存放结果
let newArr = []
arr.replace(reg, (val, group1) => {
  newArr.push(Number(group1.split('@')[0]))
  // newArr.push(Number(group1.slice(0, group1.length - 1))) // 也可以
})

res = newArr
数组排序
const arr1 = [2, 5, 3, 4, 7, 1]
// 循环比较:冒泡法
for (let i = 0; i < arr1.length - 1; i++) {
  for (let j = i + 1; j < arr1.length; j++) {
    if (arr1[i] > arr1[j]) { // 小的放前面
      [arr1[i], arr1[j]] = [arr1[j], arr1[i]]
    }
  }
}

const arr2 = [2, 5, 3, 4, 7, 1]
// 快速排序法
const newArr = [arr2[0]]
for (let i = 1; i < arr2.length; i++) {
  const newItem = arr2[i]
  // 从后面开始比,大的直接插入
  for (let j = newArr.length - 1; j >= 0; j--) {
    const curItem = newArr[j]
    if (newItem > curItem) {
      newArr.splice(j+1, 0, newItem)
      break
    }
    if (j === 0) {
      newArr.unshift(newItem)
    }
  }
}

const arr3 = [2, 5, 3, 4, 7, 1]
// 二分法
function quickSort(arr) {
  if (arr.length <= 1) return arr
  const mid = arr[Math.floor(arr.length/2)]
  const[left, right] = [[], []]
  for (let i = 0; i < arr.length; i++) {
    const cur = arr[i]
    if (cur > mid) {
      right.push(cur)
    } else if (cur < mid) {
      left.push(cur)
    }
  }
  return [...quickSort(left), mid, ...quickSort(right)]
}
5. 手写call/bind/apply

三个的功能都是改变this指向
call和bind的参数都是一个一个传的
apply参数是以一个数组的形式传的
call和apply返回函数执行的结果;bind返回一个函数

// 手写call
Function.prototype.myCall = function(context = window) {
  context.fn = this
  const args = [...arguments].slice(1)
  const res = context.fn(...args)
  delete context.fn
  return res
}
// 使用
Person = function() {
  this.name = 'wsq'
}
const Stu = function() {
  this.sayInfo = function(age, height) {
    console.log( `姓名:${this.name}, 年龄:${age}, 身高:${height}`);
  }
}
const stu = new Stu()
stu.sayInfo.myCall(Person, 24, 166) // 一个一个的参数
6. 继承(ES5/ES6)
  1. 原型继承
  2. 构造函数继承
  3. 组合继承(原型继承+构造函数继承)
  4. 寄生组合继承
  5. ES6的extends继承,注意如果有写constructor,则必须加super()
7. sleep函数
function sleep(delay) {
  return new Promise(resolve => {
    setTimeout(resolve, delay)
  })
}

async function run () {
  console.time('run')
  console.log('5-1');
  await sleep(1000)
  console.log('5-2'); // 1s后打印
  await sleep(2000)
  console.log('5-3'); // 1s+2s后打印
  console.timeEnd('run')
}
run()
8. 实现promise
9. 实现promise.all
10. 实现promise.retry(重试)
// 模拟接口调用
function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const num = Math.ceil(Math.random()*10)
      console.log('num', num);
      if (num > 7) {
        resolve(num)
      } else {
        reject('数字小于7执行失败')
      }
    }, 1500);
  })
}

// 再拿一个函数包一层,加上失败重试的功能
Promise.retry = (fn, times, delay) => {
  return new Promise((resolve, reject) => {
    function attempt() {
      fn().then(resolve).catch(err => {
        console.log(`还有${times}次重试机会`);
        if (times-- > 0) {
          setTimeout(() => {
            attempt()
          }, delay);
        } else {
          reject(err)
        }
      })
    }
    attempt()
  })
}
Promise.retry(getData, 3, 1000)
11. 写一个函数,可以控制最大并发数

思路:

  1. 定义成一个类,包含三个变量:_limit(最大并发数)、_curCount(当前并发数)、_taskQueue(待执行任务队列)
  2. 创建实例并执行时,_curCount < _limit执行任务;否则任务存入队列
  3. 添加createTask方法,当任务开始执行时_curCount+1;任务执行完成(finally)时_curCount-1并且从队列取出新的任务执行
class LimitPromise {
  constructor(limit) {
    this._limit = limit // 并发限制数
    this._curCount = 0 // 当前并发数
    this._taskQueue = [] // 如果并发数大于限制数,则把新加的异步操作存到数组
  }
}

// 如果并发数大于最大限制,则将任务存入数组;否则执行任务
LimitPromise.prototype.call = function(asyncFn, ...args) {
  return new Promise((resolve, reject) => {
    const task = this.createTask(asyncFn, args, resolve, reject)
    console.log('this._curCount', this._curCount, this._limit);
    if (this._curCount >= this._limit) {
      // 将任务存入数组
      this._taskQueue.push(task)
    } else {
      task()
    }
  })
}

// 记录并发数,并且从数组中取出任务
LimitPromise.prototype.createTask = function(asyncFn, args, resolve, reject) {
  return () => {
    asyncFn(...args)
      .then(resolve)
      .catch(reject)
      .finally(() => {
      this._curCount--
      if (this._taskQueue.length) {
        const task = this._taskQueue.shift()
        task()
      }
    })

    this._curCount++
  }
}

const limitP = new LimitPromise(3)

// 添加一个sleep函数验证一下
function sleep(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`等待了${delay}`);
      console.timeEnd(`delay${delay}`)
      resolve()
    }, delay * 1000)
  })
}

console.time('delay1')
limitP.call(sleep, 1)
console.time('delay2')
limitP.call(sleep, 2)
console.time('delay3')
limitP.call(sleep, 3)
console.time('delay4')
limitP.call(sleep, 4)
console.time('delay5')
limitP.call(sleep, 5)
console.time('delay6')
limitP.call(sleep, 6)

/**
     * 最大并发数为2时,每一项的时间
     * 1s
     * 2s
     * 4s = 1+3
     * 6s = 2+4
     * 
     * 最大并发数为3时,每一项的时间
     * 1s
     * 2s
     * 3s
     * 5s = 1+4
    */
12. 实现instanceof
function myInstanceOf(left, right) {
  const RP = right.prototype
  while(true) {
    if (left === null) {
      return false
    }
    if (left === RP) {
      return true
    }
    left = left.__proto__
  }
}

function Person() {}
const p = new Person()

console.log(p instanceof Person);
console.log(myInstanceOf(p, Person));
13. 手写new
function myNew(fn) {
  // 创建一个空对象
  let obj = {}
  // 让新对象的隐式原型__proto__指向原对象的显式原型prototype
  obj.__proto__ = fn.prototype
  // 将构造函数的作用域赋值(call和apply都行)给新对象,即this指向这个新对象
  const res = fn.call(obj)
  console.log(res); // undefined
  console.log(obj); // Stu {name: "na342me"}
  // 判断函数执行有没有返回其他对象,如果有就返回其他对象,如果没有就返回新对象
  if (typeof res === 'object' || typeof res === 'function') {
    return res
  }
  return obj
}
const Stu = function() {
  this.name = 'na342me'
}
const stu = myNew(Stu)
console.log(stu.name); // na342me
14. 实现数组的flat/filter等方法

实现数组扁平化

// 第一种:ES6的flat方法, chrome版本大于69才可以使用
arr = arr.flat(Infinity)

// 第二种:转化为字符串
// 1. 使用toString()转化的方法
arr = arr.toString().split(',').map(val =>Number(val))

// 2.使用JSON.stringify()方法转化为字符串,结合正则表达式的方法
arr = JSON.stringify(arr).replace(/(\[|\])/g, '').split(',').map(val => Number(val))

// 第三种:循环法
// 1.while循环实现 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 17]
// some当有一个满足条件就返回true
while(arr.some(val => Array.isArray(val))) { 
  arr = [].concat(...arr)
}

// 2.递归实现
function fn(arr) {
  let result = []
  for (let i = 0; i <arr.length; i++) {
    const cur = arr[i]
    if (Array.isArray(cur)) {
      result = result.concat(fn(cur))
    } else {
      result.push(cur)
    }
  }
  return result
}
arr = fn(arr)
实现filter:
  1. 首先得知道filter接收哪些参数:
    filter接收两个参数:回调函数和上下文;其中回调函数又接收三个参数:单项/索引/数组本身
  2. 明确filter的特点:
    第一个参数必须是函数
    filter不会改变原数组
  3. 实现:
    声明一个新数组,
    原数组的每一项都拿给回调函数执行,
    将满足条件的存到新数组
    返回新数组
Array.prototype.myFilter = function(fn, thisArr) {
 if (typeof fn !== 'function') return
 let result = []
 const arr = this
 for (let i = 0; i < arr.length; i++) {
   if (fn.call(thisArr, arr[i], i, arr)) { // 执行过滤回调,将满足条件的存入新数组
     result.push(arr[i])
   }
 }
 return result
}

const arr = [1, 2, 3, 4, 5]
const res = arr.myFilter(function (val) {
 console.log(this); // {a: 2}
 return val > 3
}, {a: 2})
console.log(arr, res); // [1, 2, 3, 4, 5],  [4, 5]

二、ES6相关

1. let/const/var的区别
  1. var有变量提升,可以在变量赋值以后再声明;可以多次声明相同名称的变量。
  2. let和const声明的变量只在它们所在的{}内有效,而var声明的可能会变成全局变量
  3. const声明的是常量,不可修改(对象的属性可以修改,但是不能修改引用地址)
console.log(c); // undefined var声明的变量变成了全局变量
if (true) {
  var c = '你好'
  let d = '张三' // let声明的变量只在当前{}内有效
  }
console.log(c); // 你好
console.log(d); // Uncaught ReferenceError: d is not defined
2. 箭头函数和普通函数的区别
3. 变量的结构赋值
  1. 数组的结构赋值
  2. 对象的结构赋值
  3. 原始值的解构赋值
// 数组的及结构赋值
const arr = [1, 2, 3, 4]
const [a, b, ...c] = arr
console.log(a, b, c); // 1, 2, [3, 4]

// 对象的结构赋值
const obj = {
  a: 'a',
  b: 'b',
  c: 'Lucy',
  d: 'd'
}
// c: name给属性c设置别名name
const {a, c: name, ...d} = obj
console.log(a, name, d); // a, Lucy, {b: 'b', d: 'd'}


// 原始值的解构赋值(用于同时声明多个变量)
const [name, age] = ['张三', 17]
// 你好!我叫张三,我今年17岁了!
console.log(`你好!我叫${name},我今年${age}岁了!`);
4. promise/async/await/generator的区别

三者都是解决异步编程的方案!

  • promise
    promise可以理解成是一个容器,里面保存着某个未来才会结束的事件(异步操作)的结果。
    • promise特点:
      ⅰ. 状态不受外界影响。promise有三种状态:pending/fulfilled/rejected。只有异步操作的结果才能决定当前是哪种状态,其他任何操作都不会改变它的状态。
      ⅱ. 状态一旦改变,就不会再变,任何时候都可以得到这个结果。promise状态的变化只有两种:pending->fulfilled或pending->rejected。一旦状态发生变化就凝固了不会再变。
    • promise缺点:
      ⅰ. promsie无法取消,一旦创建就会立即执行,无法中途取消
      ⅱ. 如果不设置回调函数,promise内部抛出的错误不会反应到外部
      ⅲ. 当处于pending状态时,无法知道目前是处于什么阶段(是刚刚开始还是即将完成)
      ⅳ. promise真正执行回调的时候,定义promise那部分实际上已经走完了,所以promise的报错堆栈上下文不太友好。
function myPromise(isResolve, val) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      isResolve ? resolve(val) : reject(val)
    }, 1500);
  })
}
myPromise(true, 'promsie执行成功').then(res => {
  console.log('res', res); // promsie执行成功
})
  • generator(生成器)
    • generator是一个普通函数,有两个特征:function关键字后面带星号* / 函数体内部使用yield表达式;
    • 执行generator函数会返回一个遍历器(iterator)对象,可以通过调用next一次返回函数内部的每一个状态;
    • 调用generator函数后,函数并不执行,要调用next执行,返回一个对象{value:xxx, done: true/false};
    • 可以使用for…of遍历generator函数生成的对象
function* myGenerator() {
  yield myPromise('false', 'Generator执行失败')
  return 'end'
}

const myGener = myGenerator()
myGener.next().value.then(res => {
  console.log(res); // Generator执行失败
})


function* helloGenerator() {
  yield 'hello'
  yield 'world'
  return 'ending'
}
const hello = helloGenerator()
// 手动执行
// console.log(hello.next()) // {value: 'hello', done: false}
// console.log(hello.next()) // {value: 'world', done: false}
// console.log(hello.next()) // {value: 'ending', done: true}
// console.log(hello.next()) // {value: undefined, done: true}

// 使用for...of遍历
for (const item of hello) {
  console.log(item); // 分别打印hello  world
}
  • async/await
    • async/await实际上是对generator生成器函数的封装,是一个语法糖: async替换generator函数的function后面的星号*;await替换yield;
      比起星号和yield,async/await更加语义化,async表示函数内部有异步操作,await表示需要等待结果;
    • async函数自带执行器,自动执行,无需next()
    • async/await优势:
      ⅰ. 让代码更加简洁,不需要像promise一样写then,不需要写匿名函数处理resolve,避免代码嵌套
      ⅱ. async/await可以让try/catch同时处理异步和同步错误
async function myAsync() {
  const res = await myPromise(true, 'async执行成功')
  console.log(res); // async执行成功
}
myAsync()
  1. async函数的返回值是Promise对象,且async自带执行器,自动执行,无需next
  2. geenerator函数的返回值是iterator对象,可以通过next或for…of遍历执行
  3. Promise状态更加清晰,可以将异步操作以同步操作的流程表达出来,避免层层嵌套的回调函数;promise.then支持链式调用,逻辑清晰;
    promise提供promise.all/promise.race
    缺点
  4. async/await错误处理只能放在try/catch中,不过try/catch 可以同时处理同步和异步错误
  5. await命令只能在async函数中,在普通函数中会报错
  6. async函数可以保留运行堆栈,暂时保存当前执行栈
  7. promise无法取消,一旦创建就会立即执行,无法中途取消
5. ES5/ES6继承的区别
  • ES5:继承是通过prototype或构造函数机制实现的,实质上是先创建子类的实例对象,然后将父类的方法添加到this上(Parent.apply(this))
  • ES6:继承实质上是先创建父类的实例随想this,然后用子类的构造函数修改this。通过extends继承
// ES5
function Parent(){}
function Child() {}
Parent.call(Child, a, b)

/// ES6
class Parent {
  constructor() {
  }
  pMethods(){}
}
class Child extends Parent {
  constructor() {
    super() // 必须
  }
}

三、浏览器缓存/http/垃圾回收机制等

1. 从输入URL到页面呈现经历了什么
  1. 浏览器的地址栏输入地址,并按下回车
  2. 浏览器查找当前url是否存在缓存,并比较缓存是否过期
  3. DNS解析url对应的ip地址
  4. 根据ip建立TCP连接(三次握手)
  5. HTTP发起请求
  6. 服务器处理请求,浏览器接受HTTP响应
  7. 渲染页面,构建DOM树
  8. 关闭TCP连接(四次挥手)
  9. http缓存
  • 缓存分为强缓存和协商缓存,当浏览器访问一个已经访问过的资源(一般是从第二次开始):
  1. 首先看是否命中强缓存,若命中,直接返回缓存的资源,不向服务器发送请求
  2. 若未命中强缓存,向服务器发送请求查看是否命中协商缓存
  3. 若命中协商缓存,服务器返回304状态码,浏览器使用本地缓存
  4. 若未命中协商缓存,则发送请求获取新资源
  • 强缓存
    强缓存有两个字段控制:Cache-Control / Expires
    命中强缓存时,返回的状态码还是200:
    Status Code: 200 (from memory cache) // 内存中的缓存
    Status Code: 200 (from disk cache) // 硬盘中的缓存
    - Expires
    这是http1.0的标准,表示资源过期的时间,如果发送请求的时间在expires设置的时间之前就直接使用本地缓存。
    缺点:Expires是以本地时间戳计时的,而客户端和服务端时间可能不一致,导致缓存的时效不准确。
    - Cache-Control
    主要利用max-age属性判断,它是一个相对值,根据资源第一次的请求时间和max-age计算出缓存过期时间,再拿缓存过期时间与当前请求时间比较,当前请求时间在过期时间之前,说明缓存有效。
    Cache-Control是在服务器端设置的,前端无需处理
    格式:cache-control: public, max-age=31536000 // 表示缓存365天有效
    Cache-Control的常用选项:
    ○ max-age=100 表示缓存100s后过期
    ○ public 客户端和代理服务器都可以缓存,刷新会重新发起请求
    ○ immutable 在缓存有效期内,即使刷新也不会重新发起请求
    ○ private 只让客户端缓存,代理服务器缓存
    ○ no-cache 不允许强缓存,允许协商缓存
    ○ no-store 不允许任何缓存

  • 优先级
    Cache-Control和Expires同时存在的话,Cache-Control优先级高

  • 协商缓存
    协商缓存是在没有命中强缓存之后,浏览器携带http头部的缓存标识向服务器发起请求,由服务器判断缓存是否有效,若命中缓存,则返回304状态码,浏览器直接使用缓存。
    缓存标识有:Last-Modified/If-Modified-Since 和 Etag/If-None-Match

  • Last-Modified/If-Modified-Since

  1. 浏览器第一次请求时,服务器在返回的header中加上Last-Modified(当前资源最后修改时间)
  2. 浏览器再次访问资源,在请求头带上If-Modified-Since,这个值是服务器上次返回的Last-Modified
  3. 服务器对比Last-Modified和If-Modified-Since判断缓存是否有效
  4. 如果有效就返回304状态码,不返回资源和Last-Modified,浏览器读取缓存
  5. 如果缓存失效就返回200 + 资源 + Last-Modified
    缺点:
    1.只能精确到秒
    2.如果在缓存周期内对资源修改了又还原了,按理是可以用缓存的,但是Last-Modified发生了改变导致缓存被判为失效
  • Etag/If-None-Match
  1. 浏览器请求资源,服务器对资源内容进行编码,返回Etag(如果资源发生改变,Etag就会变化)
  2. 再次请求,浏览器在请求头带上If-None-Match,值是服务器上次返回的Etag
  3. 服务器进行校验,如果缓存有效则返回304 + Etag
  4. 否则返回200 + 新资源 + Etag
  • 优先级
    Etag优先级比较高,先校验Etag再校验Last-Modified

    应用中,静态资源(CSS/图片等)使用强缓存;HTML使用协商缓存

  1. CDN缓存
    CDN是内容分发网络(Content Delivery Network)的缩写。是建立并覆盖在承载网之上,由分布在不同区域的边缘节点服务器群组成的分布式网络。其目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”(边缘服务器),使用户可以就近取得所需的内容,提高用户访问网站的响应速度。
    CDN边缘节点缓存机制:一般遵守HTTP标准协议,通过http响应头的cache-control和max-age字段设置cdn的缓存时间:
    a. 客户端向CDN服务器请求数据,先从本地缓存获取数据,如果数据没有过期,直接使用,过期了就向CDN边缘节点请求数据
    b. CDN接受请求,先校验自己本地数据是否过期,未过期,返回客户端数据,过期就向源服务器发送请求,获取数据,进行缓存,返回客户端。
    优点:CDN缓存主要起到客户端跟服务器之间地域的问题,减少延时,分流作用,降低服务器的流量,减轻服务器压力。
2. HTTP发展历程

HTTP版本分为:HTTP 0.9、HTTP 1.0、HTTP 1.1、HTTP 2.0、HTTP 3.0

  • HTTP 0.9
  1. 只有GET方法,后面跟着目标资源的路径
  2. 没有请求头和请求体
  3. 服务器只返回资源,没有返回头信息(没有状态码和错误码)
  4. 只有html文件可以传输,无法传输其他类型的文件
  • HTTP 1.0
  1. 引入了请求头和响应头,都是以key-value形式保存的
  2. 增加了POST、HEAD、OPTION、PUT、DELETE等方法
  3. 引入了协议版本号的概念
  4. 传输的数据不再仅限于文本
  • HTTP 1.1
  1. 增加了PUT、DELETE等新方法
  2. 增加了缓存管理和控制,如etag/cache-control
  3. 增加了长连接keep-alive
  4. 允许响应数据分块(chunked),有利于传输大文件
  5. 强制要求host头(解决一个ip地址对应多个域名的问题),让互联网主机托管成为可能(节约可宽带)
    host头的作用:一个IP地址可以对应多个域名: 一台虚拟主机(服务器)只有一个ip,上面可以放成千上万个网站。当对这些网站的请求到来时,服务器根据Host这一行中的值来确定本次请求的是哪个具体的网站
  • HTTP 2.0
  1. 二进制协议 , 不再是纯文本
  2. 多路复用,可以发起多个请求,废弃了1.1里的管道
  3. 头部压缩,减少数据传输量
  4. 允许服务器主动向客户端推送数据
  5. 增强了安全性,要求加密通信
  • HTTP 3.0
    目前HTTP 3.0处于制定和测试阶段,是未来的全新的HTTP协议,HTTP3.0协议运行在QUIC(Quick UDP Internet Connection是谷歌制定的一种基于UDP的低时延的互联网传输层协议)协议之上,是在UDP的基础上实现了可靠传输,权衡传输速度与传输可靠性并加以优化,使用UDP将避免TCP的队头阻塞问题,并加快网络传输速度,但同样需要实现可靠传输的机制,HTTP3.0不是HTTP2.0的拓展,而是一个全新的协议。
3. HTTP状态码

● 2开头表示成功
○ 200 服务器已成功处理了请求,并提供了请求的网页
● 3开头表示重定向
○ 301 永久重定向
○ 302 临时重定向
○ 304 表示可以在缓存中取数据(协商缓存)
● 4开头表示客户端错误
○ 400 错误请求Bad Request
○ 401 未授权
○ 403 跨域/禁止
○ 404 请求的资源不存在
● 5开头表示服务端错误
○ 500 服务器正在执行请求时发生错误

4. 三次握手/四次挥手
  • 三次握手
  1. 客户端向服务端发送SYN包,进入SYN_SENT
  2. 服务端收到SYN后给客户端返回SYN+ACK,进入SYN_RECEIVE
  3. 客户端再向服务端返回ACK确认,然后开始通信
  • 四次挥手
  1. 客户端向服务端发送FIN,进入FIN_WAIT1状态
  2. 服务端收到后向客户端发送ACK,进入CLOSE_WAIT,客户端进入FIN_WAIT2
  3. 服务端将未完成的数据继续传给客户端,然后发送FIN+WAIT,进入LAST_ACK
  4. 客户端向服务端发送ACK,然后断开链接
5. 跨域的原因/怎么处理
  • 原因
    由于浏览器的同源策略的限制,同源策略是浏览器的一种安全机制,而服务端之间则不存在跨域。
    所谓同源是指协议/主机和端口三者必须都一样,任意一个不同都会产生跨域。
  • 解决跨域的方法
  1. jsonp
    jsonp是利用同源策略涉及不到的“漏洞”,也就是像img的src、link标签的href、script标签的src都没有被同源策略限制。
    但是这些标签只支持get请求。
  2. cors
    通过自定义请求头来让浏览器和服务器进行沟通。
    分为简单请求和非简单请求:
  • 简单请求:
    请求方法是:HEAD、POST、GET其中的一种
    请求头中的字段只有:Accept、Accept-Launage、Content-Language、Last-Even-ID
    Content-Type的值只有三种:application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 非简单请求:
    请求方法为put、delete
    发送JSON格式的ajax
    http中带自定义请求头
header('Access-Control-Allow-Origin:*');//允许所有来源访问
header('Access-Control-Allow-Method:POST,GET');//允许访问的方式

● 对于简单请求:如果浏览器发现是跨域请求,就自动在请求头加上Origin字段,代表请求来自哪个域。服务器收到请求后,根据Origin字段判断是否允许跨域请求通过。具体实现方法是服务器在响应头Access-Control-Allow-Origin字段中设置允许跨域的域名。如果Origin包含在这些值中,则跨域请求通过。
● 对于非简单请求:在发送http请求前,浏览器会先发送一个header为option的“预检”请求。预检请求会事先询问服务器当前域名是否在服务器允许的范围内,只有得到肯定答复后,浏览器才会发起真正的http请求。一定那通过预检请求,接下来的请求就跟简单请求类似。
3. nginx
由于服务器之间不存在跨域问题。可以找一个中间的服务器:
请求时:客户端 -> nginx -> 服务器
响应时:服务器 -> nginx -> 服务器

6. 跨域时如何处理cookie
  • 使用cors处理跨域时,把withCredentials设置为true。
    Credentials表示用户凭证,withCredentials表示允许携带用户凭证(一般是cookie)。
7. 垃圾回收机制有哪些策略

https://wushiqi.yuque.com/u1894743/afzky3/tehts0#Sk67l

8. HTTP和HTTPS的区别
  • HTTP是(HyperText Transfer Protocol)超文本传输协议的缩写,是一种发布和接收HTML页面的方法,被用于在web浏览器和网站服务器之间传递信息。
  • HTTPS是(HyperText Transfer Protocol Secure)超文本传输安全协议的缩写,是一种透过计算机网络进行安全通信的传输协议。是在HTTP的基础上多了一层SSL加密,提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
    区别
  • http是明文传输,数据都是未加密的,安全性较差;https数据传输过程是加密的,安全性较好
  • https需要到CA申请证书,一般需要一定的费用
  • http页面响应速度比https快,因为http使用tcp三次握手建立连接,客户端和服务器需要交换3个包;而https除了交换3个包,还要加上ssl握手需要的9的包,所以一共是12个包
  • http和https使用的是完全不同的连接方式,用的端口也不一样:http是80,https是443
  • https其实就是构建在SSL/TLS之上的HTTP协议,所以要比http更加耗费服务器资源
9. 什么是XSS攻击?如何防御?

XSS是指跨站脚本攻击,用户注入恶意代码,浏览器和服务器没有对用户输入的内容进行过滤,导致用户注入的脚本嵌入到了页面中。

  • XSS攻击分类:
    ○ 反射型:攻击者构造一个有恶意代码的url链接诱导用户点击,服务器收到这个url对应的请求读取出其中的参数然后没有做过滤就拼接到html页面发送给浏览器,浏览器解析执行
    ○ 存储型:攻击者将带有恶意代码的内容发送给服务器(如表单提交),服务器没有过滤就将数据存到数据库,下次在请求这个页面的时候服务器从数据库中读取出内容拼接到html上,浏览器解析之后执行
    ○ dom型:前端js获取到用户的输入没有进行过滤就拼接到html中
  • 预防:
    a. 前/后端对用户输入进行校验,防止不安全的输入被写入网页或数据库中
    b. 利用CSP安全内容策略:CSP本质上是建立白名单,告诉浏览器哪些外部资源可以进行加载和执行,我们只需要配置规则,如何拦截是浏览器实现的。
    有两种方式开启CSP:
    ■ 设置HTTP请求头中的Content-Security-Polilcy
    ■ 设置meta标签的方式
10. 什么是CSRF攻击?如何防御?
  • CSRF攻击
    CSRF(Cross-Site Request Forgecy)跨站请求伪造的缩写。是指攻击者盗用已登录用户的身份,以用户的名义发起恶意请求。如:登录了A银行的网站,然后在未登出的情况下,点击了某个链接(钓鱼网站),这样身份就被盗用了。

  • 防御

  1. 验证HTTP的Referer字段,referer字段记录了该http请求的来源 地址。
  2. 使用验证码,在关键页面加上验证码,后台通过验证码判断是否是csrf攻击,这个方法对用户不太友好
  3. 请求接口加上token
  4. 在http请求头加上自定义字段,如Authorise
11. TCP和UDP的区别
  1. TCP是面向连接的,UDP是无连接的
  2. TCP仅支持单播传输,UDP支持单播、多播、广播
  3. TCP的三次握手保证了连接的可靠性;UDP是无连接/不可靠的一种数据传输协议
  4. UDP的头部开销比TCP小,传输速率更高,实时性好
  5. TCP常用于文件/邮件传输;UDP常用于实时视频会议、广播

[…待续]

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值