前端面试题第一弹

180 阅读56分钟

5160ecb271a64e7da062e489a7f7ab2b.jpg

**# **

2、介绍数字化大屏,你的职责是什么

①、根据用户需求搭建页面框架

根据 ui 稿绘制图表,调细节

②、自适应适配

自适应宽度:

宽高都自适应,和上一种方案相比,这种横竖都不会出现滚动条,且能完全铺满屏幕。

实现也很简单,在上一个【自适应宽度】的基础上加上高度自适应即可。

// 保存原始画布的宽度
const originCanvasWidth = ref(canvasWidth.value);
// 宽度缩放比例
const ratioWidth = ref(1);

// 当前窗口的宽度
let windowWidth = window.innerWidth; 
// 将画布宽度设置为当前窗口的宽度
canvasWidth.value = windowWidth;
// 计算当前宽度和原始宽度的比例
ratioWidth.value = windowWidth / originCanvasWidth.value;

自适应屏幕:

即宽高都自适应,和上一种方案相比,这种横竖都不会出现滚动条,且能完全铺满屏幕。

实现也很简单,在上一个【自适应宽度】的基础上加上高度自适应即可。

// 画布原始宽高
const originCanvasWidth = ref(canvasWidth.value);
const originCanvasHeight = ref(canvasHeight.value);
// 缩放比例
const ratioWidth = ref(1);
const ratioHeight = ref(1);

// 当前窗口的宽高
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
// 将画布宽高设置为当前窗口的宽高
canvasWidth.value = windowWidth;
canvasHeight.value = windowHeight;
// 计算当前宽高和原始宽高的比例
ratioWidth.value = windowWidth / originCanvasWidth.value;
ratioHeight.value = windowHeight / originCanvasHeight.value;

③、根据后台提供数据,实现数据的自动轮播和替换 => setInterval(定时器)

3、如何实现性能优化

① 、所有的图表和使用到的插件包都可以使用较近的CDN或dns预解析,尽量较少本地加载体积,并且现在各个云端服务器都有相应的加速,所以响应速度的前后对比几乎可以忽略

②、图片的格式需要换成bese64,因为体积更小,质量没变多少,并且加载更快

③、减少http请求的数量,合并公共资源、合并代码块、按需加载资源

④、 js、css命名尽量简短

⑤、 使用防抖和节流对UI进行优化

⑥、 减少反复操作dom

4、vue2的响应式原理 / vue3的响应式原理

vue2的响应式原理

通过 Object.defineProperty 遍历对象的每一个属性,把每一个属性变成一个 getter 和 setter 函数,读取属性的时候调用 getter,给属性赋值的时候就会调用 setter.

当运行 render 函数的时候,发现用到了响应式数据,这时候就会运行 getter 函数,然后 watcher(发布订阅)就会记录下来。当响应式数据发生变化的时候,就会调用 setter 函数,watcher 就会再记录下来这次的变化,然后通知 render 函数,数据发生了变化,然后就会重新运行 render 函数,重新生成虚拟 dom 树。

深入了解:

我们要明白,响应式的最终目标:是当对象本身或对象属性发生变化时,会运行一些函数,最常见的就是 render 函数。不是只有 render,只要数据发生了变化后运行了一些函数,就是响应式,比如 watch。

在具体实现上,vue 采用了几个核心部件:

1.Observer

2.Dep

3.Watcher

4.Scheduler

img

vue3的响应式原理

①、通过Proxy代理:拦截对象中任意属性的变化(属性的读写、增加、删除)

const p = new Proxy(target, handler)
  1. target:源数据
  2. handler:一个对象,声明了代理源数据的指定行为,支持的拦截操作如get、set、deleteProperty

②、通过Reflect反射:一个内置对象,对被代理对象(源对象)的属性进行操作;可以通过内置的get、set、deleteProperty方法操作源对象

③、响应式原理实现简易代码如下:

setup () {
    // 源对象
    let person = {
        name: '张三',
        age: 20,
        sex: '男'
	}

	new Proxy(person, {
        // 当源对象person被读取时调用
        // getter有两个参数,target代表源对象person,propName为源对象中被读取属性的属性名
        get(target, propName) {
            // 通过Reflect反射对源对象进行操作
            return Reflect.get(target, propName)
        },
        
        // 当源对象被修改或增加属性时调用
        // setter有两个参数,target代表源对象person,propName为源对象中被修改或增加属性的属性名,value为被修改或增加属性的属性值
        set(target, propName, value) {
            // 通过Reflect反射对源对象进行操作
            Reflect.set(target, propName, value)
        },
        
        // 当源对象中属性被删除时调用
         // deleteProperty有两个参数,target代表源对象person,propName为源对象中被删除属性的属性名
        deleteProperty(target, propName) {
            return Reflect.deleteProperty(target, propName)
        }
    })
}

5、数组reduce方法用过吗?

array.reduce((preValue, curValue, index, curArr) => { 
    ... }, initValue);

preValue: 必需。初始值, 或者计算结束后的返回值。 curValue: 必需。当前元素。 index: 可选。当前元素的索引。 curArr: 可选。当前元素所属的数组对象。 initValue: 可选。传递给函数的初始值,相当于preValue的初始值。 reduce里面一定要有return,return出去的值也要注意

**用法:**累加累乘、求和、代替reverse、筛选、监听某个元素的执行次数

6、cors如何配置

首先说下vue中的跨域该如何处理:

配置devServer中的proxy

 devServer: {
    proxy: {
      // https://www.bilibili.com/index/ding.json  
      '/aa': {
        target: 'https://www.bilibili.com',
        ws: true, //如果要代理 websockets,配置这个参数
        changeOrigin: true, //用于控制代理服务器请求头中的host值;
        // 若changeOrigin: true时发送请求,代理服务器请求头中的host值与后端服务器的host值一样
        // 若changeOrigin: false时发送请求,代理服务器请求头中的host值不变(即:http://localhost:8080)
        // ws 和 changeOrigin的默认值都是true,一般都把changeOrigin设置为true
        pathRewrite: {
          '^/aa': ''
        }
      },
      // https://smk.tcewm.cn:5443/interf/frontEnd/OP/OP01
      '/dd': {
        target: 'https://smk.tcewm.cn:5443',
        // ws: true,
        changeOrigin: true,
        pathRewrite: {
          '^/dd': ''
        }
      },
    }
  }

然后是react的跨域配置:

①、下载插件

# 安装http-proxy-middleware包
npm install http-proxy-middleware --save

②、新建 setupProxy.js文件

  • 在项目 src 根目录 下创建名叫 setupProxy.js 的文件

③、配置内容

let proxy = require('http-proxy-middleware');

// 本地代理
module.exports = function(app) {
  app.use( 
      '/mock',
      proxy.createProxyMiddleware(
        { 
          target: 'http://yapi.demo.qunar.com/mock/80992/react-antd',
          changeOrigin: true,
          pathRewrite: {
            '^/mock': '/'
          }
      })
  );
  app.use(
      '/api',
      proxy.createProxyMiddleware(
        { 
          target: 'https://quicklyweb.cn',
          changeOrigin: true,
          pathRewrite: {
            '^/api': '/'
          }
      })
  );
};

7、移动端处理过哪些兼容性问题?

① 禁止ios识别长串数字为电话

解决方法:添加 meta 属性

<meta content="telephone=no" name="format-detection">

② 禁止 ios 弹出各种窗口

解决方法:添加全局 CSS 样式

-webkit-touch-callout:none;

③ 禁止 Android 手机识别邮箱

解决方法:添加 meta 属性

<meta content="email=no" name="format-detection" />

④ 禁止 ios 和 Android 用户选中文字

解决方法:添加全局 CSS 样式

-webkit-user-select: none;

⑤ ios 环境下,取消 input 输入框在输入时英文首字母默认大写

解决方法:给 input 标签加上相应属性

<input autocapitalize="off" autocorrect="off" />

⑥ Android 环境下取消语音输入按钮

解决方法:input 框添加样式

input::-webkit-input-speech-button {
    display: none;
 }

⑦ 修改移动端的点击高亮效果(ios,Android均有效果)

-webkit-tap-highlight-color: rgba(0,0,0,0);

⑧ ios 环境下 input 按钮设置 disabled 属性为 true 时显示异常

input[type=button]{
    opacity: 1;
}

⑨ 移动端字体小于 12px 时的异常显示

解决方法:可以先使用整体放大1倍(width、height、font-size等等),再使用 transform 进行缩小

十、在移动端图片上传图片兼容低端机的问题

解决方法:input 加入 accept=“image/*” multiple 属性

十一、在 Android 环境下 placeholder 文字设置行高时会偏上

解决方法:input 有 placeholder 属性时不要设置行高

十二、当设置样式 overflow:scroll 或 auto时,在 IOS 上滑动会出现卡顿

-webkit-overflow-scrolling: touch;

十三、移动端 click 事件存在延迟的问题

window.addEventListener("load",function () {
    FastClick.attach(document.body);
},false);

十四、移动端1px边框问题

.box{
    position: relative;
    border: none;
}
.box:after{
    content: '';
    position: absolute;
    bottom: 0;
    background: #000;
    width: 100%;
    height: 1px;
    transform: scaleY(0.5);
    transform-origin: 0 0;
}

8、是否碰到过移动端弹窗滑动穿透事件?如何处理?

①、通过body的 position: fixed 来解决滑动穿透。当然也会发生页面会自动滚动到顶部的情况。我们可以记录下当前页面的背景内容滚动位置来解决这个问题。

②、通过css属性overscroll-behavior

.mask .popup_content {
  /* position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%,-50%);
  z-index: 9;
  width: 100px;
  height: 200px;
  overflow-y: scroll; */
  overscroll-behavior: contain;
}

