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


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

    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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    数组排序
    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)]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    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) // 一个一个的参数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    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
        */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    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));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    实现filter:
    
    • 1
    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    二、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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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}岁了!`);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    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执行成功
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 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
    • 2
    • 3
    • 4
    • 5
    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() // 必须
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    三、浏览器缓存/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');//允许访问的方式
    
    • 1
    • 2

    ● 对于简单请求:如果浏览器发现是跨域请求,就自动在请求头加上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常用于实时视频会议、广播

    […待续]

  • 相关阅读:
    不同类型跨链桥中可能存在的安全隐患
    人工智能、深度学习、机器学习常见面试题41~55
    代码运行出现了堆栈溢出错误及解决方法
    TCO-PEG-FITC 荧光素-聚乙二醇-反式环辛烯 TCO-PEG-荧光素
    【STL】:list的模拟实现
    【JVM】G1垃圾收集器知多少
    C# 短消息提示 窗口位置
    web前端-javascript-标识符(说明,命名规则、不以数字关键字保留字开头、驼峰命名,补充)
    高性能 Java 计算服务的性能调优实战
    git 将本地分支与远程master主分支合并
  • 原文地址:https://blog.csdn.net/qq_42345108/article/details/126027654