分类 默认分类 下的文章

一、Vue计算和监听

  • 计算属性(computed):基于响应式数据进行复杂计算,有缓存机制,依赖数据未变则不会重新计算。例如:
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName;
  }
}
  • 监听属性(watch):用于观察数据变化并执行特定操作,如异步操作、深度监听等。例如:
watch: {
  someData(newVal, oldVal) {
    // 数据变化时执行的操作
  }
}

二、echart实例方法更新

Echarts 实例可通过如 setOption 等方法更新图表数据和配置。例如,先获取 Echarts 实例 let myChart = echarts.init(dom),然后在数据变化时 myChart.setOption({...}) 来更新图表显示。

三、你刚才说用watch监听echart数据更新,为什么不用自带的update之类的函数

  • 数据处理逻辑:如果数据更新前需要进行一些 Vue 实例中的特定数据处理、状态变更或与其他数据的关联操作,watch 可以方便地在数据变化时统一处理这些逻辑,而不是仅依赖 Echarts 自身的更新函数。
  • 与 Vue 响应式系统结合:当 Vue 中的数据变化驱动 Echarts 图表更新时,watch 能更好地融入 Vue 的响应式体系,保证数据和视图的一致性更新,便于管理和维护代码结构。

四、Echarts 中update方法

update 方法可用于更新 Echarts 实例的配置项和数据。例如,在某些场景下可以直接调用 chart.update({...}) 来更新图表部分配置或数据,但要注意配置项的结构和数据的对应关系,不然可能导致图表显示异常。

五、Vue的状态

Vue 的状态主要是响应式数据,存储在 data 函数返回的对象中,这些数据变化会触发视图更新。也可以通过 Vuex 或 Pinia 等状态管理库管理全局状态,方便多组件共享和修改数据。

六、Vue生命周期函数

  • beforeCreate:实例刚被创建,数据观测和事件配置未开始。
  • created:实例创建完成,数据观测、属性和方法计算已完成,但未挂载到 DOM。
  • beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。
  • mounted:实例挂载到 DOM 后调用,可进行 DOM 操作等。
  • beforeUpdate:数据更新时调用,此时虚拟 DOM 已重新渲染,但未更新到真实 DOM。
  • updated:数据更新且 DOM 更新后调用。
  • beforeDestroy:实例销毁之前调用,可进行一些清理操作。
  • destroyed:实例销毁后调用。

七、Vuex和Pinia

  • Vuex:是 Vue 的状态管理模式,集中式存储管理应用的所有组件的状态。有 state(存储状态)、mutations(同步修改状态)、actions(可包含异步操作来提交 mutations)、getters(获取派生状态)等核心概念。例如:
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  }
})
  • Pinia:是 Vue 的轻量级状态管理库,旨在提供更简洁、直观的 API。它简化了很多 Vuex 的概念,例如不再区分 mutations 和 actions,使用 store.$patch 等方式直接修改状态,同时有更好的类型推断支持。例如:
import { defineStore } from 'pinia';
const useStore = defineStore('main', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
    }
  }
})

八、Vue静态全局数据

可以通过在 Vue 原型上挂载属性来设置全局数据,例如 Vue.prototype.$globalData = {... },这样在所有组件中都可以通过 this.$globalData 访问,但要注意数据的变更可能不易追踪和管理,适用于一些全局的配置信息等。

九、前端开发(数据可视化)中做过的组件

  • 柱状图组件:封装 Echarts 柱状图,可接收数据、标题、坐标轴标签等属性,方便在不同页面复用。
  • 折线图组件:类似柱状图组件,用于展示数据随时间或其他连续变量的变化趋势,支持动态数据更新和交互功能。
  • 数据表格组件:结合 Element UI 或其他 UI 框架的表格组件,实现数据的展示、排序、筛选等功能,可根据需求定制表格样式和单元格内容展示形式。

十、做组件时候的心得

  • 封装性:注重组件的独立性和复用性,将内部逻辑和外部接口清晰分离,减少组件间的耦合度,便于维护和扩展。
  • 数据驱动:以数据为核心,通过 props 接收数据并根据数据变化更新视图,使组件的功能和显示更具灵活性和可预测性。
  • 交互性:考虑组件的交互设计,如点击事件、鼠标悬浮效果等,提升用户体验,但也要注意避免过度设计导致的性能和复杂性问题。

十一、手写代码:Vue实现一个todolist 上面列表和checkbox,下面输入框和按钮

<template>
  <div>
    <ul>
      <li v-for="(todo, index) in todos" :key="index">
        <input type="checkbox" v-model="todo.done">
        <span :class="{ 'done': todo.done }">{{ todo.text }}</span>
      </li>
    </ul>
    <input type="text" v-model="newTodoText" placeholder="添加新事项">
    <button @click="addTodo">添加</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      todos: [],
      newTodoText: ''
    }
  },
  methods: {
    addTodo() {
      if (this.newTodoText) {
        this.todos.push({ text: this.newTodoText, done: false });
        this.newTodoText = '';
      }
    }
  }
}
</script>

<style>
.done {
  text-decoration: line-through;
}
</style>

十二、手写代码:js实现判断单链表是否环形

可以使用快慢指针法。定义两个指针,慢指针每次移动一步,快指针每次移动两步。如果链表有环,快指针最终会追上慢指针;如果快指针到达链表末尾(null),则链表无环。

function hasCycle(head) {
  let slow = head;
  let fast = head;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) {
      return true;
    }
  }
  return false;
}

十三、快慢指针法一定会相遇吗

在单链表有环的情况下,快慢指针一定会相遇。因为快指针每次比慢指针多走一步,随着循环的进行,它们之间的距离会逐渐缩小,最终必然会重合。

十四、HTTP缓存

  • 强缓存:通过 Cache-ControlExpires 等头部字段控制,浏览器直接从缓存中读取资源,不向服务器发送请求。例如 Cache-Control: max-age=3600 表示资源在 3600 秒内有效。
  • 协商缓存:当强缓存过期后,浏览器向服务器发送请求,服务器根据资源的 Last-ModifiedETag 等字段判断资源是否有更新。如果没有更新,服务器返回 304 状态码,浏览器继续使用缓存资源;如果有更新,则返回新资源。

十五、上线项目后怎么确定客户端刷新缓存

  • 添加版本号或哈希值:在资源文件名中添加版本号或文件内容的哈希值,如 script.js?v=1.0.1script.123456.js,每次更新文件时修改版本号或哈希值,强制浏览器重新下载资源。
  • 设置 HTTP 头部:使用 Cache-Control: no-cacheCache-Control: must-revalidate 等头部信息,让浏览器在每次访问资源时都向服务器验证资源是否更新。

十六、还有哪些方法可以确保客户获取到最新的项目资源?

  • Service Worker:可以拦截网络请求,缓存资源并控制缓存策略,实现离线访问和资源更新推送等功能。例如,在 Service Worker 中监听 fetch 事件,根据策略返回缓存资源或从网络获取最新资源并更新缓存。
  • CDN 缓存更新:如果使用 CDN 服务,可通过 CDN 提供商的缓存刷新接口或配置,手动或自动清除 CDN 节点上的缓存资源,使客户端获取到最新资源。

十七、你了解React吗,Vue和React区别

  • 模板与 JSX:Vue 使用基于 HTML 的模板语法,将模板与逻辑分离;React 使用 JSX,在 JavaScript 中编写类似 HTML 的代码,更灵活但对开发者 JavaScript 能力要求较高。
  • 数据绑定:Vue 采用双向数据绑定,通过 v-model 等指令方便地实现数据与视图的双向同步;React 是单向数据绑定,数据流动更明确,需要开发者手动处理数据更新时的视图更新。
  • 状态管理:Vue 有 Vuex 和 Pinia 等状态管理库;React 常用 Redux 或 Recoil 等,它们在概念和使用方式上有一定差异,但都用于管理组件间共享状态。
  • 组件化:两者都有完善的组件化体系,但在组件的创建、传参、生命周期等细节上有不同的 API 和实现方式。例如 Vue 组件有单文件组件的形式,便于组织和维护代码。
继续阅读->

一、vue异步处理

  1. 概念
    • 在 Vue 中,异步操作极为关键,常用于应对如数据获取、耗时任务等场景,其核心目的在于防止阻塞主线程,以此提升用户体验。例如,在组件的 created 或 mounted 生命周期钩子内发起网络请求以获取数据,而实际操作中,常借助 axios 或原生的 fetch 这类异步函数达成。
  1. 示例
export default {
  created() {
    axios.get('https://api.example.com/data')
    .then(response => {
        this.data = response.data;
      })
    .catch(error => {
        console.error('Error fetching data:', error);
      });
  }
}
  • 在此组件创建之际,运用 axios 异步获取数据,一旦成功,便将数据赋值给组件实例的 data 属性;倘若失败,则在控制台打印错误信息。通过这种异步处理模式,页面在等待数据的时段,能够正常渲染其他部分内容,待数据成功返回后,再针对性地更新相应区域。

