前端基础面试题(二)

文章目录

      • c3新增
      • h5新增
      • ccc3布局形式
      • 预解析
    • 构造函数和原型
    • 继承的方法
    • 高阶函数
    • 纯函数
    • 柯里化函数
    • 闭包
    • 垃圾回收机制
    • 异步流程、事件循环EventLoop
      • 宏任务
      • 微任务
    • promise
        • 静态方法-all
    • es6
    • 深拷贝和浅拷贝
  • vue常见面试题
        • 说说你对SPA单页面的理解
        • 单页应用优缺点
        • v-if和v-show的区别
        • Vue在挂载实例的过程中发生了什么
        • keep-ailve的理解
    • 浏览器的进程
    • 浏览器的渲染原理
    • 浏览器产常见请求状态码
    • 输入url到网页中页面加载完毕经历了哪些步骤
    • 重绘和回流
    • 跨域问题
    • 权限管理
    • 文件上传

c3新增

文字效果:word-wrap,text-overflow,text-shadow, text-decoration

边框效果:border-radius、box-shadow和border-image。border-radius,box-shadow可以为元素添加阴影,border-image可以使用图片来绘制边框。

背景:background-clip、利用这个属性可以设定背景颜色或图片的覆盖范围。
background-origin、用于确定背景的位置,它通常与background-position联合使用,可以从 border、padding、content来计算background-position(就像background-clip)
background-size用来调整背景图片的大小

渐变:transition,Transform

动画:Animation,媒体查寻: @media,字体图标

flex布局,盒子模型box-sizing

h5新增

  1. header和footer标签:页面中一个内容区块的头部和尾部布局;

  2. nav:导航区域;

  3. article标签:页面中独立的内容部分布局;

  4. aside标签:在独立内容之外,但是又与article有关联的部分布局;

二、新增媒体标签

  1. audio(音频);

  2. video(视频);

三、新增canvas和svg绘画元素

canvas表示位图区域;svg定义矢量图;

四、新增表单增强元素

表单元素 input 的 type 属性扩充:

date(输入日期);email(输入邮件);url(输入url地址);search(呈现搜索常规的文本域);

range(输入一定范围内的数值);month(输入月份);color(颜色);number(输入数值);

ccc3布局形式

  • 静态布局
  • 自适应布局
  • 流式布局(又别名 百分比布局 %)
  • 响应式布局:媒体查询
  • 弹性布局 (rem/em flex布局)

###js数据类型

基本类型:字符串(String)、数字(Number)、布尔值(boolean)、对空(null)、未定义(Undefined)、Symbol。

引用数据类型(对象类型):对象(Object)、数组(Array)、函数(function),还有两个特殊的对象:正侧(RegExp)和日期(Date)。

Symbol

Symbol 是 ES6 新推出的一种基本类型,它表示独一无二的值,它可以接受一个字符串作为参数,带有相同参数的两个Symbol值不相等,这个参数只是表示Symbol值的描述而已,主要用于程序调试时的跟踪,当然你也可以不传入参数,同样的我们可以通过typeof来判断是否为Symbol类型。

###检测数据类型

01、typeof

typeof可以正常检测出:number、boolean、string、object、function、undefined、symbol、bigint

  • 检测基本数据类型,null会检测object,因为null是一个空的引用对象
  • 检测复杂数据类型,除function外,均为object

02、instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,注意:只能检测复杂数据类型。

03、toString

toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

Object.prototype.toString.call('') ;              // [object String]
Object.prototype.toString.call(1) ;               // [object Number]

04、constructor

constructor代表获取由哪个构造函数创建而出,可以检测出字面量方式创建的对象类型,因为字面方式创建,实际由对应类创建而出

 function Animal(name) {
      this.name = name
    }
    const dog = new Animal('大黄')
    console.log(dog.constructor === Object) // false
    console.log(dog.constructor === Animal) // true

05、isArray

isArray可以检测出是否为数组

const arr = []
Array.isArray(arr)  // 返回true

06、Object.hasOwnporperty 检测当前属性是否为对象的私有属性

语法: obj.hasOwnporperty(“属性名(K值)”)

let obj = {
	name:"lxw"
	};
	console.log(obj.hasOwnProperty('name'));//true
	console.log(obj.hasOwnProperty('lxw'));//false

###作用域

作用域的概述

通常来说,一段程序代码中所用到的名字并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

作用域的作用:

  • 提高了程序逻辑的局部性。
  • 增强了程序的可靠性,减少了名字冲突。

JavaScript(es6前)中的作用域有两种:

  • 全局作用域
  • 局部作用域(函数作用域)

全局作用域

作用于所有代码执行的环境(整个 script 标签内部)或者一个独立的 js 文件。

全局作用域

作用于函数内的代码环境,就是局部作用域。 因为跟函数有关系,所以也称为函数作用域。

###作用域链

只要是代码都一个作用域中,写在函数内部的局部作用域,未写在任何函数内部即在全局作用域中;如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域;根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,就称作作用域链

预解析

JavaScript 代码是由浏览器中的 JavaScript 解析器来执行的,浏览器中有一套专门解析JS代码的程序,这个程序称为js的解析器(js引擎)

