前端面试题尚硅谷最新

面试题

JS

说说JS的数据类型

  1. 基本类型

    number/string/boolean

    undefined/null

    symbol/bigint

  2. 引用/对象类型

    Object/Array/Function

    其它内置或自定义类型

  • 注意
    • number/string/boolean对就的包装类型Number/String/Boolean, 在读取属性或调用方法时会自动转换
    • undefined/null 没有对应的包装类型, 不能读取属性或调用方法
    • symbol/bigint也有对应的包装类型, 但不能我们new调用 ==> 说出它的一些特点就行, 开发中很少

说说JS如何判断数据类型

6种判断方式

  1. ===
    undefined
    null
  2. typeof
    string
    number
    boolean
    undefined
    symbol
    bigint
    function
    object
    null
    所有非函数对象
  3. instanceof
    A instanceof B: 判断A或者A原型链上的对象是否是B类型的实例
    可以用于判断某个对象是否某个特定类型的
    区别Object对象与数组对象
  4. obj.constructor
    得到对象的构造函数
    得到number/string/boolean值的包装类型
    null/undefined没有constructor
  5. Object.prototype.toString.call(obj)
    得到构造函数的名称
  6. Array.isArray()
    专门判断数组

说说常见的数组方法

  1. 更新数组的7个
    push / pop / unshift / shift
    splice
    sort / reverse

  2. 遍历元素
    forEach
    map / filter / reduce
    find / findIndex
    every / some

  3. 其它
    slice
    concat
    join
    includes
    indexOf

函数传参是值传递, 还是引用传递?

  • 函数调用时, 是将实参变量的数据拷贝一份赋值给形参变量

  • 只是实参变量数据可能是基本类型值 ==> 值传递

  • 也可能是引用类型的值(也就是地址值) ==> 引用传递/值传递

  • 注意下面的代码, 准确的说不是将a内存的地址赋值给b, 而是将a中保存的地址值赋值给b

    var a = {}
    var b = a
    
    var a1 = 2
    var a2 = {}
    function fn (x) {
      
    }
    fn(a1) // 值传递
    fn(a2) // x = a2  拷贝a2内存中保存的值(地址值)传递给x
    

说说你对作用域与作用域链的理解

  • 作用域

    • 一个变量可以合法使用的范围/区域
    • 作用域起到了隔离变量, 避免了变量重名冲突的问题(也就是允许了不同作用域中可以有同名的变量)
    • 分类:
      • 全局作用域
      • 函数作用域
      • 块作用域 => ES6的let或const变量
  • 作用域链

    • 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
    • 本质: 包含由内向外的多个变量对象的数组 ==> 这个可以不用说
    • 当查找一个变量, 在整个作用域链中都找不到时: 会报引用错误(RefrenceError), 错误信息(message)为这个变量没有定义

说说变量提升与函数提升

  • 变量提升(变量声明提升)
    • 变量声明语句会提升到当前作用域的最前面执行
    • 在变量声明语句之前, 就可以访问到这个变量(undefined)
  • 函数提升(函数声明提升)
    • 函数声明语句提升到当前作用域的最前面执行
    • 在函数声明语句之前, 就可以执行该函数
  • 原因: 简单来说就是在执行全局代码和函数前会进行预解析/处理
    • 将var变量声明放在最前面执行
    • 将function函数声明放在最前面执行
  • 注意
    • const / let / class 没有提升
    • var 变量 / function 函数声明 才有提升

区分执行函数定义与执行函数

  • 执行函数定义: 创建函数对象, 如果指定了函数名, 同时会定义变量并指向这个函数对象
  • 执行函数: 执行函数内部的语句
  • 必须先执行函数定义, 再执行函数 ===> 注意: 函数定义有可能会提升到最上面执行

说说你对闭包的理解

  • 是什么?

    • 通过chrome的debugger调试工具得知: 闭包本质是内部函数中的一个容器(非js对象), 这个容器中包含了引用的变量
  • 如何产生?

    • 嵌套的内部函数引用了外部函数的变量, 当调用外部时就会产生闭包
    • 闭包不是在调用内部函数时产生, 而是在创建内部函数对象时产生
  • 作用?

    • 延长局部变量的生命周期
    • 让函数外部能间接操作内部的局部变量
  • 区别产生闭包与使用闭包及释放闭包?

    • 产生闭包: 内部函数对象创建时产生, 包含那个被引用的变量的容器(不是js对象)
    • 使用闭包: 执行内部函数
    • 释放闭包: 让内部函数对象成为垃圾对象, 断开指向它的所有引用
  • 应用?

    • 举删除删除列表中的的某个商品的例子(带确定框)
    • IIFE
    • 模块编译之后的运行代码
  • 写一个简单的闭包程序

    function fn1() {
      var a = 2;
      var b = 3;
      function fn2() {
        a++;
        console.log(a);
      }
     return fn2;
    }
    var f = fn1();
    f();
    f();
    f = null;
    

如何判断函数中的this?

  • 常规情况下, 函数中的this取决于执行函数的方式(四种绑定规则)
    • 默认绑定: 直接调用 fn() => 严格模式下是undefined, 非严格模式下是window
    • 隐式绑定: 通过对象调用 obj.fn()=> 对象
    • 显式绑定: 通过call/apply/bind调用 fn.call/apply/bind()=> 第一个参数对象
    • 构造函数绑定: 通过new调用 new fn() => new出来的对象
  • 特殊情况:
    • 箭头函数 > this是? 外部作用域的this》 沿着作用域链去外部找this
    • bind(obj)返回的函数 ==> this是? obj
    • 回调函数 它不是我们调用的
      • 定时器/ajax/promise/数组遍历相关方法回调 ==> this是? window
      • DOM事件监听回调 ==> 发生事件的DOM元素
      • vue控制的回调函数(生命周期/methods/watch/computed) ==> this是? 组件的实例
      • React控制的生命周期回调, 事件监听回调 ==> this是? 组件对象 / undefined

如何改变(指定)函数中的this?

  • 情况一: 将任意对象指定为当前函数中的this
    • 函数立即调用: call() / apply()
    • 函数后面某个时候调用: bind()
  • 情况二: 将外部的this指定为当前函数中的this
    • ES6之前: 将外部this保存为其它名称变量, 当前函数中不使用this, 而使用这个变量
    • ES6之后: 箭头函数

说说原型与原型链

  • JavaScript 中的原型(prototype)和原型链(prototype chain)是理解对象和继承机制的关键概念。原型(prototype):每个 JavaScript 对象都有一个关联的原型对象,它是一个普通对象,包含一些共享的属性和方法。当你访问对象的属性或方法时,如果对象本身没有这个属性或方法,JavaScript 引擎会沿着对象的原型链去查找。对象可以通过 prototype 属性来访问它的原型对象。原型链(prototype chain):原型链是由一系列连接的原型对象组成的链。当访问对象的属性或方法时,如果对象本身没有找到,引擎会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(Object.prototype)。这样的搜索路径就构成了原型链。

instanceof的内部原理和自定义实现

  • 作用:

    • 判断一个任意类型对象的具体类型
  • instanceof内部如何判断?

    • 对于 A instanceof B
    • A是实例对象, B是构造函数 (Object Function)
    • 如果B的prototype属性所指向的原型对象是A实例对象的原型链接上的某个对象, 返回true, 否则返回false
  • 自定义instanceof功能函数

    /* 
    自定义instanceof工具函数: 
      语法: myInstanceOf(obj, Type)
      功能: 判断obj是否是Type类型的实例
      实现: Type的原型对象是否是obj的原型链上的某个对象, 如果是返回true, 否则返回false
    */
    function myInstanceOf(obj, Type) {
      // 得到原型对象
      let protoObj = obj.__proto__
    
      // 只要原型对象存在
      while(protoObj) {
        // 如果原型对象是Type的原型对象, 返回true
        if (protoObj === Type.prototype) {
          return true
        }
        // 指定原型对象的原型对象
        protoObj = protoObj.__proto__
      }
    
      return false
    }
    

编码实现继承

  • 基于构造函数的继承

    • 原型链 + 借用构造函数的组合式继承
      • 让子类的原型为父类的实例: Student.prototype = new Person()
      • 让子类型原型的构造器为子类型: Student.prototype.constructor = Student
      • 借用父类型构造函数: Person.call(this, name, age)
    // 父类型
    function Person(name, age) {
        this.name = name
        this.age = age
    }
    
    Person.prototype.fn = function () {}
    Person.prototype.sayHello = function () {
    	console.log(`我叫${this.name}, 年方${this.age}`)
    }
    // 子类型
    function Student(name, age, price) {
        // this.name = name
        // this.age = age
        // 借用父类型的构造函数
        Person.call(this, name, age)  // 相当于执行this.Person(name, age)
        this.price = price
    }
    // 让子类的原型为父类的实例
    Student.prototype = new Person()
    // 让原型对象的构造器为子类型
    Student.prototype.constructor = Student
    // 重写方法
    Student.prototype.sayHello = function () {
    	console.log(`我叫${this.name}, 年方${this.age}, 身价: ${this.price}`)
    }
    
    const s = new Student('tom', 23, 14000)
    s.sayHello()
    s.fn()
    
  • 基于ES6的类的继承

    • 子类 extends 父类: class Teacher extends Person2
    • 子类构造器中调用父类的构造: super(name, age)
    // 父类
    class Person2 {
        constructor (name, age) {
            this.name = name
            this.age = age
        }
    
        fn () {}
    
        sayHello () {
        	console.log(`我叫${this.name}, 年方${this.age}`)
        }
    }
    // 子类
    class Teacher extends Person2 {
        constructor (name, age, course) {
            super(name, age)
            this.course = course
        }
    
        // 重写父类的方法
        sayHello () {
        	console.log(`我叫${this.name}, 年方${this.age}, 课程:${this.course}`)
        }
    }
    
    const t = new Teacher('bb', 34, 'CC')
    t.sayHello()
    t.fn()
    

说说面向对象的三大特征

  • 封装:

    • 将可复用的代码用一个结构包装起来, 后面可以反复使用
    • js的哪些语法体现了封装性: 函数 ==> 对象 ==> 模块 ==> 组件 ==> 库
    • 封装都要有个特点: 不需要外部看到的必须隐藏起来, 只向外部暴露想让外部使用的功能或数据
  • 继承

    • 为什么要有继承? 复用代码, 从而减少编码
    • js中的继承都是基于原型的继承: ES6的类本质也是
    • 编码实现: 原型链+借用构造函数的组合 / ES6的类继承
  • 多态: 多种形态

    • 理解
      • 声明时指定一个类型对象, 并调用其方法,
      • 实际使用时可以指定任意子类型对象, 运行的方法就是当前子类型对象的方法
    • JS中有多态:
      • 由于JS是弱类型语言, 在声明时都不用指定类型
      • 在使用时可以指定任意类型的数据 ==> 这已经就是多态的体现了