③ 还有设置在body上也可以禁止下拉刷新和回弹效果

body {
	/* 禁用默认的下拉刷新和边界效果
     但是依然可以进行滑动导航 */
	overscroll-behavior-y: none;
}

9、nginx负载均衡

nginx应用场景之一就是负载均衡。在访问量较多的时候,可以通过负载均衡,将多个请求分摊到多台服务器上,相当于把一台服务器需要承担的负载量交给多台服务器处理,进而提高系统的吞吐率;另外如果其中某一台服务器挂掉,其他服务器还可以正常提供服务,以此来提高系统的可伸缩性与可靠性。

nginx负载均衡策略:1、最少连接

​ 请求分配给活动连接数最少的服务器,哪台服务器连接数最少,则把请求交给哪台服务器,由nginx统计服务器连接数

​ 2、ip hash

​ 这个方法确保了相同的客户端的请求一直发送到相同的服务器,这样每个访客都固定访问一个后端服务器。如用户需要分片上传文件到服务器下,然后再由服务器将分片合并,这时如果用户的请求到达了不同的服务器,那么分片将存储于不同的服务器目录中,导致无法将分片合并,该场景则需要使用ip hash策略。

​ 3、轮询

​ 以循环方式分发对应用服务器的请求,将请求平均分发到每台服务器上。

10、css如何使一个定高不定宽的盒子始终垂直居中

① 使用flex中的 justify-content: center;align-item: center

11、上拉加载怎么做的?

微信小程序中,onReachBottom生命周期函数可以实现上拉加载功能

onReachBottom() {
        //上拉加载分页数据
        var that = this
        
            wx.showLoading({
                title: '玩命加载中',
            })
            that.data.pageIndex++ //页码自增
            //发起请求
            wx.request({
                url: `${config}/wxHomework/getStuUploadRecord`,
                data: {
                    page: that.data.pageIndex,
                    limit: that.data.pageSize,
                    stuId: wx.getStorageSync('studentInfo').id
                },
                success(resp) {
                    if (resp.data.code === 0) {
                        that.setData({
                            //拼接记录数据
                            uploadRecord: that.data.uploadRecord.concat(resp.data.page.list)
                        })
                    }
                    // 隐藏加载框
                    wx.hideLoading();
                }
            })

12、搜索框防抖怎么做的?

  • 场景需求:
    • 搜索输入时 ,判断用户在输入完成后 实现即时的自动搜索
    • 并且要防止过度自动搜索消耗性能

vue中的具体思路

思路: 使用 watch + v-model

  • v-model 实现数据输入的同步更新(数据双向绑定)
  • watch 监听输入变化,使用防抖函数实现后续操作

防抖和节流的原理都使用到了闭包的概念:闭包的好处是里面的变量只会在闭包中使用,也就是外部的函数是不能使用这个变量的,也就不会修改这个变量,就不会受到外部的污染;

<body>
  <input id="int" type="text">
</body>
<script>
  const int = document.getElementById("int")

  // 调接口的函数
  const getData = function () {
    console.log("调接口了");
  }

  int.addEventListener('input', double(getData, 1000))

  // 封装防抖函数
  function double(fn, time) {
    let timeout
    return () => {
      // 先清除掉上一次的计时器
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        fn()
      }, time)
    }
  }
</script>

13、自适应你是怎么做的?

rem: 将 px 替换为 rem,动态修改 HTML 根元素的 font-size 适配

通过媒体查询,根据不同屏幕设置 font-size 大小

vw、vh =》视口单位

vw : 相对于视口的宽度,1vw 等于视口宽度的1%(总视口宽度为100vw)

vh : 相对于视口的高度, 1vh 等于视口高度的1%(总视口高度为100vh)

提示:横屏时要交换视口单位哦~

14、rem的基准值具体是什么?

html 中的根字号 font-size

15、webpack讲一讲

16、ajax和axios你讲一讲,具体做了什么

ajax

var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';

xhr.onload = function() {
  console.log(xhr.response);
};

xhr.onerror = function() {
  console.log("Oops, error");
};

xhr.send();

ajaxaxios其实都是基于XHR来实现,所以这里还是要简单介绍一下XHR

一个相对完整的 XHR 主要监听了两个时间,onloadonerror ,其通过这两个方法来绑定成功和失败的回调事件,并调用 opensend 两个方法来完成一次 requset 的请求

$.ajax({
  url: 'https://xxx',
  type: 'POST',
  data: {
      username: 'root',
      password: '123123'
  },
  success:function(){...}
});

Ajax

  • 属 js 原生,基于XHR进行开发,XHR 结构不清晰。
  • 针对 mvc 编程,由于近来vue和React的兴起,不符合mvvm前端开发流程。
  • 单纯使用 ajax 封装,核心是使用 XMLHttpRequest 对象,使用较多并有先后顺序的话,容易产生回调地狱。

Axios

  • Axios 是一个基于 promise 封装的网络请求库
  • 在浏览器中创建XMLHttpRequest请求,在node.js中创建http请求。
  • 解决回调地狱问题。
  • 自动转化为json数据类型。
  • 支持Promise技术,提供并发请求接口。
  • 可以通过网络请求检测进度。
  • 提供超时处理。
  • 浏览器兼容性良好。
  • 有拦截器,可以对请求和响应统一处理。

Fetch

  • 属于原生 js,脱离了xhr ,号称可以替代 ajax技术。
  • 基于 Promise 对象设计的,可以解决回调地狱问题。
  • 提供了丰富的 API,使用结构简单。
  • 默认不带cookie,使用时需要设置。
  • 没有办法检测请求的进度,无法取消或超时处理。
  • 返回结果是 Promise 对象,获取结果有多种方法,数据类型有对应的获取方法,封装时需要分别处理,易出错。
  • 浏览器支持性比较差。

兼容性

axios是基于 XHR,通过封装实现,这个库本身就考虑过兼容性问题,基本不存在浏览器兼容问题

17、小程序兼容问题你有没有遇到过?有没有遇到过什么坑?

①、 iOS中new Date() 时间格式不兼容

比如newDate(“2018-08-08”),在ios会出现NaN的情况,ios只支持newDate(“2018/08/08”) 我这里写了一个封装方法,来处理这个问题