JavaScript 解析器在运行 JavaScript 代码的时候分为两步:预解析和代码执行.

  • 预解析:在当前作用域下, JS 代码执行之前,浏览器会默认把带有 var 和 function 声明的变量在内存中进行提前声明或者定义
  • 代码执行: 从上到下执行JS语句。

预解析会把变量和函数的声明在代码执行之前执行完成。

预解析也叫做变量、函数提升。
变量提升(变量预解析): 变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升。

构造函数和原型

###静态成员和实例成员

实例成员

实例成员就是构造函数内部通过this添加的成员 如下列代码中uname age sing 就是实例成员,实例成员只能通过实例化的对象来访问

静态成员

静态成员 在构造函数本身上添加的成员 如下列代码中 sex 就是静态成员,静态成员只能通过构造函数来访问

构造函数原型prototype

构造函数通过原型分配的函数是所有对象所共享的。

JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

对象原型

对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。
__proto__对象原型和原型对象 prototype 是等价的
__proto__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

constructor构造函数

对象原型( proto)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。
constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

 function Star(uname, age) {
     this.uname = uname;
     this.age = age;
 }
 // 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数
 Star.prototype = {
 // 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
   constructor: Star, // 手动设置指回原来的构造函数
   sing: function() {
     console.log('我会唱歌');
   },
   movie: function() {
     console.log('我会演电影');
   }
}
var zxy = new Star('张学友', 19);

原型链

前端基础面试题(二)_第1张图片

原型对象中this指向

构造函数中的this和原型对象的this,都指向我们new出来的实例对象

继承的方法

在es5之前我们可以用组合继承,修改构造函数的this指向(call(),apply(), bind())继承父构造函数身上的属性,可以通过修改子构造函数的–proto–,让他指向父构造函数,继承父构造函数的方法。可以通过使用Object.create()创建一个空对象让他的prototype指向需要继承的父亲原型上。

在es6有了类的概念,我们可以通过super和expend关键字继承父亲的属性和方法,具体代码如下

// 创建人的类
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.say = function() {
  console.log('说话了')
}
function Dog(name, age) {
  Person.call(this, name, age)
}

// 此方式不推荐 子原型和父原型公用同一个原型
// Dog.prototype = Person.prototype
Dog.prototype = Object.create(Person.prototype)

const d1 = new Dog('大黄', 3)
const d2 = new Dog('小黄', 2)

##常见的es5的操作数组的方法

forEach:和之前for循环作用基本一样,只不过比for更灵活方便一些

参数:回调函数,该回调函数有三个参数

indexOf:用于查找在数组中的位置,返回值为索引,如果没有找到返回-1,第一个参数为要查找的数据

第二个参数为重哪个开始查找,lastindexOf刚好和他相反

some:some方法可以用作条件查找,循环的过程中,只要有一个能满足条件,即停止循环,并返回布尔值

every:every方法是返回的条件全部成立即返回true,否则返回false,只要有一项不成立,循环立即停止,并返回false。

map:进行数组映射,及数组格式化,会返回一个新数组,新数组的值来自于map回调函数内的返回值

  • 新数组的长度一定和原数组长度是一致的
  • 如果没有返回值,新数组中的值则为undefined

filter:实现数组过滤,返回值为新数组,新数组中的数据来自于,回调函数中返回条件为true的数据

reduce:方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

参数:

  • 回调函数
    • 参数1: 上一次计算出结果的值
    • 参数2: 当前循环项
  • 初始值: 如果不传,默认值为数组第一项(并且第一项不在参与运算)
  • currentIndex: 正在处理的索引
  • 处理的数组

高阶函数

概念

高阶函数英文叫 Higher-order function,高阶函数是一个接收函数作为参数或将函数作为输出返回的函数

function fn(a, b, total) {
  return total(a) + total(b)
}

function total(num) {
  return num * 1000
}

fn(1, 2, total)

function fn() {
  return function() {}
}

纯函数

概念

一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。

结果只依赖参数

以下函数不是纯函数,因为最终计算的结果是不可预料,函数内部计算使用了外部的数据,而外部的数据是可变的,虽然现在是 101,后续在运行有可能是 其他结果

let num = 100
function fn(a) {
  return a + num
}

fn(1) // 101

副作用

一个函数执行过程对产生了外部可观察的变化那么就说这个函数是有副作用的

下面代码,函数在执行时,修改了外部的 obj 的数据,则产生副作用

一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 Ajax 请求,还有调用 window.reload 刷新浏览器,甚至是 console.log 往控制台打印数据也是副作用。

柯里化函数

概念

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

例如:add(1,2,3) 变为 add(1)(2)(3)

 function add(a, b, c) {
        return a + b + c
    }
    //    定义柯里化函数
    function curry(fn, args) {
        args = args || []
        return function () {
            args.push(...arguments)
            console.log(args)
            if (args.length >= fn.length) {
                return fn.apply(this, args)
            } else {
                return curry(fn, args)
            }
        }
    }
    const curryAdd = curry(add)
    console.log(curryAdd(1)(2)(3));

作用

参数复用、提高实用性、统一规则重复使用、延迟计算/运算

闭包

概念

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。大白话也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

原理

闭包的原理就是利用作用域链的特性,首先在当前作用域访问数据,当前作用域访问不到,则向父级访问,父级也没有,一直找到全局。