说说JS的垃圾回收机制

  • 在JS中对象的释放(回收)是靠浏览器中的垃圾回收器来回收处理的

  • 垃圾回调器

    • 浏览器中有个专门的线程, 它每隔很短的时间就会运行一次
    • 主要工作:判断一个对象是否是垃圾对象, 如果是, 清除其内存数据,并标记内存是空闲状态
  • 如何判断对象是垃圾对象呢?

    • 机制1:引用计数法
    • 机制2:标记清除法
    • V8垃圾回调机制: 分代回收
  • 垃圾回收机制1:引用计数法

    • 最初级的垃圾收集算法: 判断一个对象的引用数,引用数为0就垃圾对象,引用数大于0就不是垃圾对象
    • 问题: 有循环引用问题
      • 如果2个对象内部存在相互引用,断开对象的引用后, 它们还不是垃圾对象
  • 垃圾回收机制2:标记-清除法

    • 现代垃圾回收算法的基础: 将可达的对象标记起来,不可达的对象当成垃圾回收
    • 从根对象(也就是window)开始递归深度查找所有引用的对象, 并标记为‘活动’,没有标记为活动的对象就是垃圾对象
    • 没有循环引用问题
    • 问题: 反复对所有可达的对象进行遍历标记, 但有的对象存话时间是较长的,这样效率太低了
  • V8垃圾回收机制: 分代回收

    • 将堆分为两个空间,一个叫新生代区,一个叫老生代区

    • 对象初始创建时都在新生代, 经过几次收集后还在存在, 就会转移到老生代存储

      新生代区 老生代区
      大小 较小(1-8M), 包含2个子空间 较大
      对象的特点 存活周期短 存活周期长
      回收器 副垃圾回收器 主垃圾回收器
      核心算法 Scavenge算法 Mark-Sweep && Mark-Compact算法
    • 新生代 (以空间换时间)

      • 1、标记活动对象和非活动对象(根据可达性)
      • 2、复制from-space的活动对象到to-space中并进行排序
      • 3、清除from-space中的非活动对象
      • 4、将from-spaceto-space进行角色互换,以便下一次的Scavenge算法垃圾回收
    • 老生代

      • Mark-Sweep算法(标记清理)

        • 标记阶段:对老生代对象进行第一次扫描,对活动对象进行标记

        • 清理阶段:对老生代对象进行第二次扫描,清除未标记的对象,即非活动对象

        • 问题: 清除非活动对象之后,留下了很多零零散散的空位, 不利于对象分配空间

      • Mark-Compact算法(标记整理)

        • 把剩下的活动对象,整理到内存的一侧,整理完成后,直接回收掉边界上的内存

区别一下内存溢出与内存泄漏

  • 内存溢出

    • 运行程序需要分配的内存超过了系统能给你分配的最大剩余内存

    • 抛出内存溢出的错误,程序中断运行

    • 演示代码

      const arr = []
      for (let index = 0; index < 100000000; index++) {
      	arr[index] = new Array(1000)
      }
      
  • 内存泄漏

    • 理解: 当程序中的某个内存数据不再需要使用, 而由于某种原因, 没有被释放

    • 常见情况:

      • 意外的全局变量

        function fn () {a = new Array(100000)}
        fn()
        
      • 没有及时清除的定时器

        this.intervalId = setInterval(() => {}, 1000)
        // clearInterval(this.intervalId)
        
      • 没有及时解绑的监听

        this.$bus.$on('xxx', this.handle)
        // this.$bus.$off('xxx')
        
      • 没有及时释放的闭包

区别一下同步与异步

  • 同步:
    • 从上往下按顺序依次执行
    • 只有将一个任务完全执行完后, 才执行后面的
    • 会阻塞后面的代码执行
  • 异步
    • 启动任务后, 立即向下继续执行, 等同步代码执行完后才执行回调函数
    • 不会阻塞后面的代码执行
    • 异步回调函数会即使触发了, 也是要先放入队列中待执行, 只有当同步任务或前面的异步任务执行完才执行

说一下JS的事件循环机制

  • JS内部是通过事件循环机制来实现单线程异步执行执行的

  • 分析事件循环机制

    • 所有任务(同步/异步)都在主线程上执行,形成一个执行栈
    • 执行栈之外有用于存储待执行异步回调的任务队列(task queue) ==> 准备的说是2个(宏列队与微队列)
    • 浏览器中有在其它分线程执行相关管理模块
      • 定时器管理模块
      • ajax请求管理模块
      • DOM事件管理模块
    • JS引擎执行代码的顺序为
      • 在执行栈中执行初始化同步代码
      • 执行过程中如果有启动异步任务, 交给对应的管理模块处理, 管理模块会在后面特定时间将回调函数放入任务队列中待执行
      • 在执行栈中所有代码都执行完后, 依次取出任务队列中的回调到执行栈中依次执行
  • 宏任务与微任务

    • script(整体代码)
    • setTimeout / setInterval,
    • Ajax
    • DOM事件监听
    • postMessage (H5, 向其它窗口分发异步消息)
    • setImmediate(Node.js 环境)
  • 宏队列与微队列

    • Promise
    • async & await
    • mutationobserver(H5, 监视DOM元素变化)
  • 整体的执行顺序

    1. script(整体代码)

    2. 所有微队列中的微任务

    3. 宏队列中的第一个宏任务

    4. 所有微队列中的微任务

      后面3-4循环处理

说说ES6的promise

  • ES6推出的更好的异步编程解决方案(相对于纯回调的方式)
    • 解决嵌套回调的回调地狱问题 ==> 通过promise.then的链式调用
    • 指定读取结果数据的回调函数时机更灵活 ==> 请求后/请求前/请求完成后都可以
  • promise对象有3种状态
    • pending
    • resolved/fulfilled
    • rejected
  • promise状态的2种变化
    • pending --> resolved
    • pending --> rejected
  • 如何改变promise的状态
    • 调用resolve()
    • 调用reject()
    • throw error

说说ES6中Promise的then方法

  • then指定成功和失败的回调, 用于得到成功/失败的结果数据
  • then()总是返回一个新的promise
  • 新promise的结果状态由then指定的回调函数执行的结果决定
    • 抛出错误 => 失败且reason就是抛出的错误
    • 返回失败的promise => 失败且reason是返回的promise的reason
    • 返回成功的promise => 成功且value是返回的promise的value
    • 返回其它任何值 => 成功且value是返回的值
    • 返回pending的promise => pending的promise

区别Promise的all与race方法

  • Promise.all([p1, p2, p3])
    • 接收包含多个promise的数组, 返回一个新的promise
    • 只有当所有接收的promise都成功了, 返回的promise才成功, 且成功的value为所有成功promise的value组成的数组
    • 一旦有一个失败了, 返回的promise就失败了, 且失败的reason就是失败promise的reason
  • Promise.race([p1, p2, p3])
    • 接收包含多个promise的数组, 返回一个新的promise
    • 返回的promise的结果由第一个完成的promise决定

说说async与await的理解和使用

  • async与await是异步编程的终极解决方案 => 消灭回调函数
  • 作用: 简化promise对象的使用, 不用再使用then/catch来指定回调函数
  • 使用
    • await一般在结果为promise的表达式左侧
    • async在await所在函数定义的左侧
  • 注意:
    • 调用async函数得到是一个promise, 其结果状态由async函数体执行的结果决定
    • await的右侧也可以不是promise, 如果不是, 直接返回表达式的值

var、let、const之间的区别

  • const定义常量, let和var定义变量
  • let相对于var
    • 有块作用域
    • 没有变量提升
    • 不会添加到window上
    • 不能重复声明

箭头函数的特点

  • 编码简洁
  • 没有自己的this, 使用外部作用域中的this, 不能通过bind来绑定this
  • 不能通过new来创建实例对象
  • 内部没有arguments, 可以通过rest参数来代替

ES6的模块化语法

  • 导出语法: 整个模块总是一个对象, 所有导出语法都是向模块对象中添加属性或方法
    • 分别导出: export const a = 2
    • 默认导出: export default 3
    • 统一导出: export {b, c}
  • 导入
    • 静态导入
      • import {a} from ‘./test’
      • import value from ‘./test’ import {default as xxx} from ‘./test’
      • import * as test from ‘./test’
      • 静态导入的模块打包到一起
    • 动态导入
      • import(‘./test’).then(module => {})
      • 动态导入的模块会被单独打包

说出几个ES6常用新语法

  • const与let
  • 箭头函数
  • 解构赋值
  • 形参默认值
  • rest/剩余参数
  • 类语法: class / extends / constructor / static /super
  • 扩展运算符: …
  • 模板字符串
  • 异步语法: promise / async & await
  • 对象的属性与方法简写
  • 模块化语法: export / default / import / import()
  • set / map

说说事件冒泡与事件委托

  • 事件冒泡
    • 事件在传递给目标元素后, 会由内向外传递给外层的元素处理
  • 事件委托
    • 不直接给多个子元素绑定多个事件监听, 而是给它们共同的父元素绑定一个监听
    • 当操作任意子元素时, 事件会冒泡到父元素上处理
    • 在事件回调中通过event.target得到发生事件的目标元素, 并进行相关处理

HTTP请求的常用响应状态码

  • 2XX: 表示成功处理请求, 如200(成功), 201(添加数据成功)
  • 3XX: 需要重定向, 浏览器直接跳转, 如302(重定向)
  • 4XX: 客户端请求错误, 如: 401(token失效), 404 (不存在)
  • 5XX: 服务器端错误, 如: 500

对原生ajax进行简单的封装

/* 
xhr + promise 封装一个异步ajax请求的通用函数  简洁版
ajax ('xxx.json')
*/
function ajax(url) {
  return new Promise((resolve, reject) => {
    // 创建一个XHR对象
    const xhr = new XMLHttpRequest()
    // 初始化一个异步请求(还没发请求)
    xhr.open('GET', url, true)
    // 绑定状态改变的监听
    xhr.onreadystatechange = function () { 
        /*
        ajax引擎得到响应数据后
        	将xhr的readyState属性指定为4
        	将响应数据保存在response / responseText属性上
        	调用此回调函数
        */
        
      // 如果状态值不为4, 直接结束(请求还没有结束)
      if (xhr.readyState !== 4) {
        return
      }
      // 如果响应码在200~~299之间, 说明请求都是成功的
      if (xhr.status>=200 && xhr.status<300) {
        // 指定promise成功及结果值
        resolve(JSON.parse(xhr.responseText))
      } else { // 请求失败了
        // 指定promise失败及结果值
        reject(new Error('request error staus '+ request.status))
      }
    }
    xhr.send()
  })
}

说说你对跨域问题的理解和解决办法

  • 同源: 协议, 域名, 端口, 三者都相同
  • 同源策略
    • ajax请求时, 浏览器要求当前网页和Server必须同源(安全), 否则会抛出跨域的错误
    • 加载image/link/script不受同源策略限制
  • 解决ajax跨域问题的办法
    • JSONP
    • CORS
    • Proxy

说说你知道的ajax跨域解决方案

  • JSONP: 利用script发跨域请求目标接口, 得到响应数据
  • CORS: 浏览器直接请求跨域的目标接口, 服务器返回响应头告诉浏览器允许跨域
  • Proxy: 浏览器发同源请求代理服务器, 代理服务器转发请求跨域的目标接口