// 处理时间日期,在ios手机下日期显示问题
const strToDate = (str) => {
  // let d = new Date(str)
  let d = null;
  if (typeof str === 'object' || typeof str === 'number') return new Date(str);
  if (!str) return new Date();
  str = str.replace('T', ' ').replace(/\//g, '-');
  //if(d == null) {
    let date = str.split('.')[0].split(' ')
    let days = date[0].split('-')
    let times = date.length == 1 ? ['00', '00', '00'] : date[1].split(':')
    d = new Date()
    d.setFullYear(days[0])
    d.setMonth(parseInt(days[1]) - 1)
    d.setDate(days[2])
    d.setHours(times[0])
    d.setMinutes(times[1])
    d.setSeconds(times[2])
    if (d.getHours() != times[0]) {
      d.setHours(parseInt(times[0]) + 8) 
    }
    //}
  // console.log('over:', str, ',', d);
  return d
}

②、 input ios自带键盘选择文字更多文字向下滑动,会出现键盘消失问题

关于这个问题,官方文档给出的方法是用,always-embed="{{true}}",但是实际情况有一些ios版本会出现第一次键盘消失,第二次的时候就不会出现

<input type="text" 
  placeholder="搜索" 
  confirm-type="search" 
  bindconfirm="searchBind" 
  placeholder-class="placeholder" 
  value="{{inputName}}" 
  always-embed="{{true}}" 
/>

③、页面滚动卡顿

// 设置
-webkit-overflow-scrolling:touch;

④、长按图片安卓能显示,iOS系统不能显示问题

这个图片路径,我之前设置的是本地路径,以至于安卓手机能访问,苹果手机访问不了。

解决办法:将图片路径改为网页路径,即可兼容iOS版本

  previewImage: function (e) {
    var that = this
    var current = e.target.dataset.src;   //这里获取到的是一张本地的图片
    wx.previewImage({
      current: current,//需要预览的图片链接列表
      urls: [current]  //当前显示图片的链接
    })
  },

⑤、video 视频格式m3u8,在ios里边儿不能播放,但是安卓能正常播放?

官方给的例子,是mp4格式的文件,ios可以播放,微信小程序开发者平台的论坛也有类似的问题,但是没有准确的答案,我开始考虑转换下格式,奈何,技术不够,尝试添加在image 标签 添加 custom-cache={{cache}} ,data 中添加 cache:false问题解决了。

<video src="{{sbDress}}" controls   custom-cache="{{cache}}"></video>

17、echarts你用过哪些图表?影响最深刻的是什么?展开讲一讲

柱状图、折线图、饼图、散点图、雷达图

对我印象最深的是蓝丁格尔玫瑰图,

1、首先是辨识度,圆润的外表,配上显示的数据,可以更直观的展示出具体信息

2、当初因为是第一次接触,对每个配置项都是懵懵懂懂

例如:

type:series图表类型,常用值:bar(柱形图)、line(折线图)、pie(饼图)、scatter(散点图)

radius:饼图的半径,如:10,10%,[0,10](内半径,外半径)

startAngle:起始角度,从0到360度,默认90度

labelLine.show:是否显示视觉引导线

emphasis.scale:是否开启高亮后扇区的放大效果,默认true

selectMode:是否支持多选,可选值:true、false(默认)、single、multiple

tooltip.position:提示框位置,如:[10, 10](左侧10px、上侧10px)、[10%,10%]

18、数组方法有哪些?介绍一下

一、 检测方法

Array.isArray()

判断传入的值是否是一个数组。

// true
Array.isArray([1, 2, 3])
// false
Array.isArray({foo: 123})
// false
Array.isArray('foobar')   
// false
Array.isArray(undefined)  
复制代码

二、 创建数组方法

Array.from()

Array.from()方法用于将类数组对象可迭代对象转为真正的数组,并且返回一个新的,浅拷贝的数组实例。

// 报错
Array.from(undefined)
// 报错
Array.from(null)
// ["f", "o", "o"]
console.log(Array.from('foo'))
// []
console.log(Array.from(''))
// []
console.log(Array.from(123))
// []
console.log(Array.from(NaN))

// arguments对象转为数组
function foo() {
  const args = Array.from(arguments)
  //true
  console.log(Array.isArray(args))
}
foo(1, 2, 3)

// NodeList对象转为数组
Array.from(document.querySelectorAll('p'))

// Set对象转为数组:['a','b']
Array.from(new Set(['a', 'b'])) 

// Map对象转为数组:[[1, 2], [2, 4]]
Array.from(new Map([[1, 2], [2, 4]])) 
复制代码
// 传入第二个参数回调函数:[2, 4, 6]
Array.from([1, 2, 3], x => x + x)
复制代码
let obj = {
  num: 1,
  handle: function(value){
    return n + this.num
  }
}
// 传入第三个参数修改this指向:[2, 3, 4, 5, 6]
const arrs = Array.from([1, 2, 3, 4, 5], obj.handle, obj)
复制代码
// 得到数组对象里的id属性:[1, 2]
const obj = [{id: 1,name: 'zhangsan'},{id: 2,name: 'lisi'}]
Array.from(obj,(el) => {
  return el.id
})
复制代码

注意: Array.from(null)或者Array.from(undefined)会抛出异常

Array.of()

Array.of()创建一个包含所有传入参数的数组,不考虑参数的数量或类型,返回一个新数组。

使用Array.of() 创建新数组:

Array.of()                  // []
Array.of(undefined)         // [undefined]
Array.of(null)              // [null]
Array.of(NaN)               // [NaN]
Array.of(1)                 // [1]
Array.of(1, 2)              // [1, 2]
Array.of([1,2,3])           // [[1,2,3]]
Array.of({id: 1},{id: 2})   // [{id:1}, {id:2}]
复制代码

三、 遍历(迭代)方法

forEach()

对数组中的每一项运行指定的函数。这个方法返回undefined,即使你return了一个值。

Array.forEach()参数语法:

  1. 第一个参数(必填): callback在数组每一项上执行的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1,name: 'zhangsan'},{id: 2,name: 'lisi'}]
// 1 - zhangsan
// 2 - lisi
arr.forEach(el => {
    console.log(`${el.id} - ${el.name}`);
});

const obj = {
  handle: function(n){
    return n + 2
  }
};
// true 
[{id: 1,name: 'zhangsan'},{id: 2,name: 'lisi'}].forEach(function(el,index,arr){
  if(el.id === 1) {
    return
  }
  console.log(this === obj)
},obj);
复制代码

Array.forEach()不能中断循环(使用break,或continue语句)。只能用return退出本次回调,进行下一次回调。

map()

返回一个新数组,结果是该数组中的每个元素都调用提供的函数后返回的结果。

Array.map()参数语法:

  1. 第一个参数(必填): callback生成新数组元素的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const newArr = arr.map((el,index,arr) => {
  el.age = 20
  return el
});
//[{id: 1,age: 20},{id: 2,age: 20},{id: 3,age: 20}]
console.log(newArr);
复制代码

filter()

对数组中的每一项运行指定的函数,返回该函数会返回true的项组成的新的数组。如果没有任何数组元素通过测试,则返回空数组。

Array.filter()参数语法:

  1. 第一个参数(必填): callback用来测试数组的每个元素的函数。返回true 表示该元素通过测试,保留该元素,false 则不保留。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const newArr = arr.filter((el,index,arr) => {
  el.age = 20
  return el
});
// [{id: 1,age: 20},{id: 2,age: 20},{id: 3,age: 20}]
console.log(newArr);
复制代码

some()

检测数组中的是否有满足判断条件的元素。

对数组中的每一项运行指定的函数,如果该函数对任一项返回true,则返回true,并且剩余的元素不会再执行检测。如果没有满足条件的元素,则返回false

Array.some()参数语法:

  1. 第一个参数(必填): callback用来测试每个元素的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const someResult = arr.some((el,index,arr) => {
  return el.id === 1
});
// true
console.log(someResult)
复制代码

every()

检测数组所有元素是否都符合判断条件。

对数组中的每一项运行指定的函数,如果该函数对每一项都返回true,则返回true。若收到一个空数组,此方法在一切情况下都会返回true。如果数组中检测到有一个元素不满足,则返回 false,且剩余的元素不会再进行检测。

Array.every()参数语法:

  1. 第一个参数(必填): callback用来测试每个元素的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身(可选)
  1. 第二个参数(可选):当执行回调函数时用作 this 的值。
// true
[].every(() => {})
复制代码
const arr = [{id: 1},{id: 2},{id: 3}]
const everyResult = arr.every((el,index,arr) => {
  return el.id > 0
});
// true
console.log(everyResult)
复制代码

find()

返回数组中匹配的第一个元素的值,否则返回undefined

Array.find()参数语法:

  1. 第一个参数(必填): callback在数组每一项上执行的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引 (可选)数组本身 (可选)
  1. 第二个参数(可选):当执行回调函数时 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
const findResult = arr.find((el,index,arr) => {
  return el.id  === 1
},obj);
// {id: 1}
console.log(findResult)
复制代码

findIndex()

返回数组中匹配的第一个元素的索引。否则返回-1

Array.findIndex()参数语法:

  1. 第一个参数(必填): callback在数组每一项上执行的函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引值数组本身
  1. 第二个参数(可选):当执行回调函数时 this 的值。
const arr = [{id: 1},{id: 2},{id: 3}]
// 2
const findResult = arr.findIndex((el,index,arr) => {
  return el.id  === 3
},obj)
复制代码

entries()keys()values()

用于遍历数组,它们都返回一个遍历器Array Iterator对象。可以用for...of循环进行遍历,他们的区别是keys()是对键名的遍历、values()是对键值的遍历entries()是键值对的遍历。

// 0
// 1
for (let i of ['a', 'b'].keys()) {
  console.log(i)
}

// a
// b
for (let el of ['a', 'b'].values()) {
  console.log(el)
}

// 0-a
// 1-b
for (let [i, el] of ['a', 'b'].entries()) {
  console.log(`${i}-${el}`)
}
复制代码

可以手动调用遍历器对象的next方法,进行遍历。

const arr = ['a', 'b', 'c']
const tempIterator = arr.entries()
// [0, "a"]
console.log(tempIterator.next().value)

// [1, "b"]
console.log(tempIterator.next().value)
复制代码

四、操作方法

push()

将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

var numbers = [1, 2, 3]
// 5
console.log(numbers.push(4,5))
// [1,2,3,4,5]
console.log(numbers)
复制代码

pop()

从数组中删除最后一个元素,并返回删除的元素。

const arr = ['a', 'b', 'c']
// c
console.log(arr.pop())
// ["a", "b"]
console.log(arr);
复制代码

shift()

shift() 方法从数组中删除第一个元素,并返回删除的元素。

const arr = ['a', 'b', 'c']
// a
console.log(arr.shift())
// ["b", "c"]
console.log(arr)
复制代码

unshift()

将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。

const arr = ['a', 'b', 'c']
// 5
console.log(arr.unshift('d', 'e'))
// ["d", "e", "a", "b", "c"]
console.log(arr)
复制代码

concat()

用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。如果省略参数,则concat会返回当前数组的浅拷贝。

const arr = [1, 2, 3]
const newArr = arr.concat()
// [1,2,3]
console.log(newArr)
// false
console.log(newArr === arr)
复制代码
const arr = [1, 2, 3]
const newArr = arr.concat([4, 5])
// [1, 2, 3, 4, 5]
console.log(newArr)
复制代码

indexOf()lastIndexOf()