作用

数据私有化,防止污染全局

将闭包代码修改后也同样可以访问到 a 数据,但是此时 a 的数据在全局,全局可以直接对 a 数据进行修改,而且全局也多了一个 a 这个数据,要尽量避免直接将数据直接放到全局

缺点

闭包会造成内存泄漏,因为闭包的数据没有被回收

​ 解决方案:将全局指向的函数重新置为 null,利用标记清除的特性

内存泄漏的问题,settimeout一定要记得清除,使用过的闭包变量要手动置为null,使用一些插件时没有销毁该插件生成的DOM,记得销毁。被遗忘的回调

垃圾回收机制

垃圾回收概念

js 的内存是自动进行分配和回收的,内存在不使用的时候会被垃圾回收器自动进行回收,但是我们需要了解垃圾回收的机制,从而防止内存泄漏(内存无法被回收)

生命周期

内存创建分配: 申请变量\对象\函数等

内存使用: 对内存进行读写,也就是使用变量或函数对象等

内存销毁: 变量\函数\对象等不再使用,即被垃圾回收自动回收掉

核心算法

判断内存是否不再使用,如果是则回收 面试遇到可以和闭包一起说

ie是用引用计数 现在的浏览器是用标记清除

异步流程、事件循环EventLoop

概念

js 是单线程的,也就代表 js 只能一件事情一件事情执行,那如果一件事情执行时间太久,后面要执行的就需要等待,需要等前面的事情执行完成,后面的才会执行。

所以为了解决这个问题,js 委托宿主环境(浏览器)帮忙执行耗时的任务,执行完成后,在通知 js 去执行回调函数,而宿主环境帮我们执行的这些耗时任务也就是异步任务

js 本身是无法发起异步的,但是 es5 之后提出了 Promise 可以进行异步操作

执行流程

  1. 主线程先判断任务类型
    • 如果是同步任务,主线程自己执行
    • 如果是异步任务,交给宿主环境(浏览器)执行
  2. 浏览器进行异步任务的执行,每个异步执行完后,会将回调放进任务队列,先执行完成的先放进任务队列,依次放入
  3. 等主线程任务全部执行完后,发现主线程没有任务可执行了,会取任务队列中的任务,由于任务队列里是依次放入进来的,所以取得时候也会先取先进来的,也就是先进先出原则
  4. 在任务队列中取出来的任务执行完后,在取下一个,依次重复,这个过程也称为 eventLoop 事件轮训

宏任务

由宿主环境发起的异步:宏任务

setTimeOut、setInterval、特殊的(代码块、script)

setTimeOut

微任务

由 javascript 自身发起的异步:微任务

执行顺序

  1. 先执行宏任务
  2. 宏任务执行完后看微任务队列是否有微任务
  3. 没有微任务执行下一个宏任务
  4. 有微任务将所有微任务执行
  5. 执行完微任务,执行下一个宏任务

##Generator函数

概念

Generator 函数是 es6 的,作用是可以将函数执行时可以进行暂停,而普通函数是一直执行到 return,同样也是异步的一种解决方案

定义 Generator 函数

  • 定义函数时通过 function* 进行区分为 Generator 函数
  • 函数内部可以通过 yield 进行暂停 定义普通函数
function* fn() {
  yield console.log(1)
  yield console.log(2)
  return 3
}

调用函数

函数直接调用即可,但是函数不会执行,此时只是得到了一个 Generator 对象,需要调用 Generator 对象的 next 方法逐步执行。

最后执行完会通过对象返回结果

yield 向外传递数据

generator 函数可通过 yield 传递数据到函数外,函数外通过调用 next 接收

向函数内传递数据

第一次 next 传递参数无效,第一次的 next 的参数由函数调用传递

下一次的 next 传递的参数,由上一次的 yield 暂停后重新执行进行返回

处理异步

配合promise,可以实现async和await的效果

在没有async和await的时候我们使用的是co库,co库是由,TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行。

co库的约定yield命令后面只能是Thunk函数或Promise对象

thunk 函数

多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数,那这就是一个thunk函数

promise

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。使得控制异步操作更加容易。可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

Promise 使用介绍

Promise 是一个构造函数

Promise 构造函数原型上的方法,可以通过创建一个 Promise 实例进行调用

  • Promise.prototype.then()
  • Promise.prototype.catch()
  • Promise.prototype.finally()

Promise 构造函数静态方法,直接通过 Promise 函数调用

  • Promise.all()
  • Promise.allSettled()
  • Promise.race()
  • Promise.reject()
  • Promise.resolve()
静态方法-all

Promise.all()参数可以传递一个数组,数组中记录所有的 promise 异步处理

返回值是一个 Promise 的实例对象 then 方法可以获取到所有的 promise 异步处理的结果,一旦有某一个调用执行了 reject,则终止进入 catch

####静态方法-allSettled

Promise.all 和 allSettled 基本一样,区别是,then 始终可以获取到异步的状态,哪怕其中有一个失败

####静态方法-race

Promise.race 使用和 all 一样,但是只返回第一个结果,不管成功或失败

####静态方法-any

Promise.any 返回第一个成功的结果

es6

let、const

es6新增了let声明扁蕾的方式和var相似

const

const用来声明常量,

特点