说说你项目中的axios的二次封装

  1. 配置通用的基础路径和超时

  2. 显示请求进度条

    1. 显示进度条: 请求拦截器回调
    2. 结束进度条: 响应拦截器回调
  3. 成功返回的数据不再是response, 而直接是响应体数据response.data

  4. 统一处理请求错误, 具体请求也可以选择处理或不处理

  5. 每个请求自动携带userTempId的请求头: 在请求拦截器中实现

  6. 如果当前有token, 自动携带token的请求头

  7. 对token过期的错误进行处理

说说axios的整体执行流程

  • 基本执行顺序

    • 请求拦截器的回调函数
    • xhr发请求
    • 响应拦截器成功/失败的回调
    • 具体请求成功/失败的回调
  • 内部原理: 通过promise.then的链式调用将这4个任务串连越来, 依次执行并进行数据传递

    Promise.resolve(config)
    .then((config) => {  // 请求拦截器
        // 显示进度/携带token/userTempId
        return config
    })
    .then((config) => {  // 使用xhr发ajax请求
        return new Promise((resolve, reject) => {
            // 根据config创建xhr对象发送异步ajax
            // 如果请求成功(响应状态码200--299之间)
            const response = {
                data: JSON.parse(xhr.responseText),
                status: xhr.statusCode
            }
            resolve(response)
            // 如果请求失败
            reject(new Error('请求失败 code ' + xhr.statusCode))
        })
    })
    .then( // 响应拦截器
        response => {  
        	return response.data
        },
        error => {
        	throw error
        }
    )
    .then(result => { // 特定请求的回调
    
    }).catch(error => {
    
    })
    

从输入url到渲染出页面的整个过程

  1. 得到服务器对应的IP地址 ==> DNS解析
  2. 通过IP连接上服务器: 3次握手
  3. 向服务器发请求, 接收服务器返回的响应
  4. 解析响应数据(html/css/js)显示页面
    解析html => dom树
    解析css => cssom树
    解析js => 更新dom树/cssom树
    生成渲染树 = dom树 + cssom树
    布局
    渲染
  5. 断开连接:4次挥手

说说前台数据存储

存储方式:
	cookie: 会话/持久化两种
	sessionStorage
	localStorage
	数据库(很少用)
区别:
	---------------------sessionStorage VS localStorage
						刷新在/ 关闭不在       关闭还在
	---------------------cookie VS sessionStorage与localStorage
    - 容量                小
    - 请求时是否自动携带     会
    - API易用性            不好用
    - 浏览器是否可禁用       可

自定义call和bind

/* 
自定义函数对象的call方法
*/
function call (fn, obj, ...args) {
  // 如果传入的是null/undefined, this指定为window
  if (obj===null || obj===undefined) {
    // obj = window
    return fn(...args)
  }
  // 给obj添加一个方法: 属性名任意, 属性值必须当前调用call的函数对象
  obj.tempFn = fn
  // 通过obj调用这个方法
  const result = obj.tempFn(...args)
  // 删除新添加的方法
  delete obj.tempFn
  // 返回函数调用的结果
  return result
}

/* 
  自定义函数对象的bind方法
  重要技术:
    闭包
    call()
    三点运算符
*/
function bind (fn, obj, ...args) {
  return function (...args2) {
    return call(fn, obj, ...args, ...args2)
  }
}

区别函数节流与防抖

1. 事件高频触发处理的问题
	如果更新界面 => 界面更新卡顿
	如果发送ajax请求 => 发送了很多没必要的请求
2. 解决办法
	函数节流
	函数防抖
3. 区别:
	当事件高频发生很多次时, 防抖只执行最后一次, 而节流执行少量几次
4. 应用场景
	节流: 窗口调整(resize)/ 页面滚动(scroll)/ OM元素的拖拽功能实现(mousemove)
	防抖: 输入搜索联想功能

自定义函数节流与防抖

/* 
用于产生节流函数的工具函数
*/
function throttle (callback, delay) {
  // 用于保存处理事件的时间, 初始值为0, 保证第一次会执行
  let start = 0
  // 返回事件监听函数 ==> 每次事件发生都会执行
  return function (event) {
    console.log('---throttle')
    // 发生事件的当前时间
    const current = Date.now()
    // 与上一次处理事件的时差大于delay的时间
    if (current-start>delay) {
      // 执行处理事件的函数
      callback.call(event.target, event)
      // 保证当前时间
      start = current
    }
  }
}

/* 
用于产生防抖函数的工具函数
*/
function debounce (callback, delay) {
  // 返回事件监听函数 ==> 每次事件发生都会执行
  return function (event) {
    console.log('---debounce')
    // 如果还有未执行的定时器, 清除它
    if (callback.timeoutId) {
      clearTimeout(callback.timeoutId)
    }
    // 启动延时delay的定时器, 并保证定时器id
    callback.timeoutId = setTimeout(() => {
      // 执行处理事件的函数
      callback.call(event.target, event)
      // 删除保存的定时器id
      callback.timeoutId = null
    }, delay);
  }
}

自定义数组扁平化

/* 
数组扁平化: 取出嵌套数组(多维)中的所有元素放到一个新数组(一维)中
  如: [1, [3, [2, 4]]]  ==>  [1, 3, 2, 4]
*/
/*
方法一: 递归 + reduce() + concat() + some()
*/
function flatten1 (array) {

  return array.reduce((pre, item) => {
    if (Array.isArray(item) && item.some((cItem => Array.isArray(cItem)))) {
      return pre.concat(flatten1(item))
    } else {
      return pre.concat(item)
    }
  }, [])
}

/*
方法二: ... + some() + concat()
*/
function flatten2 (arr) {
  // 只要arr是一个多维数组(有元素是数组)
  while (arr.some(item => Array.isArray(item))) {
    // 对arr进行降维
    arr = [].concat(...arr)
  }
  return arr
}

自定义new

/* 
自定义new工具函数
  语法: newInstance(Fn, ...args)
  功能: 创建Fn构造函数的实例对象
  实现: 创建空对象obj, 调用Fn指定this为obj, 返回obj
*/
function newInstance(Fn, ...args) {
  // 创建一个新的对象
  const obj = {}
  // 执行构造函数
  const result = Fn.apply(obj, args) // 相当于: obj.Fn()
  // 如果构造函数执行的结果是对象, 返回这个对象
  if (result instanceof Object) {
    return result
  }
  
  // 给obj指定__proto__为Fn的prototype
  obj.__proto__ = Fn.prototype
  // 如果不是, 返回新创建的对象
  return obj
}

自定义深拷贝

/* 
1). 大众乞丐版
  问题1: 函数属性会丢失
  问题2: 循环引用会出错
*/
export function deepClone1(target) { // 从后台获取的数据都可以用
  return JSON.parse(JSON.stringify(target))
}

/* 
获取数据的类型字符串名
*/
function getType(data) {
  return Object.prototype.toString.call(data).slice(8, -1)  // -1代表最后一位
}

/*
2). 面试基础版本
  解决问题1: 函数属性还没丢失
*/
function deepClone2(target) {
  const type = getType(target)

  if (type==='Object' || type==='Array') {
    const cloneTarget = type === 'Array' ? [] : {}
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        cloneTarget[key] = deepClone2(target[key])
      }
    }
    return cloneTarget
  } else { // 基本类型 / function
    return target
  }
}

/* 
3). 面试加强版本
  解决问题2: 循环引用正常
*/
function deepClone3(target, map = new Map()) {
  const type = getType(target)
  if (type==='Object' || type==='Array') {
     // 从map容器取对应的clone对象
    let cloneTarget = map.get(target)
    // 如果有, 直接返回这个clone对象
    if (cloneTarget) {
      return cloneTarget
    }
    cloneTarget = type==='Array' ? [] : {}
    // 将clone产生的对象保存到map容器
    map.set(target, cloneTarget)
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        cloneTarget[key] = deepClone3(target[key], map)
      }
    }
    return cloneTarget
  } else {
    return target
  }
}
deepClone3(obj)

/* 
4). 面试加强版本2(优化遍历性能)
    数组: while | for | forEach() 优于 for-in | keys()&forEach() 
    对象: for-in 与 keys()&forEach() 差不多
*/
function deepClone4(target, map = new Map()) {
  const type = getType(target)
  if (type==='Object' || type==='Array') {
    let cloneTarget = map.get(target)
    if (cloneTarget) {
      return cloneTarget
    }

    if (type==='Array') {
      cloneTarget = []
      map.set(target, cloneTarget)
      target.forEach((item, index) => {
        cloneTarget[index] = deepClone4(item, map)
      })
    } else {
      cloneTarget = {}
      map.set(target, cloneTarget)
      Object.keys(target).forEach(key => {
        cloneTarget[key] = deepClone4(target[key], map)
      })
    }

    return cloneTarget
  } else {
    return target
  }
}

实现数组的冒泡排序和sort排序

/* 
冒泡排序的方法
*/
function bubbleSort (array) {
  // 1.获取数组的长度
  var length = array.length;
  // 2.反向循环, 因此次数越来越少
  for (var i = length - 1; i >= 0; i--) {
    // 3.根据i的次数, 比较循环到i位置
    for (var j = 0; j < i; j++) {
      // 4.如果j位置比j+1位置的数据大, 那么就交换
      if (array[j] > array[j + 1]) {
        // 交换
        // const temp = array[j+1]
        // array[j+1] = array[j]
        // array[j] = temp
        [array[j + 1], array[j]] = [array[j], array[j + 1]];
      }
    }
  }
  return arr;
}

const products = [{price: 23, sales: 103}, {price: 22, sales: 101}]
/*
根据销量升序
*/
products.sort((p1, p2) => {  // 比较函数  ==> 返回数值   如果大于o, p2放在左边
    return p1.sales - p2.sales
})

说说重排(回流)与重绘

  • 页面显示过程
    • 解析HTML生成DOM树
    • 解析CSS生成CSSOM树
    • 解析JS更新DOM树和CSSOM树
    • DOM树 + CSSOM树生成渲染树
    • 布局(也称回流, 确定个节点显示的位置)
    • 渲染绘制

前端面试题尚硅谷最新_第1张图片

  • 更新DOM或Style

    • 可能会导致局部重排(也称回流, 重新布局)
    • 可能会导致局部重绘
  • 注意:

    • 重排肯定会重绘, 但重绘不一定有重排
    • 重排比重绘开销更大, 更消耗性能
  • 哪些操作会导致重排

    • 浏览器窗口尺寸改变
    • 元素位置和尺寸发生改变的时候
    • 新增和删除可见元素
    • 内容发生改变(文字数量或图片大小等等)
    • 元素字体大小变化。
    • 激活CSS伪类(例如::hover)。
    • 设置style属性
    • 查询某些属性。比如说:
      offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
  • 哪些操作会导致重绘

    • 更新元素的部分属性(影响元素的外观,风格,而不会影响布局),比如
      visibility、outline、背景色等属性的改变。
  • 例子代码

    var s = document.body.style;
    s.padding = "2px"; // 回流+重绘
    s.border = "1px solid red"; // 再一次 回流+重绘
    s.color = "blue"; // 重绘
    s.backgroundColor = "#ccc"; // 重绘
    s.fontSize = "14px"; // 再一次 回流+重绘
    document.body.appendChild(document.createTextNode('abc!'));// 添加node,再一次 回流+重绘
    
  • 如何减少重排次数

    • 更新节点的样式, 尽量通过类名而不是通过style来更新
    • 分离样式的读定操作,不要将读写操作混合调用
    • 将DOM操作离线处理,比如使用DocumentFragment