这两个方法都返回要查找的元素在数组中的位置,或者在没找到的情况下返回-1indexOf()方法从数组的开头开始向后查找,lastIndexOf()方法则从数组的末尾开始向前查找。

Array.indexOf()、Array.lastIndexOf()参数语法:

  1. 第一个参数 searchElement(可选):被查找的元素。
  2. 第二个参数 fromIndex(可选):indexOf()方法表示开始向后查找的位置。默认值为0lastIndexOf()方法表示从此位置开始逆向查找。默认为数组的长度减 1 (arr.length - 1)。
indexOf() 
const numbers = [1, 2, 3, 4, 5, 4]
// 3
console.log(numbers.indexOf(4))
// 5
console.log(numbers.indexOf(4, 4)) 
复制代码
lastIndexOf()
const numbers = [1, 2, 3, 4, 5, 4]
// 5
console.log(numbers.lastIndexOf(4))
// 3
console.log(numbers.lastIndexOf(4, 4))
复制代码

slice()

创建一个新的数组并返回。该方法接受两个参数:是一个由起始索引和结束索引的提取出来的原数组的浅拷贝。原始数组不会被改变。

Array.slice()参数语法:

  1. 第一个参数(可选):起始索引 begin(默认从 0 开始),从该索引开始提取原数组元素。
  2. 第二个参数(可选):结束索引 end 在该索引结束提取原数组元素。如果该参数省略,则一直提取到原数组末尾结束。slice 会提取原数组中beginend的所有元素(包含 begin,但不包含end)。
const arr = [1, 2, 3, 4]
const newArr = arr.slice(1)
// [2,3,4]
console.log(newArr);
const newArr1 = arr.slice(1, 3)
// [2,3]
console.log(newArr1)
复制代码

如果结束位置小于起始位置,则返回空数组。

const arr = [1, 2, 3, 4]
const newArr = arr.slice(2, 1)
// []
console.log(newArr)
复制代码

splice()

向数组的中删除插入替换元素。返回值是被删除的元素组成的一个数组。如果没有删除元素,则返回空数组。此方法会改变原数组。

删除任意数量的元素,传入 2 个参数,要删除的元素开始索引和要删除的个数。

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
//删除前两个元素
arr.splice(0, 2)
// [{id: 3}]
console.log(arr)
复制代码

向指定位置插入任意数量的元素,传入3个参数:起始位置、0(要删除的元素个数) 和要插入的元素。如果要插入多个元素,可以再传入第四、第五,以至任意多个元素。

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
// 从索引 1 开始插入两个元素
arr.splice(1, 0, { id: 4 }, { id: 5 })
// [{ id: 1 }, { id: 4 }, { id: 5 },{ id: 2 }, { id: 3 }]
console.log(arr)
复制代码

向指定位置插入任意数量的元素,且同时删除任意数量的元素。传入3个参数:起始位置、要删除的元素个数和要插入的元素。

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
// 从索引 1 开始,删除一个元素,并切插入两个元素
arr.splice(1, 1, { id: 4 }, { id: 5 })
// [{ id: 1 }, { id: 4 }, { id: 5 },{ id: 3 }]
console.log(arr)
复制代码

copyWithin()

在数组内部替换自身元素,返回修改后的当前数组。

Array.copyWithin()参数语法:

  1. 第一个参数(必填):从该位置开始替换元素。
  2. 第二个参数(可选):从该位置开始复制数据,默认为 0
  3. 第三个参数(可选):停止复制的索引(不包含自身),默认值为数组的长度。
// 将数组的前两个元素替换数组的最后两个位置:[1,2,1,2]
// 从索引2的位置开始替换
// 从索引0的位置开始复制数据
[1, 2, 3, 4].copyWithin(2,0)

const arr = [{id: 1},{id: 2},{id: 3}]
// [{id: 3},{id: 2},{id: 3}]
arr.copyWithin(0, 2)

// 从索引2的位置开始替换
// 从索引0的位置开始复制
// 在遇到索引1的时候停止复制(不包含自身)
// [1,2,1,4]
[1, 2, 3, 4].copyWithin(2,0)
复制代码

fill()

使用固定值填充一个数组中一个或多个元素。

  1. 第一个参数:用来填充数组元素的值。
  2. 第二个参数(可选):起始索引,默认值为0
  3. 第二个参数(可选):终止索引,默认值为数组的长度。

当传入一个参数的时候,会用这个参数的值填充整个数组:

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]

arr.fill({ id: 4 })
// [{ id: 4 }, { id: 4 }, { id: 4 }]
console.log(arr)
// true
console.log(arr[0] === arr[1])
复制代码

当传入多个个参数的时候,用这个参数的值填充部分数组:

const arr = [{ id: 1 }, { id: 2 }, { id: 3 }]
// 从数组下标索引为1的元素开始填充
arr.fill({ id: 4 }, 1)
// [{ id: 1 }, { id: 4 }, { id: 4 }]
console.log(arr)

// 填充的元素不包括终止的索引元素。
const numbers = [1, 2, 3, 4]
numbers.fill(0, 1, 2)
// [1, 0, 3, 4]
console.log(numbers)
复制代码

flat()

将嵌套的数组,变成一维的数组。返回一个新数组。

Array.flat()参数语法:

  1. 第一个参数(可选):指定要提取嵌套数组的结构深度,默认值为 1。

展开一层

const arr = [1, 2, [3, 4]]
const newArr = arr.flat()
//[1,2,3,4]
console.log(newArr)
复制代码

展开两层

const arr = [1, 2, [3, [4, 5]]]
const newArr = arr.flat(2)
// [1, 2, 3, 4, 5]
console.log(newArr)
复制代码

使用 Infinity,可展开任意深度的嵌套数组

var arr = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
const newArr = arr.flat(Infinity)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(newArr)
复制代码

移除数组中的空项

var arr = [1, 2, , 4, 5]
const newArr = arr.flat()
// [1, 2, 4, 5]
console.log(newArr)
复制代码

flatMap()

对原数组的每个成员执行一个函数,然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

Array.flatMap()参数语法:

  1. 第一个参数(必填): callback遍历函数。该函数接收三个参数:
elementindexarray
当前元素当前元素的索引(可选)数组对象本身(可选)
  1. 第二个参数(可选):当执行回调函数时this的值。
var arr = [1, 2]
const newArr = arr.flatMap(el => [el, el * 2])
[1,2,2,4]
console.log(newArr)
复制代码

flatMap()只能展开一层数组

var arr = [1, 2]
const newArr = arr.flatMap(el => [[el, el * 2]])
// [[1,2],[2,4]]
console.log(newArr)
复制代码

includes()

判断一个数组是否包含一个指定的值,如果包含则返回true,否则返回false。使用 includes()比较字符串和字符时是区分大小写的。

Array.includes()参数语法:

  1. 第一个参数:需要查找的元素值。
  2. 第二个参数:表示搜索的起始位置,默认为0
const obj = { id: 1 }
var arr = [obj, { id: 2 }]
// true
console.log(arr.includes(obj))
复制代码

传入第二个参数

console.log([1, 2, 3].includes(3));    // true
console.log([1, 2, 3].includes(3, 3))  // false
console.log([1, 2, 3].includes(3, 2))  // true
复制代码

五、排序方法

sort()

对数组的元素进行排序,并返回排序后的原数组。

Array.sort()参数语法:

  1. 第一个参数(可选):用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符串的ASCII码进行排序。该函数接收二个参数:
firstsecond
第一个用于比较的元素第二个用于比较的元素
// Array的sort()方法默认把所有元素先转换为String再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的ASCII码小。
const arr = [10, 20, 1, 2].sort()
//[1, 10, 2, 20]
console.log(arr);
复制代码

可以接收一个比较函数作为参数,实现自定义的排序。该函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等返回0,如果第一个参数应该位于第二个之后则返回一个正数

const arr = [10, 20, 1, 2]
arr.sort((value1, value2) => {
  if (value1 < value2) {
    return -1
  }
  if (value1 > value2) {
    return 1
  }
  return 0
})
// [1, 2, 10, 20]
console.log(arr)
复制代码

reverse()

将数组中元素的反转,并返回该数组。该方法会改变原数组。

const values = [1, 2, 3, 4, 5]
values.reverse()
//[5, 4, 3, 2, 1]
console.log(values)
复制代码

六、 转换方法

toLocaleString()

toLocaleString() 返回一个字符串表示数组中的元素。数组中的元素将使用各自的 toLocaleString 方法转成字符串,这些字符串将使用一个特定语言环境的字符串,并用逗号隔开。

const array1 = [1, 'a', { id: 1 }, new Date()]
// 1,a,[object Object],2020/1/15 上午7:50:38
console.log(array1.toLocaleString())
复制代码

toString()

返回一个由逗号连接起来的字符串。

const array1 = [1, 'abc', { id: 1 }]
// 1,abc,[object Object]
console.log(array1.toString())
复制代码

join()

将一个数组的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个元素,那么将返回该元素,而不使用分隔符。

Array.join()参数语法:

  1. 第一个参数(可选):指定一个字符串来分隔数组的每个元素。如果不传,默认数组元素用逗号(,)分隔。如果是空字符串(""),则所有元素之间都没有任何字符。