具有块级作用域,不具变量提成,暂时性死区:在一个作用域里声明了一个变量在声明前不能使用变量,作用域外有相同名变量,也不能访问到,不允许重复声明

结构赋值

对象支持将属性解构后进行重命名

// 数组,使用的时候名字可以任意
const [a,b,c,d] = [1,2,3,4]
// 对象,解构默认定义对象中的key
const {name, id} = {
  name: '张三',
  id: 19
}

模版字符串

方便了字符串的拼接,使用反引号语法``,反引号内部可以通过${}插入表达式

字符串的 includes

用来查找一个字符串中是否包含另一个字符串,返回值为布尔

字符串的 startsWith

用来判断某字符串是否以某字符串结尾

函数-尖头函数

es6引入了尖头函数,可以写更简单的函数

  1. 尖头函数花括号可以省略,省略后会默认return
  2. 尖头函数中没有绑定this,this指向上下文

函数-形参默认值

es6可以给形参设置默认值

函数-形参解构

function fn(a = 1,b =2) 

函数-形参剩余参数 …angr

扩展运算符

对象简写 键值一致可以简写

对象方法简写 fn(){}

对象key使用表达式 【key】

Object.assign 可以用来浅拷贝

Object.create

创建一个对象,并将对象的__proto__指向参数对象

可以创建一个没有原型的对象

Promise

es6模块化

common

commonJs模块化: 规范

导出:module.exports

导入: require()

web过渡时期的产品

AMD: AMD加载完模块后,就立马执行该模块 代表(require.js)

CMD:CMD加载完某个模块后没有立即执行而是等到遇到require语句的时再执行(Sea.js)

web现在流行es6

默认导出,默认导入

按需导出,按需导入

按需导入可以起别名 a as 名

同时引入默认和按需

全部引入 通过*号

Symbol

例如:已有 person 对象,该对象包含的方法属性很多,现在需要给 person 添加一个 say 的方法,我们不需要考虑 person 内部有没有 say 这个方法,我们就是要添加一个 say 方法,并且如果 person 内部有 say 这个方法,还不能影响内部的 say 方法

此时就可以使用 Symbol 来解决该问题

Symbol 是 es6 引入的第七个原始数据类型,Symbol 数据表示一个唯一值,独一无二的

Symbol 数据无法进行运算

Symbol 数据无法通过 new 关键字使用

Symbol 数据如果作为对象的 key 不能通过 for in 遍历

Symbol(‘该 Symbol 数据的注释’)

const name = Symbol('猴哥')
const name2 = Symbol('猴哥')
console.log(name === name2) // false

Symbol.for 创建,该方法会先检查是否有 Symbol.for()创建的同名数据,如果有则返回,如果没有则创建

const name3 = Symbol.for('猴哥')
const name4 = Symbol.for('猴哥')
console.log(name3 === name4) // false

set

Set 是 es6 新增的数据结构,类似于数组,区别在于不包含重复的值

const nameArr = new Set(['张三', '李四', '张三', '王麻子'])
// Set(3) {"张三", "李四", "王麻子"}

add

const nameArr = new Set(['张三', '李四', '张三', '王麻子'])
nameArr.add('赵六') // Set(4) {"张三", "李四", "王麻子", "赵六"}

delete删除元素

const nameArr = new Set(['张三', '李四', '张三', '王麻子'])
nameArr.delete('张三') // // Set(2) {"李四", "王麻子"}

has监测

const nameArr = new Set(['张三', '李四', '张三', '王麻子'])
nameArr.has('张三') // true

map

  • Object 的对象 key 只能是字符串或者 Symbols, Map 的键可以是任意值
  • Object 的对象是无序的, 而 Map 的是有序的
  • Object 的键值对个数只能手动计算, 而 Map 的键值对个数可以从 size 属性获取

Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

创建 Map 对象

const map = new Map()

运算拓展

链式判断运算符

当我们在访问某一对象内部的多层级数据时,为了代码的安全,我们会先判断第一层是否存在,存在再取下一层的数据,以此类推最后访问到需要用到的数据

null判断运算符

读取对象属性的时候,如果某个属性的值是nullundefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

深拷贝和浅拷贝

​ js数据类型分为简单数据类型和复杂数据类型,简单数据类型是存储在栈里的,复杂数据类型是储存在堆里的,栈里有一个地址指向堆,而简单数据类型外面可以使用浅拷贝直接赋值,因为每一次他都会开辟一个新的内存存储他。但是复杂数据类型是不行的,直接赋值只是赋值了栈里的引用地址,修改属性还是会影响到原来的数据。浅拷贝我们一般一会for in Object.assign ,展开运算符。深拷贝我们一般使用递归函数,通过层层递归判断数据的数据类型,直到找到简单数据类型再进行赋值,而递归会有一个循环调用的问题我们一般使用map数据结构解决,也可以再递归的时候定义一个数组,每次递归深拷贝过的数据存放进去,下一次递归后循环数据一一对比如果返回是已经拷贝过的那就直接return结束深拷贝。

vue常见面试题

####vue的核心

数据驱动视图

  • Model:模型层,负责处理业务逻辑以及和服务器端进行交互
  • View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面
  • ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁

组件化