Vue

1. 区别v-if与v-show

  • 隐藏: v-if干掉标签, v-show通过样式来隐藏
  • 重新显示: v-if需要重新创建标签对象, v-show只需要修改样式显示出来就可以
  • v-show重新显示更快, 但隐藏时还占用着内存空间: 以空间换时间
  • v-show更适合切换频繁/需要隐藏的DOM结构比较大
  • 使用v-if解决模板中初始解析undefined的bug 比如: {{a.b.c}} a初始为一个空对象

2. 为什么v-for与v-if不适合一起使用

  • 对遍历的item数据进行限制判断
    • 问题: 如果使用v-if, 每个数组元素都会解析指令来判断 ==> 效率低
    • 解决: 不使用v-if, 使用计算属性, 过滤产生一个子数组 ==> 效率高
  • 根据外部的数据判断
    • 问题: 如果在当前标签上用v-if, 执行n次 ==> 效率低
    • 解决: 添加一个/在父标签, 使用v-if, 执行1次 ==> 效率高

3. computed与watcher以及method的区别

  • computed与watch的区别
    • 计算属性必须同步返回计算结果, 而watch中可以在异步操作后更新数据显示
    • watch可以深度监视, 计算属性只是监视了使用到的数据
    • 选择:
      • 如果是根据现在的数据同步计算就可以确定要显示的另一个数据 ==> computed
      • 如果涉及到异步操作/深度监视 ==> watch
      • 一旦一个数据变化, 我们需要做一系列操作 ===> watch
  • computed与method的区别
    • 计算属性有缓存, 多次读取显示只计算一次
    • method, 多处显示计算多次

4. 说说Vue的常用指令

  • v-text
  • v-html
  • v-show
  • v-if / v-else / v-else-if
  • v-for
  • v-on
  • v-once: 只初始渲染一次, 用于优化更新性能
  • v-bind
  • v-model
  • v-slot: 插槽

5. Vue组件的生命周期

  • 单个组件生命周期
    • 初始化:
      • beforeCreate: 不能通过this读取data数据和调用methods中的方法
      • 执行了一些初始化的准备工作
      • created: 可以通过this读取data数据和调用methods中的方法
      • 编译模板
      • beforeMount: 不能通过ref读取到页面中内容
      • 挂载编译好的模板, 显示页面
      • mounted: 能通过ref读取到页面中内容
    • 更新: this.msg += ‘–’
      • beforeUpdate (在数据更新后, 界面更新前调用): 读取的数据是最新的, 但页面是老的
      • 更新页面
      • updated: 读取的数据和页面都是新的
    • 死亡: $destroy()
      • beforeDestroy: 做一些收尾的工作, 比如: 清除定时器/解绑监听/…
      • destroyed

6. Vue父子组件的生命周期顺序

  • 初始化:
    • beforeCreate
    • created
    • beforeMount
    • –child beforeCreate
    • –child created
    • –child beforeMount
    • –child mounted
    • mounted
  • 更新:
    • beforeUpdate
    • –child beforeUpdate
    • –child updated
    • updated
  • 死亡:
    • beforeDestroy
    • – child beforeDestroy
    • – child destroyed
    • destroyed

7. 区别组件的钩子函数 actived与mounted

  • mounted:初始化执行一次
  • activated: 初始化mounted之后 / 每次再回到当前路由

8. 说说对动态组件、缓存组件与异步组件的理解

  • 动态组件
    • 通过的is属性动态加载一个组件
    • is属性初始为A组件名, 加载A组件, 切换为B组件名, 加载B组件
  • 缓存组件
    • 默认路由组件离开或动态组件被切换, 组件都会立即死亡
    • 能让原本要死亡的组件不死亡, 在背后缓存起来, 后面需要时, 直接使用缓存的
    • 可以通过include与exclude属性来控制哪些组件要缓存或不缓存
  • 异步组件
    • 在引入组件时使用import动态引入: const Home = () => import(‘./Home.vue’)
    • 组件会被单独打包, 且只有在第一次访问时才会请求加载对应的打包文件 ==> 减小首屏打包文件打小
    • 除了组件, 其它模块也可以异步懒加载

9. 说说对递归组件的理解

  • 递归组件: 组件内部有自己的子组件标签

  • 应用场景: 用于显示树状态结构的界面

  • 注意: 递归组件必须有name

  • 编码: 实现一个简单的可开关的树状结构界面的 Tree 组件

    
    
    
    

10. Vue组件间有哪些通信方式

  • 根据组件间关系分类

    1. 父向子
      props(非函数)
      v-model
      $refs, $children
      插槽
    2. 子向父
      props(函数)
      vue自定义事件
      v-model
      .sync
      $parent
      作用域插槽
    3. 祖孙间
      a t t r s 与 attrs与 attrslisteners 与v-bind/v-on配合使用
      provide与inject
    4. 兄弟或其它/任意
      全局事件总线
      Vuex
  • 另一种分类方式

    1. 属性相关

    ​ props

    ​ v-model

    a t t r s 与 attrs与 attrslisteners

    ​ 作用域插槽(子向父传递)

    1. 自定义事件相关

    ​ 自定义事件

    ​ 全局事件总线

    ​ v-model

    ​ .sync

    1. 其它

    ​ $refs, $children, $parent

    ​ provide与inject

    ​ 插槽

    ​ vuex

11. 子向父通信有哪些方式

  • props(函数)
  • vue自定义事件
  • v-model
  • .sync
  • $parent
  • 作用域插槽

12. 说说Vue的自定义事件与全局事件总线

  • vue自定义事件

    • 实现子组件向父组件通信

    • 相关语法:

      • 父组件中绑定自定义事件监听:

        child.$on(‘eventName’, callback)

      • 子组件中分发事件

        this.$emit(‘eventName’, 2)

    • 应用:
      elment-ui的组件的事件监听语法都用的是自定义事件
      我们项目中的组件也用了不少自定义事件

  • 全局事件总线

    • 实现任意组件间通信
    • 编码
      将入口js中的vm作为全局事件总线对象:
      beforeCreate() {
      Vue.prototype.KaTeX parse error: Expected 'EOF', got '}' at position 20: …= this }̲ 传递数据的组件分发事…bus. e m i t ( ′ e v e n t N a m e ′ , d a t a ) 接收数据的组件处理监听 : t h i s . emit('eventName', data) 接收数据的组件处理监听: this. emit(eventName,data)接收数据的组件处理监听:this.bus.$on(‘eventName’, (data) => {})
    • 应用:
      前台项目中使用全局事件总线
    • 理解: 为什么将vm放到Vue的原型对象上, 所有组件都可见呢?
      • VueComponent.prototype = Object.create(Vue.prototype);
      • VC的原型对象的原型对象就是Vue的原型对象

13. Vue响应式数据原理

大佬的图文解释

  • 简洁表达

    • 对象: 通过Object.defineProperty()添加setter方法来监视属性数据的改变 + 订阅-发布

    • 数组: 重写更新数组元素的一系列方法 + 订阅-发布

      • 调用原生的对应对数组元素进行相应的操作
      • 更新界面去
  • 详细表达(主要说对象的)

    • 初始化

      • 实现数据代理
        • 通过defineproperty给vm定义与data中属性对应的带getter/setter的属性
        • 在getter中, 读取data中对应的属性值返回 => 读取this.msg => 读取的是data中msg属性值
        • 在setter中, 将最新值保存到data对应的属性上 => this.msg = ‘abc’ => 'abc’会保存到data的msg上
      • 创建observer
        • 目标: 对data中所有层次的属性进行监视/劫持
        • 通过defineproperty给data中所有层次属性, 都重新定义, 加上getter与setter
          • getter: 用来建立dep与watcher的关系
          • setter: 用来当data数据发生改变去更新界面
        • 为data中所有层次的属性创建一个对应的dep ==> 用来将来更新界面的
      • 创建compile
        • 目标1: 实现界面的初始化显示 / 目标2: 为将更新做准备
          • 为模板中每个包含表达式(事件表达式除外)的节点创建一个对应的watcher
          • 给watcher绑定用于更新对应节点的回调函数
          • 将watcher添加到n个对应的dep中
    • 更新

      • this.msg = ‘abc’
      • 由于有数据代理 ==> data的msg更新为了’abc’
      • 由于有数据劫持 ==> data中msg的setter调用了
      • 在setter中, 通过对应的dep去通知所对应的watcher去更新对应的节点 ==> 使用了订阅发布模式
  • Vue数据响应式原理结构图

前端面试题尚硅谷最新_第2张图片

14. Vue双向数据绑定原理

  • 通过v-model来实现双向数据绑定
  • v-model的本质
    • 将动态的data数据通过value属性传给input显示 ==> data到view的绑定
    • 给input标签绑定input监听, 一旦输入改变读取最新的值保存到data对应的属性上 ==> view到data的绑定
  • 双向数据绑定是在单向数据绑定(data–>view)的基础, 加入input事件监听(view ==> data)

15. s e t 和 set和 setnextTick的使用场景

这里是有案例的解释

  • 项目功能: 列表项点击动态显示输入框, 并自动获取焦点
  • 编码实现:
    • 动态给列表项数据对象添加edit属性为true ==> 标识显示输入框
    • 获取当前input对象, 调用focus() ==> 获取焦点
  • 问题
    • 向响应式对象上直接点添加属性不是响应式 ==> 输入框不会显示 ==> 使用$set添加edit属性
    • 得不到input, 调用focus会报错 ==> 因为界面还没有更新, 还没有input ==> 使用$nextTick指定在DOM更新后才去执行回调, 在回调中获取input

16. 区别MVVM与MVC

  • MVVM: 前台的技术
    • M: Model模型, 也就是包含数据的js对象 ==> data对象
    • V: View视图,动态显示模型对象中的数据的页面(前台渲染) ==> 模板页面
    • VM: ViewModel视图模型, 通过vm读取model中的数据显示到view上, 同时view输入数据改变, vm也可以将输入数据保存到model中 ==> Vue/组件的实例
  • MVC: 后台的技术
    • M: Model(模型)包含从数据库中查询得到的数据的对象
    • V: View(视图)动态显示模型对象中的数据的页面(后台渲染)
    • C: Controller(控制器)接收用户提交的请求参数, 操作数据库生成动态数据并产生模型对象