二、浏览器缓存策略

  1. 缓存类型
    • 强缓存
      • 浏览器会直接依据本地缓存读取资源,此过程不会向服务器发起请求,其控制手段主要依托 Cache-Control(如借助 max-age 来指定缓存有效期)以及 Expires 头部字段。例如,设定 Cache-Control: max-age=3600,这意味着在 1 小时内,浏览器可径直从缓存中获取资源。
    • 协商缓存
      • 浏览器首先向服务器发送请求,并携带资源的标识,诸如 Last-Modified 和 ETag。服务器依据这些标识判别资源是否有更新,若资源未更新,便返回 304 Not Modified,此时浏览器会启用本地缓存。举例来说,服务器首次响应时附上 Last-Modified 时间戳,下次请求时浏览器则带上 If-Modified-Since 字段,服务器通过对比时间来决定是否返回全新资源。
  1. 应用场景
    • 针对静态资源,像是样式表、脚本、图片等,可设置相对较长的强缓存时间,以此减少重复加载的频次;而对于那些频繁更新的 API 数据,通常会禁用缓存或者设定较短的缓存时间,以此确保数据的及时性。合理且巧妙地运用缓存策略,不但能够加快页面的加载速度,还能有效减轻服务器的压力。

三、 同源策略

  1. 定义
  1. 限制范围
    • 其限制范畴覆盖了 DOM 访问、AJAX 请求、Cookie 读写等多个关键领域。不过,同源策略并非完全杜绝跨源交互,借助一些合规的机制,诸如 CORS(跨域资源共享)、JSONP 等,能够实现安全可靠的跨域操作。

四、解决跨域

  1. CORS 方式
    • 服务器在响应头中设置 Access-Control-Allow-Origin 字段,以此明确指定允许跨域访问的源。例如:
Access-Control-Allow-Origin: https://client.example.com
  • 如此一来,客户端发起的跨域 AJAX 请求便能顺利接收响应。与此同时,还能够配置诸如 Access-Control-Allow-Methods 等其他字段,用以限制允许使用的 HTTP 方法。
  1. JSONP
    • 充分利用script标签不受同源策略限制的特性,客户端动态创建script标签,并使其指向跨域服务器所提供的一个附带回调函数名的 URL,随后服务器返回包裹于回调函数内的数据,以此实现跨域数据的获取。这种方式常用于旧浏览器的兼容场景,不过需要注意的是,它仅能发起 GET 请求。
  1. 代理服务器
    • 在同源的后端服务器设置代理,前端请求首先发送至同源代理,再由代理将请求转发至目标跨域服务器,最后把结果返还给前端,这般操作巧妙地隐藏了跨域的实际过程,在开发环境配置中较为常用,例如 webpack-dev-server 中的 proxy 选项便是典型应用。

五、 前端常见的安全问题

  1. XSS(跨站脚本攻击)
    • 攻击者蓄意将恶意脚本注入目标网站,进而窃取用户信息等重要数据。预防此类攻击的关键举措包括对用户输入输出内容进行转义、过滤处理,例如可运用 DOMPurify 库来净化 HTML 内容,坚决避免直接将用户输入嵌入页面代码之中。
  1. CSRF(跨站请求伪造)
    • 当用户登录受信任网站 A 后,在访问恶意网站 B 时,网站 B 会诱导用户浏览器向网站 A 发起请求,借此利用用户的登录态执行非预期操作。防范该风险的有效手段是在表单或 AJAX 请求中添加随机 Token,服务器通过验证 Token 的合法性,确保请求源自真实用户的操作。

六、水平垂直居中

  1. CSS 方法
    • 对于已知宽高的元素:
.center {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 200px;
  height: 100px;
  margin-top: -50px; /* 高度的一半 */
  margin-left: -100px; /* 宽度的一半 */
}
  • 采用绝对定位结合负 margin 的方式,能够精准实现居中效果。通过将元素的左上角定位至父容器的中心位置(即 top: 50%; left: 50%;),再利用负 margin 抵消元素自身宽高的一半,从而达成水平垂直居中。
  • 使用 flexbox 布局:
.container {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
}
  • 借助 flexbox 布局的强大特性,只需简单设置容器的 display 为 flex,并运用 justify-content 和 align-items 属性分别控制水平和垂直方向的对齐方式,即可自动实现容器内元素的居中效果,且无需提前知晓子元素的尺寸,适用于多种复杂场景。
  1. JavaScript 方法(不常用但特殊场景)
    • 通过 JavaScript 获取元素的尺寸以及窗口尺寸,依据这些数据动态计算并设置元素的 style.top 和 style.left 属性,以此实现居中效果。不过,这种方式会带来较大的性能开销,因此在实际应用中,优先考虑采用 CSS 方案。

七、box-sizing属性

  1. 属性值含义
    • content-box(默认)
      • 元素的宽度和高度仅仅涵盖内容区域,边框和内边距会额外向外扩展,这就导致实际占据的空间会大于所设置的值。例如,当设置 width: 100px,并添加 2px 的边框和 10px 的内边距后,实际宽度将变为 124px。
    • border-box
      • 此时元素的宽度和高度包含内容、边框以及内边距,所设置的值即为元素最终占据的空间大小,这极大地方便了布局计算。以设置 width: 100px 为例,即便添加了边框和内边距,元素的总宽依然固定为 100px,内容区宽度会自动进行适应性调整。
  1. 应用场景
    • 在响应式设计或是复杂布局构建过程中,border-box 属性能够确保元素尺寸在添加各类样式后保持稳定,有效规避因样式调整引发的意外布局错乱问题。鉴于此,建议在 CSS 初始化阶段,全局设置 box-sizing: border-box。

八、看代码回答问题

2024-12-24T01:39:27.png

九、手写css实现无限旋转

.rotate {
  animation: rotateAnimation 2s linear infinite;
}

@keyframes rotateAnimation {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
  • 将 rotate 类赋予需要旋转的元素,充分利用 CSS 动画的 animation 属性,精准指定动画名称为 rotateAnimation、持续时间设定为 2s、运动方式采用线性 linear 并设置为无限循环 infinite。同时,借助 @keyframes 规则明确定义起始(0 度)与结束(360 度)的旋转状态,通过这般设置,即可顺利实现元素的无限旋转效果,此效果在加载提示等场景中应用颇为广泛。

十、看代码回答问题

2024-12-24T01:39:52.png

十一、事件循环机制

  1. 概念
    • JavaScript 作为一门单线程语言,却得益于浏览器提供的异步执行能力,而事件循环正是用于协调同步与异步任务执行的关键机制。它主要包含一个执行栈(用于存放同步任务)以及一个任务队列(用于存放异步任务的回调)。主线程率先执行同步任务,待同步任务执行完毕后,便会从任务队列中取出异步任务的回调,并将其放入执行栈中执行。
  1. 示例
console.log('start');
setTimeout(() => {
  console.log('timeout');
}, 0);
Promise.resolve().then(() => {
  console.log('promise');
});
console.log('end');
  • 上述代码的输出顺序为 start -> end -> promise -> timeout。原因在于 setTimeout 的回调属于宏任务,即便延迟时间设置为 0,它也需在当前执行栈为空且微任务队列执行完毕之后,才会进入执行栈执行;而 Promise.then 的回调属于微任务,会在当前执行栈的同步任务结束后即刻执行。

十二、宏任务和微任务和执行机制

  1. 宏任务
    • 涵盖 setTimeout、setInterval、I/O 操作、script 整体代码等诸多类型。每当一个宏任务执行结束后,都会对微任务队列进行检查,并将其中的微任务全部清空执行。
  1. 微任务
    • 诸如 Promise.then、MutationObserver 回调等皆属于微任务范畴。它们会在当前宏任务执行结束后的下一个微任务检查点执行,并且优先于下一个宏任务执行,以此确保异步任务能够依照优先级有序推进,避免阻塞主线程过长时间,进而维持页面的良好响应性能。
  1. 执行流程
    • 同步任务 -> 微任务队列(全部执行完) -> 宏任务队列(取出一个执行,再重复检查微任务队列),如此循环往复,有条不紊地保障了 JavaScript 代码在浏览器环境中的高效运行。

十三、看代码回答问题

2024-12-24T01:40:13.png

十四、手写js合并有序数组

function mergeSortedArrays(arr1, arr2) {
  const result = [];
  let i = 0, j = 0;
  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) {
      result.push(arr1[i++]);
    } else {
      result.push(arr2[j++]);
    }
  }
  return result.concat(i < arr1.length? arr1.slice(i) : []).concat(j < arr2.length? arr2.slice(j) : []);
}

const array1 = [1, 3, 5];
const array2 = [2, 4, 6];
console.log(mergeSortedArrays(array1, array2)); 
// 输出: [1, 2, 3, 4, 5, 6]
  • 通过双指针法,分别对两个有序数组进行遍历。在遍历过程中,持续比较当前指针指向元素的大小,将较小值优先放入结果数组,并相应地移动指针。待其中一个数组遍历完毕后,将剩余未遍历完的数组部分通过 concat 方法拼接到结果数组,以此实现高效合并有序数组。该算法的时间复杂度为 O(m+n) ,其中 m 、n 分别代表两个数组的长度,空间复杂度同样为 O(m+n) ,主要用于存储结果数组。

十五、分析代码的时间复杂度空间复杂度

继续阅读->