每一个.vue结尾的文件都是一个组件,组件化开发可以高效的编程,把每个模块拆分开来便于维护,降低变量名耦合度,调试方便

vue和传统开发的区别

没有落地使用场景的革命不是好革命,就以一个高频的应用场景来示意吧注册账号这个需求大家应该很熟悉了,如下,用jquery来实现大概的思路就是选择流程dom对象,点击按钮隐藏当前活动流程dom对象,显示下一流程dom对象如下图(代码就不上了,上了就篇文章就没了…)

总结就是:

Vue所有的界面事件,都是只去操作数据的,

Jquery操作DOMVue所有界面的变动,都是根据数据自动绑定出来的,Jquery操作DOM

说说你对SPA单页面的理解
单页面应用(SPA) 多页面应用(MPA)
组成 一个主页面和多个页面片段 多个主页面
刷新方式 局部刷新 整页刷新
url模式 哈希模式 历史模式
SEO搜索引擎优化 难实现,可使用SSR方式改善 容易实现
数据传递 容易 通过url、cookie、localStorage等传递
页面切换 速度快,用户体验良好 切换加载资源,速度慢,用户体验差
维护成本 相对容易 相对复杂
单页应用优缺点

优点:

  • 具有桌面应用的即时性、网站的可移植性和可访问性
  • 用户体验好、快,内容的改变不需要重新加载整个页面
  • 良好的前后端分离,分工更明确

缺点:

  • 不利于搜索引擎的抓取
  • 首次渲染速度相对较慢

####SPA首屏加载速度慢怎么解决

在页面渲染的过程,导致加载速度慢的因素可能如下:

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载了
  • 加载脚本的时候,渲染内容堵塞了

常见的几种SPA首屏优化方式

  • 减小入口文件积
  • 静态资源本地缓存
  • UI框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启GZip压缩
  • 使用SSR

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化

下图是更为全面的首屏优化的方案

v-if和v-show的区别

共同点:我们都知道在 vuev-showv-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示

不同点:

  • 控制手段不同
  • 编译过程不同
  • 编译条件不同

控制手段:v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除

编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

  • v-showfalse变为true的时候不会触发组件的生命周期
  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

使用场景v-ifv-show 都能控制dom元素在页面的显示

v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)

如果需要非常频繁地切换,则使用 v-show 较好

如果在运行时条件很少改变,则使用 v-if 较好

Vue在挂载实例的过程中发生了什么
  • new Vue的时候调用会调用_init方法
    • 定义 $set$get$delete$watch 等方法
    • 定义 $on$off$emit$off等事件
    • 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候主要是通过mountComponent方法
  • 定义updateComponent更新函数
  • 执行render生成虚拟DOM
  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

####请描述一下Vue生命周期钩子,created和munted中有什么区别

vue的生命周期钩子有四个大阶段和八个小阶段。分别是初始化beforeCreate/create,

挂载阶段beforeMount/mounted,组件跟新数据beforeUpdate/Update,组件销毁前beforeDestroy/destroyed。keep-ailve缓存组件activated/deactivated,捕获一个来自子孙组件的错误时被调用errorCaptured。

created和munted的区别