17. Vue.use()做了什么

  • 对象插件: 调用插件对象install方法(传入Vue)来安装插件(执行定义新语法的代码)
  • 函数插件: 直接将其作为install来调用(传入Vue)来安装插件(执行定义新语法的代码)

18. 说说Vue的mixin技术

  • 用来复用多个组件中相关的js代码的技术
  • 将多个组件相同的js代码提取出来, 定义在一个mixin中配置对象
  • 在多个组件中通过mixins配置引入mixin中的代码, 会自动合并到当前组件的配置中

19. Vue的组件data为什么必须是一个函数

  • 同一个组件的多个组件实例的data必须是不同的对象(内容初始数据可以相同)
  • 如果是data是对象, 组件的多个实例共用一个data对象
  • 如果是函数, 组件对象通过调用函数得到的一个新的data对象

20. vuex的5大属性

  • state
  • mutations
  • actions
  • getters
  • modules
  • namespaced: true

21. vuex的数据流结构图

前端面试题尚硅谷最新_第3张图片

22. vuex中的mutation可以执行异步操作吗?

  • 功能可以 ==> 异步更新数据后界面确实会自动更新
  • 问题 ==> vuex的调试工具监视不到mutation中的异步更新, 工具记录还是更新前的数据(不对)
  • 扩展: 工具如何记录数据变化? ==> 每次mutation函数执行完后, 立即记录当前的数据 ==> 在mutation中同步更新state, 才能被记录到

23. vuex中的状态数据的响应式的原理?

  1. 创建了一个vm对象

  2. state中的数据都是vm的data数据(是响应式的)

  3. 组件中读取的state数据本质读取的就是data中的数据

  4. 一旦更新了state中的数据, 所有用到这个数据的组件就会自动更新

    new Vue({
    	data: {
            home: {
                categoryList: [],
                xxx: {}
            },
            user: {
                userInfo: {}
            }
    	}
    })
    

24. vuex数据刷新丢失的问题

  • 绑定事件监听: 在卸载前保存当前数据
window.addEventListener('beforeunload', () => { // 当页面刷新时, 页面卸载前的事件回调
	sessionStorage.setItem('CART_LIST_KEY', 
		JSON.stringify(this.$store.state.shopCart.cartList))
})

window.removeEventListener('beforeunload')
  • 在初始时读取保存数据作为状态的初始值
cartList: JSON.parse(sessionStorage.getItem('CART_LIST_KEY')) || [],

25. 路由组件间通信方式

  • query参数
  • params参数
  • props(需要配置, 而不是标签属性)
    • true ==> 将路由的params参数映射成props ==> 只能传递param参数
    • 对象 ==> 将对象中的属性映射成props ==> 只能传递自定义的参数
    • route => {} ==> 将函数返回的对象中的属性映射成props ==> 能会传递params和query参数和自定义的
      meta(也是配置)
  • meta
  • vuex

26. 跳转携带的参数, 刷新就丢失了

​ 如果注册没有指定/:xxx的点位, 而跳转时通过params配置携带的参数数据, 刷新时就会丢失

​ 因为url中没有携带的参数数据路径

27. 编程式路由跳转到当前路由, 参数不变, 会报出错误?

  • 前一个项目没这个问题, 后一个项目有问题

  • 3.1.0版本(2019.8)没这个问题, 3.1.0这后才有这个问题

    • 3.1.0之前: 返回值为undefined
      • push(location)
      • push(location, () => {}, () => {})
    • 3.1.0之后: 如果没有指定回调函数返回promise对象
      • push(location).then(() => {}).catch(() => {})
    • vue-router在3.1.0版本(2019.8)引入了push()的promise的语法, 如果没有通过参数指定回调函数就返回一个promise来指定成功/失败的回调, 且内部会判断如果要跳转的路径和参数都没有变化, 会抛出一个失败的promise
    • 说明文档: https://github.com/vuejs/vue-router/releases?after=v3.3.1
  • 解决:

    • 办法1: 在每次push时指定回调函数或catch错误

      push('/xxx', () => {})   ===> 
      push('/xxx').catch(() => {})
      
    • 办法2: 重写VueRouter原型上的push方法 (比较好)

      • 1). 如果没有指定回调函数, 需要调用原本的push()后catch()来处理错误的promise
      • 2). 如果传入了回调函数, 本身就没问题, 直接调用原本的push()就可以
    const originPush = VueRouter.prototype.push
    VueRouter.prototype.push = function (location, onComplete, onAbort) {
      console.log('push()', onComplete, onAbort)
      // 判断如果没有指定回调函数, 通过call调用源函数并使用catch来处理错误
      if (onComplete===undefined && onAbort===undefined) { // 使用的新语法
        return originPush.call(this, location).catch(() => {})
      } else { // 如果有指定任意回调函数, 通过call调用源push函数处理
        return originPush.call(this, location, onComplete, onAbort)
      }
    }
    
  • 扩展问题

    声明式路由跳转之所有没有问题, 是因为默认传入了成功的空回调函数

28. history与hash路由的区别和原理

  • 区别:

    • history: 路由路径不#, 刷新会携带路由路径, 默认会出404问题, 需要配置返回首页

      • 404:

        • history有: 刷新请求时会携带前台路由路径, 没有对应的资源返回
        • hash没有: 刷新请求时不会携带#路由路径
      • 解决:

        • 开发环境: 如果是脚手架项目本身就配置好

          ==> webpack ==> devServer: {historyApiFallback : true}

          当使用 HTML5 History API 时, 所有的 404 请求都会响应 index.html 的内容

      • 生产环境打包运行:

        • 配置nginx

          location / {
            try_files $uri $uri/ /index.html; # 所有404的请求都返回index页面
          }
          
    • hash: 路由路径带#, 刷新不会携带路由路径, 请求的总是根路径, 返回首页, 没有404问题

  • 原理:

    • history: 内部利用的是history对象的pushState()和replaceState() (H5新语法)
    • hash: 内部利用的是location对象的hash语法
      • 写hash路径 location.hash = ‘#/xxx’
      • 读hash路径: location.hash
      • 监视hash路径的变化: window.onhashchange = () => {}

29. 如何实现登陆后, 自动跳转到前面要访问的路由界面

  • 在全局前置守卫中, 强制跳转到登陆页面时携带目标路径的redirect参数

    if (userInfo.name) {
      next()
    } else {
      // 如果还没有登陆, 强制跳转到login
      next('/login?redirect='+to.path)  // 携带目标路径的参数数据
    }
    
  • 在登陆成功后, 跳转到redirect参数的路由路径上

    await this.$store.dispatch('login', {mobile, password})
    // 成功了, 跳转到redirect路由 或 首页
    const redirect = this.$route.query.redirect
    this.$router.replace(redirect || '/')
    

30. 路由导航守卫的理解和使用

导航守卫是什么?

  • 导航守卫是vue-router提供的下面2个方面的功能
    • 监视路由跳转 -->回调函数
    • 控制路由跳转 --> 放行/不放行/强制跳转到指定位置 next(path)
  • 应用
    • 在跳转到界面前, 进行用户权限检查限制(如是否已登陆/是否有访问路由权限)
    • 在跳转到登陆界面前, 判断用户没有登陆才显示

导航守卫分类

  • 全局守卫: 针对任意路由跳转

    • 全局前置守卫

      router.beforeEach((to, from, next) => {
        // ...
      })
      
    • 全局后置守卫

      router.afterEach((to, from) => {})

  • 路由独享守卫

    • 前置守卫

      {
      	path: '/foo',
      	component: Foo,
      	beforeEnter: (to, from, next) => {}
      },
      {
      	path: '/ff',
      	component: Foo,
      },
          
      
  • 组件守卫: 只针对当前组件的路由跳转

    • 进入

      beforeRouteEnter (to, from, next) {
        // 不能使用this, this是undefined
        next((comp) => { // 延迟到组件对象创建后才执行
            // comp就是组件对象
        })
      },
      
  • 更新:

    beforeRouteUpdate (to, from, next) {}

  • 离开

    beforeRouteLeave (to, from, next) {}

31. 请说明key的作用和原理

1. 虚拟DOM的key的作用?
    1). 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用
    2). 详细的说: 当列表数组中的数据发生变化生成新的虚拟DOM后, 进行新旧虚拟DOM的diff比较
        a. 有一个对应的key
            item数据没变, 直接使用原来的真实DOM
            item数据变了, 对原来的真实DOM进行数据更新
        b. 没有一个对应的key
           根据item数据创建新的真实DOM显示
2. key为index的问题
    1). 添加/删除/排序 => 产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低
    2). 如果item界面还有输入框 => 产生错误的真实DOM复用 ==> 不仅效率低, 界面效果也有问题
    注意: 如果不存在添加/删除/排序操作, 用index没有问题
3. 解决:
    使用item数据的标识数据作为key, 比如id属性值

32. 理解Vue的虚拟DOM

一个较轻的普通JS对象    ==> 真实DOM是一个较重的对象   => 轻重看对象内部属性多少
虚拟DOM对象有时也称为虚拟节点对象(vNode), 它包含了用于生成一个真实DOM/Node的必要信息, 比如:
标签结构:
    
  • abc1
  • abc2
虚拟DOM/节点 { tagName: 'ul', props: { id: 'list', }, children: [ {tagName: 'li', props: {}, children: ['abc1'], key: '1'}, {tagName: 'li', props: {}, children: ['abc2'], key: "2"}, ] }

33. 说说Vue的 Diff算法

 目标: 
 	比较的结果是要确定: 哪些原来真实DOM可以复用(但内部内容可能要更新), 要创建哪些真实DOM
 处理流程
  	只做同层比较 => 虚拟DOM也是一个倒立树状态结构, 只进行同层比较, 这样比较次数少,效率高
  	确定要比较的新旧虚拟节点
    	没有key: 依次比较 ==> 多出的虚拟DOM, 直接创建新的真实DOM
    	有key: 找同名的key比较 ==> 没有找到, 直接创建新的真实DOM
  	先比较标签名
    	如果不同, 直接创建新的真实DOM
    	如果相同, 复用原来对应的真实DOM => 如果数据内容有变化, 更新真实DOM内部内容
 简单流程: 同层比较 => 比较key => 比较标签名 => 复用

34. nextTick的理解

是什么?
	官方描述: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
    在数据更新后, 调用nextTick(callback),在callback中才可以读取到更新后的DOM
使用场景?
	更新数据后, 想操作更新后的DOM
	项目功能1: 使用swiper实现动态轮播
	项目功能2: 动态显示input输入框后, 需要自动获取焦点
原理?
	更新数据后, DOM不会立即更新, 而是在所有数据都变化完后, 将DOM更新作为一个异步任务统一执行
	理解Tick: 取出队列中的一个回调任务到调用栈中执行就是一个tick, 有时也指定队列中的一个回调任务
	nextTick(callback): 将callback指定为下一个异步回调任务执行
	为什么要先更新数据再调用nextTick()呢?  
		第一个数据更新才触发将DOM更新放入任务队列
		这样nextTick指定的callback就是放在DOM更新任务之后
		这样callback中才能读取到更新后的DOM
	nextTick中用的是哪个异步技术呢?
  		简单说: 优先使用微任务, 如果不支持才选择宏任务
  		详细说: promise => MutationObserver => setImmediate => setTimeout