const arr = [1, 2, 3]
// 1,2,3
console.log(arr.join())
// 123
console.log(arr.join(''))
// 1+2+3
console.log(arr.join('+'))
复制代码

七、 归并方法(迭代数组的所有项,然后构建一个最终返回的值)

reduce()

reduce()方法从数组的第一项开始,迭代数组的所有元素,构建一个最终返回的值,返回函数累计处理的结果。

Array.reduce()参数语法:

  1. 第一个参数(必填): callback执行数组中每个值的函数。该函数接收四个参数:
prevcurindexarray
初始值, 或者上一次调用回调函数返回的值(必填)当前元素值 (必填)当前元素的索引值(可选)数组对象本身(可选)

这个函数返回的任何值都会作为第一个参数自动传给下一项。

  1. 第二个参数(可选): initialValue作为第一次调用callback函数时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。在没有初始值的空数组上调用reduce将报错。
 //Uncaught TypeError: Reduce of empty array with no initial value
 [].reduce(() => {})
复制代码
const arr = ['L', 'O', 'V', 'E'].reduce((prev, cur) => {
  console.log('prev: ', prev)
  console.log('cur: ', cur)
  return prev + cur
})
// LOVE
console.log(arr)
复制代码

第一次执行回调函数,prev 是 L,cur 是 O。第二次,prev 是 LO,cur 是 V(数组的第三项)。这个过程会持续到把数组中的每一项都访问一遍,最后返回结果LOVE。

reduceRight()

reduceRight()reduce()作用类似,使用reduce()还是reduceRight(),主要取决于要从哪头开始遍历数组。除此之外,它们完全相同。

var values = [1,2,3,4,5]
var sum = values.reduceRight(function(prev, cur, index, array){
  return prev + cur
});
//15
alert(sum)
复制代码

第一次执行回调函数,prev 是 5,cur 是 4。第二次,prev 是 9(5 加 4 的结果),cur 是 3(数组的第三项)。这个过程会持续到把数组中的每一项都访问一遍,最后返回结果。

八、demo

实现由短划线分隔的单词变成骆驼式的

camelize("background-color") === 'backgroundColor'
function camelize(str) {
  return str
    .split('-') // my-long-word -> ['my', 'long', 'word']
    .map(
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    ) // ['my', 'long', 'word'] -> ['my', 'Long', 'Word']
    .join(''); // ['my', 'Long', 'Word'] -> myLongWord
}
复制代码

数组去重

function unique(arr) {
  let result = [];
  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str)
    }
  }
  return result
}
复制代码

在已有的数组上创建一个对象,id作为键,数组的每一项作为值。

let users = [
  { id: '111', name: "zhangsan", age: 20 },
  { id: '222', name: "lisi", age: 24 },
  { id: '333', name: "wangwu", age: 31 },
]
function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value
    return obj
  }, {})
}
/* 
  {
    111: { id: "111", name: "zhangsan", age: 20 },
    222: { id: "222", name: "lisi", age: 24 },
    333: { id: "333", name: "wangwu", age: 31 }
  } 
*/
console.log(groupById(users))

19、防抖和节流的原理和应用场景,你讲一讲

①、防抖

image-20230131101229183

image-20230131101312040

原理: 当事件被触发 n 秒后再执行回调,如果在 n 秒内又被触发,则重新计时。 使用场景: 频繁触发按钮点击事件、input框搜索等等。

2.gif

// 输入框
<el-form-item label="整机名称" prop="name" class="filter-item">
  <el-input v-model.trim="queryForm.name" placeholder="请输入" @input="handleInputChange"></el-input>
</el-form-item>

// methods
import { debounce } from '@/utils/common.js';

handleInputChange: debounce(() => {
  console.log(new Date());
}, 1000)

// common.js
/**
 * 函数防抖: 当事件被触发 n 秒后再执行回调,如果在 n 秒内又被触发,则重新计时。
 * @param {*} func 
 * @param {*} wait 
 * @returns { Function }
 */
export const debounce = (func, wait) => {
  let delay = wait || 500;
  let timer;
  return () => {
    const _this = this;
    let args = arguments;
    if(timer) clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      func.apply(_this, args)
    }, delay)
  }
}

②、节流

image-20230131101334131

image-20230131101555888

原理: 规定在一个单位时间内只能触发一次函数,如果在单位时间内触发多次,只执行一次。比如两秒内的点击事件,无论点多少次,两秒内只执行一次。 使用场景: 下拉加载更多(无限滚动)事件、浏览器的resize,scroll事件等等

4.gif

// button
<el-form-item class="filter-item">
  <el-button type="primary" @click="handleSubmit">提交</el-button>
</el-form-item>

// methods
handleSubmit: throttle(() => {
  console.log(new Date());
}, 2000),

// common.js
/**
 * 函数节流: 规定在一个单位时间内只能触发一次函数,如果在单位时间内触发多次,只执行一次。
 * @param {*} func 
 * @param {*} wait 
 * @returns { Function }
 */
export const throttle = (func, wait) => {
  let delay = wait || 500;
  let timer;
  return () => {
    let _this = this;
    let args = arguments;
    if(!timer) {
      timer = setTimeout(() => {
        func.apply(_this, args);
        timer = null;
      }, delay)
    }
  }
}

20、闭包

概念:mdn中说,一个函数对周围状态的引用捆绑在一起,内层函数中访问到外层函数的作用域

简单理解:闭包 = 内层函数 + 引用的外层函数变量

image-20230131094212808

作用:实现了数据的私有

image-20230131094452787

通常会再使用一个函数包裹住闭包结构,以起到对变量保护的作用

闭包不一定非要有return,也不一定会造成内存泄漏

image-20230131094803074

js高级程序设计里说:

闭包是函数,是一个可以访问别的函数作用域里面东西的函数。

21、事件循环

image-20230131095552634

image-20230131095631178

22、宏任务和微任务

image-20230131095839394

22、原型和原型链

image-20230131102024025

23、h5新特性和c3新特性

H5 的新特性

1.自定属性

2.语义化更好的内容标签(header,nav,footer,aside,article,section)

3.音频视频的自动播放控件 autoplay

4.canvas和image在处理图片时的区别:

image是通过对象的形式描述图片, canvas 通过专门的API将图片绘制到画布

  1. 本地离线存储 localStorage 用于长久保存整个网站的数据,保存没有时间限制,直到手动删 除

  2. sessionStorage 该数据对象临时保存同一窗口(或标签页)的数据,在关闭窗口或 标签页之后将会删除这些数据。

  3. 表单控件 calendar ,date ,time , email , url , search , tel , file , number

C3 的新特性

1、颜色: 新增 RGBA , HSLA 模式

2、文字阴影(text-shadow)

3、边框: 圆角(border-radius) 边框阴影 : box-shadow

4、盒子模型: box-sizing

5、背景:background-size background-origin background-clip

6、渐变: linear-gradient , radial-gradient

7、过渡 : transition 可实现属性的渐变

8、自定义动画 animate @keyfrom

9、媒体查询 多栏布局 @media screen and (width:800px) {…}

10、border-image 图片边框

11、2D 转换/3D 转换;transform: translate(x,y) rotate(x,y) skew(x,y) scale(x,y)

12、字体图标 iconfont/icomoon

13、弹性布局 flex

24、跨域你是怎么解决的?

一、考点一,什么是跨域?

从字面上看,跨域就是跨域名,但是真正的跨域的并没有那么简单,因为浏览器存在同源策略,是浏览器厂商设置的一个重要的安全策,看一下MDN对浏览器同源策略的介绍:

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

如果缺少同源策略,浏览器很容易受到XSSCSRF等攻击。

  • XSS全称叫跨站脚本攻击(Cross Site Scripting),目的是把script脚本注入到网页中在浏览器执行该脚本

  • CSRF全称叫跨站请求伪造(Cross Site Request Forgery),目的是冒充用户做其他操作;

    解决CSRF方式:

    设置SameSite属性

    同源检测:后端验证Origin和Referer禁止外域或者不受信任的域名的请求

    后端添加Token验证(如常见的Bearer Token)

同源的定义:

只有协议、域名、端口都相同的情况下,才属于同源,如图所示,阴影区域必须都保持一致才属于同源。反之,如果三者有一个不一致,就会产生跨域。

二、考点二,服务器能否收到跨域的请求?

这里很容易搞混,既然跨域了,那发送的请求应该会被浏览器给砍掉,不会到达服务器端,实则不然。跨域是可以正常发起请求的,服务器端能够收到请求并且正确返回结果,只是被浏览器拦截了

image.png

三、解决跨域的方式:

image.png

①、 CORS是一个W3C标准,全称是"跨域资源共享",它允许浏览器向跨域服务器,发出XMLHttpRequest请求,从而克服同源的限制。

CORS实现的本质,就是在浏览器请求中,自动添加一些附加的头信息。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的Ajax通信没有差别,代码完全一样,对于用户来说,也是无感知的。

如果是是使用Node + Express开发服务端,推荐使用npm的包:cors,使用方式:

//安装包
npm install cors
​
const express = require('express')
const app = express()
​
//引入cors
const cors = require('cors')
​
//使用
app.use(cors())

②、 Node代理