一、CSS 垂直居中

1. 基于 line - height(适用于单行内联元素)

  • 对于一个单行的文本元素,设置元素的heightline - height值相等,就可以实现垂直居中。例如:
div {
    height: 100px;
    line - height: 100px;
}
  • 这里的div中的文本就会在垂直方向上居中。

2. 基于 table - cell(适用于块级元素)

  • 把父元素设置为display: table - cell,并且设置vertical - align: middle,可以让子元素垂直居中。例如:
.parent {
    display: table - cell;
    vertical - align: middle;
}
  • 这种方法对于一些复杂的布局,如表格布局的模拟场景很有用。

3. 基于 flex 布局

  • 当父元素设置为display: flex时,使用align - items: center可以让子元素在交叉轴(通常是垂直方向)上居中。例如:
.container {
    display: flex;
    align - items: center;
}
  • 并且,如果需要在主轴和交叉轴都居中,可以同时设置justify - content: centeralign - items: center

4. 基于 grid 布局

  • 在网格布局中,使用place - items: center可以让子元素在网格单元格中垂直和水平都居中。例如:
.grid - container {
    display: grid;
    place - items: center;
}
  • 或者也可以分别设置align - items: center(垂直居中)和justify - items: center(水平居中)。

二、JS 数据类型

1. 基本数据类型

  • Number(数字):包括整数和浮点数,例如13.14。可以进行数学运算,如加法、减法等。
  • String(字符串):是由零个或多个字符组成的序列,用单引号或双引号包裹,如'hello'"world"。可以进行字符串拼接、截取等操作。
  • Boolean(布尔值):只有两个值truefalse,用于条件判断,比如if语句中。
  • Null(空值):表示一个空对象指针,只有一个值null
  • Undefined(未定义):当一个变量声明但未赋值时,它的值是undefined

2. 引用数据类型

  • Object(对象):是一组无序的属性和方法的集合。例如{name: 'John', age: 30},对象的属性可以通过点语法(obj.name)或者方括号语法(obj['name'])访问。
  • Array(数组):是一种特殊的对象,用于存储多个值,值可以是不同的数据类型。例如[1, 2, 'three'],可以通过索引访问数组元素,如arr[0]
  • Function(函数):函数也是对象,它可以包含可执行的代码块。例如:
function add(a, b) {
    return a + b;
}
  • 函数可以被调用,传递参数并返回结果。

三、ES6 新特性

1. 变量声明(let 和 const)

  • let
  • 块级作用域变量声明。例如:
{
    let x = 10;
    console.log(x);
}
console.log(x); // 报错,x在块外不可访问
  • 不存在变量提升,必须先声明再使用。
  • const
  • 用于声明常量,一旦赋值就不能再重新赋值。例如:
const PI = 3.14;
PI = 3.15; // 报错
  • 但如果常量是对象或数组,其内部的属性或元素可以被修改。

2. 箭头函数(Arrow Functions)

  • 简化函数的写法。例如:
// 普通函数
function add(a, b) {
    return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
  • 箭头函数没有自己的this,它会继承外层作用域的this,这在处理回调函数中的this指向问题时很有用。

3. 模板字符串(Template Strings)

  • 用反引号(`)包裹,可以包含变量和表达式。例如:
const name = 'John';
const greeting = `Hello, ${name}!`;
console.log(greeting);
  • 可以多行书写,不需要使用+来拼接字符串。

4. 解构赋值(Destructuring Assignment)

  • 可以从数组或者对象中提取值并赋值给变量。例如:
// 数组解构
const [a, b] = [1, 2];
// 对象解构
const {x, y} = {x: 10, y: 20};
  • 可以用于函数参数,方便地获取对象的属性作为参数。

四、对象拷贝

1. 浅拷贝

  • Object.assign()
  • 用于将一个或多个源对象的可枚举属性复制到目标对象。例如:
const source = {a: 1, b: 2};
const target = {};
Object.assign(target, source);
console.log(target); // {a: 1, b: 2}
  • 但是对于对象中的对象属性,只是复制了引用。例如:
const source = {a: {x: 1}, b: 2};
const target = {};
Object.assign(target, source);
source.a.x = 2;
console.log(target.a.x); // 2,因为只是浅拷贝,内部对象引用相同

2. 深拷贝

  • JSON.parse(JSON.stringify())
  • 先将对象转换为 JSON 字符串,再将 JSON 字符串解析回对象。例如:
const source = {a: {x: 1}, b: 2};
const target = JSON.parse(JSON.stringify(source));
source.a.x = 2;
console.log(target.a.x); // 1,深拷贝后互不影响
  • 但是这种方法有局限性,不能处理函数、循环引用等情况。对于复杂的深拷贝场景,可能需要使用递归等自定义方法。

五、字符串数组去重

1. 使用 Set

  • Set 是 ES6 中新增的数据结构,它的成员具有唯一性。例如:
const arr = ["a", "b", "a", "c"];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // ["a", "b", "c"]
  • 首先通过new Set(arr)创建一个 Set 对象,它会自动去除重复元素,然后通过扩展运算符...将 Set 对象转换回数组。

2. 循环遍历并检查

  • 可以通过一个新数组来存储不重复的元素。例如:
const arr = ["a", "b", "a", "c"];
const uniqueArr = [];
for (let i = 0; i < arr.length; i++) {
    if (!uniqueArr.includes(arr[i])) {
        uniqueArr.push(arr[i]);
    }
}
console.log(uniqueArr); // ["a", "b", "c"]
  • 这种方法比较直观,但是对于大型数组效率可能不如使用 Set。

六、ES6 中的数字去重(和上面的数组去重类似)

1. 使用 Set

  • 例如:
const numbers = [1, 2, 1, 3];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3]
  • 原理和字符串数组去重一样,利用 Set 的唯一性。

2. 传统的循环和条件判断

  • 例如:
const numbers = [1, 2, 1, 3];
const uniqueNumbers = [];
for (let i = 0; i < numbers.length; i++) {
    if (!uniqueNumbers.includes(numbers[i])) {
        uniqueNumbers.push(numbers[i]);
    }
}
console.log(uniqueNumbers); // [1, 2, 3]
  • 当遇到大型的数字数组时,还可以考虑先对数组进行排序,这样可以减少比较次数,提高效率。

七、浏览器本地存储、Local Storage 和 Session Storage 有什么区别

1. 生命周期

  • Local Storage
  • 数据存储是持久化的,除非用户手动清除浏览器缓存或者通过代码清除,否则数据会一直存在。例如,一个网站的用户登录信息可以存储在 Local Storage 中,下次用户打开浏览器访问该网站时,仍然可以获取到登录信息。
  • Session Storage
  • 数据的生命周期和会话(session)相关。当浏览器窗口或者标签页关闭时,存储在 Session Storage 中的数据就会被清除。比如,一个购物车应用在用户购物过程中,可以把购物车数据存储在 Session Storage 中,当用户关闭浏览器窗口后,购物车数据就不再需要保留。

2. 存储容量

  • 通常,Local Storage 和 Session Storage 的存储容量在大多数浏览器中大约是 5MB 左右,但是不同浏览器可能会有所差异。

3. 作用域

  • Local Storage
  • 同一个域名下的所有页面(包括不同的标签页、不同的浏览器窗口)都可以共享 Local Storage 中的数据。例如,一个网站的多个子页面可以通过访问 Local Storage 来获取和更新用户的偏好设置。
  • Session Storage
  • 数据仅在当前浏览器窗口或者标签页的会话中有效。不同的浏览器窗口或者标签页之间不能共享 Session Storage 中的数据。

八、IndexDB

1. 概述

  • IndexedDB 是一个在浏览器中存储大量结构化数据(包括文件 / Blobs)的 API。它是一个事务型的数据库系统,类似于传统的关系型数据库,但是它是基于 JavaScript 对象存储的。

2. 特点

  • 异步操作:所有的操作都是异步的,避免了阻塞主线程。例如,打开数据库、读取数据等操作都通过回调函数或者async/await来处理异步结果。
  • 数据存储结构:数据以对象仓库(Object Store)的形式存储,每个对象仓库可以存储具有相似结构的对象。例如,可以有一个存储用户信息的对象仓库,其中每个对象包含nameageemail等属性。
  • 索引(Index)支持:可以为对象仓库中的属性创建索引,方便快速查询。比如,为用户信息仓库中的email属性创建索引,这样就可以通过email快速查找用户。

3. 使用场景

  • 适合存储大量的离线数据,如 Web 应用的缓存数据、文档管理应用中的文档数据等。

九、节流和防抖

1. 节流(Throttle)

  • 概念:节流是指在一定时间内,只允许函数执行一次。比如,一个滚动事件,不管滚动多快,我们只希望每 100 毫秒执行一次相关的函数,这样可以避免频繁地执行函数导致性能问题。
  • 实现示例(使用定时器)
function throttle(func, delay) {
    let timer = null;
    return function() {
        if (!timer) {
            func.apply(this, arguments);
            timer = setTimeout(() => {
                timer = null;
            }, delay);
        }
    };
}
  • 这个节流函数返回一个新的函数,在新函数内部,只有当timernull时才执行原始函数func,然后设置一个定时器,在delay时间后将timer重置为null,这样就限制了函数的执行频率。

2. 防抖(Debounce)

  • 概念:防抖是指在事件被触发后,延迟一段时间执行函数,如果在这段延迟时间内事件又被触发,则重新计算延迟时间。例如,一个搜索框的输入事件,只有当用户停止输入一段时间(如 500 毫秒)后才执行搜索函数,这样可以避免在用户输入过程中频繁地进行搜索。
  • 实现示例(使用定时器)
function debounce(func, delay) {
    let timer = null;
    return function() {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            func.apply(this, arguments);
        }, delay);
    };
}
  • 当事件触发时,如果已经有定时器存在(说明之前的延迟还没结束),就清除之前的定时器,然后重新设置一个新的定时器,只有当定时器结束后才执行函数。

十、轮询两秒请求一次

1. 使用 setInterval

  • 例如,在 JavaScript 中使用setInterval函数来实现轮询请求。假设我们有一个fetchData函数用于发送请求获取数据:
function fetchData() {
    // 这里是发送请求的代码,例如使用fetch API
    return fetch('https://example.com/api/data')
      .then(response => response.json());
}
setInterval(fetchData, 2000);
  • 这样就会每隔 2 秒调用一次fetchData函数发送请求。不过要注意,在页面关闭时应该清除这个定时器,避免不必要的请求和内存占用。可以使用clearInterval函数来清除。

2. 考虑使用 WebSockets 或 Server - Sent Events(SSE)作为替代方案

  • 在一些场景下,轮询可能不是最优解。WebSockets 可以实现双向通信,服务器可以主动推送数据给客户端。SSE 主要用于服务器向客户端单向推送事件流,它们可以提供更实时的数据更新,并且在性能上可能优于轮询。

十一、第一个请求一直 pending 第二个请求的数据回来,第一个没有回来,数据覆盖怎么处理

1. 为每个请求添加唯一标识

  • 可以在发送请求时为每个请求添加一个唯一的标识符,比如一个递增的数字或者一个唯一的字符串(如 UUID)。在处理响应时,根据这个标识符来判断数据应该更新到哪个请求对应的状态。
  • 例如,在一个 JavaScript 应用中,假设我们有一个requests对象来存储请求相关的信息:
const requests = {};
let requestId = 0;
function sendRequest() {
    const id = requestId++;
    requests[id] = {status: 'pending'};
    // 发送请求,假设使用fetch API
    fetch('https://example.com/api/data')
      .then(response => response.json())
      .then(data => {
            // 根据标识符更新对应的请求状态和数据
            if (requests[id]) {
                requests[id].status = 'fulfilled';
                requests[id].data = data;
            }
        });
    return id;
}
  • 这样,当数据返回时,就可以根据请求的标识符正确地更新状态和数据,避免覆盖错误的请求。

2. 使用队列来处理请求顺序

  • 维护一个请求队列,确保请求按照发送的顺序依次处理。当一个请求的数据返回时,检查队列头部的请求是否是这个数据对应的请求,如果是,则处理数据,然后将队列头部的请求移除;如果不是,则将数据暂存,直到对应的请求到达队列头部。这种方法可以保证数据的顺序和请求的顺序一致。

十二、关于请求的库有没有方案处理

1. Axios 拦截器

  • 请求拦截器
  • 可以在请求发送之前进行统一的处理,比如添加请求头、对请求参数进行处理等。例如:
axios.interceptors.request.use((config) => {
    // 在请求头中添加认证信息
    const token = localStorage.getItem('token');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});
  • 响应拦截器
  • 用于在接收到响应后进行处理,如处理错误状态码、对响应数据进行格式化等。例如:
axios.interceptors.response.use((response) => {
    // 假设服务器返回的数据格式是{data: {actualData}},提取实际数据
    return response.data.data;
}, (error) => {
    // 处理错误,比如401错误(未授权)
    if (error.response && error.response.status === 401) {
        // 跳转到登录页面或者进行重新认证操作
        window.location.href = '/login';
    }
    return Promise.reject(error);
});

2. 并发请求处理

  • Axios 可以同时发送多个请求,并且可以使用axios.allaxios.spread来处理并发请求的结果。例如:
axios.all([axios.get('/api/user'), axios.get('/api/posts')])
  .then(axios.spread((userResponse, postsResponse) => {
        // 同时获取用户信息和帖子信息后的处理
        const user = userResponse.data;
        const posts = postsResponse.data;
    }));
  • 这样可以提高效率,特别是当多个请求之间没有依赖关系时。

十三、Vue 数据传递

1. 父子组件数据传递

  • 父组件向子组件传递数据
  • 通过props属性。在父组件中,在使用子组件标签时,通过属性绑定的方式传递数据。例如,
<template>
  <ChildComponent :message="parentMessage" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentMessage: 'Hello from parent'
    };
  }
};
</script>
  • 在子组件中,通过props选项来接收数据,像这样:
<template>
  <div>{{ message }}</div>
</template>
<script>
export default {
  props: ['message']
};
</script>
  • 子组件向父组件传递数据
  • 通过自定义事件。在子组件中,使用$emit方法触发自定义事件,并传递数据。例如:
<template>
  <button @click="sendDataToParent">Send Data</button>
</template>
<script>
export default {
  methods: {
    sendDataToParent() {
      const data = 'Data from child';
      this.$emit('childData', data);
    }
  }
};
</script>
  • 在父组件中,监听子组件触发的自定义事件来接收数据,如下:
<template>
  <ChildComponent @childData="handleChildData" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: {
    ChildComponent
  },
  methods: {
    handleChildData(data) {
      console.log(data);
    }
  }
};
</script>

2. 非父子组件数据传递(兄弟组件等)

  • 通过事件总线(Event Bus)
  • 创建一个 Vue 实例作为事件总线,通常在一个单独的文件中定义,比如event-bus.js
import Vue from 'vue';
export default new Vue();
  • 在发送数据的组件中,引入事件总线并使用$emit触发事件来传递数据,例如:
<template>
  <button @click="sendData">Send Data</button>
</template>
<script>
import eventBus from './event-bus.js';
export default {
  methods: {
    sendData() {
      const data = 'Data to share';
      eventBus.$emit('sharedData', data);
    }
  }
};
</script>
  • 在接收数据的组件中,通过$on来监听事件获取数据,比如:
<script>
import eventBus from './event-bus.js';
export default {
  mounted() {
    eventBus.$on('sharedData', (data) => {
      console.log(data);
    });
  }
};
</script>
  • 使用 Vuex(适合更复杂的全局数据管理场景,后面会详细介绍):当多个组件需要共享和操作同一份数据时,可以将数据存储在 Vuex 的状态中,各个组件通过mapState等辅助函数来获取数据,通过commitdispatch等方式来修改数据。

十四、介绍 Vuex 和 Pinia 以及区别

1. Vuex

  • 概述:Vuex 是 Vue.js 的一个状态管理模式 + 库,用于管理应用中多个组件共享的状态。它采用集中式存储管理应用的所有组件的状态,并以可预测的方式进行状态变更。
  • 核心概念
  • State(状态):存储应用的状态数据,类似于组件中的data选项,是响应式的,组件可以通过mapState等辅助函数获取并使用这些状态数据。例如:
const store = new Vuex.Store({
  state: {
    count: 0
  }
});
  • Mutations(突变):是唯一能改变状态的途径,它接收一个state对象作为第一个参数,通过提交mutation来同步地修改状态,并且有严格的类型限制,便于调试和追踪状态变化。例如:
mutations: {
  increment(state) {
    state.count++;
  }
}
  • Actions(动作):用于处理异步操作,比如发送网络请求等,它可以提交mutation来间接改变状态。例如:
actions: {
  incrementAsync({ commit }) {
    setTimeout(() => {
      commit('increment');
    }, 1000);
  }
}
  • Getters(获取器):类似于计算属性,用于从state中派生出一些新的数据,方便组件获取和使用经过处理后的状态数据。例如:
getters: {
  doubleCount: state => state.count * 2
}

2. Pinia

  • 概述:Pinia 是 Vue.js 的下一代状态管理库,旨在提供一个更简单、更直观且类型友好的方式来管理应用的状态,它对 Vue 3 有更好的适配性,也能在 Vue 2 中使用(通过插件)。
  • 核心概念
  • Store(仓库):是 Pinia 的核心概念,通过defineStore函数来定义一个仓库,里面可以包含状态、操作状态的方法等。例如:
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
    }
  }
});
  • 与 Vuex 不同,Pinia 中没有像mutation那样严格区分同步和异步操作的概念,操作状态的方法可以直接在actions中处理异步逻辑,更加简洁灵活。

3. 区别

  • API 简洁性
  • Pinia 的 API 相对更简洁直观,没有像 Vuex 那样复杂的概念区分(如mutation的严格同步限制),开发者可以更直接地定义和操作状态。
  • 对 Vue 版本的适配
  • Pinia 对 Vue 3 的适配更加自然,利用了 Vue 3 的新特性如Composition API等,而 Vuex 虽然也能用于 Vue 3,但 Pinia 在这方面的集成感觉更流畅。同时,Pinia 也能通过插件支持 Vue 2,具有更好的跨版本使用能力。
  • 类型支持
  • Pinia 在类型定义方面更加友好,能更好地结合 TypeScript 等进行类型检查,有助于提高代码的可维护性和健壮性,Vuex 在这方面相对弱一些,虽然也可以进行一定的类型定义,但不如 Pinia 方便。

十五、介绍 MVVM

1. 概念

  • MVVM(Model-View-ViewModel)是一种软件架构模式,主要用于构建用户界面,它分离了数据(Model)、用户界面(View)以及连接数据和界面的中间层(ViewModel),使得它们各自的职责更加清晰,便于代码的维护和扩展。

2. 各部分职责

  • Model(模型)
  • 代表应用的数据和业务逻辑,通常是与后端交互获取的数据或者本地存储的数据等,比如用户信息、商品数据等,它与视图层是独立的,不关心数据如何展示。
  • View(视图)
  • 就是用户看到和交互的界面,比如网页中的 HTML 页面、移动端应用的界面等,它负责展示数据以及接收用户的操作输入,但不直接处理业务逻辑和数据的存储、变更等。
  • ViewModel(视图模型)
  • 是连接 Model 和 View 的桥梁,它一方面从 Model 获取数据,并进行必要的格式转换、处理等,使其适合在 View 中展示;另一方面,它监听 View 中用户的操作事件,并将其转化为对 Model 的操作,从而更新数据。例如,在一个基于 MVVM 的前端框架中,ViewModel 可以通过数据绑定机制,自动将 Model 中的数据更新反映到 View 上,同时将 View 上用户输入的数据变化更新到 Model 中。

3. 在 Vue 中的体现

  • 在 Vue.js 中,data选项可以看作是 Model 的一部分,存储着组件的数据;模板(template)就是 View,负责展示数据;而 Vue 实例本身以及其内部的计算属性、方法等就相当于 ViewModel,它通过双向绑定等机制,实现了数据和视图的自动关联和更新。例如,当在data中修改一个属性的值时,与之绑定的模板中的内容会自动更新,反之,用户在视图中输入等操作导致的数据变化,也会同步更新到data中的对应属性上。

十六、Vue2 双向绑定原理

1. Object.defineProperty () 方法

  • Vue2 实现双向绑定的核心是通过Object.defineProperty()方法来劫持数据的读写操作。在 Vue 实例初始化时,会对data选项中的数据进行遍历,使用Object.defineProperty()为每个属性添加getset访问器属性。例如:
function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      return value;
    },
    set: function(newValue) {
      if (newValue!== value) {
        value = newValue;
        // 当数据变化时,通知相关的依赖(如模板中的绑定表达式等)进行更新
      }
    }
  });
}
  • 这样,当读取数据时,会触发get函数,当修改数据时,会触发set函数,在set函数中可以进行更新视图等相关操作。

2. 依赖收集与发布 - 订阅模式

  • Vue2 通过一个依赖收集机制来知道哪些地方使用了某个数据,也就是哪些模板、计算属性等依赖了这个数据。在get函数被触发时,会收集依赖(通常是一些更新函数),当数据发生变化触发set函数时,会遍历这些依赖并执行它们,实现视图的更新。这其实就是一种发布 - 订阅模式,数据变化是发布者,依赖的更新函数是订阅者,当有新的数据变化消息(发布)时,对应的订阅者(依赖的更新函数)就会被通知并执行更新操作。
  • 例如,在一个组件的模板中有{{ message }}这样的绑定表达式,当解析模板时,读取message数据就会触发其get函数进行依赖收集,后续如果message的值改变,就会通过之前收集的依赖来更新模板中对应的部分。

十七、Vue2 响应式原理

1. 响应式数据的创建

  • 同上面双向绑定原理中的数据劫持部分,通过Object.defineProperty()data等选项中的数据进行处理,使其变为响应式数据,也就是让数据的读写能够被 Vue 感知并进行相应的处理。例如,在 Vue 组件的初始化阶段:
function initData(vm) {
  let data = vm.$options.data;
  data = vm._data = typeof data === 'function'? data.call(vm) : data;
  // 遍历_data中的属性,使其成为响应式
  Object.keys(data).forEach(key => {
    defineReactive(vm._data, key, data[key]);
  });
}
  • 这样创建出来的数据就是响应式的,其变化可以触发视图的更新等操作。

2. 组件更新机制

  • 当响应式数据发生变化时,会触发set函数,在set函数中会通过之前收集的依赖(如Watcher实例,它代表了对某个数据的依赖,通常由模板解析、计算属性等创建)来通知相关的更新操作。比如,一个组件的data中的属性变化了,会找到所有依赖这个属性的Watcher,然后执行Watcher的更新函数,这个更新函数可能会触发虚拟 DOM 的重新渲染等操作,最终将更新后的内容反映到真实的 DOM 上,实现组件的更新。
  • 同时,Vue2 还会进行一些优化,比如异步更新队列等,避免频繁地进行 DOM 操作,将多次数据变化的更新合并为一次,提高性能。

十八、设计模式加实际例子

1. 单例模式

  • 概念:单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。
  • 实际例子:在前端应用中,比如创建一个全局的日志记录器,只需要一个实例来记录各种日志信息。代码实现可以如下:
class Logger {
  constructor() {
    if (!Logger.instance) {
      Logger.instance = this;
    }
    return Logger.instance;
  }
  log(message) {
    console.log(message);
  }
}
const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true,说明获取到的是同一个实例
  • 这里,无论在应用的哪个地方通过new Logger()创建实例,得到的都是同一个Logger实例,方便统一管理日志记录。

2. 工厂模式

  • 概念:工厂模式是一种创建对象的设计模式,它将对象的创建和使用分离,通过一个工厂函数或者工厂类来创建对象,根据不同的条件可以返回不同类型的对象。
  • 实际例子:假设要创建不同类型的图形对象(圆形、矩形等),可以使用工厂模式。例如:
function createShape(type, options) {
  switch (type) {
    case 'circle':
      return new Circle(options.radius);
    case'rectangle':
      return new Rectangle(options.width, options.height);
    default:
      throw new Error('Invalid shape type');
  }
}
class Circle {
  constructor(radius) {
    this.radius = radius;
  }
}
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
}
const circle = createShape('circle', { radius: 5 });
const rectangle = createShape('rectangle', { width: 10, height: 20 });
  • 通过createShape这个工厂函数,根据传入的类型参数来创建不同的图形对象,使用者不需要知道具体的对象创建细节。

3. 观察者模式

  • 概念:观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象的状态发生变化时,会通知所有的观察者对象,使它们能够自动更新。
  • 实际例子:在一个新闻订阅系统中,新闻网站是主题,订阅用户是观察者。代码示例如下:
class NewsPublisher {
  constructor() {
    this.observers = [];
  }
  subscribe(observer) {
    this.observers.push(observer);
  }
  unsubscribe(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  notify(news) {
    this.observers.forEach(observer => observer.update(news));
  }
}
class NewsSubscriber {
  update(news) {
    console.log(`Received news: ${news}`);
  }
}
const publisher = new NewsPublisher();
const subscriber1 = new NewsSubscriber();
const subscriber2 = new NewsSubscriber();
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.notify('Breaking news!');
  • 新闻发布者(NewsPublisher)维护着订阅者列表,当有新新闻时,通过notify函数通知所有订阅者(NewsSubscriber),订阅者接收到通知后执行update函数进行相应的处理。

十九、平时开发使用的 git 操作

1. 基本操作

  • 克隆仓库(clone)
  • 使用git clone <repository-url>命令从远程仓库(如 GitHub、GitLab 等)克隆代码到本地。例如,要克隆一个名为my-project的仓库,命令如下:
git clone https://github.com/username/my-project.git
  • 添加文件(add)
  • 使用git add <file(s)>命令将文件添加到暂存区,准备提交。可以添加单个文件,如git add index.html,也可以添加所有修改的文件,用git add.表示添加当前目录下所有修改的文件。
  • 提交更改(commit)
  • 通过git commit -m "<commit-message>"命令将暂存区的文件提交到本地仓库,<commit-message>是对本次提交内容的简要描述,例如:
git commit -m "Fix a bug in the login page"
  • 推送更改(push)
  • 使用git push origin <branch-name>命令将本地仓库的更改推送到远程仓库对应的分支上,通常origin是远程仓库的默认别名,<branch-name>是分支名称,比如:
git push origin master

2. 分支管理操作

  • 创建分支(branch)
  • git branch <new-branch-name>命令创建一个新的分支,例如:
git branch feature-branch
  • 切换分支(checkout)
  • 通过git checkout <branch-name>命令切换到指定的分支,如:
git checkout feature-branch
  • 也可以使用git checkout -b <new-branch-name>命令同时创建并切换到新分支。
  • 合并分支(merge)
  • 假设要把feature-branch分支合并到master分支,首先切换到master分支(git checkout master),然后使用git merge feature-branch命令进行合并。

3. 查看操作

  • 查看状态(status)
  • 使用git status
  • 查看状态(status)
    使用git status命令可以查看当前工作目录的状态,比如哪些文件被修改了、哪些文件是新增的、当前处于哪个分支等信息。例如:
git status

执行后会输出类似如下内容:

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")

这表明当前在master分支,index.html文件被修改了但还没添加到暂存区。

  • 查看日志(log)
    通过git log命令可以查看提交历史记录,它会显示每次提交的作者、时间、提交信息以及对应的哈希值等内容,方便追踪项目的变更情况。例如:
git log

输出类似如下:

commit 123456789abcdef (HEAD -> master)
Author: Your Name <your@email.com>
Date:   Mon Dec 16 10:00:00 2024

    Add new feature to the project

commit 987654321fedcba
Author: Another Author <another@email.com>
Date:   Fri Dec 13 14:30:00 2024

    Fix a bug in the login page

可以添加一些参数来更方便地查看日志,比如--oneline可以让每条提交记录显示在一行,更简洁直观:

git log --oneline
  • 查看文件差异(diff)
    git diff命令用于查看文件修改前后的差异内容,能帮助了解具体做了哪些代码改动。如果想查看工作目录中已修改但未暂存文件与暂存区版本的差异,可以直接执行:
git diff

若想查看已暂存文件与上次提交版本的差异,使用:

git diff --cached

二十、面向对象和面向过程

1. 面向过程

  • 概念:面向过程编程是一种以过程(或函数)为中心的编程思想,它将解决问题的步骤分解为一个个具体的操作,按照顺序依次执行这些操作来达成目标。程序的执行流程是线性的,重点关注的是 “怎么做”,数据和对数据的操作相对分离程度较低。
  • 示例

    :比如计算一个班级学生成绩的平均分。可以通过以下步骤实现:

    1. 定义一个数组来存储所有学生的成绩。
    2. 通过循环依次读取每个学生的成绩并累加到一个变量中。
    3. 用总成绩除以学生人数得到平均分。
      代码示例如下:
scores = [80, 90, 75, 85, 95]  # 假设的学生成绩数组
total_score = 0
for score in scores:
    total_score += score
average_score = total_score / len(scores)
print(average_score)

这里就是按照顺序依次执行各个操作来完成计算平均分这个任务,整个过程注重的是操作步骤的顺序和执行。

2. 面向对象

  • 概念:面向对象编程(OOP)将现实世界中的事物抽象成对象,对象包含属性(数据)和方法(对数据的操作),通过对象之间的交互来解决问题。它强调的是 “谁来做” 以及对象之间的关系,具有封装、继承、多态等特性。
  • 示例:同样计算班级学生成绩平均分的例子,用面向对象的思路可以这样设计:
    首先定义一个Student类:
class Student:
    def __init__(self, score):
        self.score = score

    def get_score(self):
        return self.score

然后创建学生对象的列表,计算平均分:

students = [Student(80), Student(90), Student(75), Student(85), Student(95)]
total_score = 0
for student in students:
    total_score += student.get_score()
average_score = total_score / len(students)
print(average_score)

这里把学生抽象成了Student类,每个学生对象有自己的成绩属性以及获取成绩的方法,通过对象的交互(调用get_score方法获取成绩并累加)来完成计算平均分的任务,并且可以方便地对Student类进行扩展,比如添加更多关于学生的属性和方法等。

3. 两者区别与适用场景

  • 区别

    • 数据与操作的关联:面向过程中数据和操作相对松散结合,而面向对象将数据和操作封装在对象内部,关联性更强。
    • 代码组织方式:面向过程按照解决问题的步骤顺序组织代码,面向对象围绕对象及其关系来组织代码。
    • 可维护性和扩展性:面向对象由于有封装、继承、多态等特性,在面对复杂系统和需求变更时往往更容易维护和扩展,而面向过程在简单、流程固定的场景下更直观、易理解。
  • 适用场景

    • 面向过程:适合简单、线性、需求不太会变化的小型程序,比如一些简单的脚本任务,像数据文件的格式转换等,按照固定的步骤操作即可完成任务。
    • 面向对象:更适用于大型、复杂、需要频繁扩展和维护的项目,比如企业级的软件系统开发,通过将不同的业务实体抽象成对象,利用对象之间的关系和特性来构建整个系统,方便应对不断变化的业务需求。

二十一、描述一下登录页(我说的微信小程序登录)

1. 界面布局

  • 顶部:通常会有小程序的标题栏,显示小程序的名称或者相关标识,有的还会有返回按钮方便用户返回上一级页面(如果存在上一级页面的话)。
  • 主体部分

    • 账号输入框:一般会有一个清晰的文本框提示用户输入账号,账号可以是手机号码、邮箱或者自定义的用户名等,文本框旁边可能会有相应的提示文字,比如 “请输入手机号码”,并且有的还会设置输入格式的限制,例如只允许输入数字(针对手机号情况),通过合适的占位符来引导用户输入正确格式的内容。
    • 密码输入框:和账号输入框类似,用于用户输入登录密码,为了安全起见,输入内容通常会以加密的小圆点或者星号形式显示,同时也会有相应的提示文字,如 “请输入密码”,有的还支持密码可见切换功能,方便用户确认密码是否输入正确。
    • 登录按钮:是页面比较突出的元素,一般会有醒目的颜色(比如常用的主题色)和较大的点击区域,按钮上会明确写有 “登录” 字样,引导用户进行登录操作。
    • 第三方登录区域(可选):如果小程序支持第三方登录,比如微信登录、QQ 登录等,会在此区域展示相应的第三方登录图标,用户点击对应的图标即可快速使用第三方账号授权登录,图标通常设计得比较简洁直观,易于识别,例如微信登录就是微信的官方图标。
    • 忘记密码 / 找回密码链接:为了方便用户在忘记密码的情况下重置密码,会设置这样一个链接,文字描述清晰明了,点击后可跳转到相应的密码找回页面,引导用户通过验证手机号、邮箱等方式重置密码。
  • 底部(可选):可能会有一些版权信息、小程序相关的服务条款链接或者其他一些辅助性说明文字,不过这部分相对简洁,不会过于突出影响主体登录功能。

2. 功能逻辑

  • 账号密码验证:当用户输入账号和密码后点击登录按钮,小程序会获取用户输入的内容,首先会对输入的格式进行基本验证,比如手机号是否符合格式要求、密码长度是否在规定范围内等。然后将账号和密码信息发送到后端服务器进行验证,服务器会根据存储的用户数据来判断账号密码是否匹配,如果匹配则登录成功,返回相应的登录凭证(如 token 等),小程序端接收到登录凭证后进行存储(一般存储在本地缓存中),用于后续的接口请求等操作来标识用户身份;如果不匹配,则会在小程序端弹出相应的提示框告知用户账号或密码错误,请重新输入。
  • 第三方登录逻辑:若用户选择第三方登录方式,比如微信登录,小程序会调用微信的登录授权接口,用户确认授权后,微信会返回一个唯一标识(如 openid 等)给小程序,小程序将这个标识发送到自己的后端服务器,服务器根据这个标识来判断该用户是否是新用户,如果是新用户则进行注册流程(一般会自动为用户创建一个账号并关联相关信息),如果是已注册用户则直接登录成功,同样返回登录凭证供小程序存储和后续使用。
  • 记住密码功能(可选):有的登录页还会提供记住密码的功能,当用户勾选此功能并登录成功后,小程序会将账号和密码信息进行加密存储在本地,下次用户打开登录页面时,会自动填充之前记住的账号和密码,方便用户快速登录,但需要注意保障存储信息的安全性,防止信息泄露。
  • 密码找回逻辑:当用户点击忘记密码 / 找回密码链接,会跳转到相应的密码找回页面,一般会要求用户输入注册时使用的手机号或者邮箱等信息,然后发送验证码到对应的手机号或邮箱,用户输入正确的验证码后,可设置新的密码,新密码设置成功后,下次登录就可以使用新密码进行登录了。

二十二、重复登录怎么解决

1. 前端层面

  • 按钮状态控制:在登录按钮上添加点击事件处理,当用户第一次点击登录按钮后,立即将按钮设置为不可点击状态(比如改变按钮的样式为灰色、禁用点击事件等),并显示一个加载中的提示(如旋转的小图标),这样可以避免用户在登录请求还未处理完的情况下多次点击登录按钮造成重复登录请求发送。等后端返回登录结果(成功或者失败)后,再恢复按钮的可点击状态以及隐藏加载提示。例如,在微信小程序中可以这样做:
<button bindtap="login" disabled="{{isLoggingIn}}" class="{{isLoggingIn?'disabled-button' : 'normal-button'}}">登录</button>
<view wx:if="{{isLoggingIn}}">
  <image src="/images/loading.gif" style="width:20px;height:20px"></image>
</view>

在对应的 JavaScript 逻辑中:

Page({
  data: {
    isLoggingIn: false
  },
  login() {
    this.setData({
      isLoggingIn: true
    });
    // 这里发送登录请求,假设使用wx.request
    wx.request({
      url: 'https://example.com/login',
      success: (res) => {
        // 登录成功处理
        this.setData({
          isLoggingIn: false
        });
      },
      fail: (res) => {
        // 登录失败处理
        this.setData({
          isLoggingIn: false
        });
      }
    });
  }
});
  • 路由控制:当用户已经处于登录成功后的页面或者正在登录流程中(比如已经跳转到登录页面且登录请求正在处理),如果用户再次尝试通过某些入口(如点击某个引导登录的按钮等)进入登录页面,可以在路由跳转逻辑中进行判断,阻止重复跳转到登录页面。例如,在小程序的页面跳转拦截中:
wx.onAppRoute((res) => {
  const route = res.path;
  if (route === '/pages/login/login' && isLoggedIn) {  // isLoggedIn表示是否已登录,可根据存储的登录凭证判断
    wx.redirectTo({
      url: '/pages/home/home'  // 已登录就重定向到首页等已登录页面
    });
  }
});

2. 后端层面

  • 请求去重处理:后端服务器接收到登录请求时,可以根据一些唯一标识来判断是否已经有相同的登录请求正在处理。比如可以基于用户的 IP 地址、设备 ID(如果小程序能获取到相关信息并传递给后端)或者为每个登录请求生成一个唯一的请求 ID(由小程序端在请求时一并带上)等,当收到新的登录请求时,先检查是否存在正在处理且标识相同的请求,如果有则忽略这个新请求,等待之前的请求处理完成并返回结果,这样可以避免因网络延迟等原因导致用户多次点击登录按钮而产生的重复请求同时到达后端进行处理。
  • 登录状态校验:服务器要维护用户的登录状态,当用户成功登录后,会在服务器端记录该用户已登录(比如通过在数据库中更新登录状态字段或者在缓存中存储登录相关信息等方式),后续再接收到该用户的登录请求时,先检查其登录状态,如果已经处于登录状态,则直接返回相应的提示信息告知用户已经登录,无需再次登录,避免重复登录操作带来的额外资源消耗和可能的逻辑混乱。
继续阅读->

1. 讲一下微信小程序中登录是怎么实现的

  • 原理概述

    • 微信小程序登录主要基于微信官方提供的登录接口。其核心是通过调用微信的登录 API 获取用户的登录凭证(code),然后将这个 code 发送到开发者服务器,服务器再使用这个 code 向微信服务器换取用户的唯一标识(openid)和会话密钥(session_key)。
  • 具体步骤

    • 小程序端:使用wx.login()方法获取用户登录凭证(code)。例如:
       wx.login({
         success: function (res) {
           if (res.code) {
             // 将code发送到服务器
             wx.request({
               url: 'https://yourserver.com/login',
               data: {
                 code: res.code
               },
               method: 'POST',
               success: function (response) {
                 // 处理服务器返回的用户信息等
               }
             });
           } else {
             console.log('获取用户登录态失败!' + res.errMsg);
           }
         }
       });
  • 服务器端:收到小程序发送的 code 后,向微信服务器发送请求来换取 openid 和 session_key。一般是通过向https://api.weixin.qq.com/sns/jscode2session接口发送请求,需要传入小程序的appidsecret和收到的code。示例代码(以 Node.js 为例):
       const https = require('https');
       const appid = 'YOUR_APPID';
       const secret = 'YOUR_SECRET';
       function getOpenId(code) {
         const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`;
         return new Promise((resolve, reject) => {
           https.get(url, (res) => {
             let data = '';
             res.on('data', (chunk) => {
               data += chunk;
             });
             res.on('end', () => {
               try {
                 const result = JSON.parse(data);
                 resolve(result.openid);
               } catch (e) {
                 reject(e);
               }
             });
           }).on('error', (err) => {
               reject(err);
           });
         });
       }
  • 之后,开发者可以根据 openid 等信息来实现自己的业务逻辑,如生成自定义的用户登录态,存储用户信息等。

2. 谈一下你对小程序的理解

  • 定义和特点

    • 微信小程序是一种不需要下载安装即可使用的应用。它基于微信生态,具有轻便、快捷的特点。用户可以通过扫描二维码或在微信中搜索等方式直接打开使用。
  • 开发模式

    • 小程序采用了类似前端开发的技术栈,主要使用 JavaScript、WXML(类似于 HTML)和 WXSS(类似于 CSS)。它有自己的一套组件库,方便开发者快速构建用户界面。例如,小程序提供了视图容器组件(如view)、基础内容组件(如text)等。
  • 应用场景

    • 小程序适用于多种场景,如电商购物场景,用户可以在小程序中浏览商品、下单购买;生活服务场景,像预约挂号、点餐等服务都可以通过小程序实现。其优势在于能够快速触达用户,并且在微信的生态环境下,能够方便地与微信的其他功能(如支付、社交分享)相结合。

3. Echart底层原理你了解吗

  • 数据驱动

    • Echarts 是一个数据可视化库,它的核心是数据驱动的可视化设计。它将数据和可视化的配置进行分离。开发者只需要提供数据和对应的可视化配置(如图表类型、坐标轴设置等),Echarts 会根据这些信息来绘制图表。
  • 渲染流程

    • 首先,Echarts 会对传入的数据进行解析,根据配置确定图表的基本结构,如坐标轴、图例、图形元素(柱状、折线等)的布局。然后,通过使用底层的渲染引擎(如在浏览器环境下主要使用 HTML5 Canvas 或 SVG)将这些图形元素绘制出来。
    • 以使用 Canvas 渲染为例,Echarts 会根据数据计算出每个图形元素的坐标、大小等属性,然后使用 Canvas 的 API(如drawRect用于绘制矩形,beginPathlineTo等用于绘制折线等)将图形绘制在 Canvas 上。如果是 SVG 渲染,它会生成相应的 SVG 元素(如<rect><line>等)来构建图表。
  • 交互原理

    • Echarts 支持多种交互功能,如缩放、数据提示等。当用户进行交互操作时,如鼠标移动到图表元素上,Echarts 会根据鼠标的位置和图表的布局等信息,通过事件监听机制(如监听mouseover事件)来触发相应的交互行为。例如,显示数据提示框,这个数据提示框的内容是根据当前鼠标位置对应的数据点来动态生成的。

4. 图表刷新、点击穿透、回流重构等问题

  • 图表刷新

    • 原因和场景:图表需要刷新通常是因为数据发生了变化。例如,在实时数据监控的场景下,数据每隔一段时间就会更新,这就需要刷新图表来展示最新的数据。
    • 解决方法:对于 Echarts 等图表库,一般可以通过更新数据并重新设置图表的数据集来实现刷新。以 Echarts 为例,如果是折线图,假设myChart是已经初始化的图表实例,newData是新的数据,可以通过以下方式刷新:
       myChart.setOption({
         series: [{
           data: newData
         }]
       });
  • 点击穿透

    • 定义和问题表现:点击穿透是指当一个元素(如蒙层)覆盖在另一个可点击元素上时,点击蒙层后,下层的元素也触发了点击事件。例如,在一个模态框(蒙层)覆盖在页面内容上,当用户点击模态框关闭按钮以外的区域来关闭模态框时,下层的页面元素的点击事件也被触发了。
    • 解决方法:一种常见的解决方法是在蒙层元素上添加@click.stop(在 Vue 中)或者event.stopPropagation()(在原生 JavaScript 中)来阻止事件冒泡。例如在 Vue 中:
       <div class="overlay" @click.stop>
         <!-- 模态框内容 -->
       </div>
  • 回流重构

    • 定义和影响:回流是指浏览器重新计算元素的布局位置等属性,重构是指浏览器重新绘制元素的外观。回流的触发场景包括元素的大小、位置、内容等发生变化。频繁的回流和重构会影响性能,导致页面卡顿。
    • 优化方法:尽量减少对布局有影响的操作,如避免频繁地改变元素的尺寸和位置。如果需要批量操作元素,可以通过将元素从文档流中脱离(如使用position:absolutedisplay:none),进行操作后再将其恢复,来减少回流次数。

5. 你简历上谈到了跨域问题处理,你是怎么处理的

  • 跨域产生的原因

    • 浏览器的同源策略限制,当一个资源(如一个 JavaScript 文件、一个 API 请求)的协议、域名、端口与当前页面的不一致时,就会产生跨域问题。例如,前端页面是在http://localhost:8080访问,而后端 API 是在http://api.example.com提供服务,就会产生跨域。
  • 解决方法(以 Node.js 后端为例)

    • CORS(跨域资源共享):在后端服务器设置响应头来允许跨域访问。在 Node.js 中使用express框架可以这样设置:
       const express = require('express');
       const app = express();
       app.use((req, res, next) => {
         res.header('Access - Control - Allow - Origin', '*');
         res.header('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE');
         res.header('Access - Control - Allow - Headers', 'Content - Type, Authorization');
         next();
       });
  • 代理服务器:在开发环境下,前端开发工具(如 Vue - CLI)可以设置代理。以 Vue - CLI 为例,在vue.config.js文件中可以这样设置:
       module.exports = {
         devServer: {
           proxy: {
             '/api': {
               target: 'http://api.example.com',
               changeOrigin: true,
               pathRewrite: {
                 '^/api': ''
               }
             }
           }
         }
       };

6. Vue 中监听和计算属性的区别

  • 计算属性(computed)

    • 定义和特点:计算属性是基于它们的依赖进行缓存的。一个计算属性只有在它的依赖数据发生变化时才会重新计算。例如,假设有一个计算属性fullName,它依赖于firstNamelastName
       const app = Vue.createApp({
         data() {
           return {
             firstName: 'John',
             lastName: 'Doe'
           };
         },
         computed: {
           fullName() {
             return this.firstName + ' ' + this.lastName;
           }
         }
       });
  • 应用场景:适用于需要对已有数据进行复杂计算并缓存结果的情况。比如根据商品的单价和数量计算总价,总价是一个计算属性,只要单价和数量没有变化,总价就不需要重新计算,直接从缓存中获取。
  • 监听属性(watch)

    • 定义和特点:监听属性用于在数据变化时执行异步操作或开销较大的操作。它更侧重于观察数据的变化并作出响应。例如,监听一个表单输入框的值的变化:
       const app = Vue.createApp({
         data() {
           return {
             inputValue: ''
           };
         },
         watch: {
           inputValue(newValue, oldValue) {
             console.log(`输入值从 ${oldValue} 变为 ${newValue}`);
           }
         }
       });
  • 应用场景:适用于当数据变化后需要执行一些副作用操作,如发送网络请求、更新其他相关数据等。比如当用户在搜索框输入关键词后,监听关键词的变化并发送请求获取搜索结果。

7. Vue3 和 Vue2 的区别

  • 性能方面

    • 响应式系统改进:Vue3 采用了 Proxy 来实现响应式,相比 Vue2 的Object.defineProperty,Proxy 可以代理整个对象,而不是像Object.defineProperty那样只能代理对象的已有属性。这使得在添加或删除属性时也能被响应式地追踪,并且在性能上有一定的提升,特别是在处理大型复杂对象时。
  • API 设计

    • 组合式 API(Composition API):Vue3 引入了组合式 API,它允许开发者以函数的形式组织和复用代码逻辑。例如,在 Vue3 中可以使用setup函数来组合多个逻辑相关的代码块,包括数据、方法、生命周期钩子等。相比 Vue2 主要基于选项式 API(datamethodsmounted等选项),组合式 API 提供了更灵活的代码组织方式。
  • 模板语法

    • 片段支持:Vue3 支持在模板中有多个根元素(片段),而 Vue2 要求模板必须有且只有一个根元素。例如,在 Vue3 中可以这样写模板:
       <template>
         <div>第一个元素</div>
         <div>第二个元素</div>
       </template>

8. Vue 组件间通信

  • 父子组件通信

    • 父传子:在父组件中通过属性(props)将数据传递给子组件。例如,父组件中有一个数据message,在子组件中通过props接收:
       <!-- 父组件 -->
       <template>
         <child-component :message="message"></child-component>
       </template>
       <script>
       import ChildComponent from './ChildComponent.vue';
       export default {
         components: {
           ChildComponent
         },
         data() {
           return {
             message: 'Hello from parent'
           };
         }
       };
       </script>
       <!-- 子组件 -->
       <template>
         <div>{{ message }}</div>
       </template>
       <script>
       export default {
         props: ['message']
       };
       </script>
  • 子传父:子组件通过触发自定义事件将数据传递给父组件。例如,子组件中有一个按钮,点击按钮时向父组件传递一个数据:
       <!-- 子组件 -->
       <template>
         <button @click="sendData">发送数据</button>
       </template>
       <script>
       export default {
         methods: {
           sendData() {
             this.$emit('data - from - child', 'Data from child');
           }
         }
       };
       </script>
       <!-- 父组件 -->
       <template>
         <child-component @data - from - child="handleData"></child-component>
       </template>
       <script>
       import ChildComponent from './ChildComponent.vue';
       export default {
         components: {
           ChildComponent
         },
         methods: {
           handleData(data) {
             console.log(data);
           }
         }
       };
       </script>
  • 非父子组件通信(事件总线或 Vuex)

    • 事件总线:在 Vue2 中可以创建一个空的 Vue 实例作为事件总线。例如,定义一个EventBus.js文件:
       import Vue from 'vue';
       export const eventBus = new Vue();

然后在组件 A 中发送事件:

       import { eventBus } from './EventBus.js';
       eventBus.$emit('custom - event', 'Data to share');

在组件 B 中接收事件:

       import { eventBus } from './EventBus.js';
       created() {
         eventBus.$on('custom - event', (data) => {
           console.log(data);
         });
       }
  • Vuex:Vuex 是一个专门用于 Vue 应用的状态管理模式。它通过一个集中式的存储(store)来管理应用的所有组件的状态。组件可以通过commit方法触发mutations来修改状态,通过dispatch方法触发actions来执行异步操作并修改状态。例如,在store.js文件中定义一个简单的 Vuex store:
       import Vue from 'vue';
       import Vuex from 'vuex';
       Vue.use(Vuex);
       export default new Vuex.Store({
         state: {
           count: 0
         },
         mutations: {
           increment(state) {
             state.count++;
           }
         },
         actions: {
           asyncIncrement(context) {
             setTimeout(() => {
               context.commit('increment');
             }, 1000);
           }
         },
         getters: {
           doubleCount(state) {
             return state.count * 2;
           }
         }
       });

在组件中使用:

       <template>
         <div>
           <p>Count: {{ count }}</p>
           <button @click="increment">Increment</button>
         </div>
       </template>
       <script>
       import { mapState, mapMutations, mapActions } from 'vuex';
       export default {
         computed: {
          ...mapState(['count'])
         },
         methods: {
          ...mapMutations(['increment']),
          ...mapActions(['asyncIncrement'])
         }
       };
       </script>

9. Vue 两种路由以及区别

  • Hash 模式

    • 原理:Hash 模式是基于浏览器的location.hash来实现路由的。location.hash的值就是 URL 中#后面的部分。当#后面的值发生变化时,浏览器不会向服务器发送请求,而是会触发hashchange事件。例如,http://example.com/#/home中的#/home就是hash值。
    • 特点:这种模式比较简单,兼容性好,因为它不会引起浏览器的页面刷新。但是hash值在 URL 中比较明显,不太美观。
  • History 模式

    • 原理:History 模式是利用 HTML5 的history.pushState()history.replaceState()方法来改变 URL,并且不会引起页面刷新。例如,通过history.pushState({}, '', '/home')可以将当前 URL 修改为http://example.com/home。同时,当用户点击浏览器的前进或后退按钮时,会触发popstate事件,根据这个事件可以实现路由的切换。
    • 特点:History 模式的 URL 比较美观,更符合传统的 URL 格式。但是需要服务器端的配合,因为当用户直接访问一个不存在的 URL(如http://example.com/about)时,服务器需要返回正确的页面(通常是单页应用的index.html),否则会出现 404 错误。

10. 实现页面元素垂直居中

    • 方法一:使用 Flexbox 布局

      • 原理:Flexbox 是一种强大的布局模式,通过设置父元素为display: flex,并使用align-items: centerjustify-content: center属性,可以轻松地将子元素在水平和垂直方向上居中。例如,对于一个简单的 HTML 结构:
       <div class="parent">
         <div class="child">
           这是要居中的内容
         </div>
       </div>

CSS 样式如下:

      .parent {
         display: flex;
         justify-content: center;
         align-items: center;
         height: 300px; /* 假设父元素有一定高度 */
       }
  • 方法二:使用 CSS Grid 布局

    • 原理:CSS Grid 布局也提供了方便的居中方式。通过将父元素设置为display: grid,然后使用place-items: center属性来同时在水平和垂直方向上居中子元素。例如:
       <div class="grid - parent">
         <div class="grid - child">
           这是要居中的内容
         </div>
       </div>

CSS 样式:

      .grid - parent {
         display: grid;
         place-items: center;
         height: 300px; /* 假设父元素有一定高度 */
       }
  • 方法三:绝对定位和负边距

    • 原理:对于已知高度和宽度的元素,可以使用绝对定位将其定位到父元素的中心位置。首先将父元素设置为相对定位(position: relative),然后子元素使用绝对定位(position: absolute),并通过设置topleft属性为50%将元素的左上角定位到父元素的中心,最后使用负边距(margin - topmargin - left)将元素向回拉回自身高度和宽度的一半,从而实现居中。例如:
       <div class="parent - abs">
         <div class="child - abs">
           这是要居中的内容
         </div>
       </div>

CSS 样式:

      .parent - abs {
         position: relative;
         height: 300px;
       }
      .child - abs {
         position: absolute;
         top: 50%;
         left: 50%;
         width: 200px;
         height: 100px;
         margin - top: - 50px;
         margin - left: - 100px;
       }
  • 方法四:使用transform属性结合绝对定位

    • 原理:与绝对定位和负边距类似,不过这种方法不需要知道元素的具体尺寸。同样先将父元素设置为相对定位,子元素设置为绝对定位,将topleft设置为50%,然后使用transform: translate(-50%, -50%)来将元素在水平和垂直方向上移动自身宽度和高度的一半,实现居中。例如:
       <div class="parent - trans">
         <div class="child - trans">
           这是要居中的内容
         </div>
       </div>

CSS 样式:

      .parent - trans {
         position: relative;
       }
      .child - trans {
         position: absolute;
         top: 50%;
         left: 50%;
         transform: translate(-50%, -50%);
       }
  
继续阅读->