前端面试题尚硅谷最新_第4张图片

前台 PC 项目

1. 有哪些功能模块?

首页
商品搜索列表
商品详情
购物车
登陆与注册
订单交易/结算
支付
个人中心/订单列表

2. 使用了哪些库?

vue
vue-router
vuex
vee-validate
vue-lazyload

axios
mockjs
nprogress
uuidjs

swiper
qrcode
lodash

3. 使用了哪些组件间通信方式

- 自定义事件
	Pagination组件 ==> 内部改变当前页码后分事件通知父组件, 并传递出最新的页码
- 全局事件总线
	Search组件与Header组件 ==> Search组件内部清除关键字条件时, 通过全局事件总线分发事件通知Header组件清除输入

4. axios二次封装

1). 配置通用的基础路径和超时
2). 显示请求进度条
3). 成功返回的数据不再是response, 而直接是响应体数据response.data
4). 统一处理请求错误, 具体请求也可以选择处理或不处理
5). 每次请求都携带一个userTempId请求头, 数据值在state中
6). 每次请求(已登陆)都携带一个token请求头, 数据值在state中
7). 对token失效的401错误, 进行处理

5. 如何封装组件?

实现静态组件: 模板/样式写好
设计从外部接收的数据: props
设计内部的数据: data
设计基于props和data的计算属性数据: computed
根据props和data数据和computed进行动态显示
更新数据, 更新界面, 通知父组件

可以以Pagination组件为例分析

6. 说说分类导航功能

先动态请求显示一级列表, 鼠标移入某个分类项显示对应的二三级列表(可能要请求)
点击某个分类项, 跳转到搜索页面, 携带分类条件参数

- 使用编程式导航代替声明式导航
  - router-link太多 ==> 创建很多组件对象 ==> 占用内存大, 效率低

- 优化事件处理效率
  - 利用事件委托: event.target
  - 理解事件委托与事件冒泡

- 如何携带点击的分类的数据?
  - event.target得到a标签
  - 利用自定义的data标签属性来保存分类信息

- 对mouseEnter高频事件进行节流处理
  - 使用lodash的throttle进行节流处理
  - 对lodash库实现按需引入

7. 说说搜索功能

- 准备各种搜索条件
	- category1Id: '', // 一级分类ID
    - category2Id: '', // 二级分类ID
    - category3Id: '', // 三级分类ID
    - categoryName: '', // 分类名称
    - keyword: '', // 关键字
    - trademark: '', // 品牌  "ID:品牌名称"
    - props: [], // 商品属性的数组: ["属性ID:属性值:属性名"] 示例: ["2:6.0~6.24英寸:屏幕尺寸"]
    - order: '1:desc', // 排序方式  1: 综合,2: 价格 asc: 升序,desc: 降序  示例: "1:desc"
    - pageNo: 1, // 当前页码
    - pageSize: 10, // 每页数量
删除搜索关键字条件
	清除输入框中的关键字   ===> 全局事件总线
删除分类条件/关键字条件
	直接发请求 ==> 有问题: 地址栏上的条件参数没有删除
  - 解决:  重新跳转到search, 不再携带要删除条件参数, search组件监视路由的变化, 发请求获取数据

8. 说说购物车功能

- 购物车数据是保存在后台的, 标识是什么?
  - 未登陆: 标识为用户临时ID(userTempId)
    - 第一次访问时前台利用uuid库生成的唯一字符串, 保存保存在local中
    - 每次请求时通过请求头自动携带它(利用请求拦截器)
  - 登陆: 登陆用户的token对应的userId
    - 用户请求登陆时, 服务器端生成并返回给浏览器, 浏览器收到后自动保存到local中
    - 每次请求时通过请求头自动携带它(利用请求拦截器)
- 进入购物车页面 ==> 请求获取购物车列表显示
- 修改购物项数量
	- 提交请求时, 携带商品的skuid和数量(变化的)
	- 对点击进行节流限制
- 删除购物项(一个/多个)
  - 请求接口, 携带一个skuId或多个skuId的数组
  - 参数: skuId的数组   [2,3]
- 勾选购物项(一个/多个)
  - 请求接口, 携带一个skuId或多个skuId的数组 和 是否勾选的标识数据(0/1)

9. 说说注册/登陆/自动登陆的流程

  • 注册流程

    • 前台: 输入注册需要的相关信息(用户名/密码/…), 进行前台表单校验, 如果不通过, 提示错误

    • 前台: 发送注册的ajax请求(post), 携带注册接口需要的相关数据(用户名/密码/…)

    • 后台: 获取到注册请求携带的参数, 去数据库中判断是否已经存在

      • 如果已经存在, 返回提示此用户已存在的提示信息
      • 如果不存在, 保存到数据库, 返回成功的数据
    • 前台: 接收到响应

      • 如果是不成功的数据, 提示
      • 如果是成功的数据, 自动跳转到登陆页面
  • 登陆流程

    • 前台: 输入登陆需要的相关信息(用户名/密码), 进行前台表单校验, 如果不通过, 提示错误
    • 前台: 发送登陆的ajax请求(post), 携带登陆接口需要的相关数据(用户名/密码)
    • 后台: 获取到登陆请求携带的参数, 去数据库中查询看是否存在
      • 如果不存在, 返回登陆失败的信息
      • 如果存在, 生成一个新的token字符串, 将token与用户信息一起返回
    • 前台: 接收到响应
      • 如果是不成功的数据, 提示
      • 如果是成功的数据,
        • 将用户信息和token都保存到vuex中
        • 将token保存到localStorage中 ==> 不保存用户信息
        • 跳转到首页或redirect页面
  • 自动登陆流程

    • 前台: 页面一加载时, 发送请求根据token获取用户信息
    • 后台: 得到请求头中的token值, 解析出里面包含的userId和失效时间,
      • 已经失效了: 返回代表token失效的401的错误响应
      • 还在有效期内: 根据userId查询数据库, 得到用户信息返回

10. 说说项目的优化

  • 懒加载
    • 组件懒加载 => const Home = () => import(‘./Home.vue’) ==> 预加载
    • 图片懒加载 => vue-lazyload
  • 函数节流
    • 鼠标移入显示对应的子分类列表 ==> 避免快速划过每个分类都显示子分类
    • 点击+/-修改购物项数量 ==> 避免快速点击, 增加服务器压力

11. 说一个记忆深刻的开发过程中的问题

  • 问题: 编程式路由跳转到当前路由, 参数不变, 会报出错误

  • 3.1.0版本(2019.8)没这个问题, 3.1.0这后才有这个问题

  • 3.1.0之前: 返回值为undefined

    • push(location)
    • push(location, () => {}, () => {})
    • 3.1.0之后: 如果没有指定回调函数返回promise对象
      • push(location).then(() => {}).catch(() => {})
    • vue-router在3.1.0版本(2019.8)引入了push()的promise的语法, 如果没有通过参数指定回调函数就返回一个promise来指定成功/失败的回调, 且内部会判断如果要跳转的路径和参数都没有变化, 会抛出一个失败的promise
  • 解决: 重写VueRouter原型上的push方法 (比较好)

    • 1). 如果没有指定回调函数, 需要调用原本的push()后catch()来处理错误的promise
    • 2). 如果传入了回调函数, 本身就没问题, 直接调用原本的push()就可以
  • 扩展问题

    • 声明式路由跳转之所有没有问题, 是因为默认传入了成功的空回调函数

后台/中台admin项目

1. 有哪些功能模块?

商品管理功能
	分类查询
	品牌管理
	平台属性管理
	SPU管理
	SKU管理
权限管理
	权限数据(用户/角色/菜单)管理  CRUD
	权限(路由(页面)/按钮的)控制
其它功能:
	优惠管理
	订单管理
	客户管理
	数据报表可视化

2. 使用了哪些库?

vue
vue-router
vuex
element-ui
axios
nprogress
lodash
echarts

3. 使用了哪些组件间通信方式

	自定义事件
		CategorySelector
		SpuForm
	v-model
		el-input
		el-select
	.sync
		控制SpuForm的隐藏
		Dialog/Drawer
	$attrs与v-bind
	$listeners与v-on
		封装HintButton
	ref
		SpuForm/SkuForm数据初始化加载
	插槽
		默认插槽
			通过标签体向子组件传入标签结构
			Table / Form/ Upload/...
		命名插槽
			通过标签体向子组件传入多种不同的标签结构
			Dialog / Upload
		作用域插槽
			决定父组件传递什么标签结构的数据在子组件中
			el-table-column
	vuex
		集中管理状态数据
		多模块编程
            user
            routes
            app

4. 封装了哪些组件?

- HintButton
	- 带标题提示的按钮
	- elementUI组件: ToolTip / Button
	- 使用$attrs与$listeners
	
- CategorySelector
	- 通过Select动态显示三级分类列表
	- 当选择某个分类时, 分发事件通知父组件, 并携带分类ID和级别值

5. 说说登陆和自动登陆的流程

前端面试题尚硅谷最新_第5张图片

注意: 与前台项目的登陆接口不同的返回的数据只有token, 没有用户信息

6. 使用elementUI构建界面相关

模板项目: vue-admin-template    二次开发
element-ui快速搭建项目界面
对element-ui实现按需引入打包
按需引入打包bug: 显示PopConfirm组件的背景是透明的(不是白色)
    原因: 白色背景的样式是PopOver组件提供, 而我们没有对PopOver进行按需引入打包,
    	最终就没有它的样式, 那背景就是透明的
    解决: 引入并注册PopOver

7. 使用深度作用域选择器的理解和应用

    scoped的作用和原理
        作用: scoped样式只能影响当前组件和子组件的根标签
        原理: 加了scoped后产生了哪些变化
            标签: 当前组件的所有原生标签和子组件的根标签都添加了同一个且唯一的data自定义属性
                
选择器: 在选择器的最右边加上了当前data自定义属性的选择器 ==> 只有可能匹配上当前组件和子组件的根标签(子组件的子标签没有这个属性) .box .title[data-v-6777eaa2] { color: red; } deep的作用和原理 deep的作用: 让我们的样式在scoped下也可以影响子组件内部子标签的样式 使用: 原生css: >>> css预编译器: /deep/ (vue-cli3之前) 或 ::v-deep (vue-cli3及之后) 原理: data自定义属性选择器就会从最右侧转移到deep声明的位置 ==> 对子组件的内部子标签没有当前data属性的条件限制 ==> 那就可以匹配了 .box { ::v-deep .title { color: red; } } .box[data-v-6777eaa2] .title { color: red; } 应用: 让抽屉组件Drawer形成垂直滚动 修改轮播组件Carousel的分页指示器样式

8. 深克隆技术的理解和应用

功能: 平台属性修改取消 / 权限控制中
说清楚功能和数据结构
区别浅克隆与深克隆
如何编码实现深克隆
扩展: 自定义深克隆