②.1、在node中实现

通过设置cors需要的首部字段,并转发的方式,实现跨域

//server1 ---http://www.abc.comconst http = require('http')
​
const server = http.createServer((req, res) => {
    //因为server1直接和浏览器通信,需要设置cors需要的首部字段
    req.setHeader({
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Access-Control-Allow-Headers': 'Content-Type'
    })    
    
    //转发给server2服务器
    http.request({
        host: 'http://www.example.com',
        path: '/getDaata',
        port: 80,
        method: req.method,
        headers: req.headers
    }, request => {
        let body = '';
        request.on('data', (chunk) => {
            body += chunk;
        }).on('end', () => {
            res.end(body);
        })
    })
    
})
​
server.listen(80, () => {
    console.log('server running at http://www.abc.com')
})

②.2、 在webpack中实现

//webpack.config.js

module.exports = {
    ....
    devServer: {
        port: 80,
        proxy: {
          "/api": {
            target: "http://www.example.com"
          }
        }
    }
}

②.3 、在vue-cli中实现

//vue.config.js

module.exports = {
    devServer: {
        proxy: {
          '/api': {
             target: 'http://www.example.com',
              changeOrigin: true,
              pathRewrite: {
                 '^/api': ''
              }
          }
        }
    }
}

③、JSONP

JSONP也是常见的一种解决跨域的方式,但是存在一个缺陷,JSONP只支持GET请求,而CORS支持所有类型的HTTP请求。但是它的浏览器兼容性更好,支持较低版本的浏览器。

利用这个特点,就产生了JSONP这种跨域通信的方式,它的基本思想是,网页通过添加一个<script>元素,向服务器请求数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

网页动态插入<script>元素,由它向跨源网址发出请求

25、es6新增有哪些?

①、新增let const 变量

let声明变量,const声明常量,都存在块级作用域

②、新增...展开运算符

③、新增解构

从复杂数据(数组或对象)中,快速用变量从里面取出数据(复杂数据简单化)
[ ]解构数组
{ }解构对象

④、新增Set Map方法

⑤、新增Class类

class Persosn{
    //属性
    constructor(name,age){
        this.name = name
        this.age = age
    }
    //方法
    say(){
        console.log('方法')
    }
}

//实例化
let person = new Person('80E140E',18)

⑥、新增Promise对象

⑦、新增箭头函数

26、在网页上输入一个网址,弹出一个页面,要经过哪些程序步骤?

总体来说分为以下几个过程:

1.DNS解析,找到IP地址 2.根据IP地址,找到对应的服务器 3.建立TCP连接(里面有个 三次握手) 4.连接建立后,发出HTTP请求 5.服务器根据请求作出HTTP响应 6.浏览器得到响应内容,进行解析与渲染,并显示 7.断开连接(四次挥手)

详情

27、TCP协议了解多不多

①、TCP/IP 包含了五层(或四层)模型,从上层往下层分别是:

  • 应用层:负责应用程序间的数据通讯。

  • 传输层:负责两台主机之间的数据传输。

  • 网络层:负责网络地址的管理和路由选择。

  • 数据链路层:负责设备之间的数据帧的传送和识别。

  • 物理层(可选):负责数据和信号间的转换。(作为软件工程师一般不需要关注物理层,所以通常我们说 TCP/IP 四层模型更多一些。)

②、什么是TCP?

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;

  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;

  • 字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。

TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

建立连接

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手而进行的。

img

img

断开连接

TCP 断开连接是通过四次挥手方式,双方都可以主动断开连接,断开连接后主机的资源将被释放。

img

img

28、vue生命周期

生命周期.webp

29、深拷贝和浅拷贝

浅拷贝 : 浅拷贝是指对基本类型的值拷贝,以及对对象类型的地址拷贝

最简单直接的浅拷贝就是直接赋值,如:let obj = xxx或者Array.prototype.slice()

深拷贝:指复制并创建一个一模一样的对象,不共享内存,修改新对象,旧对象保持不变。

Object.assign() - 有限制,首先它只能用于对象的拷贝,其次它是只能深拷贝第一层,第二层开始就是浅拷贝了(一深二浅),不能拷贝循环引用类型。

拓展运算符 - 有限制。仅作为第一层是为深拷贝,可以拷贝复杂类型,对于数组和对象都一样,对于第二层极其以后的值,扩展运算符将不能对其进行打散扩展,也不能对其进行深拷贝,即拷贝后和拷贝前第二层中的对象或者数组仍然引用的是同一个地址,其中一方改变,另一方也跟着改变。

JSON.parse(JSON.stringify(obj)):

如果obj里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串。

如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象。

如果obj里有函数,undefined,则序列化的结果会把函数, undefined丢失。

如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null。

JSON.stringify()只能序列化对象的可枚举的自有属性。如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor。

如果对象中存在循环引用的情况也无法正确实现深拷贝。

想要使用较为完整的深拷贝:

建议使用Lodash库,这是一个很强大的库,里面有各种各样的封装方法,十分强大。

30、小程序做没做过?

之前做过,在第一家公司的时候负责过一段时间的维护。

31、小程序开发和web app 开发有什么区别?

微信小程序是继原生APP、WebAPP之后出现的一种新的APP形态。原生APP随着Android和iOS两大平台的迅速发展而逐渐变得强大起来,但由于两个平台的互不兼容,就需要开发两个版本,开发成本比较高。使用HTML5开发的Web APP很好地解决了跨平台的问题,但是性能和用户体验不佳。为此,微信小程序借鉴了现有的Web技术,通过独立的运行环境实现了跨平台,并提供了接近原生APP的使用体验,具有明显的优势。下面通过表1-3对比微信小程序与原生APP、Web APP的区别。

微信小程序和原生app、webapp的区别

为了降低微信小程序的开发成本,提高开发效率,微信小程序的开发方式与WebAPP相似,即通过WXML(类似HTML)、WXSS(类似CSS)和JavaScript进行开发,可以使Web开发人员快速上手,并且提供了丰富的组件和接口,从而具有更接近原生APP的使用体验。微信小程序相比Web APP也存在一些缺点,它不支持HTML标签和DOM(文档对象模型)操作,对于CSS的支持也是受限的,一些成熟的前端库和框架(如jQuery、Vue.js)也无法使用。如果将Web APP修改成微信小程序,需要进行大量的改动。

由此可见,微信小程序适合开发一些业务逻辑简单、低频次使用、对性能要求不高的应用。例如,偶尔点一次外卖、偶尔买一张车票或电影票、偶尔租赁一次自行车等。有了微信小程序,就不用专门下载、安装一个原生APP,使用更加快捷、方便。

32、小程序开发工具

uniapp

33、微信小程序基本的文件类型?

通常一个页面有四个文件组成:

①、 js文件 =》 实现业务逻辑

②、 json文件 =》 当前页面的配置文件,如可以配置顶部背景色,字体等

③、 wxss文件 =》 小程序的样式文件,类似于HTML的css文件

④、 wxml文件 =》小程序的页面结构文件,类似于HTML页面中扩展名为html文件

34、计算属性和侦听属性的区别

第一:先看看computed, watch两个的定义,并说一下使用上的差异?

计算属性可以从组件数据产生出新的数据,经常的使用方式是设置一个函数,返回计算之后的结果。

可能面试官会突然问到computed和methods的区别?

computed和methods的差异是它具备缓存性,如果依赖项不变时不会重新计算。

侦听器可以检测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑。(一般用来请求接口)

第二:在使用场景上的差异,怎么去选择?

计算属性computed常用场景是简化template模板中的复杂表达式,模板中出现太多逻辑判断会造成模板不易维护。

侦听器Watch常用场景是状态变化之后做一些异步操作。

至于选择哪种方案时,个人觉得有异步请求的用Wach,其他情况能用计算属性就首选计算属性。

第三:一些使用上的细节、注意事项

使用过程中有一些细节,比如计算属性可以成为既可读get又可写set的计算属性。watch可以额外设置deep、immediate等选项。

当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。但是这样会带来性能问题,优化的话可以使用字符串形式监听。

追问:说说computed的原理是什么?

computed 原理,首先得讲 vue 响应式原理,因为 computed 的实现是基于 Watcher 对象的。 那么 vue 的响应式原理是什么呢?众所周知,vue 是基于 Object.defineProperty 实现监听的。在 vue 初始化数据 data 和 computed 数据过程中。会涉及到以下几个对象:

  • Observe 对象(观察者)
  • Dep 对象(订阅者)
  • Watch 对象 Observe 对象是在 data 执行响应式时候调用,因为 computed 属性基于响应式属性,所以其不需要创建 Observe 对象。 Dep 对象主要功能是做依赖收集,有个属性维护多个 Watch 对象,当更新时候循环调用每个 Watch 执行更新。 Watch 对象主要是用于更新,而且是收集的重点对象。

35、换肤,不实用饿了么ui,你该怎么做,细说一下

36、UniAPP的生命周期