created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成;mounted是在页面dom节点渲染完毕之后就立刻执行的。触发时机上created是比mounted要更早的,两者的相同点:都能拿到实例对象的属性和方法。 讨论这个问题本质就是触发的时机,放在mounted中的请求有可能导致页面闪动(因为此时页面dom结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在created生命周期当中

####v-if和v-for为什么不能一起使用

v-if是判断组件是否能熏染的条件判断

v-for是用来循环渲染数据的,其中有一个key值要求是唯一值,为了vue底层的diff算法效率存在的。

v-for的优先级比v-if高,放在一起使用就会导致每一次v-if判断带条件为true也会进去v-for熏染,而页面什么效果也没有,会导浪费性能。解决办法是在v-for循环的盒子外再套一层父盒子v-if放在父盒子判断,这样每一次v-if判断为true时DOM节点就不会熏染,v-for也不会执行。

  1. 永远不要把 v-ifv-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
  2. 如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
  3. 如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项

####为什么data是一个函数而不是对象

如果data是一个函数的话,因为函数是引用数据类型,如果遇到多个组件服用每个复用的组件用的都是同一组data里的数据,修改data的数据就会互相影响。

根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

####动态给vue的data添加一个属性时会发生什么?怎么解决?

数据会跟新,但视图不会实时跟新,因为vue底层时用Object.defineProperty实现数据响应式的,当我们访问foo属性或者设置foo值的时候都能够触发settergetter,但是我们为obj添加新属性的时候,却无法触发事件属性的拦截,原因是一开始objfoo属性被设成了响应式数据,而bar是后面新增的属性,并没有通过Object.defineProperty设置成响应式数据。

解决方案:

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

####Vue组件通信有什么方式

整理vue中8种常规的通信方案

  1. 通过 props 传递
  2. 通过 $emit 触发自定义事件
  3. 使用 ref
  4. EventBus
  5. p a r e n t 或 parent 或 parentroot
  6. attrs 与 listeners
  7. Provide 与 Inject
  8. Vuex

####双向数据绑定的原理

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

实现双向数据绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  5. 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

####Vue中$nextTick的作用

$nextTick的作用是当vue再运行中执行代码中遇到nextTick会把他放在一个任务队列中不会马上执行函数里面的代码,等到全部代码执行完毕后再回来执行nextTick函数里的代码,会安排队列一个个执行,这样我们就可以再跟新dom后动态的拿到dom实例从而对他进行操作。

####说说你对Vuemixin的理解

本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如datacomponentsmethodscreatedcomputed等等

我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来

Vue中我们可以局部混入全局混入:Vue.mixin()

我们在开发中遇到重复的逻辑就可以给他抽离出来放在mixin里面,在需要用到的地方进行一个局部混入,这样可以减少我们相同重复逻辑代码的书写,让整体代码更整洁,在混入后的执行顺序是先执行mixin里的代码再执行组件的代码,组件的优先级比mixin的权重高。

####对slot的理解

slot是vue组件中的插槽,我们一般使用最多的场景是在封装通用组件时,当我们组件内部的结构和样式不满足使用者的开发场景的时候,他就可以通过具名插槽或默认插槽,来传入标签结构,自定义组件某一块的结构或样式。

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理

如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情

通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

####Vue中key的作用

1、当前节点的唯一标识

2、高效的diff

3、如果key一致,当前节点直接复用,如果key不一致,替换当前节点

当我们在使用v-for时,需要给单元加上key

  • 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
  • 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed

+new Date()生成的时间戳作为key,手动强制触发重新渲染

  • 当拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧key Comp会被移除,新key Comp触发渲染

####diff算法是如何工作的

首先是深度优先,同级比较,比较的过程中,循环从两边向中间收拢。先判断新的第一个节点和老的第一个节点对比,再盘掉新的最后一个节点和老的最后一个节点,再新的第一个节点和老的最后一个阶段,新的最后一个节点和老的第一个 节点对比。然后继续向内对比遍历。遇到相同的之间复用,全部对比完没有遇到相同的节点则创建新的节点。如果key是index相当于没有加,以为每一次对比index是他的唯一值他还是回去找index其对比,新老index发现一致还是会去对比具体的节点。

####请你说一下 虚拟dom 的理解

是js对象

作用:描述了真实dom结构,核心属性。

真实的dom节点有很多属性的,如果直接操作真是的dom,消耗性能。

vue里面template写的结构转换render函数,render函数执行会返回虚拟dom结构 >> 渲染出真实dom。

优势:

1、数据更新的时候,虚拟dom对比(旧的虚拟dom结构和新的虚拟dom结构进行对比),对比出差异以后,更新到真实的dom里面(最小化操作dom)

2、同级比较,如果标签名不一致或者key不一致直接替换,不会进行深层次对比

3、先序深度优先

跨平台:浏览器,小程序 , 支付宝小程序,抖音小程序,打包安卓,ios应用 ,electron桌面应用

vue/react >> 虚拟dom 渲染的时候根据不同的平台去进行渲染

keep-ailve的理解
  1. vue自带的组件 >> 主要功能是缓存组件 >> 提升性能
  2. 使用场景:可以少网络请求,如果当前组件数据量比较大,就可以节省网络请求 >> 提升用户体验
  3. 举例:如果详情页面之间进行切换,就可以使用keep-alive进行缓存组件,防止同样的数据重复请求

keep-alive` 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

keep-alive可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max - 数字。最多可以缓存多少组件实例

####vue中常见的修饰符

Vue中,修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理

vue中修饰符分为以下五种:

  • 表单修饰符

    • lazy
    • trim
    • number
  • 事件修饰符

    • stop
    • prevent
    • self
    • once
    • capture
    • passive
    • native
  • 鼠标按键修饰符

    • left 左键点击
    • right 右键点击
    • middle 中键点击
  • 键值修饰符

    • 普通键(enter、tab、delete、space、esc、up…)
    • 系统修饰键(ctrl、alt、meta、shift…)
  • v-bind修饰符

    • async
    • prop
    • camel

####vue-router执行的顺序

全局路由守卫

router.beforEach、router.beforeResolve、router.afterEach

路由独享守卫

beforEnter

组件内的守卫

beforRouterEnter、beforRouterUpdate、beforRouterLeave

执行顺序
一、打开页面的任意一个页面,没有发生导航切换。
全局前置守卫beforeEach (路由器实例内的前置守卫)
路由独享守卫beforeEnter(激活的路由)
组件内守卫beforeRouteEnter(渲染的组件)
全局解析守卫beforeResolve(路由器实例内的解析守卫)
全局后置钩子afterEach(路由器实例内的后置钩子)

二、如果是有导航切换的(从一个组件切换到另外一个组件)
组件内守卫beforeRouteLeave(即将离开的组件)
全局前置守卫beforeEach (路由器实例内的前置守卫)
组件内守卫beforeRouteEnter(渲染的组件)
全局解析守卫beforeResolve(路由器实例内的解析守卫)
全局后置钩子afterEach(路由器实例内的后置钩子)

####Vue.use(VueRouter)做了什么事情

1、vue的原型上挂载$router$rout

2、注册了两个组件router-view 、router-link两个组件

3、其他的初始化事件

####vue的三种路由模式

哈希模式

使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载,其显示的网路路径中会有 “#” 号,有一点点丑。这是最安全的模式,因为他兼容所有的浏览器和服务器。

history模式

美化后的hash模式,会去掉路径中的 “#”。依赖于Html5 的history,pushState API,所以要担心IE9以及一下的版本,感觉不用担心。并且还包括back、forward、go三个方法,对应浏览器的前进,后退,跳转操作。就是浏览器左上角的前进、后退等按钮进行的操作。但是history也是有缺点的,不怕前进后退跳转,就怕刷新(如果后端没有准备的话),因为刷新是实实在在地去请求服务器了。

abstract模式

适用于所有JavaScript环境,例如服务器端使用Node.js。如果没有浏览器API,路由器将自动被强制进入此模式。

浏览器的进程

进程和线程的区别

进程:就是在内存中正在经行的程序就叫一个进程。

线程:是进程内的一个独立执行的单位,是CPU调度的最小单位。

一个进程中可以包含多个线程。

chrome浏览器中五个进程

1、浏览器进程

主要负责界面显示、用户交互、子进程管理、同时提供存储等功能。

2、渲染进程

核心任务是将HTML、CSS、Javascript转化未用户可以与之交互的网页,排版引擎Btink和Javascript引擎V8引擎都运行在该进程中,默认情况下,Chrome为每一个Tab标签页创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下的。此进程包括了GUI线程、javascript解析引擎、事件触发线程。

3、GPU进程

GPU图形处理器(英语:graphics processing unit,缩写:GPU),负责3D css效果,网页,Chrome ui的绘制。

4、网络进程

主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立处理,成为单独一个进程。

5、插件进程

主要负责插件的运行,因为插件易崩溃,所以通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

浏览器的渲染原理

  1. 获取 HTML ⽂件并进⾏解析,生成一棵 DOM 树(DOM Tree)
  2. 解析 HTML 的同时也会解析 CSS,⽣成样式规则(Style Rules)
  3. 根据 DOM 树和样式规则,生成一棵渲染树(Render Tree)
  4. 进行布局(Layout),即为每个节点分配⼀个在屏幕上应显示的确切坐标位置
  5. 进⾏绘制(Paint),遍历渲染树节点,调⽤ GPU 将元素呈现出来

浏览器产常见请求状态码

  • 200:即为服务器响应成功 请求成功并且服务器创建了新的资源。
  • 201:请求成功 服务器已接受请求,但尚未处理。
  • 301:重定向 请求的网页已永久移动到新位置。
  • 302:重定向 服务器目前从不同位置的网页响应请求
  • 400:服务器不理解的语法
  • 401:未授权 请求要求身份验证。对于登录后请求的网页,服务器可能返回此响应。
  • 404:服务器找不到请求的网页
  • 413:服务器无法处理请求
  • 500:服务器内部错误
  • 502:网关错误
  • 504:网关响应超时

输入url到网页中页面加载完毕经历了哪些步骤

1、首先,在浏览器地址栏中输入url

2、浏览器先查看浏览器缓存-系统缓存-路由缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到第三步骤。

3、在发送http请求前。需要域名解析(DNS解析)(DNS(域名系统,Domain,Name、system)是互联网的一项核心服务。它作为可以讲域名的IP地址相互映射的一个分布式数据库。能够使人更方面的访问互联网,而不用去记住IP地址。)解析获取相应的IP地址。

4、浏览器向服务器发起tcp链接,浏览器tac三次握手。(TCP即传输控制协议。TCP链接是互联网链接协议集的一种。)

5、握手成功后,浏览器向服务器发起http请求,请求数据包。

6、服务器处理收到的请求,将数据返回值浏览器

7、浏览器收到HTTP响应

8、读取页面内容,浏览器渲染,解析html源码

9、生成Dom树,解析css样式,js交互

10、客户端和服务器交互

11、ajax查选

重绘和回流

什么是重绘?

重绘: 当渲染树中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的操作,比如 background-color,我们将这样的操作称为重绘。

什么是回流?

回流:当渲染树中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建的操作,会影响到布局的操作,这样的操作我们称为回流。

常见引起回流属性和方法:

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。

(1)添加或者删除可见的 DOM 元素;
(2)元素尺寸改变——边距、填充、边框、宽度和高度
(3)内容变化,比如用户在 input 框中输入文字
(4)浏览器窗口尺寸改变——resize事件发生时
(5)计算 offsetWidth 和 offsetHeight 属性
(6)设置 style 属性的值
(7)当你修改网页的默认字体时。

小结 :

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

##git分支管理

主分支 >> master

开发新功能 >> feature分支 >> feat/login_wxh_2022_08_11

bug分支 >> master 拉出一个bug分支修改代码 >> bug/login_banzhang_2022_08_11 修改完以后先测试,在上线,再合并

所有功能开发完成以后feat/login_wxh_2022_08_11功能完成 >> 上线

先从master拉出一条realease/login_wxh_2022_08_11分支 >> 功能分支合并到release分支 >> 预发环境测试(2-3小时)

发布release分支(再拉取一下master) >> 功能分支合并到master

正在开发功能(代码没有提交)add commit之后 >> 测试告诉你线上有bug了 >> 切换到master重新拉取bug分支

git commit -m ‘临时存储’ git log记录多了一个没用的提交

git stash 暂时把修改之后代码存储起来 切换分支到线上修改代码

git stash pop把存储起来的代码放出来

测试环境 测试环境的代码 + 测试环境的数据库 如果push的developmen分支,打包的文件,会复制到测试环境

预发环境 测试环境的代码 + 线上环境的数据库 谨慎 ,如果push的是realease分支,打包的文件,会复制到预发环境

##项目打包上线

我们公司有jenkins持续集成 CI/CD

前端做的事情,只需要push代码

jenkins和我们的git仓库做了关联了,只要git仓库检测到push,会向服务器发送请求,开始构建

构建的脚本一开始就配置好了,只需要按照顺序执行

打包好的文件复制到静态网站服务器下面就可以访问了

开发环境 >> 测试环境(公司内部或者开发人员自己用来调试用的)只要push了代码就会构建(发布上线)

正式环境 >> 没有配置自动部署(只要提交代码就进行部署) ,需要开发人员(leader)登录jenkins后台手动点击构建,防止错误的提交

-------------------git服务器用的是什么?-------------------

自己搭建git服务 用的最多的是 gitlab

-------------------开发项目的流程?------------------------

1、需求从哪来? >> 产品经理 >> 需求评审

​ 需求的开发,这个需求谁给你的?产品经理

需求评审:如果这个需求是需要你开发,leader拉着你参与需求评审

排期:功能比较多,业务比较多 leader会一起参与需求评审,帮你排期

​ 小功能(不需要leader和你一起进行需求评审),自己就可以排期

开发

联调 >> 开发完成以后,提测之前,需要先跑一遍流程,需要接入后端接口,测试两个方面

​ 1、前端页面有没有什么问题?

​ 2、后端接口是否存在问题?

// 测试 >> 找bug >> 在线任务协作平台 >> 如果bug指定给你了就需要你进行解决 >> 解决完以后 >> 点击在线平台的已解决 >> 测试再测试

如果测试通过,测试会关闭掉你需要解决的这个bug

测试通过以后上线

封装组件, 上传文件组件,form table 通用的

跨域问题

线上环境的跨域问题

1、cors >> 后端加响应头 access-control-allow-origin: ‘*’ 允许所有的页面请求

2、代理(主流方案)

​ 开发环境 :webpack的devServer进行代理,本地请求转发到接口 vue.config.js > devServer > proxy {}

​ 线上环境: 服务器端使用nginx反向代理 拦截请求转发到线上接口, 解决跨域问题

hrsass系统

www.itheima.com

www.itheima.com/api

1、不跨域

服务器nginx转发

api >> www.chuanzhi.com

www.itheima.com >> www.itheima.com/api

拦截了/api的请求的时候,由于页面服务器没有/api的接口

所以使用nginx转发到www.chuanzhi.com/api 数据就获取到了

权限管理

前端基础面试题(二)_第2张图片

​ 在传统项目中我们大部分对于权限管理的分配曹勇的是每个根据用户来培肥不同的权限,但是这种方式只使用宇用户少,或者对于权限的区分不是很明显的情况。当用户的种类很多,并且对应的权限比较复杂的时候,这种方法在编写的时候就会非常的麻烦

​ 因此对于用户权限比较多的情况,我们会在用户喝权限中间再加一层角色,通过给角色分配权限,给用户分配角色的方式来实行不同用户的权限分配,(RBAC模式),每一个用户拥有一个角色,而角色可以拥有多个的权限,不同角色我权限分配不同,因此对应角色的用户的权限也会不同。

​ 在日常的工作环境中,每个用户会根据不同的权限,对于一些操作页面的访问有权限,而实现这一功能的主要方式就是动态路由。对于不同的页面蛇者不同的标识,根据用户是否拥有这一页面的标识来判断该用户是否能够访问该页面,他主要实现是利用vue-router里所用拥有的addRouter()方法,来添加路由

动态路由的实现过程

​ 现在vuex中创建新的模块用来存储用户的动态路由表,初始只包含静态路由,在vuex中定义设置里路由表方法和引入全部动态路由,定义筛选动态路由的方法,以及对已有路由表的修改函数。在全局路由前置守卫中先获取用户的全部个人信息,其中包含了能够访问动态路由的标识字段,触发vuex中过滤动态路由函数的方法将其传给它,这些删选出来的动态路由列表就是当前用户能访问的理由列表,在将筛选出来的动态路由表调用addRouter()方法添加到项目的静态路由表中,然后立马调用next.to(to.path)进行下一次跳转。在的渲染函数中绑定获取到的路由表,从而实现给不同用户角色的动态路由展示。

文件上传

  1. axios >> 根据你的参数的形式自动设置的ContentType
  2. 查询字符串(name=xx&age=18) >> application/x-www-form-urlencoded
  3. object >> application/json
  4. formData >> multipart/form-data; boundary=----WebKitFormBoundaryGnwmBAvekLZAnBt2

如果是上传文件,通过后端定义的接口contentType为formData形式

前端调用接口的时候,需要传递FormData对象给接口

axios里面不需要手动设置contentType,会自动判断

步骤

  1. 定义formData对象 const fd = new FormData()
  2. 添加需要上传的文件对象fd.append('key', file)
  3. 传递到接口axios.post(‘/url’, fd)

这种情况下,对应的请求contentType为multipart/form-data

你可能感兴趣的:(前端)