9. n e x t T i c k 与 nextTick与 nextTickset两个方法的理解和应用

功能: 列表项动态显示输入框并自动获得焦点
给响应式对象添加新属性: 使用$set()添加edit属性为true  ==> 动态显示input
界面更新后执行: $nextTick()指定回调在Input DOM更新之后执行 => 自动获取焦点
扩展: $nextTick()的原理

10. 路由权限控制的实现

	路由全局前置守卫
	动态添加路由: addRoutes()
	
	基于后台数据的权限控制(user/role/permission)
		给用户分配了对应的角色
		给角色分配了权限(按钮和路由)
	权限控制的2个级别: 路由权限和按钮权限
	
	初始化时先只注册不需要登陆或所有用户都可见的常量路由(Login/Home)
	登陆请求成功后/刷新访问项目: 根据token获取用户和权限数据, 并根据权限数据生成权限路由并动态注册
            全局前置守卫: 当有token, 但还没有用户信息就发请求获取用户信息和权限数据
            根据路由权限数据来从所有异步路由的数组中过滤出当前用户权限路由
            动态添加注册用户的权限路由: router.addRoutes()
            将用户的权限路由与常量路由合并用来显示左则导航菜单

11. 路由权限控制中的2个bug

bug1: 模板项目自身代码的问题

    描述: 在权限路由上刷新 页面是空白的
    原因: 动态添加注册的路由只能在后面的路由跳转才可见, 当次跳转看不到
    	而next()是放行当次路由跳转, 自然就看不到刚动态注册的权限路由
    解决:
        // next() // 放行, 没有重新跳转, 看不到最新添加的动态路由
        // next(to.path) // 重新跳转到目标路由, 但丢失了参数(如果有的话)
        next({...to}) // 重新跳转到目标路由, 且参数不会丢失
        NProgress.done() // 结束进度条

bug2: 我们代码的问题

    描述: 如果先用一个A用户登陆, 退出后用B用户登陆, 结果只能看到部分有权限的路由
    原因:  我们在过滤总的异步路由数组中, 过滤掉了内部部分子路由, 另一个用户登陆看不到总的路由数组了
    解决: 深拷贝然后再进行过滤 ==> 不去改变总的异步路由数组

12. 按钮权限控制的实现

    按钮权限数据: 
        在哪?  vuex的user模块的state中  ==> 映射到getters中了 butttons
        结构: ['按钮1权限值', '按钮2权限值']  ==>  ["btn.Attr.add",  "btn.Trademark.add"] 
    如何判断当前用户是否有某个按钮的权限?
        定义判断的函数, 接收特定按钮的权限值, 返回是否有权限的布尔值
        function hasBtnPermission (btnPer)  {
            return store.getters.buttons.includes(btnPer)
        }
    将判断的函数挂载到Vue的原型上, 让所有组件都可见
        Vue.prototype.$hasBP = hasBtnPermission
    在权限组件中利用$hasBP来判断是否显示某个功能按钮
        v-if="$hasBP('btn.Attr.update')"    权限值去菜单管理列表中查看

13. 说一个记忆深刻的开发过程中的问题

HintButton封装的bug
	1. 删除table中的一行, 下一行的hintButton会自动显示文本提示
		原因: 没有table的遍历的key, 用了index作为key, 下一个数据会复用上一个被删除数据的真实DOM
		解决: row-key属性给table的遍历指定key为id
	2. 关闭确定框后, hint-button上的文本提示又会显示
		原因: 按钮在确定框后自动获取焦点 => 显示文本提示
		解决: 在点击按钮时, 让其失去焦点  event.target.blur()
		问题: 如果点击按钮中图标, 不可以
			原因: target此时是图标标签i, 而不是button
			解决: event.currentTarget.blur()

14. 详细说说你实现的某个业务功能的过程

商品平台属性管理
	一个平台属性包含一个平台属性名和多个平台属性值, 每个属性值都是包含属性值名称和其它属性的对象
	先根据选择的某个3级分类ID, 请求所有对应的所有平台属性的列表
	点击添加或某个分类的修改按钮进入相同的添加修改界面
	
	点击修改进入时, 要保存要修改的平台属性对象
		问题: 输入修改平台属性值名称后, 不能取消
		原因: 修改界面和列表界面共用一个属性对象
		解决: 保存的不能是列表中的平台属性对象的引用或浅拷贝, 必须是它的深拷贝对象
	
	在编辑平台属性值名称时, 需要实现点击从查看模式变为编辑模式, 也就是从span变为input
		设计: 给每个平台属性值对象添加一个edit属性为true
		问题: 通过row.edit=true后, input没有显示
		原因: 新添加的属性不是响应式的
		解决: 通过$set给row添加edit属性, 值为true
		
		设计: 显示input时, 自动获取焦点
		问题: 通过ref得到input后, 调用focus方法 => input是undefined的错误
		原因: dom还没有更新==>也就是input还没有产生
		解决: 调用$nextTick()在回调函数中获取input调用focus => 回调是在dom更新之后执行的
		
	点击保存发送请求添加或更新平台属性, 在发送请求前, 需要对收集的数据进行一些处理
		过滤掉属性值名称为空的属性值对象
		删除属性值对象中的edit属性

15. 说说你项目中echarts的使用和遇到的问题

- 如何动态显示图表
  - echarts: 
    - 在mounted设置option  ==> 如果是静态数据就已经可以了
    - 监视数据改变, 当数据时, 再重新设置带数据option
  - vue-echarts: 
    - 只需要给v-chart指定动态option属性, 通过调用getOption得到配置对象

- 动态显示图表不能正常显示
  - 原因: 初始data数据是undefined         reportData: {}
  - 解决: 给data对应的数据一个空数组的默认值
- 图表显示后不能自适应大小
  - 原因: 窗口大小改变, 且父元素大小改变时, 图表没有重新绘制
  - 解决:
    - echarts: 给window绑定resize监听, 在回调中调用chart对象的resize()方法
    - vue-echarts: 只需要指定autoresize属性即可
- tooltip提示框有时会自动隐藏超出容器区域的部分
  - 解决一: confine: true, // 将 tooltip 框限制在容器的区域内
    - 问题: 档住了鼠标
  - 解决二: position (point, params, dom, rect, size) 返回位置坐标 [x, -40]

前台移动 WEB 应用

1. 有哪些功能模块?

首页
搜索
分类
值得买

用户
商品详情
购物车
下单支付

2. 使用了哪些库?

vue
vue-router
vuex
vant ui

axios
mockjs
nprogress
uuidjs
lodash
amfe-flexible
postcss-pxtorem

3. 说说项目的适配如何实现的

  • 实现rem适配
    1. 下载依赖包
yarn add amfe-flexible postcss [email protected]

说明: 
	amfe-flexible: 将页面宽度
	postcss-pxtorem不能下载最新版本, 与postcss不适配
    1. 配置postcss-pxtorem

postcss.config.js

module.exports = {
	plugins: {
		'postcss-pxtorem': {
			rootValue: 37.5, // 设计图页面宽度为375, 划分成10份, 指定1rem=37.5
			propList: ['*'],
		},
	},
};
    1. 在入口js中加载amfe-flexible

main.js

import 'amfe-flexible'

4. 项目可说的一些功能技术点

- 移动端rem适配,使用amfe-flexible和postcss-pxtorem插件
- 整体界面使用vant ui库, 并实现按需引入打包
- 通过深度作用域选择器修改vant组件内部样式
- 使用axios请求后台接口, 并对xios进行二次封装, 使用NProgress显示请求进度提示
- 重写路由器的push和replace方法, 解决路由重复跳转报错的bug
- 配置路由的滚动行为, 路由跳转总能停留在顶部, 返回时能停留在原来的位置
- 利用路由的meta配置保存是否显示底部tab的标识, 来控制tab的显示隐藏
- 实现推荐页面与分类频道页的切换显示, 并解决频道标题不能正常选中的相关Bug
- 封装搜索框组件,使用v-model和$attrs&$listeners实现组件间通信
- 使用vant ui的Image组件实现图片懒加载
- 值得买界面: 实现小单元格的自定义轮播效果
- 值得买界面: 使用vue-waterfall-easy插件实现瀑布流分页加载效果

5. 说一个记忆深刻的开发过程中的问题

问题: 首页选中某个频道显示对应的列表页面后, 刷新显示的列表正确, 但会自动选中推荐项
原因:
	推荐的是静态的, 初始时就会渲染
	其它频道列表的在初始时不存在, 只有请求得到列表数据后才会产生渲染
	导致的问题 => 
		用来存储标识哪个tab显示的navId在初始值为当前对应的分类Id值
		但由于初始只有推荐的, navId会自动被赋值为推荐的的标识name值-1
		等到频道列表的产生时, 不可能再选中对应的频道

Vue项目优化

1. Vue代码层面优化

  • v-for 遍历列表
    • 指定非下标的唯一key, 尽量不用index, 如果只用于展示就没关系
    • 不同时使用 v-if
  • 图片资源懒加载
    • 使用vue-lazyload element-ui/vant-ui Image组件本身就有懒加载的功能
  • 路由组件懒加载 ==> 预加载
  • 第三方插件的按需引入打包
    • element-ui / vant /lodash
  • 对高频事件进行节流或防抖处理
  • 及时销毁事件监听
  • 大数组优化
    • 冻结响应式数据
    • 虚拟列表

2. webpack配置层面优化

  • 浏览器兼容处理
    • JS: @babel/polyfill => core-js配置useBuiltIns: ‘usage’
    • CSS: 给C3样式自动添加浏览器厂商前缀 => autoprefixer => postcss-loader
  • 拆分打包与压缩
  • 资源预加载(prefetch)
  • 生产环境时不生成 SourceMap
  • 文件名hash化=>利用浏览器缓存
  • 代码Tree Shaking

3. 基础的Web技术层面的优化

  • 对打包文件开启 Gzip压缩
  • 静态资源(css/js/img)使用CDN引入

TS + Vue3

TS

1. 区别TS与JS?(说说TS的特点)

  • 强类型, 声明变量时可以指定特定类型, 编码时可以有更友好的提示(错误或补全), 易于写出更健壮的程序

  • TS支持JS的所有语法特性, 也扩展了一些新的数据类型

    • 新的基本类型: 联合类型, 元组, 枚举
    • 新的复杂类型: 接口, 泛型
  • TS浏览器是不能直接运行的, 需要编译为JS才能运行

2. 说说你对接口的理解

  • 接口是对状态(属性)或行为(方法)的抽象(描述)
  • 接口可以用来约束一个对象/函数/类

3. 说说你对泛型的理解

  • 泛型: 代表不确定的类型
  • 泛型可以用在函数/接口/类上
  • 什么时候需要泛型呢?
    • 定义函数/接口/类时, 要操作数据类型不确定
  • 泛型的3个操作
    • 定义泛型类型
      • 函数: 定义函数的函数名的右侧
      • 接口: 定义接口的接口名的右侧
      • 类: 定义类的类名的右侧
    • 使用泛型: T
      • 函数: 参数/返回值/函数体
      • 接口: 接口体内
      • 类: 类体内
    • 指定泛型对应的具体类型: <具体类型名>
      • 函数: 调用函数时函数名的右侧
      • 接口: 定义实现类时接口名的右侧
      • 类: 创建实例时类名的右侧