①、应用生命周期:

  • onLaunch :初始化完成时触发(全局只触发一次)。通常用来初始化前置数据,比如获取用户信息。

  • onShow :启动或从后台转入前台显示。例如将这个app放到了后台,现在又从后台中进入。可以用于第二次打开页面需要刷新数据的情况下。

  • onHide :从前台进入后台。例如使用app时返回桌面,这个app将会放到后台这时候就会触发。可以由于清除缓存或者保护隐私的情况下使用。

    上面的更常用,但下面的也很重要

  • onError :报错时触发。

  • onUniNViewMessage :对 nvue 页面发送的数据进行监听。

  • onUnhandledRejection :对未处理的 Promise 拒绝事件监听函数。

  • onPageNotFound :页面不存在时的监听函数。

  • onThemeChange :监听系统主题变化。如果你想为应用适配一个暗色主题,就可以在这个函数中监听系统主题变化然后切换对应的程序主题。

②、页面生命周期:

onLoad :监听页面加载,其参数为上个页面传递的数据,参数类型为 Object。

onShow :监听页面显示,页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面。

onReady :监听页面初次渲染完成。

onHide :监听页面隐藏。

onUnload :监听页面卸载。

onPullDownRefresh :监听用户下拉动作,一般用于下拉刷新。

37、路由权限你是怎么做的

image-20230131192403057

38、详细说一下vuex

vuex的概念

vuex是一个专为 Vue.js 应用程序开发的状态管理模式, 采用集中式存储管理应用的所有组件的状态,解决多组件数据通信。(简单来说就是管理数据的,相当于一个仓库,里面存放着各种需要共享的数据,所有组件都可以拿到里面的数据)

要点:

1.vue官方搭配,专属,有专门的调试工具

2.数据变化是可预测的(响应式)

3.集中式管理数据状态方案

vue11.png

vuex的学习内容

1.state 统一定义管理公共数据

2.mutations: 使用它来修改数据

3.getters: 类似于vue中的计算属性

4.actions: 类似于methods,用于发起异步请求,比如axios

5.modules: 模块拆分

**在组件中使用store:**this.$store.模块名.state.属性名

在模块中,则可以直接省略this而直接写成:{{$store.模块名.state.属性名}}

在组件中修改state使用mutations: this.$store.commit('模块名/mutation事件名',mapper参数)

注意点:

  1. 要修改vuex中的数据,就要使用mutations去修改
  2. 在methods里面$store.state.xxx = xxx这种方式虽然可以直接修改数据,但是vue不推荐,并且在严格模式下通过这种方式修改数据,会直接报错

**vuex中用getters的派生状态:**作用:在state中的数据的基础上,进一步对数据进行加工得到新数据。(与组件中computed一样)

Vuex-用modules来拆分复杂业务

访问数据和修改数据的调整

  • 访问模块中的数据,要加上模块名
获取数据项:  {{$store.state.模块名.数据项名}}
获取getters: {{$store.getters['模块名/getters名']}}

访问模块中的mutations/actions:

  • 如果namespaced为true,则需要额外去补充模块名
  • 如果namespaced为false,则不需要额外补充模块名
$store.commit('mutations名')        // namespaced为false
$store.commit('模块名/mutations名')  // namespaced为true

Vuex-辅助函数mapState来使用公共数据

vuex核心api小结.png

39、let 和const 有什么区别

let 关键字用来声明变量,使用 let 声明的变量有几个特点:

  1. 不允许重复声明
  2. 块儿级作用域(局部变量)
  3. 不存在变量提升
  4. 不影响作用域链

const 关键字用来声明 常量 ,const 声明有以下特点:

  1. 声明必须赋初始值·;
  2. 标识符一般为大写(习惯);
  3. 不允许重复声明
  4. 值不允许修改
  5. 块儿级作用域(局部变量)

var、let和const的区别

  1. var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问. 并不是全局作用域

  2. let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。

  3. const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。

  4. var声明的变量存在变量提升现象,let和const声明的变量不存在变量提升现象(遵循:“先声明,后使用”的原则,否则会报错)。

  5. var不存在暂时性死区,let和const存在暂时性死区。

    解析:只要块级作用域内存在let命令,那么它所声明的变量就会绑定到这个区域,不再受外部的影响。

  6. var允许重复声明同一个变量,let和const在同一作用域下不允许重复声明同一个变量。

    解析:var允许重复声明变量,后一个变量会覆盖前一个变量,const和let会报错。

40、路由怎么传参,怎么接收

Vue-Router 传参可以分为两大类

分别是编程式的导航 router.push 和声明式的导航<router-link>

编程式的导航 router.push

编程式导航传递参数有两种类型:字符串、对象。

字符串 : 字符串的方式是直接将路由地址以字符串的方式来跳转,这种方式很简单但是不能传递参数:

this.$router.push("home");

对象 : 想要传递参数主要就是以对象的方式来写,分为两种方式:命名路由、查询参数