Vue3

Vue3 比 Vue2 有什么优势

  • 更好的代码组织和逻辑抽离

    • 设计 composition API 来代替 option API
    • 更便于可复用功能代码的封装提取, 代码可阅读性更高
  • 体积更小

    • 将功能以多个函数提供出来, 我们会进行按需引入使用
    • 引入tree-shaking, 可以在打包压缩时, 将无用模块“摇掉”
  • 性能更好/更快

    • 使用proxy代替defineProperty来实现数据劫持

    • diff算法优化: 静态虚拟节点添加静态标记, 不进行diff比较

    • 静态提升: 静态虚拟节点提升到render的外面, 缓存起来, 不创建新的

  • 更好的 TS 支持

    • 对TS的类型检查更友好
  • 更好的脚手架工具vite

    • 启动运行快了很多

Vue3 生命周期

前端面试题尚硅谷最新_第6张图片

组合API VS 选项API

  • composition API 优点:

    • 更好的代码组织
    • 更好的逻辑复用
    • 更好的类型推导
  • 如何选择:

    • 不建议共用,会引起混乱
    • 小型项目,业务逻辑简单,用 Options API
    • 中大型项目,逻辑复杂,用 Composition API
  • 选项式 API(Options API

    • 所有方法都写在 methods 中,如果 data 中数据越来越多,找数据会非常困难

      
      
      
      
      
  • 组合式 API(Composition API

    • 逻辑会清晰,可以让功能的代码集中抽取到一个函数中进行逻辑复用

      
      
      
      
      

常用的组合API

  • 启动函数
    • setup()
  • 响应式: 核心
    • ref()
    • reactive()
    • computed()
    • watch()
  • 响应式: 工具
    • toRefs()
  • 生命周期勾子
    • onMounted()
    • onBeforeUnmount()

比较Vue2与Vue3的响应式(重要)

1) vue2的响应式
  • 核心:
    • 对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
    • 数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
Object.defineProperty(data, 'count', {
    get () {}, 
    set () {}
})
  • 问题
    • 对象直接新添加的属性或删除已有属性, 界面不会自动更新
    • 直接通过下标替换/添加元素或更新length, 界面不会自动更新
2) Vue3的响应式
  • 核心:
    • 通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…
    • 通过 Reflect(反射): 动态对代理对象的相应属性进行特定的操作
    • 文档:
      • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
      • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
const p = new Proxy(data, {
	// 拦截读取属性值
    get (target, prop) {
    	return Reflect.get(target, prop)
    },
    // 拦截设置属性值或添加新属性
    set (target, prop, value) {
    	return Reflect.set(target, prop, value)
    },
    // 拦截删除属性
    deleteProperty (target, prop) {
    	return Reflect.deleteProperty(target, prop)
    }
})

p.name = 'tom'
DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Proxy 与 Reflecttitle>
head>
<body>
  <script>
    
    const user = {
      name: "John",
      age: 12
    };

    /* 
    proxyUser是代理对象, user是被代理对象
    后面所有的操作都是通过代理对象来操作被代理对象内部属性
    */
    const proxyUser = new Proxy(user, {

      get(target, prop) {
        console.log('劫持get()', prop)
        return Reflect.get(target, prop)
      },

      set(target, prop, val) {
        console.log('劫持set()', prop, val)
        return Reflect.set(target, prop, val); // (2)
      },

      deleteProperty (target, prop) {
        console.log('劫持delete属性', prop)
        return Reflect.deleteProperty(target, prop)
      }
    });
    // 读取属性值
    console.log(proxyUser===user)
    console.log(proxyUser.name, proxyUser.age)
    // 设置属性值
    proxyUser.name = 'bob'
    proxyUser.age = 13
    console.log(user)
    // 添加属性
    proxyUser.sex = '男'
    console.log(user)
    // 删除属性
    delete proxyUser.sex
    console.log(user)
  script>
body>
html>

pinia VS vuex

  • Pinia 没有 mutations, 在actions 中可以直接同步更新state或异步更新state

  • Pinia中可以包含多个store, 而且相互独立, 且不进行合并, 没有模块的嵌套结构

  • 无需手动注册 store,创建出的store直接就可以使用

  • 更好的 TypeScript 支持, 提示补全很到位

vue-router V4的变化

  • 创建路由器有变化
    • new Router 变成 createRouter
    • createWebHistory()与createWebHashHistory() 取代了 ‘history’ 与 ‘hash’
  • 动态添加路由
    • 以前可以一次添加多个: router.addRoutes(routes)
    • 现在只能一次添加一个: router.addRoute(route)
  • 通配路由的path变了
    • 以前的path: *
    • 现在的path: /:pathMatch(.*)

vue3 & TS 语法

  • 声明接收props

    // 定义接口, 约束prop
    interface Props {
      count: number;
      updateCount(val: number): void;
    }
    defineProps()
    
  • 原生事件

    • 原生标签上绑定

    • 组件标签上绑定: 组件内部没有声明为自定义事件

  • 自定义事件

    // 声明事件
    const emit = defineEmits<{
      (e: 'xxx', val: string): void
      (e: 'click', val: object): void
      (e: 'increment', val: number): void
    }>()
    
    // ts 中分发事件
    emit('xxx', 'abc')
    
    // 模板中分发事件
    $emit('increment', 5)
    
  • 全局事件总线

    • vue本身不再提供事件总线的API: 没有$on方法了
    • 需要使用 mittpubsub-js第三方工具包
  • v-model的本质

    • 原生标签上: 动态value和原生的input监听

      
      
    • 组件标签上: 动态modelValue(默认)和自定义的input监听

      
      
  • 向外暴露方法

    • 组件内部的方法默认在外部是不能调用的

    • 可以通过 defineExpose暴露

      defineExpose({
      	borrowMoney
      })
      
  • 通过ref得到组件对象

    // 使用ref标识子组件
    
    
    // 定义ref
    const sonRef = ref | null>(null)
    
    // 通过ref得到子组件对象, 调用其暴露的方法
    sonRef.value?.borrowMoney(num)
    

微信小程序音乐播放器项目

1. 有哪些功能模块?

首页
视频
个人中心
登录
搜索
推荐歌曲
歌曲播放

2. 使用了哪些api/库?

api:
wx.request() 发送请求
wx.navigateTo() 保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。
wx.redirectTo() 关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面。
wx.switchTab() 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
wx.reLaunch() 关闭所有页面,打开到应用内的某个页面
wx.showToast() 显示消息提示框
wx.showLoading() 显示 loading 提示框
wx.showModal() 显示模态对话框
wx.hideLoading() 关闭提示框
wx.setNavigationBarTitl() 动态设置当前页面的标题
wx.setStorageSync() 将数据存储在本地缓存中指定的 key 中
wx.setStorage() 将数据存储在本地缓存中指定的 key 中。
wx.removeStorageSync() 从本地缓存中移除指定 key
wx.removeStorage() 从本地缓存中移除指定 key
wx.getStorage() 异步获取当前storage的相关信息
wx.getStorageSync() 从本地缓存中同步获取指定 key 的内容。
wx.createVideoContext() 视频播放对象的获取
VideoContext.play() 播放
VideoContext.pause() 暂停
VideoContext.stop() 停止
VideoContext.seek() 跳到指定位置
wx.getBackgroundAudioManager() 背景音频对象的获取
BackgroundAudioManager.play()播放音乐
BackgroundAudioManager.pause()暂停音乐
BackgroundAudioManager.stop()停止音乐
BackgroundAudioManager.onPlay()监听背景音频播放事件
BackgroundAudioManager.onPause()监听背景音频暂停事件
BackgroundAudioManager.onEnded()监听背景音频自然播放结束事件
BackgroundAudioManager.onStop()监听背景音频停止事件
BackgroundAudioManager.onTimeUpdate()监听背景音频播放进度更新事件
wx.login() 获取登录凭证
wx.getUserProfile() 获取用户信息

库:
pubsub-js.js 实现页面与页面之间的通信
moment.js 格式化日期

3. 说说项目的适配如何实现的

  • 小程序本身非常支持flex布局,默认使用rpx进行适配
1px=2rpx

4. wx.request的封装

wx.request 是一个异步的方法
success回调函数的作用域发生了改变,所以this的指向不是当前函数,另外使用promise可以解决异步嵌套的问题
config.js文件:
export default {
  host:'http://localhost:3000'
}
request.js文件:
import config from './config.js'
export default (url, data = {}, method = 'GET') => {
  return new Promise((resolve, reject) => {
    wx.request({
      url: config.host + url,
      data,
      method,
      success: res => {
        resolve(res.data)
      },
      fail: err => {
        reject(err)
      }
    })
  })
}

5. 页面之间如何实现通信

缓存的方式:
wx.setStorage()
wx.setStorageSync()
应用:可以缓存cookie的信息,实现异步请求携带cookie数据

全局唯一实例对象
getApp()
应用:缓存音乐播放/暂停的状态及音乐id数据,从而实现音乐监听相关操作

插件
pubsub-js
应用:自动播放下一曲

eventChannel 事件通道
wx.navigateTo({
  url: 'test?id=1',
  events: {
    // 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
    acceptDataFromOpenedPage: function(data) {
      console.log(data)
    },
    someEvent: function(data) {
      console.log(data)
    }
    ...
  },
  success: function(res) {
    // 通过eventChannel向被打开页面传送数据
    res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
  }
})
Page({
  onLoad: function(option){
    console.log(option.query)
    const eventChannel = this.getOpenerEventChannel()
    eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'});
    eventChannel.emit('someEvent', {data: 'test'});
    // 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据
    eventChannel.on('acceptDataFromOpenerPage', function(data) {
      console.log(data)
    })
  }
})

6. 微信小程序中事件的理解

事件是视图层到逻辑层的通讯方式。
事件可以将用户的行为反馈到逻辑层进行处理。
事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。
事件对象可以携带额外信息,如 id, dataset, touches。

事件分为冒泡事件和非冒泡事件:

冒泡事件:当一个组件上的事件被触发后,该事件会向父节点传递。
非冒泡事件:当一个组件上的事件被触发后,该事件不会向父节点传递。

事件处理函数中的event参数中可以携带相关的参数,如:id/data-
id的方式携带的数据的类型最终为string类型
data-的方式可以直接存储数值类型的数据,使用的时候无需转换


7. 自定义组件的理解

微信小程序中自定义组件通常存放在components目录中
自定义组件component和页page的区别在于xxx.js文件

组件的js文件中代码:
Component({
  data: {}, 自身使用的数据
  properties: {  }, 某个页面传递进来的数据
  methods: { } 当前组件所需的方法
})

页page的js文件中代码:
Page({
data:{},
onLoad(){},
....
})



8. 模版的理解

WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用。
定义模板
使用 name 属性,作为模板的名字。然后在