命名路由 这种方式传递参数,目标页面刷新会报错      
 this.$router.push({name:"news",params:{userId:123})      

 查询参数 和 name 配对的式 params,和 path 配对的是query
 
 this.$router.push({path:"/news',query:{uersId:123})
    
 接收参数 this.$route.query

声明式的导航<router-link>

字符串

 <router-link to:"news"></router-link>

命名路由

<router-link :to="{ name: 'news', params: { userId: 1111}}">click to news page</router-link>

查询参数

<router-link :to="{ path: '/news', query: { userId: 1111}}">click to news page</router-link>

41、有哪几种路由模式,详细说一下?

hash与history区别初印象

image.png

Hash模式:

  • URL中#后面的内容作为路径地址
  • 监听hashchange事件
  • 根据当前路由地址找到对应组件重新渲染

history模式:

  • 通过history.pushState()方法改变地址栏
  • 监听popstate事件
  • 根据当前路由地址找到对应组件重新渲染
  • 通过history.pushState,通过history.replaceState:不会触发popstate

精简总结

  • hash(核心hashchange):
    • 初始化render
    • 所有跳转都是hashchange->render
      • 手动改变url(hashchange->render)
      • router-link跳转,push跳转(hashchange->render)
      • 浏览器前进后退(hashchange->render)
  • history(核心pushstate,popstate):
    • 初始化render
    • 跳转情况:
      • 手动改变url(触发重定向->初始化render)
      • router-link跳转,push跳转(pushstate更改url,添加历史记录,调用render)
      • 浏览器前进后退(popstate->render)

42、你是怎么样做H5页面适配的?

1、css3媒体查询方式容器适配

媒体查询,需要你通过媒体查询来根据不同的屏幕宽度进行不同的css编写,也就是说会存在多套css

@media screen and (max-width: 320px){
    ....适配iphone4的css样式
}
@media screen and (max-width: 375px){
     ....适配iphone6/7/8的css样式
}
@media screen and (max-width: 414px){
    ....适配iphone6/7/8 plus的css样式
}
......

2 、百分比布局容器适配

给元素设定不同的百分比占比,这样在不同的大小的容器下面就能够呈现对应的尺寸大小,但是有几个需要注意的点 元素的width/height是相对于直接父元素的width/height进行计算的 元素的margin/padding是相对于直接父元素的width进行计算的 元素的border-radius等是根据自身宽度进行计算的

3、rem相对单位容器适配 rem单位:rem是一个只相对于浏览器的根元素(HTML元素)的font-size的来确定的单位。默认情况下,html元素的font-size为12px 通过rem来实现适配:rem单位都是相对于根元素html的font-size来决定大小的,根元素的font-size相当于提供了一个基准,当页面的宽度发生变化时,只需要改变font-size的值,那么以rem为固定单位的元素的大小也会发生响应的变化。需要先动态设置html根元素的font-size,再计算出其他页面元素以rem为固定单位的值

var deviceWidth = document.documentElement.clientWidth;
deviceWidth = deviceWidth < 320 ? 320 : deviceWidth > 640 ? 640 : deviceWidth;
document.documentElement.style.fontSize = deviceWidth / 7.5 + 'px';
复制代码

4、vh/vm视口单位容器适配

视口单位相对于视口的高度和宽度,而不是父元素的(CSS百分比是相对于包含它的最近的父元素的高度和宽度)。1vh 等于1/100的视口高度,1vw 等于1/100的视口宽度。(有没有感觉到跟百分比相似,只是百分比是对比父元素的宽高,视口单位对比的是视口的宽高)

43、数组去重

①、ES6的Set去重(最推荐)

new Set是ES6新推出的一种类型。他和数组的区别在于,Set类型中的数据不可以有重复的值。当然,数组的一些方法Set也无法调用。

使用方法:其实很简单,将一个数组转化为Set数据,再转化回来,就完成了去重。

    const arr = [1,1,2,2,3,3,4,4,5,5];
    const setData = Array.from(new Set(arr));
    console.log(setData);

image.png

但是Set去重有一个弊端,他无法去重引用类型的数据。比如对象数组。

image.png

所以如果您的数组中都是值类型的数据(比如全string或者全number),那么使用Set进行去重一定是首选,会为您减少很多的麻烦。

②、双重for循环

//双重循环去重
const handleRemoveRepeat = (arr) => {
    for (let i=0,len = arr.length; i < len; i++) {
        for (let j = i + 1; j < len; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1);
                j--;
                len--;
            }
        }
    }
    return arr;
};

image.png

③、最鸡肋的去重方式,indexOf去重

//去重
const handleRemoveRepeat = (arr) => {
    let repeatArr = [];
    for (let i = 0,len = arr.length ; i < len; i++) 
     if (repeatArr.indexOf(arr[i]) === -1)  repeatArr.push(arr[i])
    return repeatArr;
}

④、includes的去重方法

使用includes的去重方法和indexOf不能说很像,基本上一模一样。变换的仅仅只是判断方法。

includes的判断方法更简单了。循环数组的每一样,用新数组检测当前数组中是否包含数组项,如果不包含,则追加该元素

const handleRemoveRepeat = (arr) => {
    let repeatArr = [];
    for (let i = 0,len = arr.length ; i < len; i++)
        if (!repeatArr.includes(arr[i])) repeatArr.push(arr[i])
    return repeatArr;
}

image.png

⑤、最有趣的去重方法,使用filter去重

//去重
const handleRemoveRepeat = (arr) => arr.filter((item,index) => arr.indexOf(item,0) === index);

image.png

indexOf的特性是返回被查找的目标中包含的第一个位置的索引

如图,下标为0和下标为4的位置存储的都是“1”。但是indexOf()只返回了0。因为indexOf的特性是返回被查找的目标中包含的第一个位置的索引。

同样的,我们可以利用这个特性。来完成去重,文字描述恐怕很难表达准确,您可以看看下面的这张图。

image.png

⑥、您或许会问:如果要去重对象数组怎么办?

image.png

因为每一个场景下的对象数组都不一定一样,所以,如果您需要去重对象,根据上方的截图中的代码。使用双重for循环的方法,自己自定义一个可以满足您当前业务需求的去重方法。

image.png

44、created和mounted的区别

created中没有 $el 但可以通过vm访问到data中的数据,methods中的方法。

mounted可以访问DOM节点了

d生命周期是否获取dom节点是否可以获取data是否获取methods
beforeCreate
created
beforeMount
mounted

45、$nextTick()的作用和原理

官方定义:

Vue.nextTick([callback,context]) 在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新之后的DOM

// 修改数据 
vm.msg = 'Hello' 
// DOM 还没有更新 
Vue.nextTick(function () { // DOM 更新了 
}) 

1.nextTick是Vue提供的一个全局API由于vue的异步更新策略导致我们对数据的修改不会立刻体现在dom变化上,此时如果想要立即获取更新后的dom状态,就需要使用这个方法

2.Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用。

3.所以当我们想在修改数据后立即看到dom执行结果就需要用到nextTick方法。

4比如,我在干什么的时候就会使用nextTick传一个回调函数进去,在里面执行dom操作即可。

5.我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数然后用timerFunc异步方式调用它 们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。

47、描述一下最近一个项目

48、eCHarts 有没有做窗口变化适配

①、监听屏幕尺寸变化,重绘图表即可

import echarts from "echarts";
    let option = {…… 图表配置 ……}
    let chart = echarts.init(this.$refs.myChart);
    chart.setOption(option);
    // 添加窗口大小改变监听事件,当窗口大小改变时,图表会重新绘制,自适应窗口大小
    window.addEventListener("resize", function() {
      chart.resize();
    })

②、使用 element-resize-detector

这是一个用于监听DOM元素尺寸变化的插件。

我们对父级容器的宽度进行监听,当父级容器的尺寸发生变化时,echart能调用自身的resize方法,保持视图正常。

49、resize()函数在哪个钩子函数调用

resize() 在 mounted 中调用,想要监听到屏幕的尺寸变化,必须要在 dom 加载完成后。

50、阐述一下自定义指令的理解

分为 组件内部的自定义指令 和 全局自定义指令

// el:指令所绑定的元素,是真实 DOM节点,可以通过该元素来操作 DOM
// binding:一个对象,里面包括很多属性,但是基本上只关注 value 属性,因为这是 指令绑定的值
// vnode:Vue 编译生成的虚拟节点
// oldVnode:Vue 编译生成的上一个虚拟节点(仅在 update 和  componentUpdated 使用) 
directives: {
  big(el, binding, vnode, oldVnode) {
    console.log(el, binding, vnode, oldVnode)
    el.innerText = binding.value * 10
  }
}

51、自定义指令有哪些生命周期

自定义指令有5个生命周期(也叫作钩子函数)分别是bind ,inserted,update,componentUpdate,unbind

  1. bind 只调用一次,指令第一次绑定到元素时候调用,用这个钩子可以定义一个绑定时执行一次的初始化动作。
  2. inserted:被绑定的元素插入父节点的时候调用(父节点存在即可调用,不必存在document中)
  3. update: 被绑定于元素所在模板更新时调用,而且无论绑定值是否有变化,通过比较更新前后的绑定值,忽略不必要的模板更新
  4. componentUpdate :被绑定的元素所在模板完成一次更新更新周期的时候调用
  5. unbind: 只调用一次,指令与元素解绑的时候调用

52、axios二次封装是怎么样的

// 导入 axios 和其中的 Method 约束类型
import axios, { type Method } from 'axios'
// 导入 Pinia 仓库中的 store
import { useUserStore } from '@/stores/index'
// 导入 Vant 中的 轻提示组件
import { showToast } from 'vant'
// 导入路由组件
import router from '@/router'
// 导入 axios
// 导入自定义类型
import type { User } from '@/types/user'

// 1. 新axios实例,基础配置
const baseURL = 'https://consult-api.itheima.net/'
const instance = axios.create({
	// 本项目没有环境变量(生产和开发环境主机名是一样的)  没有反向代理
	baseURL, // 基础请求地址
	timeout: 10000, // 超时时间
})

// 2. 请求拦截器,携带token
// 请求拦截器做了什么? ①
instance.interceptors.request.use(
	// 每次发起请求时,都会在这里被截胡
	config => {
		const store = useUserStore()
		const token = store.user?.token
		if (token) {
			config.headers['Authorization'] = `Bearer ${token}`
		}
		return config
	},
	// axios 发请求,请求代码有问题就会触发这个,
	// 交给后续请求的 cathc 捕获
	err => Promise.reject(err)
)

/**
 * 3. 响应拦截器,剥离无效数据,401拦截:
 *          ①对业务失败统一进行弹框提示
 *          ②统一对 axios 返回的数据结构 data
 *          ③对token失效做处理(清空用户信息,跳转登录页,传递当前路由完整路径)
 */
instance.interceptors.response.use(
	// status:200, code:10000  说明数据正常
	// status:200, code:10001 10002...  说明数据正常,但是服务器返回的数据内部参数有问题(账号不对,密码不对)

	// 服务器返回成功的数据会经过下面的函数(status 为 200)
	res => {
		// status 是200表示响应成功,res.data.code 是10000 表示业务成功
		// 如果不是 10000,使用 vant 的轻提示,报错阻断程序
		if (res.data.code !== 10000) {
			showToast(res.data.message || '网络异常')
			return Promise.reject(res.data)
		}
		return res.data
	},
	// 服务器返回的数据,除 200 外的http 请求状态码,都经过这个函数,交给后续的catch捕获
	err => {
		// 如果服务端返回的状态码 status 是401表示请求失败,需要先删除用户信息,再跳转到登录页,并传递当前的路由路径,以便后面回跳
		if (err.response.status === 401) {
			// 删除用户信息
			const store = useUserStore()
			store.delUser()
			// 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
			router.push({
				path: '/login',
				query: {
					returnUrl: router.currentRoute.value.fullPath,
				},
			})
		}
		return Promise.reject(err)
	}
)

// const asd = 'haha'
// const obj: { name: string; age: number } = { name: 'zs', age: 20 }
// obj[asd] = 'sdf'

// obj[asd] = 123 // 通过替换属性名添加新元素
// obj // {name: 'zs', age: 20, haha: 123}
/**
 * 请求工具函数
 * 参数: url、 method、 submitData
 * 返回:instance 调用接口的 promise 对象
 * method的类型可以从 axios 中导出 import type {Method} from 'axios'
 */
type Data<T> = {
	code: number
	message: string
	data: T
}
// 4. 请求工具函数
// 因为每个接口返回的数据类型是不一样的,所以必须使用泛型,进行传参的方式来约束返回的类型
const request = <T>(url: string, method: Method = 'get', submitData?: object) => {
	// 泛型的第二个参数,可以自定义响应数据类型
	return instance.request<T, Data<T>>({
		url,
		method,
		// 区分 get 和其他请求 post
		// get 提交数据,选项:params
		// 其他请求 post 提交数据,选项:data
		[method.toLowerCase() === 'get' ? 'params' : 'data']: submitData,
	})
}

request<User>('/user', 'GET').then(res => {
	// 现在返回的数据 res,是后台返回的数据,因为剥离了一层   
	// 经过类型约束后,可以顺利点出后续变量
	res.data.avatar
})

// 导出 请求基础地址,axios实例对象
export { baseURL, instance, request }

53、nginx配置跨域,你们项目上线后难道不同源吗?(觉得项目上线之后有跨域问题很奇怪的样子)

54、权限是如何实现的(具体代码)

55、路由回调是怎么样的

56、微信小程序的用户信息授权怎么做的

57、对nextTick的理解

58、有没有小程序的项目经验

59、前端对授权的处理