前端面试知识点纪要(2024)

前言

自己总结的关于vue2/vue3,react的高频面试内容记录,其中不乏自己被面试过问到的,其中包括部分js,ts,css,promise,axios,webpack,redux,浏览器等内容


文章目录


零、1)vue2和vue3和react的响应式原理

Vue2的双向数据绑定原理:
原理:vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的set,get,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。
在这里插入图片描述

  1. 数据劫持(Data Hijacking)

Vue 2 使用 Object.defineProperty() 来实现数据劫持。对于每一个数据属性,Vue 会将它转化为一个 get 和 set,从而拦截对数据的访问和修改操作。

实现原理:

当访问一个数据属性时,get 会被调用,Vue 可以通过这个 get来跟踪这个属性依赖的所有视图组件。

当修改一个数据属性时,set会被调用,Vue 可以通知所有依赖这个属性的视图组件进行更新。

  1. 发布-订阅模式(Publish-Subscribe Pattern)

在 Vue 2 中,视图和数据之间的关联是通过一个叫做 Watcher 的类来实现的。Watcher 作为发布者和订阅者之间的桥梁,每一个 Watcher 订阅了某个数据属性,当该属性发生变化时,Watcher 就会被通知并触发相应的视图更新。

实现步骤:

Dep(依赖收集器):每个数据属性都有一个对应的 Dep 对象,用来收集所有依赖于该属性的 Watcher。

Watcher:当视图模板被解析时,Vue 会为每一个依赖数据的地方创建一个 Watcher,并将其添加到对应数据属性的 Dep 中。

通知更新:当数据属性发生变化时,对应的 Dep 会通知所有相关的 Watcher,进而触发视图更新。

  1. 双向绑定的完整流程

结合以上内容,Vue 2 的双向绑定机制大致可以描述为以下步骤:

数据劫持:通过 Object.defineProperty() 拦截对象属性的访问和修改,将属性转化为响应式属性。

依赖收集:在初始化组件时,Vue 解析模板并为每个依赖于响应式属性的地方创建一个 Watcher。这些 Watcher 会被添加到相应属性的 Dep 中。

数据变更通知:当响应式属性的值发生变化时,setter 被触发,对应的 Dep 通知所有依赖此属性的 Watcher,调用它们的 update 方法更新视图。

自己总结:

Vue 2 的双向绑定通过数据劫持和发布-订阅模式实现了数据和视图的自动同步,具体是:模版创建时先通过Object.defineProperty进行数据接触,遍历对象的每个属性,并给每个属性都创建set和get,访问属性的时候出发getter,更新属性的时候出发setter,然后在编译时,给每个属性都匹配绑定了一个watcher监听器,用于数据与视图更新的桥梁。每个被监听的数据属性还有一个dep用于收集数据更新相关的的依赖(比如data和computed的数据联系)。但数据变化时,setter被触发,对应的dep通知所有依赖此属性的watcher,调用他们的update方法从而更新视图

双向绑定的优势
‌解耦合‌:双向绑定将数据和视图分离,使得数据的变化能够自动反映到视图上,而不需要手动更新视图。
‌灵活性‌:用户可以通过表单输入等操作直接修改视图,而框架会自动更新数据,提高了用户体验。
‌性能优化‌:Vue会将多个数据的变化收集起来,在下一个事件循环或异步任务中进行批量更新,减少了不必要的重复渲染和DOM操作,提高了性能。

问题:对于数组数据无法通过数组下标直接操作,无法通过 length直接设置数组长度,无法直接给对象或数组添加属性
解决:通过 vm.$set() 或 this.set() 来对对象或数组进行操作

  1. Vue3的双向数据绑定原理
    原理:
    在 Vue 3 中,数据双向绑定的实现原理与 Vue 2 有所不同,由于 Vue 3 使用了 Proxy 对象,因此实现方式发生了变化。
    Proxy 对象:在 Vue 3 中,使用 Proxy 对象来代理目标对象,通过 Proxy 对象的拦截器捕获对目标对象的操作,并可以在这些操作上添加自定义的行为。这样可以更方便地实现数据的监听和响应。
    Reactive 和 Ref:Vue 3 中引入了 reactive 和 ref 两种响应式数据引用的方式。reactive 可以监听整个对象并使对象的属性都可响应,而 ref 则适用于对简单值进行包装。
    **Effect:Vue 3 中引入了 Effect API如Reflect,用于创建响应式的副作用。Effect 可以监听响应式数据的变化,并在数据变化时执行相应的副作用,例如更新视图等操作。
    更新视图:当响应式数据发生变化时,Effect 会自动执行相应的副作用,从而更新视图,实数据的双向绑定。

总结:Ref响应式还是通过Object.definePropoty()对数据劫持,通过get和set实现响应式。reactive则是通过 Proxy对数据进行代理劫持,实现响应式,将 object对象的一些明显属性语言内部的方法(如 Object.defineProperty)放到reflect 对象上,然后通过Reflect实现对源数据的操作(详细描述可见es6中的Reflect)
优势:Proxy是实现对象的监听,而不是对某个属性的监听。而且是惰性的,嵌套对象只有在被访问时才会被转化为响应式。这种方式避免了不必要的性能开销,尤其是在处理大型数据结构时。

//handler常见方法(详细描述可见es6中的Reflect),reflect对象的原型就是Object
var obj = { a: 1, b: 2 };
let proxy = new Proxy(obj, {
 	get(target, prop) {
		 return target[prop]; // prop:obj对象里的属性a、b ,类似于key
		 console.log('get方法');
	 },
	set(target, prop, value) {
		 // target[prop] = value; // value:新值
		 Reflect.set(target, prop, value);
		 console.log('set方法');
 },
 });
 proxy.a = 4;
 console.log(obj.a); //4
  1. react的数据响应式原理
    和vue 相比 react 并没有提供向 v-model 这样的指令来实现文本框的数据流双向绑定,因为react的设计思路就是单向数据流,所以我们需要借助 onChange 和 setState 来实现一个双向的数据流

**原理:**在React中,数据响应式是通过组件的状态(State)和属性(Props)来实现的。当状态或属性发生变化时,React会自动重新渲染组件,以确保UI与数据保持同步。这种机制背后的原理是React的虚拟DOM(Virtual DOM)。

2)MVVM和MVC有什么区别

MVC
MVC是一种设计模式:

M(Model):模型层。是应用程序中用于处理应用程序数据逻辑的部分,模型对象负责在数据库中存取数据;
V(View):视图层。是应用程序中处理数据显示的部分,视图是依据模型数据创建的;
C(Controller):控制层。是应用程序中处理用户交互的部分,控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。
在这里插入图片描述
MVVM
vue框架中MVVM的M就是后端的数据,V就是节点树,VM就是new出来的那个Vue({})对象

M(Model):模型层。就是业务逻辑相关的数据对象,通常从数据库映射而来,我们可以说是与数据库对应的model。
V(View):视图层。就是展现出来的用户界面。
VM(ViewModel):视图模型层。连接view和model的桥梁。因为,Model层中的数据往往是不能直接跟View中的控件一一对应上的,所以,需要再定义一个数据对象专门对应view上的控件。而ViewModel的职责就是把model对象封装成可以显示和接受输入的界面数据对象。
在这里插入图片描述
View与ViewModel之间通过双向绑定建立联系,这样当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然。

MVVM的优势
1、mvc和mvvm都是一种设计思想。 主要就是mvc中Controller演变成mvvm中的viewModel。 mvvm主要解决了mvc中大量DOM操作使页面渲染性能降低,加载速度变慢的问题 。
2、MVVM与MVC最大的区别就是:它实现了View和Model的自动同步:当Model的属性改变时,我们不用再自己手动操作Dom元素来改变View的显示,它会自动变化。
3、整体看来,MVVM比MVC精简很多,我们不用再用选择器频繁地操作DOM。

3)项目难点亮点

以下是一些常见的前端项目亮点:

  1. 响应式设计:如果你的项目能够适应不同屏幕尺寸和设备,提供良好的用户体验,这是一个很好的亮点。

  2. 性能优化:如果你在项目中使用了一些性能优化技术,例如懒加载、代码分割、缓存等,可以突出这些优化措施。

  3. 动画效果:如果你在项目中运用了一些炫酷的动画效果,例如CSS动画、SVG动画、Canvas动画等,可以强调这些动画效果。

  4. 前端框架或库的使用:如果你在项目中使用了流行的前端框架或库,例如React、Vue.js、Angular等,可以突出这些技术的应用。

  5. 前端工程化:如果你在项目中使用了一些前工程化的工具和流程,例如Webpack、Gulp、自动测试等,可以强调这些工程化实践。

  6. 用户交互体验:如果你在项目中注重用户交互体验,例如通过拖拽、无限滚动、实时通信等方式提升用户体验,可以突出这些方面。

  7. 跨平台开发:如果你在项目中使用了一些跨平台开发技术,例如React
    Native、Flutter等,可以强调你的项目可以同时支持多个平台。

当被问到项目难点时,你可以从以下几个方面进行介绍:

  1. 技术难点:你可以提到在项目中遇到的技术挑战,例如使用新的编程语言或框架、处理复杂的算法逻辑等。你可以详细说明这些技术难点是如何解决的,以及你在解决过程中学到了什么。

  2. 时间管理:在项目中,时间管理可能是一个常见的难点。你可以谈谈如何在有限的时间内完成项目,并保证质量。你可以提到你采取的时间管理策略,例如制定详细的计划、合理分配任务、及时调整进度等。

  3. 团队协作:如果项目是一个团队合作的项目,团队协作可能成为一个挑战。你可以讲述你在项目中如何与团队成员有效沟通、协调工作、解决冲突等。你可以提到你采取的团队协作工具和方法,以及你从中学到的团队合作技巧。

4. 需求变更:在项目进行过程中,需求变更是一个常见的难点。你可以谈谈如何应对需求变更,例如与客户或产品经理进行有效沟通、及时调整计划、灵活应对变化等。你可以提到你在处理需求变更时的经验和教训。

4)前端模块化规范

什么是模块化

  • 将程序文件依据一定规则拆分成多个文件,这种编码方式就是模块化的编码方式。
  • 本质上拆分出来的每个文件就是一个模块,模块中的数据默认都是私有的,模块之间互相隔离。
  • 同时也能通过一些手段,可以把模块内的指定数据“抛出去”,供其他外部模块使用。

为什么需要模块化
随着应用的复杂度越来越高,其代码量和文件数量都会急剧增加,会逐渐引发以下问题:

  1. 随着js文件数量增加,全局变量出现的机率增加,造成全局污染问题
  2. 依赖混乱问题

有哪些模块化规范:

1. CommonJS(服务端应用广泛)
2. es6规范化(浏览器端应用广泛)
3. AMD
4. CMD

CommonJS 规范:
在 CommonJS 标准中,导出数据有两种方式:

  1. 第一种方式:module.exports = value
  2. 第二种方式:exports.name = value
const name = 'javaScript'
const motto = '前端技术栈'

function getTel (){
  return '66666666666666'
}

function getHobby(){
  return ['数据一''数据二']
}

// 通过给exports对象添加属性的方式,来导出数据(注意:此处没有导出getHobby)
exports.name = name
exports.slogan = slogan
exports.getTel = getTel

在 CommonJS模块化标准中,使用内置的 require 函数进行导入数据

// 直接引入模块
const test = require('./test')

// 引入同时解构出要用的数据
const { name, slogan, getTel } = require('./test')

// 引入同时解构+重命名
const {name:stuName,motto,getTel:stuTel} = require('./test')

ES6 官方模块化规范:
ES6 模块化提供 3 种导出方式:

  1. 分别导出
  2. 统一导出
  3. 默认导出

【分别导出】

// 导出name
export let name = {str:'测试数据'}
// 导出slogan
export const slogan = '一个字符串数据'

// 导出getTel函数
export function getTel (){
  return '66666666'
}

【统一导出】

const name = {str:'测试数据'}
const slogan = '一个字符串数据'

function getTel (){
  return '666666666'
}

function getCities(){
  return ['北京','上海','深圳','成都','武汉','西安']
}

// 统一导出了:name,slogan,getTel
export {name,slogan,getTel}

【默认导出】

const name = '张三'
const motto = '走自己的路,让别人五路可走!'

function getTel (){
  return '13877889900'
}

function getHobby(){
  return ['前端','后端','测试']
}

//默认导出:name,motto,getTel
export default {name,motto,getTel}

备注 :「上述多种导出方式,可以同时混合使用」

// 导出name ———— 分别导出
export const name = {str:'测试数据'}
const slogan = '一个字符串数据'

function getTel (){
  return '010-56253825'
}

function getCities(){
  return ['北京','上海','深圳','成都','武汉','西安']
}

// 导出slogan ———— 统一导出
export {slogan}
// 导出getTel ———— 默认导出
export default getTel

对于 ES6 模块化来说,使用何种导入方式,要根据导出方式决定。

  1. 「导入全部」(通用)

可以将模块中的所有导出内容整合到一个对象中。

import * as test from './test.js'
  1. 「命名导入」(对应导出方式:分别导出、统一导出)
import { name,slogan,getTel } from './test.js'

通过 as 重命名

import { name as myName,slogan,getTel } from './test.js'
  1. 「默认导入」(对应导出方式:默认导出)
import test from './test.js' //默认导出的名字可以修改,不是必须为test
  1. 「命名导入 与 默认导入可以混合使用」

「命名导入」与「默认导入」混合使用,且默认导入的内容必须放在前方:

import getTel,{name,slogan} from './test.js'
  1. 「动态导入」(通用)

允许在运行时按需加载模块,返回值是一个 Promise

const test = await import('./test.js');
console.log(test)  //Promise
  1. import 可以不接收任何数据

例如只是让 mock.js 参与运行

import './mock.js'

Node 中运行 ES6 模块
Node.js中运行ES6模块代码有两种方式:

方式一:将 JavaScript 文件后缀从 .js 改为 .mjs,Node 则会自动识别 ES6 模块。
方式二:在 package.json 中设置 type 属性值为 module 。

一、关于vue2和vue3

1.vue2和vue3的区别

  1. 双向绑定原理的不同:
    vue2是使用的es5的api,Object.defineProperty劫持数据,然后再结合发布订阅者模式进行数据绑定的;
    vue3是使用的es6的api,proxy进行双向绑定的,这样相比vue2有几个优势:
    1)defineProperty只能监听某个属性,不能全对象监听
    2)可以省去for in,闭包等内容来提高效率(直接绑定整个对象即可)
    3)可以监听数组,不用在对数组做特异性操作,vue3可以直接监听数组数据的变化

  2. vue2不支持碎片(Fragment),而vue3支持碎片,也就是vue3可以拥有有多个根结点

  3. API类型不同。
    vue2:选项式API,选项型api在代码里分割了不同的属性:data,computed,methods等;而vue3
    vue3:合成式API,新的合成型api能让我们使用方法来分割(雷士于react,需要那种属性就引入)

  4. 定义数据变量和方法不同。
    vue2中定义数据变量,需要在data()的return{}对象进行定义,方法需要在methods:{}中编写;
    而vue3则直接将两项进行组合,使用了一种新的方法setup(){},可以把定义数据变量和操作方法都写入其中,当然vue3支持setup放入script标签中,这样可以更加简洁。

  5. 生命周期钩子函数不同
    vue2中的生命周期:
    beforeCreate 组件创建之前
    created 组件创建之后
    beforeMount 组价挂载到页面之前执行
    mounted 组件挂载到页面之后执行
    beforeUpdate 组件更新之前
    updated 组件更新之后

    vue3中的生命周期:
    setup 开始创建组件
    onBeforeMount 组价挂载到页面之前执行
    onMounted 组件挂载到页面之后执行
    onBeforeUpdate 组件更新之前
    onUpdated 组件更新之后

  6. 父子传参不同
    vue2:父传子,用props,子传父用事件 Emitting Events。在vue2中,会调用this$emit然后传入事件名和对象。

    vue3:父传子,用props,子传父用事件 Emitting Events。在vue3中的setup()中的第二个参数content对象中就有emit,那么我们只要在setup()接收第二个参数中使用分解对象法取出emit就可以在setup方法中随意使用了。

  7. 指令与插槽不同
    vue2:vue2中使用slot可以直接使用slot;v-for与v-if在vue2中优先级高的是v-for指令,而且不建议一起使用。

    vue3:vue3中必须使用v-slot的形式;vue3中v-for与v-if,只会把当前v-if当做v-for中的一个判断语句,不会相互冲突;vue3中移除keyCode作为v-on的修饰符,当然也不支持config.keyCodes;vue3中移除v-on.native修饰符;vue3中移除过滤器filter。

  8. mian.js文件不通过
    vue2:vue2中我们可以使用pototype(原型) 的形式去进行操作,引入的是构造函数。

    vue3:vue3中需要使用结构的形式进行操作,引入的是工厂函数;vue3中app组件中可以没有根标签。

2.vue2中data为什么是一个函数

官方文档说明:当一个组件被定义,data 必须申明为返回一个初始数据的函数,因为组件可能被用来创建多个实例。

当我们给组件中的data写成一个函数时,数据以函数返回值形式定义,这样每服用一次组件,就返回一个新的data,每个data有各自的作用域,保证各个组件的数据不被污染;
如果我们把data写成对象形式,那么组件所用的实例都用的同一个构造函数,由于javaScript特性,实际组件实例共用了一个data,就会造成一个变了全都会变的结果。

3.vue3中依然可以使用data吗?setup和data的执行顺序是如何的?如果setup与data变量重名,执行谁?

1)vue3虽然时合成式api,生命周期上把vue2中created及之前面的部分都合成setup,但是依然能使用vue2的配置项,比如data,methods,created等,也就是vue3中setup和data可以同时存在

2)执行顺序:
setup执行早于data,传统配置项中,可以调用到setup中的数据,也就是data可以调用setup中的数据;反过来,setup中,调用不到传统配置项中的数据。
因为setup执行的时机,在data配置项之前,所以在data配置项中读取数据时,setup数据已经加载完毕,可以读取到。反之则不行。(注:setup中,this值为undefined

3)如果setup配置项和传统配置项中有重名情况,setup优先。例如:两个都存在同一个变量名,不同的变量值时,取setup中的变量值。

4.vue框架下的watch和computed的区别

computed和watch都是vue框架中用于监听数据变化的属性

  1. 功能:watch监听一个值的变化而执行对应的回调,computed是计算属性。
  2. 是否调用缓存:computed函数所依赖的属性不变的时候会调用缓存;watch每次监听的值发生变化时候都会调用回调
  3. 是否调用return:computed必须有;watch可以没有
  4. 使用场景:computed当一个属性受多个属性影响的时候;例如购物车商品结算;watch当一条数据影响多条数据的时候,例如搜索框
  5. 是否支持异步:computed函数不能有异步;watch可以
  6. 第一次加载式是否监听:computed默认第一次加载的时候就监听,但是watch默认第一次加载不监听,如果需要第一次加载就监听,则需要添加immediate属性,并设置为true。

5.vue2中组件通信方式

Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信

1. props / $emit 适用 父子组件通信
2. ref 与 $parent / $children适用 父子组件通信
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
$parent / $children:访问父 / 子实例

3. EventBus ($emit / $on)适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
4. a t t r s / attrs/ attrs/listeners适用于 隔代组件通信
a t t r s :包含了父作用域中不被 p r o p 所识别 ( 且获取 ) 的特性绑定 ( c l a s s 和 s t y l e 除外 ) 。当一个组件没有声明任何 p r o p 时,这里会包含所有父作用域的绑定 ( c l a s s 和 s t y l e 除外 ) ,并且可以通过 v − b i n d = " attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind=" attrs:包含了父作用域中不被prop所识别(且获取)的特性绑定(classstyle除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(classstyle除外),并且可以通过vbind="attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
l i s t e n e r s :包含了父作用域中的 ( 不含 . n a t i v e 修饰器的 ) v − o n 事件监听器。它可以通过 v − o n = " listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on事件监听器。它可以通过 v-on=" listeners:包含了父作用域中的(不含.native修饰器的)von事件监听器。它可以通过von="listeners" 传入内部组件
6. provide / inject适用于 隔代组件通信
祖先组件中通过 provide 来提供变量,然后在子孙组件中通过 inject 来注入变量。provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
7. Vuex适用于 父子、隔代、兄弟组件通信
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation

6.vue中的key作用与原理,为什么不建议用index做为key?为什么不建议用随机数作为 key?

官方关于key的解释:
key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。
而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

简单来说:key 的作用主要是为了高效的更新虚拟 DOM。首先根据vue的diff算法生成对应的vNode,再用patch函数对比新旧vNode,更新DOM。

用index作为key会出现什么问题:
一般情况下,只是展示list,没有太大问题;如果list会动态删除、添加、排序等操作,就会出现错乱
,比如你你明明删除第一行,但是视图却删除了第二行吗,问题解析就是因为key的重复,当你删除第一行时,第二行的key也变成第一行的key ,这两个key是相同的

不用随机数作为 key,是因为diff算法执行后会把,旧节点会被全部删掉,新节点重新创建,浪费性能

7.vue中的mixin是什么?与vuex有何区别?

官方解释:
混入(mixin)提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

mixin与vuex区别:
Vuex公共状态管理,如果在一个组件中更改了Vuex中的某个数据,那么其它所有引用了Vuex中该数据的组件也会跟着变化。
Mixin中的数据和方法都是独立的,组件之间使用后是互相不影响的。

8.vue中组件和mixin中的属性使用区别

data
computed
methods
watch
props
provide和inject
以上属性和方法会被合并成一个新对象,如果出现相同的属性,组件中的属性会覆盖mixin中的属性。

beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeDestroy
destroyed
以上属性和方法会被合并成一个数组,如果出现相同的属性,mixin中的属性会排在组件中的属性前面

9.vue中组件和mixin执行顺序

生命周期函数,先执行mixin里面的,再执行组件里面的

10.你使用过vuex吗?什么是vuex,它包含哪些部分?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式, 采用集中式存储管理应用的所有组件的状态,解决多组件数据通信

state: 统一定义公共数据(类似于data(){return {a:1, b:2,xxxxxx}})
mutations : 使用它来修改数据(类似于methods,这里必须是同步函数)
getters: 类似于computed(计算属性,对现有的状态进行计算得到新的数据-------派生 )
actions: 发起异步请求
modules: 模块拆分

使用方式:
通过this.$ store.state 来获取公共数据,通过this.$store.commit(‘mutation名’, 实参)调用mutation中的同步函数

11. vue3中的reactive和ref的区别,为什么reactive不能定义基本数据类型?

reactive和ref都是vue3中提供的响应式API,用于定义响应式数据的

reactive通常用于定义对象数据类型,其本质是基于 Proxy 实现对象代理,所以reactive不能用于定义基本类型数据

ref通常是用于定义基本数据类型,取值要跟.value,其本质是基于 Object.defineProperty() 重新定义属性的方式实现,vue3源码中是基于类的属性访问器实现(本质也是 defineProperty )

12.vue父组件套子组件之后父子组件的生命周期的运行顺序

加载渲染过程
父级beforeCreate=>父级created=>父级befroeMount=>子beforeCreate=>子created=>子beforeMounted=>父mounted
销毁过程
父beforeDestorty=>子beforeDestory=>子destoryed=>父destoryed

13.解释vue3的api。如watchEffect,toValue

watchEffect()立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
而watch是变量变化的时候才执行。
toValue()将值、refs 或 getters 规范化为值。toValue(ref(1))就可以直接拿到值1。
toRefs()将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。

14.vue的路由守卫包括那些?

全局路由钩子:
beforeEach(to,from, next)、beforeResolve(to,from, next)、afterEach(to,from);
独享路由钩子:
beforeEnter(to,from, next);
组件内路由钩子:
beforeRouteEnter(to,from, next)、beforeRouteUpdate(to,from, next)、beforeRouteLeave(to,from, next)
导航守卫回调参数
to:目标路由对象;
from:即将要离开的路由对象;
next:他是最重要的一个参数,他相当于佛珠的线,把一个一个珠子逐个串起来。以下注意点务必牢记:
1.但凡涉及到有next参数的钩子,必须调用next() 才能继续往下执行下一个钩子,否则路由跳转等会停止。
2.如果要中断当前的导航要调用next(false)。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from路由对应的地址。(主要用于登录验证不通过的处理)
3.当然next可以这样使用,next(‘/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址。意思是当前的导航被中断,然后进行一个新的导航。可传递的参数与router.push中选项一致。
4.在beforeRouteEnter钩子中next((vm)=>{})内接收的回调函数参数为当前组件的实例vm,这个回调函数在生命周期mounted之后调用,也就是,他是所有导航守卫和生命周期函数最后执行的那个钩子。
5.next(error): (v2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

15.模拟实现vue2中的$nextTick,并解释作用原理

nextTick
作用:$nextTick中的回调函数是在下次DOM更新循环结束之后才会执行。

原理:基于javascript的事件循环机制与vue内部的异步更新策略。响应式数据发生变化时,vue内部会创建一个异步任务队列,将视图更新任务添加到这个异步任务队列中。而nextTick是将用户提供的回调函数添加到异步队列的末尾,保证回调函数的执行一定是在视图更新完之后。

可以用setTimeOut(fn,0)代替$nextTick。当我们需要在当前事件循环结束后,等待浏览器完成UI渲染后再执行回调函数时,可以使用0延时。这样可以确保回调在界面更新之后执行,以避免阻塞UI线程。

16.vue3/vue2自定义指令定义

Vue2
注册全局自定义指令
main.js

import Vue from 'vue'
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

注册局部指令

<template>
  ...
</template>
 
<script>
export default {
  data() {
    return {};
  },
  directives: {
    focus: {
      // 指令的定义
      inserted: function (el) {
        el.focus();
      },
    },
  },
};
</script>

使用自定义指令

<template>
  <input v-model="value"  v-focus>
</template>

Vue3
注册全局自定义指令
简化形式
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:

<div v-color="color"></div>

main.js

const app = createApp({})
 
app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

非简化形式

const app = createApp({})
 
// 使 v-focus 在所有组件中都可用
app.directive('color', {
   mounted(el, binding, vnode) {
        el.style.color = binding.value
    },
})

注册局部指令

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>
 
<template>
  <input v-focus />
</template>

17.自定义命令vue2、vue3的生命周期对照

在这里插入图片描述

18.什么是虚拟DOM?为什么使用虚拟DOM?

1.虚拟DOM,用普通js对象来描述DOM结构,因为不是真实DOM,所以称之为虚拟DOM。

2.使用虚拟DOM原因:在react,vue等技术出现之前,改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom ,这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom 树,如果建立一个与 dom 树对应的虚拟 dom 对象( js 对象),以对象嵌套的方式来表示 dom 树及其层级结构,那么每次 dom 的更改就变成了对 js 对象的属性的增删改查,这样一来查找 js 对象的属性变化要比查询 dom 树的性能开销小。

创建真实DOM成本比较高,而如果用js对象来描述一个dom节点,成本比较低,另外我们在频繁操作dom是一种比较大的开销。所以建议用虚拟dom来描述真实dom。

3.为什么操作真实DOM的成本比较高
(1) dom 树的实现模块和 js 模块是分开的这些跨模块的通讯增加了成本

(2) dom 操作引起的浏览器的回流和重绘,使得性能开销巨大。

原本在 pc 端是没有性能问题的,因为pc端的计算能力强,但是随着移动端的发展,越来越多的网页在智能手机上运行,而手机的性能参差不齐,会有性能问题。
我们之前用jquery在pc端写那些商城页面都没有问题,但放到移动端浏览器访问之后会发现除了首页会出现白屏之外在其他页面的操作并不流畅。

19.keepAlive实现原理–讲讲源码?

keep-alive 的本质是缓存管理和特殊的挂载/卸载逻辑。keep-alive 组件的实现需要渲染器层面的支持。这是因为被 keep-alive 的组件在卸载的时候,渲染器并不会真的将其卸载,而是会将该组件搬运到一个隐藏的容器中,实现 “假卸载”,从而使得组件可以维持当前状态。当被 keep-alive 的组件再次被 “挂载” 时,渲染器也不会真的挂载它,而是将它从隐藏容器中搬运到原容器。
通过 keep-alive 组件插槽,获取第一个子节点。根据 include、exclude 判断是否需要缓存,通过组件的 key,判断是否命中缓存。利用 LRU 算法,更新缓存以及对应的 keys 数组。根据 max 控制缓存的最大组件数量。

由于组件会先比被它包裹的组件先执行,所以在执行keep-alive组件的render函数时,会把该组件Vnode缓存到自己定义的cache对象中,并将vnode.data.keepAlive设置为true

所以初次渲染的时候isReactivated为false,会跟普通组件一样走正常的init过程。

20.Object.defineProperty的缺点及vue3为什么用proxy?

Object.defineProperty 是 JavaScript 中用于定义或修改对象属性的方法。尽管它在实现数据劫持和响应式编程方面有一定的应用,但也存在一些明显的缺点。

1. 无法检测对象属性的新增或删除
Object.defineProperty 只能劫持对象已有的属性,无法检测到对象属性的新增或删除

const data = { name: '' };
Object.defineProperty(data, 'name', {
enumerable: true,
configurable: true,
get: function() {
console.log('get');
},
set: function(newVal) {
console.log(`大家好,我系${newVal}`);
},
});
data.name = '渣渣辉'; // 输出: 大家好,我系渣渣辉
data.age = 30; // 无法捕捉到新增的属性

2. 无法监听数组变化
Object.defineProperty 不能监听数组的变化,尤其是通过下标方式修改数组数据或使用数组方法(如 push、pop、shift 等)时,无法触发相应的拦截操作。Vue 通过重写数组方法来解决这一问题,但这只是一个临时的解决方案。

let arr = [1, 2, 3];
Object.defineProperty(arr, '0', {
get() {
console.log('get');
return arr[0];
},
set(newVal) {
console.log('set', newVal);
arr[0] = newVal;
},
});
arr[0] = 4; // 输出: set 4
arr.push(5); // 无法捕捉到 push 操作

在 Vue3 中,Proxy 被引入作为替代方案。Proxy 可以完美地监听到任何方式的数据改变,并且不需要对每个属性进行单独处理。然而,Proxy 也有其缺点,即在一些旧版本的浏览器中不被支持。

const obj = { name: 'krry', age: 24 };
const p = new Proxy(obj, {
get(target, key) {
console.log('查看的属性为:' + key);
return Reflect.get(target, key);
},
set(target, key, value) {
console.log('设置的属性为:' + key);
console.log('新的属性:' + key, '值为:' + value);
Reflect.set(target, key, value);
},
});
p.age = 22; // 输出: 设置的属性为:age 新的属性:age 值为:22
console.log(p.age); // 输出: 查看的属性为:age 22

21.vue事件修饰符有哪些?

  1. prevent:阻止默认事件(常用);
  2. stop:阻止事件冒泡(常用);
  3. once:事件只触发一次(常用);
  4. capture:使用事件的捕获模式;
  5. self:只有event.target是当前操作的元素时才触发事件;
  6. passive:修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能@scroll.passive

二、关于Axios

1. 关于axios你知道那些?

axios是一个基于pormise的网络请求库
this. a x i o s . i n t e r c e p t o r s . r e q u e s t . u s e / / 封装请求 t h i s . axios.interceptors.request.use //封装请求 this. axios.interceptors.request.use//封装请求this.axios.interceptors.response.use//响应拦截

三、关于HTTP

1. HTTP与HTTPS的区别?

HTTPS:
是以安全为目标的 HTTP 通道,是 HTTP 的安全版。HTTPS 的安全基础是 SSL。SSL 协议位于 TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持.

区别:
HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
HTTP和HTTPS 使用完全不同的连接方式,所用的端口不同,前者是80 端口,后者是 443端口

2. 常见的HTTP状态码?

200 – 请求成功
301 – 资源(网页等)被永久转移到其它URL
302 – 资源(网页等)被临时转移到其它URL
304 – 自从上次请求后,请求的网页未修改过, 服务器返回此响应时,不会返回网页内容。
400 – 语法错误,服务器无法识别
401 – 请求需要认证
403 – 请求的对应资源禁止被访问
404 – 请求的资源(网页等)不存在
500 – 内部服务器错误
503 – 服务器正忙

3. HTTP1.0和HTTP2.0的区别

  • HTTP2使用的是二进制传送,HTTP1.X是文本(字符串)传送。
    HTTP1.X使用的是明文的文本传送,而HTTP2使用的是二进制传送,二进制传送的单位是帧和流。帧组成了流,同时流还有流ID标示
  • HTTP2支持多路复用
    http1一个连接只能提交一个请求,而http2可以同时处理多个请求,实现多路复用,每个请求是通过流ID去进行标识的。这样http2可以降低连接的占用数量,从而提高网络的吞吐量。
  • HTTP2头部压缩
    http2相对http1通过gzip与compress对头部进行了压缩,且在客户端与服务端各自维护一份头部索引表,后面的传输可以根据索引Id进行头部信息的传输,缩小头部容量,间接提高了传输效率s
  • HTTP2支持服务器推送
    http1是只能从客户端发起,服务器响应的,而http2可以服务端进行推送。

3 什么是跨域?跨域解决方法?

1.什么是跨域:
出于浏览器的同源策略限制,当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
同源策略定义:两个页面具有相同的协议(protocol=>比如http和https),主机(host=>比如www.baidu和blog.net)和端口号(port=>8080和8090)等等、、
2.跨域解决方法:

  • 改发JSONP
  • CORS
  • nginx

四、关于浏览器

1. 浏览器缓存有哪些

注:cache读音音标 /kaef/

  1. HTTP Cache(HTTP 缓存):HTTP缓存是Web开发中最重要的缓存机制之一。HTTP协议支持多种缓存策略(强制缓存,协商缓存),其中 缓存控制 是控制浏览器缓存的最基本、最重要的机制。

强制缓存:通过在HTTP响应头中设置Cache-ControlExpires等相关指令,可以让浏览器有效地缓存资源,直接从本地缓存中读取资源而不发起请求,从而提高响应速度。

协商缓存:通过设置ETagLast-Modified等响应头实现,可以让浏览器发送条件请求,询问服务器是否有更新的资源,如果服务器返回304 Not Modified相应,则表示客户端本地缓存仍然有效,可以直接使用缓存的资源。这两种验证缓存都是检车是否过期的机制,但Last-Modified只能精确到秒,但是我们前端都是精确到毫秒级的,我们应该优先使用Etag。

  1. Service Worker缓存:Service Worker是一种Web Worker,是一种特殊的js脚本,能够拦截网络请求,并根据定义的策略和缓存逻辑,返回缓存中的资源或者向服务器发起未缓存的网络请求。通过使用Service Worker来实现离线缓存,并将资源放到本地
  2. Local Storage(本地存储):Web Storage是HTML5提供的浏览器本地存储API,包含了localStoragesessionStorage两种方式。
  3. Memory Cache(内存缓存):Memory Cache 是浏览器内存中的缓存,它用于存储最近加载的资源,通常是当前会话中的资源。这包括 HTML、CSS、JavaScript 文件以及图片等。内存缓存的读取速度非常快,但它的容量有限,一旦用户关闭浏览器,缓存就会被清空。
  4. Disk Cache(磁盘缓存):Disk Cache 是浏览器磁盘上的缓存,用于存储那些不常变化的资源,例如静态文件和图片。磁盘缓存的容量通常比内存缓存大得多,这使得它可以在浏览器会话之间存储资源。

各缓存间的优先级:
Web Storage缓存=>协商缓存=>强制缓存=>Service Worker缓存,即Service Worker缓存优先级最高

2.浏览器渲染过程

具体的浏览器解析渲染机制如下所示:
浏览器渲染过程包括以下阶段:
1.解析HTML,生成DOM树,解析CSS,生成CSSOM树
2.将DOM树和CSSOM树结合,生成渲染树(Render Tree)
3.Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
4.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
5.Display:将像素发送给GPU,展示在页面上

五、关于Promise

1. 简单说一下Promise,谈谈你对Promise的理解

Promise是ES6新增的语法,是一种异步编程的一种解决方案,Promise本质上是一个绑定了回调的对象。 Promise在一定程度上解决了回调函数的书写结构问题,解决了回调地狱的问题。Promise可以看作是一个状态机,它有三种状态:pending,fulfilled,rejected,其中初始状态是pending,可以通过函数resolve(表示成功)把状态变为fulfilled,或者通过函数reject(表示失败)把状态变为rejected,状态一经改变就不能再次变化。

2.Promise有哪些方法

三个实例方法: then/catch/finally
六个静态方法: resolve/reject/all/allSettled/race/any

  1. then :方法是整个 Promise 解决的核心内容,同时因为回调函数和返回一个新的 Promise 实例
  2. catch:如果上面没有定义 reject 方法或者在抛出错误,那么所有的异常会走向 catch 方法,而 catch 可以复用 then 方法。
  3. finally :不管是 resolve 还是 reject 都会调用 finally 。那么相当于 fianlly 方法替使用者分别调用了一次 then 的 resolved 和 rejected 状态回调。
  4. Promise.all:Promise.all() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。所有参数数组 Promise 实例执行 resolve 回调后,新实例执行 resolve 回调;如果中间有任何一个 Promise 实例执行 reject 回调,那么新实例就直接执行 reject 回调了。
  5. Promise.race:返回最快完成那一个 Promise 实例。只要参数数组中有一个 Promise 实例执行 resolve 回调或 reject 回调后,新实例就直接返回结果。
  6. Promise.allSettled:只有等到参数数组的所有 Promise 实例都发生状态变更,返回的 Promise 实例才会发生状态变更,无论是执行 resolve 回调还是 reject 回调的状态。
  7. Promise.any:返回任意一个最快执行 resolve 回调的 Promise 实例。
  8. Promise.resolve:返回一个以给定值解析后的 Promise 实例。相当于执行 then 方法里面的 _resolvePromise。
  9. Promise.reject:返回一个带有拒绝原因的 Promise 实例。

六、关于CSS

1. 什么是BFC ?

BFC(块级格式化上下文)是指浏览器中创建了一个独立的渲染区域,BFC可以让元素成为隔离独立的容器,且容器内的子元素不会影响到外面的布局。

2. BFC 有什么用?

  1. 防止外边距重叠
    BFC导致的属于同一个BFC中的子元素的margin重叠。(Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠)
  2. 清除浮动的影响
    块级子元素浮动,如果块级父元素没有设置高度,其会有高度塌陷的情况发生。
    原因:子元素浮动后,均开启了BFC,父元素不会被子元素撑开。
    解决方法:由第六条原理得,计算BFC的高度时,浮动元素也参与计算。所以只要将父容器设置为BFC,就可以把子元素包含进去:这个容器将包含浮动的子元素,它的高度将扩展到可以包含它的子元素,在这个BFC4
  3. 防止文字环绕

3. 如何创建 BFC ?

  1. (子)float:left/right
  2. (子)position:absolute/fixed。
  3. (子)display:inline-block;
  4. (父)display:flex;
  5. (父)overflow:hidden/scroll/auto

4. 垂直居中方法(至少四种)?

  1. position定位+transform:translate()
.box{
	position:absolute;
    width:100px
    height:100px;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);

}
  1. text-align + line-height实现行内元素垂直居中
.box{
    height:100px;
    text-align:center;
    line-height:100px;
}
  1. 定位top、left、right、bottom:0+margin: auto
.box{
    position:absolute;
    width:100px;
    height:100px;
    top:0;
    right:0;
    bottom:0;
    left:0; 
    margin: auto;
}
  1. flex布局,justify和align
.box{
    display:flex;
    justify-content:center;
    align-items:center;
}
  1. flex布局加上margin
.fatherbox{
    display:flex;
}
.box{
    margin:auto;
    width:100px;
    height:100px;
}

5. 盒模型

盒子模型分为两种:

  1. W3C 标准的盒子模型(标准盒模型)
  2. IE 标准的盒子模型(怪异盒模型)

标准盒模型与怪异盒模型的表现效果的区别之处:

  1. 标准盒模型中 width 指的是内容区域 content 的宽度
    height 指的是内容区域 content 的高度。标准盒模型下盒子的大小 = content + border + padding + margin
  2. 怪异盒模型中的 width 指的是内容、边框、内边距总的宽度(content + border +padding);height 指的是内容、边框、内边距总的高度。怪异盒模型下盒子的大小=width(content + border + padding) + margin

6. CSS选择器的优先级及CSS权重如何计算?

!important>行内样式>ID 选择器>类选择器>标签>通配符>继承>浏览器默认属性

7. CSS 单位中 px、em 和 rem 的区别?

  1. px 像素(Pixel),绝对单位。像素 px 是相对于显示器屏幕分辨率而言的,是一个虚拟长度单位,是计算机系统的数字化图像长度单位。
  2. em 是相对长度单位,相对于当前对象内文本的字体尺寸。如当前对行内文本的 体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。它会继承父级元素的字体大小(也就是相对于父级元素) 因此并不是一个固定的值。
  3. rem是 CSS3 新增的一个相对单位(root em,根 em),使用 rem 为元素设定字体大小时,仍然是相对大小,但相对的只是 HTML 根元素

区别:
IE 无法调整那些使用 px 作为单位的字体大小,而 em 和 rem 可以缩放,rem 相对的只是 HTML 根元素。这个单位可谓集相对大小和绝对大小的优点于一身,通 过它 既可 以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐 层复合的连锁反应。目前,除了 IE8 及更早版本外,所有浏览器均已支持 rem。

8. 使用 CSS 怎么让 Chrome 支持小于 12px 的文字比如 10px?

针对谷歌浏览器内核,加 webkit 前缀,用 transform:scale() 这个属性进行缩放!

9. 谈谈你对重绘和回流(重排)的理解?

  1. 重排(重新排列)/ 回流: 布局引擎会根据所有的样式计算出盒模型在页面上的位置和大小。
    常见的重排因素(元素宽高变化,display:none,节点内容发生变化,浏览器窗口发生变化,内容改变等都会发生重排—重排必重绘
  2. 重绘(重新绘制): 计算好盒模型的位置、大小和其他一些属性之后,浏览器会根据每个盒模型的特性进行绘制。
    常见的重绘的因素(颜色,透明度,transform)

10. 三栏布局的实现,三栏布局的实现:左右固定宽度,中间自适。请至少说出两种方式?

  1. 方法一:使用 CSS Flexbox
    Flexbox 是一种非常灵活的布局方式,可以轻松实现三栏布局,尤其是在需要自适应和响应式设计时,Flexbox 提供了极大的便利。
<div class="container">
    <div class="left-column">
        <!-- 左侧固定栏 -->
        <p>左侧内容</p>
    </div>
    <div class="center-column">
        <!-- 中间自适应内容 -->
        <p>中间内容</p>
    </div>
    <div class="right-column">
        <!-- 右侧固定栏 -->
        <p>右侧内容</p>
    </div>
</div>
.container {
    display: flex;
    justify-content: space-between; /* 分布子元素 */
}

.left-column, .right-column {
    width: 200px;  /* 左右栏固定宽度 */
    background-color: #f4f4f4;
}

.center-column {
    flex: 1;  /* 中间内容区域自适应填满剩余空间 */
    background-color: #e0e0e0;
}

核心代码是display:flex;jusitify-content:space-between

  1. 使用 CSS Grid
    CSS Grid 是另一种现代的布局方案,尤其适用于复杂的布局结构。通过 grid-template-columns 属性,可以非常方便地实现三栏布局。
<div class="container">
    <div class="left-column">
        <!-- 左侧固定栏 -->
        <p>左侧内容</p>
    </div>
    <div class="center-column">
        <!-- 中间自适应内容 -->
        <p>中间内容</p>
    </div>
    <div class="right-column">
        <!-- 右侧固定栏 -->
        <p>右侧内容</p>
    </div>
</div>

.container {
    display: grid;
    grid-template-columns: 200px 1fr 200px;  /* 左右栏固定宽度,中间栏自适应 */
}

.left-column, .right-column {
    background-color: #f4f4f4;
}

.center-column {
    background-color: #e0e0e0;
}

核心代码是display:grid;grid-template-columns:200px 1fr 200px

六、JS(重中之重!)

0. js的数据类型和引用类型

基本类型: null,undefined,boolean,number,string,symbol
引用类型:Object, Array ,Function, Date, RegExp等

//这里很多人会忘记引用类型是那些

1. 关于深浅拷贝你知道那些,有没有手写过深拷贝的一些方法?

深拷贝(深拷贝后的对象与原来的对象是完全隔离的,互不影响)
:JSON.parse(JSON.stringify(obj)),递归函数

浅拷贝(当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。)
:for in、 Object.assign、 扩展运算符 … 、Array.prototype.slice()、Array.prototype.concat()

手写深拷贝方法:

  • JSON.parse(JSON.stringify(obj))
    但是注意一些缺陷:
    对象的属性值是函数时,无法拷贝。
    原型链上的属性无法拷贝
    不能正确的处理 Date 类型的数据
    不能处理 RegExp
    会忽略 symbol
    会忽略 undefined
  • 递归函数,比如自己封装一个deepClone函数用于深拷贝
 function deepClone(obj){
 	// obj = null 并且不是对象或数组的时候直接返回为空
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    }
	let newObj = obj instanceof Array ? [] : {}
	for(let k in obj){
		if(typeof obj[k]==”object”){
			newObj[k]=deepClone(obj[k])
		}else{
			newObj[k]=obj[K]
		}
	}
	return newObj
}
  1. lodash工具。使用lodash工具中cloneDeep方法实现深拷贝
  • 在组件中导入loadsh,默认使用下划线(_)命名
1 // 导入lodash
2 import _ from 'lodash'
  • 使用 cloneDeep 方法对数据进行深拷贝
1 // loodash.cloneDeep(obj)深拷贝
2 const form = _.cloneDeep(this.addForm)//使用cloneDeep方法实现深拷贝
3 form.goods_cat = form.goods_cat.join(',')//数组转字符串

2. 多维数组/对象,扁平化方法

对于数组:

  1. 递归函数
function getAtter(arr){
let newArr=[]
	function toArr(newArr){
		arr.forEach(item=>{
			item.instenseof Array? toArr(item):newArr.push(item)
		)}
	}
	toArr()
	rerturn neArr
}
  1. flat()函数
    Array.prototype.flat()
    //flat函数的参数,不传或者参数值值为1返回只展开一层后的数组;其余则返回参数值层后展开的数组;如果参数值为Infinity,则展开任意深度的数组,最终返回扁平后的一维数组。
let arr1 = [1,2,['a','b',['中','文',[1,2,3,[11,21,31]]]],3];
//均扁平一次
console.log( arr1.flat() ); //[ 1, 2, 'a', 'b', [ '中', '文', [ 1, 2, 3, [Array] ] ], 3 ]
console.log( arr1.flat(1)); //[ 1, 2, 'a', 'b', [ '中', '文', [ 1, 2, 3, [Array] ] ], 3 ]
 
//扁平两次
 console.log( arr1.flat(2) );//[ 1, 2, 'a', 'b', '中', '文', [ 1, 2, 3, [ 11, 21, 31 ] ], 3 ]
 
 //使用 Infinity,可展开任意深度的嵌套数组
 console.log( arr1.flat( Infinity ) );//[ 1,2, 'a', 'b', '中','文', 1,  2, 3, 11,21, 31,3]

对于对象,则是要灵活使用object方法,如Object.entries获取对象的[key,value]数组,遍历这个数组,进行进一步的判断(判断遍历的值的类型,如果是对象类型就递归调用,如果是普通类型就赋值给对象)。

function flat(item, preKey = "", res = {}) {
  Object.entries(item).forEach(([key, val]) => {
    if (val && typeof val === "object") {
      flat(val, preKey + key + ".", res);
    } else {
      res[preKey + key] = val;
    }
  });
  return res;
}

// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } };
console.log(flat(source));

//输出
//{a.b.c:1,a.b.d:2,a.e:3,f.g:2}

3. js事件循环原理

而js任务包含了同步任务和异步任务

浏览器事件循环
1.浏览器会率先执行同步代码,将异步代码放入消息队列(任务队列)中,待主线程任务完成后执行
2.而异步代码又分为宏任务和微任务,在同步代码全部执行完成后会先执行异步微任务再执行异步宏任务,如果异步任务中仍有异步任务,会继续放入消息队列(任务队列),以此类推,便形成了一个事件循环。(另外还需要分清楚事件循环是问的浏览器还是node的,两者不一样)
在NodeJS中使用libuv实现了Event Loop。

宏任务(macro task): 宏任务是由宿主环境(浏览器、Node)发起的,常见宏任务如下
setTimeout()
setInterval()
setImmediate()(Node.js 环境)
script( 整体代码)
I/O
UI 交互事件
特点:
(1) 不唯一,存在一定的优先级(用户I/O部分优先级更高)
(2) 异步执行,同一事件循环中,只执行一个

微任务(micro task): 微任务是由JS发起的任务,常见微任务如下:
promise.then()
promise.catch()
new MutaionObserver()
object.observe
Async/Await
process.nextTick()(Node.js 环境)
注:promise本身同步,then/catch的回调函数是异步的
特点:
(1) 唯一,整个事件循环当中,仅存在一个;
(2) 执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕;

4. 有关事件循环的代码,让你说出执行顺序

  1. 例题1
new Promise(resolve => {
  console.log('promise');
  resolve(5);
}).then(value=>{
  console.log('then回调', value)
})
function func1() {
  console.log('func1');
}
setTimeout(() => {
  console.log('setTimeout');
});
func1();
答案 :
promise
func1
then回调5
setTimeout
  1. 例题2
setTimeout(function () {
  console.log("set1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});
new Promise(function (resolve) {
  console.log("pr1");
  resolve();
}).then(function () {
  console.log("then1");
});
 
setTimeout(function () {
  console.log("set2");
});
 
console.log(2);
 
queueMicrotask(() => {
  console.log("queueMicrotask1")
});
 
new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});
答案:
pr1
2
then1
queueMicrotask1
then3
set1
then2
then4
set2
  1. 例题3
例题3
async function async1 () {
  console.log('async1 start')
  await async2();
  console.log('async1 end')
}
 
async function async2 () {
  console.log('async2')
}
 
console.log('script start')
 
setTimeout(function () {
  console.log('setTimeout')
}, 0)
 
async1();
 
new Promise (function (resolve) {
  console.log('promise1')
  resolve();
}).then (function () {
  console.log('promise2')
})
 
console.log('script end')
 
 
结果
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout3


//解析
先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务

1.从上往下执行代码,先执行同步代码,输出 script start
2.遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
3.执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
4.接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数
5.接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
6.依次执行微任务中的代码,依次输出 async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout

例题4:

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 0)
            resolve(2)
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}
 
 
p().then((res) => {
    console.log(res);
})
console.log('end');

//解析:
1.执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3
2.遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end
3.同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4
4.微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时 1不会输出。

//输出
3 
end 
2
4

将上述代码中的 resolve(2)注释掉, 此时 1才会输出,输出结果为 3 end 4 1

5. js事件捕获和冒泡

js事件流
js中事件执行的整个过程称之为事件流,分为三个阶段:事件捕获阶段,处于目标阶段、事件冒泡阶段。

事件捕获:当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。
处于目标阶段:传播到事件触发处,触发注册的事件。
事件冒泡阶段:与事件捕获阶段相反,由内到外进行事件传播,直到根节点。

6. 什么是堆(heap)和栈(stack)内存

1、堆和栈的概念
在JS钟变量都存放在内存中,而内存给变量开辟了两块区域,分别为堆区域和栈区域

堆(heap):是堆内存的简称,堆是动态分配内存,内存大小不固定,也不会自动释放,堆数据结构是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。
栈(stack):是栈内存的简称,栈是自动分配相对固定大小的内存空间,并由系统自动释放,栈数据结构遵循FILO(first in last out)先进后出的原则
2、数据类型
在JS中说到堆和栈就离不开普通数据类型和引用数据类型。
在JS中普通数据类型它是在栈内存在创建的,而引用数据类型则是在堆内存中创建的。
基本类型:采用的是值传递。
引用类型:则是地址传递。

7. 防抖和节流

相同点:防抖(Debounce)和节流(Throttle)都是用来控制某个函数在一定时间内触发次数,两者都是为了减少触发频率,以便提高性能以及避免资源浪费
不同点:节流是第一个说了算,后续都会被节流阀屏蔽掉,防抖是最后一个说了算,前面启用的都会被清除

防抖:防止重复触发事件。用户在短时间内频繁触发事件时,定时器会不断清空,直到指定时间后才执行回调函数,
所以在用户在频繁触发事件过程中,只会执行一次回调函数。
应用场景:搜索,表单提交等

function debounce(func, interval) {
    let timer;
    return function() {
	timer && clearTimeout(timer)
      let args = arguments;
      timer = setTimeout(() => {
        func.apply(this, args)
        timer = null;
      }, interval)
    }
  }

节流:在规定时间内只执行一次,也就是把在定时器事件结束时销毁定时器,执行回调函数
应用场景:onscroll滚动,鼠标的跟随动画实现,scroll,resize, touchmove, mousemove等极易持续性促发事件

// 定时器方式

function throttle1(func, interval) {
  let sign = true;
  return function() {
    // 在函数开头判断标志是否为 true,不为 true 则中断函数
    if (!sign) return;
    //  sign 设置为 false,防止执行之前再被执行
    sign = false;
    setTimeout(() => {
      func.apply(this, arguments)
      // 执行完事件之后,重新将这个标志设置为 true
      sign = true;
    }, interval)
  }
}

8. 变量提升和函数提升总结(包括立即执行函数)

函数提升会放在变量提升之前,变量提升和函数提升都只是提升它的声明,不会提升赋值和函数调用语句。比如

a = 1;
fn();  // 函数调用
var a = 2;  // 变量声明+赋值
function() {  // 函数声明
    console.log(3);
}
 
// 上面代码真实执行顺序
function fn() {  // 函数声明
    console.log(3)
}
var a;   // 变量声明,a = undefined
a = 1;   // a = 1
fn();    // 函数调用,打印出3
a = 2;   // 变量赋值,a = 2

1. 变量提升
只有var声明的变量提升能被成功执行,let和const虽然有提升,但是存在暂时性死区,不能被成功执行
2. 函数提升
在js中,函数分为:
函数声明:function fn(){}
函数表达式(字面量):let fn = function(){}
立即执行函数(匿名函数,2中的也是):(function(){})()

其中只有函数声明即function fn(){}这种形式才会进行函数提升。另外两种在代码中都不会被提升。当然,通过上面这些方式声明的函数的内部也都存在变量和函数提升。比如:

function fn() {
 var a=b=2
 console.log(a)
 console.log(b)
}
fn()

//那么其中a=undefind,b=2, 因为a在function内,是局部变量;b是全局变量
//因为变量提升实际转为如下形式
function fn(){
	var a
	a=b
	b=2
}
fn()

另外注意:
对于同名的变量声明,Javascript采用的是忽略原则。后声明的变量声明会被忽略。
对于同名的函数声明,Javascript采用的是覆盖原则。

经典例题

// a
function Foo () {
 getName = function () {
   console.log(1);
 }
 return this;
}
// b
Foo.getName = function () {
 console.log(2);
}
// c
Foo.prototype.getName = function () {
 console.log(3);
}
// d
var getName = function () {
 console.log(4);
}
// e
function getName () {
 console.log(5);
}

Foo.getName();           // 2
getName();               // 4
Foo().getName();         // 1
getName();               // 1 
new Foo.getName();       // 2
new Foo().getName();     // 3
new new Foo().getName(); // 3

解析:

**Foo.getName(),**Foo为一个函数对象,对象都可以有属性,b 处定义Foo的getName属性为函数,输出2;
**getName(),**这里看d、e处,d为函数表达式,e为函数声明,两者区别在于变量提升,函数声明的 5 会被后边函数表达式的 4 覆盖;
**Foo().getName(),**这里要看a处,在Foo内部将全局的getName重新赋值为 console.log(1) 的函数,执行Foo()返回 this,这个this指向window,Foo().getName() 即为window.getName(),输出 1;
**getName(),**上面3中,全局的getName已经被重新赋值,所以这里依然输出 1;
**new Foo.getName(),**这里等价于 new (Foo.getName()),先执行 Foo.getName(),输出 2,然后new一个实例;
**new Foo().getName(),**这里等价于 (new Foo()).getName(), 先new一个Foo的实例,再执行这个实例的getName方法,但是这个实例本身没有这个方法,所以去原型链__protot__上边找,实例.protot === Foo.prototype,所以输出 3;
**new new Foo().getName(),**这里等价于new (new Foo().getName()),如上述6,先输出 3,然后new 一个 new Foo().getName() 的实例。

9.new关键字都做了什么

1.创建一个空对象:new 操作符会创建一个空对象,这个对象会继承自构造函数的原型对象。

2.设置原型链关系:新创建的对象的 _ Proto _ 属性会被设置为构造函数的 prototype 属性,从而建立起对象与构造函数原型之间的链接。

3.绑定 this 指向:new 操作符会将构造函数内部的 this 关键字绑定到新创建的对象上,使构造函数内部的代码可以访问和操作该对象的属性和方法。

4.执行构造函数代码:new 操作符会调用构造函数,并传入任何参数。构造函数内部的代码会被执行,可以用来初始化对象的属性和方法。

5.返回新对象:如果构造函数没有显式地返回其他对象,那么 new 操作符会隐式地返回新创建的对象实例;否则,如果构造函数返回了一个非原始值的对象,则该对象会成为 new 表达式的结果,而新创建的对象实例会被丢弃。

function Foo(name) { 
	this.name = name; 
	return this; 
} 
var obj = {}; 
obj.__proto__ = Foo.prototype; // Foo.call(obj, 'mm');
var foo = Foo.call(obj, 'mm');
console.log(foo);

10.爬楼梯(斐波那契数列)经典例题

  1. 爬楼梯问题
    题目描述: 爬楼梯需要 n (n为正整数)阶才能到达楼顶。每次可以爬 1 或 2 个台阶。求有多少种不同的方法可以爬到楼顶。
    思路:
    1、分析题意不难发现:
    爬第 n 阶楼梯的方法数量等于 2 部分之和
    即爬上 n-1 阶楼梯和爬上 n-2 阶楼梯的方法数量之和
    2、定义容器数组存放爬第 n 阶楼梯的方法数量数组
    (其实该数组元素为斐波那契数列)
    题解:
/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    let fibArr = [];
    fibArr[0] = fibArr[1] = 1;
    for(let i = 2; i <= n; i++) {
        fibArr[i] = fibArr[i - 1] + fibArr[i - 2];
    }
    return fibArr[n];
};

11.隐式转换原理,和各类型转换相加结果,和总结

js隐式类型转换
JavaScript的数据类型非常弱(弱类型语言)在使用算术运算符时,运算符两边的数据类型可以任意,比如,一个字符串可以和数字相加。之所以不同的数据类型之间可以做运算,是因为JavaScript 引擎在运算之前会悄悄的把他们进行了隐式类型转换。
注意:
1)隐式转换在调用非函数,或者读取 null 或者 undefined 的属性时,会报错

"hello"(1); // Uncaught TypeError: "hello" is not a function
null.x; // Uncaught TypeError: Cannot read property 'x' of null

2)隐式类型转换,某些情况下,会隐藏一些错误,比如,
null 会转换成 0,
undefined 会转换成 NaN。
需要注意的是,NaN 和 NaN 不相等(这是由于浮点数的精度决定的),如下:

let x = NaN;
x === NaN; // false

下面各类型的运算中的隐式转换,
类型相同:
基本类型,直接比较值
引用类型,比较指针

类型不同:
如果两边类型不同,则两边都尝试转成number类型。
对于引用类型,先调用valueOf(),如果能转成数字,则进行比较。
不能转成数字就调用toString()方法转成字符串。**

  • Boolean+Number:true会被转化为1,false为0
5 + true // 6
  • String+Number:字符串和数字相加,JavaScript 会自动把数字转换成字符的,不管数字在前还是字符串在前
"2" + 3; // "23"
2 + "3"; // "23"

//需要注意:+ 的运算方向从左到右
1 + "2" + 3; // "123"
1 + 2 + "3"; // "33"
  • Boolean + String
true+"测试"; // "true测试"
  • null + String
null+"1" // null1
  • undefined + String
undefined+"1" // undefined1
  • Array + String
[1,2,3]+"测试" // 1,2,3测试
  • Object + String
{name:"田本初"}+"测试" //[object Object]测试
  • Function + String
const fn = () => {
    console.log("111")
}
console.log(fn+"测试")
// () => {
//    console.log("111")
// }测试

隐式转化总结:

  • 类型错误有可能会被类型转换所隐藏。
  • “+”既可以表示字符串连接,又可以表示算术加,这取决于它的操作数,如果有一个为字符串的,那么,就是字符串连接了。
  • 对象通过valueOf方法,把自己转换成数字,通过toString方法,把自己转换成字符串。
  • 具有valueOf方法的对象,应该定义一个相应的toString方法,用来返回相等的数字的字符串形式。
  • 检测一些未定义的变量时,应该使用typeOf或者与undefined作比较,而不应该直接用真值运算。

12.隐式转换中的NaN判定

当算术运算返回一个未定义的或无法表示的值时,NaN就产生了。比如:undefined+number返回NaN。但是,NaN并不一定用于表示某些值超出表示范围的情况。将某些不能强制转换为数值的非数值转换为数值的时候,也会得到NaN。例如,0 除以0会返回NaN —— 但是其他数除以0则不会返回NaN。

JavaScript提供了isNaN方法来检测某个值是否为NaN,但是,这也不太精确的,因为,在调用isNaN函数之前,本身就存在了一个隐式转换的过程,它会把那些原本不是NaN的值转换成NaN

isNaN("foo"); // true
isNaN(undefined); // true
isNaN({}); // true
isNaN({ valueOf: "foo" }); // true

另外两个NaN不能用等号判定是否相等

console.log(NaN==NaN)  //false
console.log(NaN===NaN) //false

有一种可靠的并且准确的方法可以检测NaN,就是用 !== 判断是否等于自身

let a = NaN;
a !== a; // true

13.隐式转换经典例题

1.如何使a==1

&&a==2

&&a==3
解题思路:把a看成一个对象,对象a跟数字类型比较的时候,会进行隐式转换,这里我们重写了它的toString方法,使其每比较一次返回的结果就加1

var a={
    num:1,
    toString:function(){
        return a.num++;
    }
}
if(a==1&&a==2&&a==3){
    console.log("哈哈");
}else{
    console.log("嘻嘻!");
}
 
//=> "哈哈"

2.如何使a===1

&&a===2

&&a===3
解题思路:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。它会控制对象属性值的getter和setter。

let num=1;
Object.defineProperty(window,'a',{
    get:function(){
        return num++;
    }
})
if(a===1&&a===2&&a===3){
    console.log("哈哈!");
}else{
    console.log("嘻嘻!");
}
/=> "哈哈"

14. 强制类型转换 - “真值运算” (if, ||, &&)

强制类型转换,我们常常称之为“真值运算”,比如,if, ||, &&,他们的操作数不一定是布尔型。
JavaScript会通过简单的转换规则,将一些非布尔类型的值转换成布尔型的值。

false, 0, -0, “”, NaN, null, undefine强制类型转换时会被转为false

15. 为什么0.1+0.2 !==0.3?如何实现等于0.3?

0.1+0.2 !==0.3的原因=>计算机的二进制存储
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100…(1100循环),0.2的二进制是:0.00110011001100…(1100循环),这两个数的二进制都是无限循环的数,二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的舍去,遵从“0舍1入”的原则。(在js中数字类型超过16位就会造成精度丢失

根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004

let n1 = 0.1, n2 = 0.2
let n3 = n1 + n2   //0.30000000000000004

//所以一般我们计算都是toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。
n3.toFixed(1) //0.3

解决方案
对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,为我们提供了Number.EPSILON属性,而它的值就是2-52,只要我们判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3

function numberepsilon(arg1,arg2){                   
  return Math.abs(arg1 - arg2) < Number.EPSILON;        
}        
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true

16. js改变原数组和不改变原数组的操作方法

改变原数组:

  1. push():push() 方法可把它的参数顺序添加到 arrayObject 的尾部。它直接修改 arrayObject,而不是创建一个新的数组,arrayObject.push(newelement1,newelement2,….,newelementX)
  2. pop(): 删除 arrayObject 的最后一个元素,把数组长度减 1,并且返回它删除的元素的值。如果数组已经为空,则 pop() 不 改变数组,并返回 undefined 值。
let arr = [2,3]
let rs = arr.pop()
console.log(rs) //3
console.log(arr) // [2]
  1. unshift():unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。
let arr = [2,3]
let rs = arr.unshift(7, 9, 10)
console.log(rs)  // 5
console.log(arr)  // [ 7, 9, 10, 2, 3 ]
  1. shift():从数组的第一个元素从其中删除,并返回第一个元素的值,如果数组是空的,那么 shift() 方法将不进行任何操作。
let arr = [2,3]
let rs = arr.shift() 
console.log(rs) //2
console.log(arr) // [3]
  1. reverse():该方法会改变原来的数组,而不会创建新的数组。arrayObject.reverse()
let arr = [2,3]
let rs = arr.reverse()
console.log(rs) //[ 3, 2 ]
console.log(arr) //[ 3, 2 ]
  1. sort():对数组的引用。请注意,数组在原数组上进行排序,不生成副本。(默认按升序排列)
arr.sort((a, b) => a - b); //升序
arr.sort((a, b) => b - a); //降序
  1. splice():splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组,另外splice可以用于新增数组,并且能页面同步渲染。
splice(index,num,obj) 
//index:必需。规定从何处添加/删除元素。
//num:可选。规定应该删除多少元素。必须是数字,但可以是 "0"。
//obj:可选。要添加到数组的新元素
let arr=[0,1,2]
arr.splice(0,1) //[1,2]:删除一个元素
arr.splice(0,1,3)//[3,2,1]:删除并在该处添加一个元素
arr.splice(0,0,3)//[3,0,1,2]:在该处添加一个元素
arr.splice(0) //[]:这会把原数组清空
arr.splice(1) //[0]:原数组变成,保留一个元素(从左到右)的数组
arr.splice(2) //[0,1]:原数组变成,保留两个元素(从左到右)的数组
arr.splice(3) //[0,1,2]:原数组变成,保留三个元素(从左到右)的数组

不会改变原数组:

  1. concat():用于连接两个或多个数组,仅会返回被连接数组的一个新数组
let arr = [2,3]
let rs = arr.concat([7, 8, 0])
console.log(rs)     // [ 2, 3, 7, 8, 0 ]
console.log(arr)    // [ 2, 3 ]
  1. join():返回一个字符串。该字符串是通过把 arrayObject 的每个元素转换为字符串,然后把这些字符串连接起来
let arr = [2,3]
let rs = arr.join('-')
console.log(rs)         // 2-3
console.log(arr)        // [ 2, 3 ]
  1. slice():slice()方法返回一个索引和另一个索引之间的字符串。
slice(index1,index2)
//index1:起始索引
//index2:结束索引

let arr=[0,1,2] 
let rs=arr.slice(0,1)
console.log(arr) //[0,1,2]
console.log(rs) //[0]

//如果不传结束索引,开始索引为负数,那么slice会从数组末尾获取元素,索引值找不到对应元素,则返回空数组
console.log(arr.slice(-1)) // [ 3 ]
console.log(arr.slice(-2)) // [ 2, 3 ]
console.log(arr.slice(-3)) // [ 1, 2, 3 ]

//如果不传结束索引,开始索引为正数,那么slice会从数组头部获取元素,索引值找不到对应元素,则返回空数组
console.log(arr.slice(1)) // [ 2, 3 ]
console.log(arr.slice(2)) // [ 3 ]
console.log(arr.slice(3)) // [ ]

//索引为0,返回新的一个跟元素组相同的数组
console.log(arr.slice(0)) // [] 
  1. filter() 方法创建一个新的数组,新数组中的元素是通过指定数组中符合的条件筛选出来的。filter() 不会对空数组进行检测。 filter() 不会改变原始数组。
// array.filter(function(currentValue,index,arr), thisValue)
//function(currentValue, index,arr):必需。函数,数组中的每个元素都会执行这个函数。
//currentValue:必需。当前元素的值
//index:可选。当前元素的索引值
//arr:可选。当前元素属于的数组对象
//thisValue:可选。对象作为该执行回调时使用,传递给函数,用作 "this" 的值。
//如果省略了 thisValue ,"this" 的值为 "undefined"

let arr=[0,1,2] 
let rs=arr.filter(item=>item>1)
let rx=arr.filter(item=>item>3)

console.log(arr) //[0,1,2]
console.log(rs) //[2]
console.log(rx) //[]
  1. reduce:reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
//function(total, currentValue, currentIndex, arr):必需。函数,数组中的每个元素都会执行这个函数。
//total:必需。初始值, 或者计算结束后的返回值。
//currentValue:必需。当前元素
//currentIndex:可选。当前元素的索引
//arr:可选。当前元素所属的数组对象。
//initialValue:可选。传递给函数的初始值。

let arr=[] 
let rs=arr.reduce((prev,cur,index,arr)=>{
  return prev+cur
})
let rx=arr.reduce((prev,cur,index,arr)=>{
  return prev+cur
},0)

console.log(arr) //[]
console.log(rs) //Reduce of empty array with no initial value
console.log(rx) //[]0

//所以设置初始值(initialValue)更加安全。
//其次如果初始值设置0,则参数index是从0开始的,不传初始值则index从1开始

reduce有很多其他的高级用法,如:
1)计算数组中每个元素出现的次数:

let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

let nameNum = names.reduce((pre,cur)=>{
  if(cur in pre){
    pre[cur]++
  }else{
    pre[cur] = 1 
  }
  return pre
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}

2)数组去重:

let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
    if(!pre.includes(cur)){
      return pre.concat(cur)
    }else{
      return pre
    }
},[])
console.log(newArr);// [1, 2, 3, 4]

3)将多维数组转化为一维:

let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
   return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]

4)对象里的属性求和:

var result = [
    {
        subject: 'math',
        score: 10
    },
    {
        subject: 'chinese',
        score: 20
    },
    {
        subject: 'english',
        score: 30
    }
];

var sum = result.reduce(function(prev, cur) {
    return cur.score + prev;
}, 0);
console.log(sum) //60
  1. find:find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
let arr = [1,2,3]
let rs = arr.find(item=>item>4)
console.log(rs)         // undefind
console.log(arr)        // [ 1,2, 3 ]
  1. findIndex: findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。
let arr = [1,2,2,2,2]
let rs = arr.findIndex(item=>item==2)
let rx = arr.findIndex(item=>item>2)
console.log(rs)         // 1
console.log(rx)         // -1
console.log(arr)        // [1,2,2,2,2]
  1. forEach:forEach()遍历数组、集合的一种方法,他不会返回新数组,也不会改变原数组。使用break不能跳出循环且会报错,return也无法跳出循环但不报错
// array.forEach(callbackFn(currentValue, index, arr), thisValue)
//currentValue	必需。当前元素
//index	可选。当前元素的索引值。
//arr	可选。当前元素所属的数组对象。
//thisValue	可选。传递给函数的值一般用 "this" 值。
//如果这个参数为空, "undefined" 会传递给 "this" 值

arr=[1,2,3]
let arr1=arr.forEach(item =>{return item * 2})
console.log(arr) //[1,2,3]
console.log(arr1) //undefined


array=[{name: '小红',age: 15},{name: '小明',age: 18},]

array.forEach(person => {
  if(person.name === '小红'){
      person = {
          name: '小红',
          age: 100
      }
  }
})
console.log(array)  //[{name: '小红',age: 15},{name: '小明',age: 18},]
array.forEach((person, index) => {
  if(person.name === '小红'){
      array[index].age = 100
  }
})
console.log(array)  //[{name: '小红',age: 100},{name: '小明',age: 18},]

//forEach不能改变数组本身,无论是基础数据类型还是引用数据类型都不可以。但是!可以用改变person.age的方式改变数组,本质上是替换当前元素
  1. map:map()遍历数组、集合的一种方法,他会返回新数组,但不会改变原数组,如果里面有逻辑判定时,不符合条件会返回undefined,使用break不能跳出循环且会报错,return也无法跳出循环但不报错
// array.map(function(currentValue,index,arr), thisValue)
//currentValue	必需。当前元素
//index	可选。当前元素的索引值。
//arr	可选。当前元素所属的数组对象。
//thisValue	可选。对象作为该执行回调时使用,传递给函数,用作 "this" 的值。
//如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象

arr=[1,2,3]
let arr1=arr.map(item =>{return item * 2})
let arr2=arr.map(item =>{if(item==2){return item*2}})

console.log(arr) //[ 1, 2, 3 ]
console.log(arr1) //[ 2, 4, 6 ]
console.log(arr2) //[ undefined, 4, undefined ]
  1. some:some()用来检测数组中的元素是否满足指定条件,若有一个元素符合条件,则返回true,且后面的元素不会再检测,不改变原数组也不产生新数组,只会返回boolean值, 不会对空数组进行检测,不会改变原始数组当内部return true时跳出整个循环。
let arr = [1,2,3];
let state=arr.some( item => {
    if ( 3 == item ) {
        item+1 //这里并不会改变元素组
        return true; //跳出循环
    }
}); 
console.log(arr) //[ 1, 2, 3]
console.log(state) //true
  1. every:every()用来检测数组中每个元素(检测所有元素)是否都符合指定条件,若有一个不满足条件,则返回false,后面的元素都不会再执行。不改变原数组也不产生新数组,只会返回boolean值,不会对空数组进行检测,不会改变原始数组,当内部return false时跳出整个循环(需要写 return true )
let arr = [1,2,3];
let state=arr.every( item => {
    if ( 3 == item ) {
        return false;
    }else{
        return true;
    }
});  
console.log(arr)//[ 1, 2, 3 ]
console.log(state) //false

18. 关于原型和原型链

原型链是‌JavaScript中一个非常重要的概念,它指的是通过原型对象的引用关系,将对象连接起来形成的链表结构。‌ 每个对象都有一个内部指针(proto)指向其原型对象,而这个原型对象也可能有一个内部指针指向另一个原型,如此层层递进,构成了原型链。

详细解释
‌原型和原型链的定义‌:
每个对象(包括原型对象)都有一个内置的[[proto]]属性,这个属性指向创建它的函数对象的原型对象,即prototype属性。
构造函数有一个prototype属性,实例对象的__proto__属性指向这个原型对象。这样,通过原型对象,实例可以共享方法或属性。
‌原型链的作用‌:
原型链的主要作用是实现对象的继承。通过原型链,子类可以继承父类的属性和方法。
原型链还允许我们通过原型为对象动态添加方法或属性,这在运行时非常有用。
‌实现方式‌:
原型链的构建是通过对象的__proto__属性和构造函数的prototype属性来实现的。实例对象的__proto__属性指向构造函数的原型对象,从而形成了一条从实例到构造函数的原型,再到更上一级的原型的链条。

所有引用类型(函数,数组,对象)都拥有__proto__属性(隐式原型);
所有函数拥有prototype属性(显式原型)(仅限函数)原型对象:拥有prototype属性的对象,在定义函数时就被创建;

19. ES6新特性?

ES6新特性
1、let和const
2、symbol
3、模板字符串${}
3.1 字符串新方法(补充)include
4、解构表达式 …语法
4.1 数组解构
4.2 对象解构
5、对象方面
5.1 Map和Set
5.1.1 Map
5.1.2 Set
5.3 数组的新方法
5.3.1 Array.from()方法
5.3.2 includes()方法
5.3.3 map()、filter() 方法
5.3.4 forEach()方法
5.3.4 find()方法
5.3.6 some()、every() 方法
5.4 object的新方法
5.4.1 Object.is()
5.4.2 Object.assign()
5.4.3 Object.keys()、Object.values()、Object.entries()
5.5 对象声明简写
5.6 …(对象扩展符)
6、函数方面
6.1 参数默认值
6.2 箭头函数
6.3 箭头函数和普通函数最大的区别在于其内部this永远指向其父级对象的this。(重点)
7、class(类)
8、promise和proxy
9、模块化
10、运算符

20. 为什么声明的两个空对象并不相等?

// 场景1
const a = () => {};  // 语句1
let b = a;  // 语句2
a === b; // 返回true
 
// 场景2
const c = () => {};  // 语句3
c === a;  // 返回false

场景如上图所示,为什么a和b相等而a和c并不相等?
原因:
js中的函数是function对象的实列,而对象和函数都是引用类型,执行语句1时其实,在堆内存中存储对象{},在栈内存中存储对象的引用地址a,而b=a相当于把a的引用地址传递给b,所以a,b其实指向相同的对象。
但是执行语句c的时候,会在堆内存中再存储一个新的对象{},然后在栈内存中华存储对象的引用地址c,所以a和c指向的是不同的堆内存存储的不同对象,自然不能全等

21. typeof和instanceof的区别是什么?

  • typeof 操作符返回一个字符串,表示未经计算的操作数的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'
  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
        const a = [0, 1, 2];
        console.log(a instanceof Array); // true
        console.log(a instanceof Object); // true

        const b = {name: 'xx'};
        console.log(b instanceof Array); // false
        console.log(b instanceof Object); // true

typeof与instanceof都是判断数据类型的方法,区别如下:

  1. typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
  2. instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
  3. 而typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function类型以外,其他的也无法判断

可以看到,上述两种方法都有弊端,并不能满足所有场景的需求

如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串

22.箭头函数与普通函数的区别?

  1. 语法更加简洁、清晰
  2. 箭头函数没有 prototype (原型),所以箭头函数本身没有this
// 箭头函数
let a = () => {};
console.log(a.prototype); // undefined
 
// 普通函数
function a() {};
console.log(a.prototype); // {constructor:f}
  1. 箭头函数不会创建自己的this
    箭头函数的this指向在定义(注意:是定义时,不是调用时)的时候继承自外层第一个普通函数的this,外层没有普通函数那么就指向window
  2. call | apply | bind 无法改变箭头函数中this的指向
  3. 箭头函数不能作为构造函数使用
  4. 箭头函数不绑定arguments,取而代之用rest参数…代替arguments对象,来访问箭头函数的参数列表
  5. 箭头函数不能用作Generator函数,不能使用yeild关键字

23.对象数组去重方法?

对象数组例如:

let arrObj = [
    { name: "小红", id: 1 },
    { name: "小橙", id: 1 },
    { name: "小黄", id: 4 },
    { name: "小绿", id: 3 },
    { name: "小青", id: 1 },
    { name: "小蓝", id: 4 }
]

去重方法有

  1. 方法一:双层for循环
    两两比较,如果后一个对象的id值和前一个对象的id值相等,就把后面的对象删除
function fn1(tempArr) {
    for (let i = 0; i < tempArr.length; i++) {
        for (let j = i + 1; j < tempArr.length; j++) {
            if (tempArr[i].id == tempArr[j].id) {
                tempArr.splice(j, 1);
                j--;
            };
        };
    };
    return tempArr;
};
  1. 方法二:indexOf()
    定义一个数组存储id的值,然后逐个比较,把id值重复的对象删除即可
function fn2(tempArr) {
    let newArr = [];
    for (let i = 0; i < tempArr.length; i++) {
        if (newArr.indexOf(tempArr[i].id) == -1) {
            newArr.push(tempArr[i].id);
        } else {
            tempArr.splice(i, 1);
            i--;
        };
    };
    return tempArr;
}
  1. 对象访问属性的方法
    采用对象访问属性的方法,判断属性值是否存在
function fn3(tempArr) {
    let result = [];
    let obj = {};
    for (let i = 0; i < tempArr.length; i++) {
        if (!obj[tempArr[i].id]) {
            result.push(tempArr[i]);
            obj[tempArr[i].id] = true;
        };
    };
    return result;
};
  1. Map()
  • has方法可以判断Map对象中是否存在指定元素,有则返回true,否则返回false
  • set方法可以向Map对象添加新元素
  • map.set(key, value) values方法可以返回Map对象值的遍历器对象
let map = new Map();
for (let item of arrObj) {
    if (!map.has(item.id)) {
        map.set(item.id, item);
    };
};
arr = [...map.values()];
console.log(arr);
 
 
 
// 方法二: (代码较为简洁)
const map = new Map();
const newArr = arrObj.filter(v => !map.has(v.id) && map.set(v.id, v));
// const newArr = [...new Map(arrObj.map((v) => [v.id, item])).values()];
console.log(newArr);

24.两个对象数组去重的3种方法?

请在两个对象数组中找到其中一个与另一不重复的对象。然后追加到另一个数组中。比如在数组1[{a:1},{a:2},{a:3},{a:5}]与数组2[{a:1},{a:2},{a:4}]中找到数组1中不与数组2重复的对象{a:3}和{a:5},最后得到的数组2为[{a:1},{a:2},{a:4},{a:3},{a:5}]。

  1. 解决方案一
    思路:通过增加属性的方式。
    var arr1 =[{a:1},{a:2},{a:3},{a:5}];
    var arr2 =[{a:1},{a:2},{a:4}];
    arr1.map((item1)=>{
        arr2.map((item2)=>{
            if(item1.a == item2.a){
                //添加属性用来标记相同的对象
                item1.isRepeat = true;
            }
        })
    });
    arr1.map((item)=>{
        //通过标记筛选对象
        if(!item.isRepeat){
            arr2.push(item);
        }
    });
   console.log(arr2);//[{a:1},{a:2},{a:4},{a:3},{a:5}];
  1. 解决方案二
    思路:直接轮循两个数组,取出不包含数组2元素的数组1,然后将数组1合并到数组2。
    var arr1 =[{a:1},{a:2},{a:3},{a:5}];
    var arr2 =[{a:1},{a:2},{a:4}];
    for (var i = 0; i < arr2.length; i++) {
        for (var j = 0; j < arr1.length; j++) {
            if (arr2[i].a == arr1[j].a) {
                arr1.splice(j, 1);
            }
        }
    }
    arr2 = arr2.concat(arr1);
    console.log(arr2);//[{a:1},{a:2},{a:4},{a:3},{a:5}]
  1. 解决方案二
    思路:将两个数组合并,然后两层循坏。
    var arr1 =[{a:1},{a:2},{a:3},{a:5}];
    var arr2 =[{a:1},{a:2},{a:4}];
    //将arr1与arr2合并。
    arr2 = arr2.concat(arr1);
    for (var i = 0; i < arr2.length; i++) {
        for (var j = i+1; j < arr2.length; j++) {
            if (arr2[i].a == arr2[j].a) {
                arr2.splice(j, 1);
            }
        }
    }
    console.log(arr2);//[{a:1},{a:2},{a:4},{a:3},{a:5}]

25.什么是柯里化,为什么要进行柯里化?

  1. 原理:柯里化是将一个接受多个参数的函数转换为一系列单参数函数的过程。换句话说,柯里化后的函数会逐步接收参数,每次只接收一个或部分参数,直到收集到足够的参数时才真正执行。比如,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©。
function sum(a, b, c) {
  return a + b + c;
}

//curry(f) 执行柯里化转换
function curry(f) { 
  return function(a) {
    return function(b) {
      return function(c){
	       return f(a, b);
       }
    };
  };
}

//柯里化后
const curriedSum = curry(sum); //curry是柯里化函数
const result = curriedSum(1)(2)(3); // 6

柯里化不会调用函数。它只是对函数进行转换。

  1. 柯里化的优点
  • 参数复用
    通过固定部分参数,柯里化使函数调用更加灵活。例如:
function greet(greeting, name) {
  return `${greeting}, ${name}!`;
}
 
const sayHello = curry(greet)("Hello");
console.log(sayHello("Alice")); // Hello, Alice!
console.log(sayHello("Bob"));   // Hello, Bob!
  • 延迟执行
    柯里化允许延迟计算,直到所有参数准备就绪。
const add = curry((a, b) => a + b);
const addFive = add(5);
console.log(addFive(10)); // 15
  • 函数组合
    柯里化与函数组合结合使用时,能够提升代码的可读性和模块化程度。例如:
const compose = (f, g) => (x) => f(g(x));
const addOne = (x) => x + 1;
const double = (x) => x * 2;
 
const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(3)); // 8

实现柯里化方法:

  • reduce实现
function add(){
    //let args = arguments;//用于获取第一个括号里的参数
    // 因为arguments是类数组结构,因此上述代码还需要进行改进,下面这行才是正确的
    let args = Array.prototype.slice.call(arguments);
    
    let inner = function(){
         args.push(...arguments);// arguments默认就为函数的参数,即使我们没有列出形参
         return inner;
    }
    
    inner.toString = function(){
        return args.reduce((prev,cur) => {
            return prev + cur;
        });
    }
    
    return inner;
}
  • 递归
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

26.用闭包,写一个once函数,让传入函数只执行一次?

什么是闭包?怎么实现闭包?

闭包(closure) 是指有权访问另一个函数作用域中的变量的函数,也可以这么理解(函数在其定义的词法作用域之外被调用时,依然能够访问该作用域内变量的特性)。可以通过函数嵌套和变量引用来实现闭包。如下所示:

function fun(){
    let count = 1
    return function fn(){
        count++
        console.log(count);
    }
    // return fn
} 
var testFun = fun()
testFun() //2 
testFun() //3
testFun() //4

实现示例
使用闭包来实现一个 once 函数,确保传入的函数只执行一次。

function once(fn) {
  let executed = false;
  return function(...args) {
    if (!executed) {
      executed = true;
      return fn.apply(this, args);
    }
  };
}

// 示例用法
var fun = once(function () {
  console.log("hello world");
});

fun(); // 输出 "hello world"
fun(); // 不输出
fun(); // 不输出

解释

  1. 闭包:在 once 函数内部,我们定义了一个局部变量 executed,并返回一个新的函数。
  2. 检查和执行:返回的这个新函数会检查 executed 的值。如果 executed 是 false,则执行传入的函数 fn,并将 executed 设置为true。这样可以确保 fn 只会被执行一次。
  3. 参数传递:使用 …args 和 apply方法,确保传入的新函数能够接收任何数量的参数,并且正确地将这些参数传递给 fn。
  • arguments是函数内部的一个类数组对象,包含所有传递给函数的参数。
  • apply 是一个函数方法,用于调用一个函数并传递参数数组,同时可以指定 this 的值。

其他
arguments是一个类数组对象,包含传递给函数的所有参数。apply 是一个函数方法,用于调用一个函数,并且可以指定 this 关键字的值,以及一个数组或类数组对象作为参数。

arguments 的作用

  • 定义:arguments 是一个类数组对象,包含函数调用时传递给该函数的所有参数
  • 作用: 1. 可以用来访问函数的参数列表。2. 适用于需要处理不定数量参数的情况。
function example() {
  console.log(arguments); // 输出传递给函数的所有参数
}
example(1, 2, 3); // 输出 [1, 2, 3]

apply 的作用

  • 定义:apply 是一个函数方法,用于调用一个函数,并且可以指定 this 的值,以及一个数组或类数组对象作为参数。
  • 作用:1. 可以用来调用函数并传递参数数组。 2。适用于当需要将一个数组或类数组对象作为参数列表传递给函数时。
function example(a, b, c) {
  console.log(a, b, c);
}

const args = [1, 2, 3];
example.apply(null, args); // 输出 1 2 3

对比

  • arguments:

主要用于函数内部访问参数。
是一个类数组对象。
不适用于箭头函数。

  • apply:

用于调用函数并传递参数。
接受一个数组或类数组对象作为参数。
可以改变函数的 this 指向。

27.理解Javascript的作用域和作用域链

作用域和作用域链在Javascript和很多其它的编程语言中都是一种基础概念

作用域: Javascript中的作用域说的是变量的可访问性和可见性
Javascript中有三种作用域:

1. 全局作用域;
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。例如:

// 全局变量
var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// 打印 'Hello World!'
greet();

2. 函数作用域;
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。例如:

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

3. 块级作用域;
ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。看例子:

{
  // 块级作用域中的变量
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。例如

let foo = 'foo';
function bar() {
  let baz = 'baz';
  // 打印 'baz'
  console.log(baz);
  // 打印 'foo'
  console.log(foo);
  number = 42;
  console.log(number);  // 打印 42
}
bar();

解释:当函数bar()被调用,Javascript引擎首先在当前作用域下寻找变量baz,然后寻找foo变量但发现在当前作用域下找不到,然后继续在外部作用域寻找找到了它(这里是在全局作用域找到的)。

28.require和import区别

require 和 import 在日常的开发过程中是经常使用的已经模块引入的方式

有以下区别:

  1. 导入require 导出 exports/module.exports 是 CommonJS 的标准,通常适用范围如 Node.js
  2. import/export 是 ES6 的标准,通常适用范围如 React
  3. require是赋值过程并且是运行时才执行,也就是同步加载
  4. import是解构过程并且是编译时执行,理解为异步加载
  5. require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。
  6. import 会提升到整个模块的头部,具有置顶性,但是建议写在文件的顶部。

require 的性能相对于 import 稍低。因为 require 是在运行时才引入模块并且还赋值给某个变量,而 import 只需要依据 import 中的接口在编译时引入指定模块所以性能稍高

29. for…in和for…of区别

一句话概括:for in是遍历(object)键名,for of是遍历(array)键值。

for in

for…in 循环只遍历可枚举属性(包括它的原型链上的可枚举属性)。像 Array和Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性,例如 String 的 indexOf() 方法或 Object的toString()方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。

for…of

for…of语句在可迭代对象(包括Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
for of不可以遍历普通对象,想要遍历对象的属性,可以用for in循环, 或内建的Object.keys()方法

for…of与for…in的区别:

无论是for…in还是for…of语句都是迭代一些东西。它们之间的主要区别在于它们的迭代方式。
for…in语句以任意顺序迭代对象的可枚举属性
for…of 语句遍历可迭代对象定义要迭代的数据。

以下示例显示了与Array一起使用时,for…of循环和for…in循环之间的区别。

Object.prototype.objCustom = function() {}; 
Array.prototype.arrCustom = function() {};

let iterable = [3, 5, 7];
iterable.foo = 'hello';

for (let i in iterable) {
  console.log(i); // 0, 1, 2, "foo", "arrCustom", "objCustom"
}

for (let i in iterable) {
  if (iterable.hasOwnProperty(i)) {
    console.log(i); // 0, 1, 2, "foo"
  }
}

for (let i of iterable) {
  console.log(i); // logs 3, 5, 7
}

总结:for in 一般用来遍历对象的key、for of 一般用来遍历数组的value

30. forEach和map的区别与实现原理

forEach()和map()方法通常用于遍历Array元素,但几乎没有区别

区别:

  1. forEach()方法返回undefined ,而map()返回一个包含已转换元素的新数组。
const numbers = [1, 2, 3, 4, 5];
 
// 使用 forEach()
const squareUsingForEach = [];
numbers.forEach(x => squareUsingForEach.push(x*x));
 
// 使用 map()
const squareUsingMap = numbers.map(x => x*x);
 
console.log(squareUsingForEach); // [1, 4, 9, 16, 25]
console.log(squareUsingMap);     // [1, 4, 9, 16, 25]

由于forEach()返回undefined,所以我们需要传递一个空数组来创建一个新的转换后的数组。map()方法不存在这样的问题,它直接返回新的转换后的数组。在这种情况下,建议使用map()方法。

  1. 链接其他方法
    map()方法输出可以与其他方法(如reduce()、sort()、filter())链接在一起,以便在一条语句中执行多个操作。

另一方面,forEach()是一个终端方法,这意味着它不能与其他方法链接,因为它返回undefined。

  1. 性能
    map()方法比forEach()转换元素要好,因为花费时间短些

  2. 中断遍历
    这两种方法都不能用 break 中断,否则会引发异常:

七、TS

1.TypeScript 与 JavaScript 有何不同?

TypeScript 是 JavaScript 的超集,这意味着所有有效的 JavaScript 代码也是有效的 TypeScript 代码。然而,TypeScript 增加了 JavaScript 所没有的特性,例如静态类型和基于类的面向对象编程。

TypeScript 还具有更严格的类型系统,允许在编译时而不是运行时检测到错误。

2. TypeScript 中什么是泛型?如何使用?

定义:泛型简单来说就是类型变量,在ts中存在类型,如number、string、boolean等。泛型就是使用一个类型变量来表示一种类型,类型值通常是在使用的时候才会设置。泛型的使用场景非常多,可以在函数、类、interface接口中使用。

使用:TypeScript 中的泛型是在函数、类和接口声明中使用方括号 <> 定义的,带有类型的占位符。并且可以在使用函数、类或接口时指定形状。

这是一个例子。

//1.使用泛型变量
function identity<T>(arg: T): T {
  return arg;
}
 
const result = identity<string>('Hello');
console.log(result); // Hello

//2、定义泛型函数
```javascript
// 泛型函数
function identityFn<T>(arg: T): T {
    return arg;
}
let myIdentityFn: { <T>(arg: T): T } = identityFn;

//3. 定义泛型接口
interface GenericIdentityFn<T> {
    (arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

//4.定义泛型类
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
//实例化调用1
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
    return x + y;
};
console.info(myGenericNumber.add(2, 5));

//实例化调用2
let stringNumberic = new GenericNumber<string>();
stringNumberic.zeroValue = 'asd';
stringNumberic.add = function (x, y) {
    return `${x}--${y}`;
};
console.info(stringNumberic.add('张三', '李四'));

3.什么是TS中的元组?

TypeScript中的元组类型允许你表达一个固定数量的元素的数组,其中每个元素都有已知的类型,而这些类型不必相同。
定义 :存储不同类型的元素集合

在TypeScript中,你可以这样定义一个元组:

let myTuple: [number, string] = [1, "Hello"];
1
这里,myTuple被定义为一个元组,它包含一个number类型的元素和一个string类型的元素,且元素的顺序是固定的。

//使用
// 声明并初始化
var mytuple = [10, "Runoob"];
var mytuple1: [number, string] = [10, "Runoob"];

// 先声明后初始化
var mytuple = []; 
mytuple[0] = 120 
mytuple[1] = "ming"

4.Ts中的as和is是什么?

1、as 关键字用于类型断言,它用于告诉编译器一个值的类型,即强制把某个值当做特定类型来处理,如下所示:

const myString: any = "hello";
const lengthOfString: number = (myString as string).length;

在这个例子中,我们使用 as 关键字将 myString 声明为一个字符串类型,以便我们可以安全地使用 length 属性来获取字符串的长度。
2、is 关键字用于类型保护,它用于在运行时检查一个值是否符合某个类型,如下所示:

function isString(value: any): value is string {
  return typeof value === "string";
}

function logIfString(value: any) {
  if (isString(value)) {
    console.log(value);
  }
}

在这个例子中,我们定义了一个 isString 函数来检查一个值是否为字符串类型,如果是字符串类型,它会返回 true。然后我们在 logIfString 函数中使用 isString 函数来检查传入的参数是否为字符串类型,如果是,我们就打印这个字符串。

5.Ts中的inter关键字?

infer 表示在 extends 条件语句中待推断的类型变量。

type ParamType<T> = T extends (arg: infer P) => any ? P : T;

在 2.8 版本中,TypeScript 内置了一些与 infer 有关的映射类型:

  • 用于提取函数类型的返回值类型:
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;

相比于文章开始给出的示例,ReturnType 只是将 infer P 从参数位置移动到返回值位置,因此此时 P 即是表示待推断的返回值类型。

  • 用于提取构造函数中参数(实例)类型:
    一个构造函数可以使用 new 来实例化,因此它的类型通常表示如下:
type Constructor = new (...args: any[]) => any;

6.TS 里几个常用的内置工具类型(Record、Partial 、 Required 、 Readonly、 Pick 、 Exclude 、 Extract 、 Omit)的使用,还有特别重要的Parameters 和 ReturnType等

概要:
可选 Partial 、必选 Required、只读 Readonly
剔除(是否 T 剔除 U 包含的类型,Exclude):Exclude、Extract、
过滤、剔除(是否取出老类型中的指定属性 及它对应的类型):Pick 、Omit
约束对象的 key 和 value:Record
去除 null 和 undefined:NonNullable
获取函数参数类型:Parameters 、ConstructorParameters
获取函数返回值类型:ReturnType、InstanceType

  1. Parameters
    Parameters 用于获取一个函数的参数类型。它接受一个函数类型作为参数,并返回一个包含该函数所有参数类型的元组。
type FuncType = (a: number, b: string) => number;
type Args = Parameters<FuncType>;
// Args 的类型为 [number, string]
  1. ReturnType 用于获取一个函数的返回类型。它接受一个函数类型作为参数,并返回该函数的返回类型。
type AnotherFuncType = (s: string) => string;
type ResultType = ReturnType<AnotherFuncType>;
// ResultType 的类型为 string
  1. Partial
    生成一个 新类型,与老类型的 属性全部相同,但是都变为 可选
//源码
type Partial<T> = {
    [P in keyof T]?: Partial<T[P]>
}

//使用
type Person = {
	name: string
	age: number
	sex: string
}

type Par = Partial<Person>
// 鼠标放上Par,可发现每个属性都加了一个 ?,如下:
// type Par = {
//   name?: string;
//   age?: number;
//   sex?: string;
// }

  1. Required
    生成一个 新类型,与老类型的 属性全部相同,但是都变为 必选
//源码
type Required<T> = {
    [P in keyof <T>]-?: T[P]
}

//使用
type Req = Required<Person>
// 鼠标放上Req,可发现每个属性都是必填,如下:
// type Req  = {
//   name: string;
//   age: number;
//   sex: string;
// }
  1. Readonly
    生成一个 新类型,与老类型的 属性全部相同,但是都变为 只读
//源码
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

//使用
type Person = {
  name: string 
  age: number
  sex?: string
}
type Read = Readonly<Person>
// 鼠标放上Read,可发现每个属性都是只读,如下:(sex仍然是可选)
// type Read = {
//   readonly name: string;
//   readonly age: number;
//   readonly sex?: string;
// }
  1. Exclude
//源码
type Exclude<T, U> = T extends U ? never : T

如果 T 是 U 的子类型则返回 never ,不是则返回 T
白话就是:从 T 出 剔除 U 包含的类型
作用 :定义一个对象的 key 和 value 类型

type Record<K extends string | number | symbol, T> = {
    [P in K]: T;
}

//使用
type A = string | number | boolean
type B = string | boolean | symbol
type C = string

type exc = Exclude<A, B>
// type exc = number

type exc2 = Exclude<A, C>
// type exc2 = number | boolean

  1. Extract
    如果 T 是 U 的子类型则返回 T 不是则返回 never
    白话就是:从 T 中 选出 U 包含的类型
    作用:如果 T 是 U 的子类型则返回 T,不是则返回 never,与Exclude相反
//源码
type Exclude<T, U> = T extends U ? never : T
  1. Pick
    生成一个 新类型,取出老类型中的 指定属性 及 它对应的类型
//源码
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

//使用
type Person = {
  name: string
  age: number
  sex: string
}
type Pic = Pick<Person, 'name' | 'age'>
// 取出的就是指定 key 所组成的映射
// type Pic = {
//   name: string;
//   age: number;
// }

  1. Omit
    生成一个 新类型,从老类型中 剔除 的 指定属性 及 它对应的类型
//源码
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>> 

与 Pick 看似 相反,但不严格相反,因为这里的 K 不限制为 T 中的 key 构成
K extends keyof any 等价于 K extends string | number | symbol

  1. Record
    记录类型:将一个类型的所有属性值都映射到另一个属性上并创建新的类型
//源码
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

//使用
// 使用示例 1:第一个参数如果传具体的 key, 使用时每个 key 需要被具体设置
type Rec = Record<'A' | 'B' | 'C', number>
// type Rec = {
//   A: number;
//   B: number;
//   C: number;
// }
let obj: Rec = {
  A: 1,
  B: 2,
  C: 10
}


// 使用示例 2:第一个参数当然可以是 string | number | symbol 的任意类型联合的值
type Rec2 = Record<'A' | 2, boolean>
// type Rec2 = {
//   A: boolean;
//   2: boolean;
// }
let obj2: Rec2 = {
  A: true,
  2: false
}


// 使用示例 3:第一个参数如果传类型,使用时每个key都要对应上这个类型
type Rec3 = Record<string, number>
// type Rec3 = {
//   [x: string]: number
// }
let obj3: Rec3 = {
  A: 1,
  B: 2,
  C: 3
}


// 使用示例 4:第一个参数也可以传联合类型
type Rec4 = Record<number | symbol, boolean>
// type Rec4 = {
//   [x: number]: boolean;
//   [x: symbol]: boolean;
// }
let obj4: Rec4 = {
  1: true,
  [Symbol('111')]: false,
  2: false,
  // 'a': false // 报错,key类型不是 number 或 symbol
}


// 使用示例 5:路由信息的使用
type page = 'home' | 'login' | 'user'
type pageInfo = {
  title: string,
  needLogin: boolean
}
let obj5: Record<page, pageInfo> = {
  home: { title: '1111', needLogin: false },
  login: { title: '1111', needLogin: false },
  user: { title: '1111', needLogin: true },
}

  1. NonNullable
    从泛型 T 中 排除掉 null 和 undefined
//源码
type NonNullable<T> = T extends null | undefined ? never : T;

//使用
  1. ConstructorParameters
    以元组的方式获得构造函数的入参类型
//源码
ConstructorParameters<T extends new (...args: any) => any>

//使用
type Con = ConstructorParameters<new (name: string) => any>
// type Con = [name: string]
const con: Con = ['blue']


// 联合类型
type Con2 = ConstructorParameters<(new (name: string) => any) | (new (age: number) => number)>
// type Con2 = [name: string] | [age: number]
const con2: Con2 = ['blue']
const con22: Con2 = [123]


// 多个参数
type Con3 = ConstructorParameters<new (name: string, age: number) => any>
// type Con3 = [name: string, age: number]
const con3: Con3 = ['blue', 123]

  1. InstanceType
    获得 构造函数 返回值的类型
//源码
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

//使用
type Ins = InstanceType<new () => number>
// type Ins = number

6. type和interface的异同点

type 和 interface 都用于定义类型,都可以描述一个对象或者函数

type

  • 别名(Alias) :type 主要用于定义类型别名,它可以用来给已存在的类型起一个新的名字,使得类型表达更加清晰或简化复杂的类型定义。类型别名不仅可以用于原始类型、联合类型、元组等,还可以用于引用其他类型别名或接口。
  • 联合类型与交叉类型:type 在定义联合类型(如 type NewType = TypeA | TypeB)和交叉类型(如 type MergedType = TypeA & TypeB)时非常方便。
  • 不支持继承:与接口不同,类型别名不能被继承,也不能继承其他类型别名或接口。它更像是给类型起一个易于理解的名字。
  • 声明合并:type 不参与声明合并。这意味着,如果在不同的文件中使用相同的 type 名称,它们不会被合并成一个类型定义。

interface

  • 接口(Interface) :interface 用于定义对象的形状(shape),描述了对象应该具有哪些属性和方法。它可以用来定义类的公共部分,支持实现多接口。
  • 类的定义与实现:接口可以直接用于类的定义(通过 implements 关键字)和类的继承(通过 extends 关键字,虽然在接口间更多体现为“继承”接口成员而非真正意义上的继承)。
  • 扩展与合并:接口支持扩展其他接口(如 interface B extends A {}),这使得接口可以逐渐累加定义,形成层次化的类型定义。同时,即使在不同文件中定义了同名接口,TypeScript 也会自动将它们合并为一个接口。
  • 索引签名与元组类型:虽然 type 也可以定义复杂的类型结构,但接口在定义索引签名(如映射类型)和描述数组或元组的结构方面更为直观。

不同点:

  1. type可以定义基本类型别名, 但是interface无法定义,也就是type 能够表示非对象类型, 而 interface 则只能表示对象类型
  2. type可以声明联合类型
    例如:type Student = {stuNo: number} | {classId: number}
  3. type可以声明元组类型
    type Data = [number, string];
  4. 索引签名问题。由于interfac可以进行声明合并(如果你多次声明一个同名的接口,TypeScript 会将它们合并到一个声明中,并将它们视为一个接口),所以总有可能将新成员添加到同一个interface定义的类型上,也就是说interface定义的类型是不确定的;而type一旦声明类型,就无法更改它们。因此,索引签名是已知的。
  5. interface可以继承其他的接口、类等对象类型, type 不支持继承。(好多文章里都说 type 也支持继承,但是我认为这种说法不严谨。对于类型别名来说,它可以借助交叉类型来实现继承的效果。而且这种方法也只适用于表示对象类型的类型别名,对于非对象类型是无法使用的),另外interface 实现继承,遇到同名属性或同类型名,后者会覆盖前者,而 type 会进行严格报错约束来把控风险。

相同点:

  1. 都可以用来定义 对象 或者 函数 的结构,而严谨的来说,type 是引用,而 interface是定义

使用场景:当你需要描述对象的形状或实现面向对象设计时(如类的定义、接口的继承和实现),通常使用 interface。 当你需要为现有类型提供一个更易读的别名,或者定义联合类型、交叉类型等复合类型时,应使用 type。

7. 为什么使用枚举,如何模拟枚举,这种模拟的方法有什么劣势的地方?

枚举(Enumeration)是一种特殊的数据类型,允许变量具有预定义的用户指定的值
使用原因如下:

  • 提高代码清晰度:当一个变量只有几个特定的可能值时,使用枚举可以使代码更清晰,易读,易维护。
  • 类型安全:如果使用枚举,编译器将检查赋给枚举变量的值是否在枚举的范围内,从而提供了类型安全
  • 方便比较:枚举值可以方便地用在switch语句或者if…else语句中。
  • 更好的性能:枚举通常是整数类型,因此在处理上会比字符串等类型更加高效

如何模拟枚举:
在 JavaScript 中并没有内建的枚举类型,我们可以用对象或者声明多个常量来模拟枚举,如

//用对象模拟枚举
const Days = {
    MONDAY: 0,
    TUESDAY: 1,
    WEDNESDAY: 2,
    THURSDAY: 3,
    FRIDAY: 4,
    SATURDAY: 5,
    SUNDAY: 6
}
 
console.log(Days.MONDAY);  // 0

//用多个声明模拟枚举
const MONDAY = 0;
const TUESDAY = 1;
const WEDNESDAY = 2;
const THURSDAY = 3;
const FRIDAY = 4;
const SATURDAY = 5;
const SUNDAY = 6;
 
console.log(MONDAY);  // 0

以上两种方式模拟枚举的劣势:

  1. 枚举是一组相关的常量,使用对象的方式模拟枚举对象内的属性值可以被修改。虽然可以设置对象的属性不可写入,但实现过程比较繁琐。
  2. 声明多个常量来模拟枚举,也可以实现枚举的功能。但这样做并没有体现出这些常量之间的关联性,只能我们人为定义它们相关联。如一个枚举作为一个 js 文件、使用注释标明等。
  3. 使用传统方法模拟枚举还存在一个语义不明确的问题,如果其他人使用这个枚举,看到枚举值为 4,如果不了解这个枚举的具体细节,他并不知道 4 代表的是哪个常量。

8.TS中常见的数据类型

基本数据类型
Number:用于表示整数和浮点数,包括 NaN 和 Infinity。
String:用于表示文本,可以用单引号 (‘’) 或双引号 (“”) 包围。
Boolean:表示逻辑值,可以是 true 或 false。
Null:表示一个刻意的空值。
Undefined:表示未定义的值,通常用于未初始化的变量。
Symbol(ES6引入):唯一且不可变的数据类型,常用于对象的唯一属性键。
引用数据类型
Object:用于存储键值对的集合,是最通用的数据结构类型。
Array:用于表示有序的数据集合,可以是单一类型或多种类型的混合。
Function:函数本身也是一种类型,可以指定参数类型和返回值类型。
特殊数据类型
Tuple(元组):表示一个已知元素数量和类型的数组。
Enum(枚举):用于定义一组命名的常量。
Any:可以表示任何类型,使用时编译器不会进行类型检查。
Void:表示没有任何返回值的函数。
Never:表示永远不会达到的终点,常用于抛出异常或无限循环的函数返回类型。
其它数据类型
Union Types(联合类型):允许一个值可以是几种类型之一。
Intersection Types(交叉类型):创建一个类型,它拥有多个类型的所有属性和方法。
Type Aliases(类型别名):为复杂类型创建一个新的名字,便于复用和理解。
Literal Types(字面量类型):直接使用具体的值作为类型,如 const myVar: ‘hello’ = ‘hello’;

9.枚举和常量枚举的区别?

常量枚举在TypeScript中提供了更进一步的优化,特别是在关心代码体积和执行效率的场景下,而普通枚举则提供了更多的灵活性和运行时功能。根据具体需求选择合适的枚举类型是很重要的。

// 枚举
enum Color { Red, Green, Blue }
// 常量枚举
const enum Color { Red, Green, Blue }

两者区别

  1. 编译产物:
    普通枚举:在编译时会被转换为一个对象,其中包含枚举成员作为属性。这意味着生成的JavaScript代码中会有一个实际的对象来存储枚举值,这可能会增加代码体积。
    常量枚举:编译器会直接将枚举成员的使用替换为对应的值,而不会生成额外的对象。这样可以减小输出的JavaScript代码大小,提高运行时效率。
  2. 值的存储:
    普通枚举:枚举值存储在枚举类型实例上,可以在运行时通过枚举实例访问。
    常量枚举:枚举值直接内联到使用它们的地方,不生成实例,因此在运行时无法直接访问枚举类型。
  3. 可计算性:
    普通枚举:允许更复杂的结构,如包含计算成员或基于其他枚举值的表达式。
    常量枚举:只允许字面量值或其它常量枚举成员,不允许计算成员,因为它们的值在编译时就需要确定。
  4. 可读性和错误预防:
    两者都提供了更清晰的代码可读性和错误预防机制,但常量枚举由于其值直接内联,可能会进一步减少类型错误,因为任何对枚举值的引用都是静态的且直接硬编码的。
  5. 使用场景:
    普通枚举:适合需要在运行时能够访问枚举类型及其成员,或者枚举值可能需要动态计算的场景。
    常量枚举:适用于那些枚举值不需要在运行时访问,且希望最小化代码体积和提升性能的场景,例如在大量使用枚举值作为字符串或数字常量的场合。

八、React

0.浅谈react的理解和特性,什么是React的虚拟DOM?

React 是靠数据驱动视图改变的一种框架,它的核心驱动方法就是用其提供的 setState 方法设置 state 中的数据从而驱动存放在内存中的虚拟 DOM 树的更新

虚拟DOM是React的一个核心概念,它是一个轻量级的、表示真实DOM结构的对象树。当React组件的状态或props发生变化时,React会更新虚拟DOM,并将其与之前的虚拟DOM进行比较,以确定需要更新的真实DOM部分。

0.1简述react和vue的异同即优缺点。

Vue是一个用于为Web构建的UI的渐进式框架。React是由Facebook开发的用于构建用户界面的JavaScript库。

  1. 相同点:
    1)都使用虚拟dom。
    2)提供了响应式和组件化的视图组件。
    3)把注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。(vue-router、vuex、react-router、redux等等)
  2. 不同点:
    1)数据是否可变。
    React:整体是函数式的思想,在react中,是单向数据流,推崇结合immutable来实现数据不可变。
    Vue:的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟do
    vue是自动档:vue是双向绑定响应式的,数据和界面发生改变时,都会自动更新
    而react是手动档:需要使用setState去触发(比较当前状态和上一个状态)

    2)编译&写法。
    React:思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等。
    Vue:把html,css,js组合到一起,用各自的处理方式,Vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
    3)重新渲染和优化。
    当你比较React和Vue时,速度不能成为决定哪个更好的重要比较因素。在性能方面,让我们考虑重新渲染功能。当组件的状态发生变化时,React的机制会触发整个组件树的重新呈现。您可能需要使用额外的属性来避免不必要地重新渲染子组件。虽然Vue的重新渲染功能是开箱即用的,但Vue提供了优化的重新渲染,其中系统在渲染过程中跟踪依赖关系并相应地工作。重新渲染是Vue最显着的特征,也使其成为全世界开发人员广泛接受的框架。
    4)类式的组件写法,还是声明式的写法。
    react是类式的写法,api很少,而Vue是声明式的写法,通过传入各种options,api和参数都很多。所以react结合typescript更容易一起写,Vue稍微复杂。
    5)路由和状态管理解决方案。
    在像React和Vue这样的基于组件的框架中,当您开始扩展应用程序时,需要更加关注状态管理和数据流。这是因为有许多组件相互交互并共享数据。在这种情况下,React提供了一种称为Flux / Redux架构的创新解决方案,它代表单向数据流,是著名MVC架构的替代方案。现在,如果我们考虑Vue.js框架,就会有一个名为Vuex的更高级架构,它集成到Vue中并提供无与伦比的体验。
    6)构建工具。
    React和Vue都有一个非常好的开发环境。只需很少或没有配置,就可以创建应用程序,能够使用最新的实践和模板。在React中,有一个Create React App(CRA),在Vue中,它是vue-cli。这两种引导工具都倾向于提供舒适灵活的开发环境,并提供开始编码的出色起点。
  3. 各自优势:
    React:
    1)构建一个大型应用项目时:React的渲染系统可配置性更强,和React的测试工具结合起来使用,使代码的可测试性和可维护性更好。大型应用中透明度和可测试性至关重要。
    2)同时适用于Web端和原生APP时:React Native是一个使用Javascript构建移动端原生应用程序(iOS,Android)的库。 它与React.js相同,只是不使用Web组件,而是使用原生组件。
    Vue:
    1)构建数据简单中小型应用时:vue提供简单明了的书写模板、大量api、指令等等,可快速上手、开发项目
    2)应用尽可能的小和快时:随着vue3.0的发布,vue的体积进一步缩小,远小于react的体积,也配合diff算法,采用proxy去实现双向绑定,渲染大幅度提升
  4. 使用场景,如果你是经理如何选择框架?
    React
    1)构建一个大型应用项目时:React的渲染系统可配置性更强,和React的测试工具结合起来使用,使代码的可测试性和可维护性更好。大型项目建议用react,因为vue模板的使用不容易发现错误、也不易拆分和测试,大型应用中透明度和可测试性至关重要。
    2)同时适用于Web端和原生APP时:React Native是一个使用Javascript构建移动端原生应用程序(iOS,Android)的库。 它与React.js相同,只是不使用Web组件,而是使用原生组件。
    Vue
    1)构建数据简单中小型应用时:vue提供简单明了的书写模板、大量api、指令等等,可快速上手、开发项目。
    2)应用尽可能的小和快时:随着vue3.0的发布,vue的体积进一步缩小,远小于react的体积,也配合diff算法,采用proxy去实现双向绑定,渲染大幅度提升,应用需要尽可能的小和快就用vue,vue渲染速度比react快

0.2简述react和vue的diff算法区别。

React 的 Diff 算法
React 的 diff 算法主要基于以下几个原则:

  • 同层比较: React 只会比较同一层级的节点,不会跨层级比较。 假设跨层级的变化较少,从而简化了算法,提高了性能。
  • 深度优先遍历: React 采用深度优先遍历的方式,从根节点开始逐层比较。 这种方式有助于尽早发现差异并进行更新。
  • Key 优化: React 使用key 属性来标识列表中的每个节点。 当 key 存在时,React 可以快速定位节点并进行复用或更新,减少不必要的重新渲染。
  • O(n)复杂度: React 的 diff 算法通过对比新旧 Virtual DOM 树,采用深度优先遍历和分层比较的方式,复杂度为 O(n)。

Vue 的 Diff 算法
Vue 的 diff 算法主要基于以下几个原则:

  • 双端比较: Vue 的 diff 算法采用双端比较策略,从两端同时进行比较。 这种策略可以更高效地处理节点的移动,减少移动操作的次数。
  • 静态标记: Vue 在编译阶段会标记静态节点。 在更新时,Vue 会跳过这些静态节点的比较,从而提高性能。
  • Key 优化: 与 React类似,Vue 也使用 key 属性来优化列表渲染。 Key 的存在使得 Vue 可以更高效地进行节点的复用和更新。
  • Patch 函数:Vue 使用一个 patch 函数来对比新旧节点,并根据差异进行更新。 这个函数会递归地对比节点的属性、子节点等,进行最小化的更新操作。

具体差异

  • 比较策略: React:同层比较,深度优先遍历。 Vue:双端比较,静态标记。
  • 性能优化: React:通过 key属性和同层比较来优化性能。 Vue:通过双端比较和静态标记来优化性能。
  • 复杂度: React:O(n)复杂度,通过深度优先遍历和分层比较实现。 Vue:通过双端比较和静态标记来减少不必要的比较和更新。

总结
React:采用同层比较和深度优先遍历,结合 key 优化来提高 diff 性能。适用于变化较少的场景。
Vue:采用双端比较和静态标记,结合 key 优化来提高 diff 性能。适用于需要频繁更新和移动节点的场景。
这些不同的比较策略和优化方法使得 React 和 Vue 在处理节点更新时各有优势,React 更注重简化算法和同层比较,而 Vue 则通过双端比较和静态标记来优化性能。

0.3简述react生命周期。

React的生命周期主要分为创建阶段、更新阶段和卸载阶段

  • 创建阶段包括constructor、getDerivedStateFromProps、rendercomponentDidMount等方法;
  • 更新阶段包括getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdatecomponentDidUpdate等方法;
  • 卸载阶段则只有componentWillUnmount方法。

1.为什么vue的渲染速度比react快?简述原因

总结:我们都知道 Vue 对于响应式属性的更新,只会精确更新依赖收集的当前组件,而不会递归的去更新子组件,这也是它性能强大的原因之一。

<template>
   <div>
      {{ msg }}
      <ChildComponent />
   </div>
</template>

我们在触发 this.msg = 'Hello, Changed~'的时候,会触发组件的更新,视图的重新渲染。

但是 这个组件其实是不会重新渲染的,这是 Vue 刻意而为之的。React 在类似的场景下是自顶向下的进行递归更新的,也就是说,React 中假如 ChildComponent 里还有十层嵌套子元素,那么所有层次都会递归的重新render(在不进行手动优化的情况下),这是性能上的灾难。(因此,React 创造了Fiber,创造了异步渲染,其实本质上是弥补被自己搞砸了的性能)。

他们能用收集依赖的这套体系吗?不能,因为react遵从Immutable的设计思想,永远不在原对象上修改属性,那么基于 Object.defineProperty 或 Proxy 的响应式依赖收集机制就无从下手了(你永远返回一个新的对象,我哪知道你修改了旧对象的哪部分?)

**同时,由于没有响应式的收集依赖,React 只能递归的把所有子组件都重新 render一遍(除了memo和shouldComponentUpdate这些优化手段),然后再通过 diff算法 决定要更新哪部分的视图,**这个递归的过程叫做 reconciler,听起来很酷,但是性能很灾难。

Vue的更新粒度
那么,Vue 这种精确的更新是怎么做的呢?其实每个组件都有自己的渲染 watcher,它掌管了当前组件的视图更新,但是并不会掌管 ChildComponent 的更新。
具体到源码中,是怎么样实现的呢?
在 patch 的过程中,当组件更新到ChildComponent的时候,会走到 patchVnode,那么这个方法大致做了哪些事情呢?
1)更新props(后续详细讲)
2)更新绑定事件
3)对于slot做一些更新(后续详细讲)

2.类组件和函数组件之间的区别是什么?

语法:类组件是使用 ES6 的类语法来定义的,而函数式组件是使用 JavaScript 函数来定义的。

生命周期:类组件可以使用生命周期方法(如 componentDidMount、componentDidUpdate 等)来处理组件的状态和行为。而函数式组件通过使用 React Hooks(如 useEffect、useState 等)来实现类似的功能。

组件状态和上下文:类组件可以使用 state 属性来管理组件的内部状态,并且可以使用 this 关键字访问组件实例和其他方法。函数式组件通过使用 useState Hook 来管理状态,并且没有实例或者其他类方法。

性能:由于函数式组件没有实例化过程,渲染时的性能通常比类组件更高效。

可读性和简洁性:函数式组件相对于类组件来说更加简洁,代码量更少,并且更易于阅读和维护。它们通常只关注数据的输入和输出,而不需要处理复杂的生命周期方

3.React中super(props)和super()以及不写super()的及ES6和ES5的区别

  1. constructor和super的基本含义
    constructor() – 构造方法
    这是es6中的类的默认方法,通过new命令生成对象实例自动调用的方法。
    并且,该方法是类中必须要有的,如果没有显示定义,则会默认添加空的constructor()方法。
    super() – 继承
    在class方法中,继承是使用extends关键字来实现继承的。
    子类必须在constructor()中调用super()方法,否则新建实例时会报错。

报错的原因是,子类是没有自己的this对象的,它只能继承父类的this对象,然后对其进行加工,而super()就是将父类中的this对象继承给子类的。
没有super,子类就得不到this对象。

  1. ES5和ES6关于继承的实现不同之处
    在ES5中,当一个构造函数前面加上new的时候,其实一共做了四件事:
    1.生成一个空的对象并将其作为this
    2.将空对象的__proto__指向构造函数的prototype
    3.运行该构造函数
    4.如果构造函数没有return或者return一个返回this值是基本类型,则返回this,如果return一个引用类型,则返回这个引用类型

简单解释,就是在ES5的继承中,先创建子类的实例对象this,然后再将父类的方法添加到this上,而ES6采用的是先创建父类的实例this(故要先调用super()方法),然后再用子类的构造函数修改this。

  1. super(props) —— super() —— 以及不写super()的区别
    如果你用到了constructor就必须写super(),是用来初始化this的,可以绑定事件到this上
    如果你在constructor中要使用this.props,就必须给super加参数,super(props)
    (注意: 无论有没有constructor,在render中this.props都是可以使用的,这是react自动附带的)
    如果没用到constructor,是可以不写的,react会默认添加一个空的constructor

4.React中hooks函数有哪些?举例说下如何使用?

React Hooks 是 React 16.8 版本引入的一项重要特性,它极大地简化和优化了函数组件的开发过程。

  1. useState:是 react 提供的一个定义响应式变量的 hook 函数。
    使用场景: 管理组件内部的状态
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
  1. useEffect:React中的一个钩子函数,用于处理副作用操作。
    使用场景: 处理组件生命周期,例如数据获取、订阅、手动更改 DOM 等
import { useState, useEffect } from 'react';

function FetchData() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('/api/data');
      const data = await response.json();
      setData(data);
    }
    fetchData();
  }, []);

  return (
    <div>
      <h1>Fetched Data</h1>
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}
  1. useContext:如果需要在组件之间共享状态,可以使用useContext()。
    使用场景: 跨组件传递数据,避免"prop drilling"
import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }}>Theme Button</button>;
}

  1. useRef:
    使用场景: 获取 DOM 元素引用,保存可变状态
import { useRef } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

  1. useCallback:
    useCallback 是一个 Hook,用于返回一个记忆化的回调函数。它接收一个回调函数和一个依赖数组,只有在依赖数组中的值发生变化时,才会重新创建回调函数。
const cachedFn = useCallback(fn, dependencies)

//例如

fn:想要缓存的函数。dependencies:有关是否更新 fn 的所有响应式值的一个列表

function ProductPage({ productId, referrer, theme }) {
  // 在多次渲染中缓存函数
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // 只要这些依赖没有改变

  return (
    <div className={theme}>
      {/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}
  1. useMemo:
    是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
const cachedValue = useMemo(calculateValue, dependencies)

calculateValue:要缓存计算值的函数。
dependencies:所有在 calculateValue 函数中使用的响应式变量组成的数组。

import { useMemo } from 'react';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}

在 useMemo 的帮助下,尽管已经被人为减速,但是它还是很快!缓慢的 filterTodos 调用被跳过,因为 todos 和 tab(你将其作为依赖项传递给 useMemo)自上次渲染以来都没有改变。

5.怎么理解react的副作用,为什么ajax、修改dom是副作用?

首先解释纯函数(Pure function):给一个 function 相同的参数,永远会返回相同的值,并且没有副作用;这个概念拿到 React 中,就是给一个 Pure component 相同的 props, 永远渲染出相同的视图,并且没有其他的副作用;纯组件的好处是,容易监测数据变化、容易测试、提高渲染性能等;
副作用(Side Effect)是指一个 function 做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是 console.log(),所以 ajax 操作,修改 dom 都是算作副作用的;

6.react生命周期

React 是靠数据驱动视图改变的一种框架,它的核心驱动方法就是用其提供的 setState 方法设置 state 中的数据从而驱动存放在内存中的虚拟 DOM 树的更新
组件的生命周期可分成三个状态:

Mounting(挂载):已插入真实 DOM
Updating(更新):正在被重新渲染
Unmounting(卸载):已移出真实 DOM

在这里插入图片描述
挂载
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

  • constructor(): 在 React 组件挂载之前,会调用它的构造函数。
  • getDerivedStateFromProps():
    在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
  • render(): render() 方法是 class组件中唯一必须实现的方法。
  • componentDidMount(): 在组件挂载后(插入 DOM 树中)立即调用。 render() 方法是 class 组件中唯一必须实现的方法,其他方法可以根据自己的需要来实现。

这些方法的详细说明,可以参考官方文档。

更新
每当组件的 state 或 props 发生变化时,组件就会更新。
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  • getDerivedStateFromProps(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。根据
  • shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。
  • shouldComponentUpdate():当 props 或 state 发生变化时,shouldComponentUpdate()
    会在渲染执行之前被调用。
  • render(): render() 方法是 class 组件中唯一必须实现的方法。
  • getSnapshotBeforeUpdate(): 在最近一次渲染输出(提交到 DOM 节点)之前调用。
  • componentDidUpdate(): 在更新后会被立即调用。

render() 方法是 class 组件中唯一必须实现的方法,其他方法可以根据自己的需要来实现。

这些方法的详细说明,可以参考官方文档。

卸载
当组件从 DOM 中移除时会调用如下方法:

  • componentWillUnmount(): 在组件卸载及销毁之前直接调用。

7.Redux是什么?它是如何工作的?

Redux是一个用于JavaScript应用的状态管理库,它遵循Flux架构的核心理念,即单向数据流

Redux通过action、reducerstore等核心概念来实现状态的管理和更新,具体是:

  • action是一个描述要执行什么操作的普通JavaScript对象;
  • reducer是一个纯函数,它接收当前的state和action作为参数,并返回一个新的state;
  • store则是Redux的核心,它保存了应用的整个状态树,并提供了一系列方法来访问和更新状态。

8.如何使用Redux中间件来处理异步操作?

Redux中间件是一个用于拦截和扩展action处理流程的函数。通过中间件,开发者可以在action被发送到reducer之前执行一些额外的操作,如处理异步请求、记录日志等。
Redux-thunk是一个常用的中间件,它允许开发者在action中返回函数来处理异步操作。

9.关于React中的setState

1. setState概述
setState 是React框架中,用于更新组件状态的方法。
setState 方法由React组件继承自 React.Component 类的一部分。通过调用 setState,可以告诉 React要更新组件的状态,并触发组件的重新渲染。

this.setState(newState, callback);

newState 是一个对象,它包含了需要更新的状态属性和值。它将合并到组件的当前状态中。
callback 是一个回调函数,它在 setState 更新状态完成之后被调用。

2. setState为何使用不可变值

  • 什么是 不可变值?
    React框架中的“不可变值”的概念,通常指的是在编写React组件时,尽量避免直接修改数据,而是创建新的数据副本。

  • setState为何使用不可变值?
    在React中,组件的状态(state)应该保持不变。通过使用 setState 方法更新状态时,React会合并现有状态和新状态,而不是直接修改状态。 这确保 React 可以正确地追踪状态的变化并进行必要的更新。

同时,直接修改数据可能会引发意外的副作用,尤其是在多个组件之间共享数据时。通过使用不可变值,可以减少出现这种问题的可能性。

常用的处理不可变值的工具包括 Object.assign、数组的 concat、slice 等方法。对数组或者对象使用slice()方法,可以生成该数组或对象的副本,类似于深拷贝。

const list5Copy = this.state.list5.slice()
list5Copy.splice(2, 0, 'a') // 中间插入/删除

this.setState({
	list1: this.state.list1.concat(100), // 追加
	list2: [...this.state.list2, 100], // 追加
	list3: this.state.list3.slice(0, 3), // 截取
	list4: this.state.list4.filter(item => item > 100), // 筛选
	list5: list5Copy // 其他操作
})

3. setState可能是异步的
setState是一个异步方法。这意味着React可能会对多个 setState 调用进行批处理,然后一次性更新组件的状态,而不是每次调用都立即更新。

然而,setTimeout 函数中的 setState 是同步的。在自定义的DOM事件中,setState也是同步的。

state要在构造函数中定义

constructor(props){
	this.state = {
		count: 0
	}
}

因为 setState 是异步的,因此在 setState 方法执行完后打印状态,是拿不到更新后的值,只能拿到当前状态的值。

this.setState({
	count: this.state.count + 1; 
})
console.log(this.state.count) //打印结果为 0

如果想要拿到最新的状态,需要在setState中,写一个回调函数。此时count的结果为1

this.setState(
	{count: this.state.count + 1},() => {console.log(this.state.count)}
)

setTimeout 中的 setState 是同步的。此时打印出来的count结果就是1

setTimeout(()=>{
	this.setState({
		count: this.state.count + 1
	})
	console.log(this.state.count)  // 打印结果为1
}, 0)

在自定义的DOM事件中,setState也是同步的在这里插入代码片

bodyClickHandler = () => {
	this.setState({
		count: this.state.count + 1
	})
	console.log(this.state.count)   // 打印结果为1
}

componentDidMount(){
	document.body.addEventListener('click', this.bodyClickHandler)
}

4. setState何时合并state

  • setState是异步更新的话,传入的参数是一个对象,那么多次更新同一个状态只会执行1次。
constructor(props){
	this.state = {
		count: 0
	}
}

this.setState({
	count: this.state.count + 1
})
this.setState({
	count: this.state.count + 1
})
this.setState({
	count: this.state.count + 1
})
console.log(this.state.count) // 打印结果为 1

由于 setState 是异步的,React 会将这些更新一起批处理,然后应用它们。这意味着所有三个 setState 调用可能几乎同时执行,使用初始值 this.state.count,导致增加只有 1 而不是 3。

上述代码的思想类似于

Object.assign({count: 1}, {count: 1}, {count: 1}) // 执行结果为 {count: 1}
  • 为了防止这种情况发生,可以在setState中传入更新函数(updater function),它接受先前的状态并返回更新后的状态。这能够确保使用的是最新的状态。
constructor(props){
	this.state = {
		count: 0
	}
}

this.setState((prevState) => ({
  count: prevState.count + 1
}));
this.setState((prevState) => ({
  count: prevState.count + 1
}));
this.setState((prevState) => ({
  count: prevState.count + 1
}));

console.log(this.state.count) // 打印结果为 3

10.useState和useCallbak区别

  1. 用途不同:
    useState 用于管理组件的状态。 useCallback 用于记忆化回调函数,避免在每次渲染时创建新的函数实例。
  2. 返回值不同:
    useState 返回一个状态值和一个函数,用于更新该状态。 useCallback 返回一个记忆化的回调函数。
  3. 依赖关系:
    useState 不依赖于其他值。 useCallback 依赖于依赖数组中的值,只有在这些值发生变化时,才会重新创建回调函数。
  4. 使用场景:
    useState 适用于需要在组件中管理和更新状态的场景。 useCallback适用于需要将回调函数传递给子组件,或者需要在依赖值不变的情况下保持函数引用不变的场景。

在实际开发中,useState 和 useCallback 经常结合使用。例如,在管理状态的同时,记忆化更新状态的回调函数:

import React, { useState, useCallback } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const addClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p >
      <button onClick={addClick}>点击按钮每次+1</button>
    </div>
  );
};

export default Counter;

11.useMemo和useCallbak区别

  • 相同点:
    useCallback 和 useMemo 都是性能优化的手段,类似于类组件中的
    shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和
    state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。
  • 区别:
    useCallback 和 useMemo的区别是useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件,
const renderButton = useCallback(
     () => (
         <Button type="link">
            {buttonText}
         </Button>
     ),
     [buttonText]    // 当buttonText改变时才重新渲染renderButton
);

useMemo返回的的是一个值,用于避免在每次渲染时都进行高开销的计算。例:

// 仅当num改变时才重新计算结果
const result = useMemo(() => {
    for (let i = 0; i < 100000; i++) {
      (num * Math.pow(2, 15)) / 9;
    }
}, [num]);

12.什么时react的合成事件,为什么使用合成事件?

定义:React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。

举例:
在 React 中,所有事件都是合成的,不是原生 DOM 事件,但可以通过 e.nativeEvent 属性获取 DOM 事件。

const handleClick = (e) => console.log(e.nativeEvent);;
const button = <button onClick={handleClick}>Leo 按钮</button>

那么 React 为什么使用合成事件?其主要有三个目的:
1. 进行浏览器兼容,实现更好的跨平台
React采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React
提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。

2. 避免垃圾回收
事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收)。

3. 方便事件统一管理和事务机制

13.合成事件与原生事件区别?

  1. 事件名称命名方式不同。
    原生事件命名为纯小写(onclick, onblur),而 React 事件命名采用小驼峰式(camelCase),如 onClick 等:
  2. 事件处理函数写法不同
    原生事件中事件处理函数为字符串,在 React JSX 语法中,传入一个函数作为事件处理函数。如下
// 原生事件 事件处理函数写法
<button onclick="handleClick()">Leo 按钮命名</button>
      
// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>Leo 按钮命名</button>
  1. 阻止默认行为方式不同
    在原生事件中,可以通过返回 false 方式来阻止默认行为,但是在 React 中,需要显式使用 preventDefault() 方法来阻止。
    这里以阻止 标签默认打开新页面为例,介绍两种事件区别:
// 原生事件阻止默认行为方式
<a href="https://www.pingan8787.com" 
  onclick="console.log('Leo 阻止原生事件~'); return false"
>
  Leo 阻止原生事件
</a>

// React 事件阻止默认行为方式
const handleClick = e => {
  e.preventDefault();
  console.log('Leo 阻止原生事件~');
}
const clickElement = <a href="https://www.pingan8787.com" onClick={handleClick}>
  Leo 阻止原生事件
</a>

注意:React会先执行原生事件,然后处理 React 事件

14. 解释下react的fiber组件

React Fiber 是针对就协调器重写的完全向后兼容的一个版本。React 的这种新的协调算法被称为 Fiber Reconciler。这个名字来自于 fiber,它经常被用来表示 DOM 树的节点。

那么如何理解react中的fiber呢,两个层面来解释:

  1. 从运行机制上来解释,fiber是一种流程让出机制,它能让react中的同步渲染进行中断,并将渲染的控制权让回浏览器,从而达到不阻塞浏览器渲染的目的。
  2. 从数据角度来解释,fiber能细化成一种数据结构,或者一个执行单元。

而关于fiber数据结构,我在虚拟dom一文其实也简单提到过,每一个被创建的虚拟dom都会被包装成一个fiber节点,它具备如下结构:

const fiber = {
stateNode,// dom节点实例
child,// 当前节点所关联的子节点
sibling,// 当前节点所关联的兄弟节点
return// 当前节点所关联的父节点
}

九、uniApp

0. UniApp 中的生命周期钩子函数及其执行顺序。

在 UniApp 中,每个页面和组件都有一系列的生命周期钩子函数,用于在特定的时机执行代码。以下是 UniApp 中常用的生命周期钩子函数及其执行顺序:

onLoad:页面/组件加载时触发。
onShow:页面/组件显示在前台时触发。
onReady:页面/组件初次渲染完成时触发。
onHide:页面/组件被隐藏在后台时触发。
onUnload:页面/组件被销毁时触发。
执行顺序为:onLoad -> onShow -> onReady -> onHide -> onUnload。

1.uniapp应用的生命周期、页面的生命周期、组件的生命周期

一、应用的生命周期
1.onLaunch——当uni-app 初始化完成时触发(全局只触发一次)
2.onShow——当 uni-app 启动,或从后台进入前台显示
3.onHide——当 uni-app 从前台进入后台
4.onError——当 uni-app 报错时触发
5.onUniNViewMessage——对 nvue 页面发送的数据进行监听,可参考 nvue 向 vue 通讯
6.onUnhandledRejection——对未处理的 Promise 拒绝事件监听函数(2.8.1+)
7.onPageNotFound——页面不存在监听函数
8.onThemeChange——监听系统主题变化

二、页面的生命周期
1.onInit——监听页面初始化,其参数同 onLoad 参数,为上个页面传递的数据,参数类型为 Object(用于页面传参),触发时机早于 onLoad
2.onLoad——监听页面加载,其参数为上个页面传递的数据,参数类型为 Object(用于页面传参),参考示例
3.onShow——监听页面显示。页面每次出现在屏幕上都触发,包括从下级页面点返回露出当前页面
4.onReady——监听页面初次渲染完成。注意如果渲染速度快,会在页面进入动画完成前触发
5.onHide——监听页面隐藏
6.onUnload——监听页面卸载
7.onResize——监听窗口尺寸变化

三、组件的生命周期
uni-app 组件支持的生命周期,与vue标准组件的生命周期相同

1.beforeCreate——在实例初始化之后被调用。
2.created——在实例创建完成后被立即调用。
3.beforeMount——在挂载开始之前被调用。
4.mounted——挂载到实例上去之后调用。详见 注意:此处并不能确定子组件被全部挂载,如果需要子组件完全挂载之后在执行操作可以使用$nextTickVue官方文档
5.beforeUpdate——数据更新时调用,发生在虚拟 DOM 打补丁之前。
6.updated——由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
7.beforeDestroy——实例销毁之前调用。在这一步,实例仍然完全可用。
8.destroyed——Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

2.uniapp中如何实现页面的下拉刷新和上拉加载更多?

可以使用uni.onPullDownRefresh方法实现页面的下拉刷新,使用uni.onReachBottom方法实现页面的上拉加载更多。

// 在页面的onPullDownRefresh方法中实现下拉刷新
onPullDownRefresh() {
  // 执行刷新操作
  console.log('下拉刷新');
  // 刷新完成后调用uni.stopPullDownRefresh()方法停止刷新
  uni.stopPullDownRefresh();
}
 
// 在页面的onReachBottom方法中实现上拉加载更多
onReachBottom() {
  // 执行加载更多操作
  console.log('上拉加载更多');
}

3.uniApp中如何获取设备信息?

可以使用uni.getSystemInfo方法获取设备信息,包括设备型号、操作系统版本等。

uni.getSystemInfo({
  success: (res) => {
    console.log(res.model, res.system);
  },
  fail: (err) => {
    console.error(err);
  }
});

4.uniApp中如何进行微信支付

可以使用uni.requestPayment方法进行微信支付,通过设置支付参数来实现支付功能。

uni.requestPayment({
  provider: 'wxpay',
  timeStamp: '1234567890',
  nonceStr: 'abcdefg',
  package: 'prepay_id=1234567890',
  signType: 'MD5',
  paySign: 'abcdefg',
  success: (res) => {
    console.log(res);
  },
  fail: (err) => {
    console.error(err);
  }
});
 

5.uniApp中如何获取地理位置

可以使用uni.getLocation方法获取用户的地理位置信息。

uni.getLocation({
  success: (res) => {
    console.log(res.latitude, res.longitude);
  },
  fail: (err) => {
    console.error(err);
  }
});

5.uniApp中路由和页面跳转

1.uni.navigateTo(OBJECT):
保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面。

uni.navigateTo({
	url: 'test?id=1&name=uniapp'
});

2.uni.reLaunch(OBJECT):
关闭所有页面,打开到应用内的某个页面。

3.uni.redirectTo(OBJECT):
关闭当前页面,跳转到应用内的某个页面。

4.uni.switchTab(OBJECT):
跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。

//page.json中
{
  "tabBar": {
    "list": [{
      "pagePath": "pages/index/index",
      "text": "首页"
    },{
      "pagePath": "pages/other/other",
      "text": "其他"
    }]
  }
}

//other.vue中
uni.switchTab({
	url: '/pages/index/index'
});

5.uni.navigateBack(OBJECT):
关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层。

/ 注意:调用 navigateTo 跳转时,调用该方法的页面会被加入堆栈,而 redirectTo 方法则不会。见下方示例代码

// 此处是A页面
uni.navigateTo({
	url: 'B?id=1'
});

// 此处是B页面
uni.navigateTo({
	url: 'C?id=1'
});

// 在C页面内 navigateBack,将返回A页面
uni.navigateBack({
	delta: 2
});

6.uniApp中数据缓存

uni.setStorage(OBJECT)
将数据存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个异步接口。

uni.setStorage({
	key: 'storage_key',
	data: 'hello',
	success: function () {
		console.log('success');
	}
});

uni.setStorageSync(KEY,DATA)
将 data 存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个同步接口。

try {
	uni.setStorageSync('storage_key', 'hello');
	}
catch{
}

获取缓存对应uni.getStorage(OBJECT)uni.setStorageSync(KEY,DATA)
删除缓存对应uni.removeStorage(OBJECT)uni.removeStorageSync(KEY,DATA)
清除本地缓存对应uni.clearStorage(OBJECT)uni.clearStorageSync(KEY,DATA)

7.uniApp中web-view通信

web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面(nvue 使用需要手动指定宽高)。

uni.postMessage(OBJECT)
网页向应用发送消息,在 的 message 事件回调 event.detail.data 中接收消息。

App端web-view的扩展
每个vue页面,其实都是一个webview,而vue页面里的web-view组件,其实是webview里的一个子webview。这个子webview被append到父webview上。

通过以下方法,可以获得这个web-view组件对应的js对象,然后参考https://www.html5plus.org/doc/zh_cn/webview.html#plus.webview.WebviewObject,可以进一步重设这个web-view组件的样式,比如调整大小

<template>
	<view>
		<web-view src="https://www.baidu.com"></web-view>
	</view>
</template>
<script>
var wv;//计划创建的webview
export default {
	onReady() {
		// #ifdef APP-PLUS
		var currentWebview = this.$scope.$getAppWebview() //此对象相当于html5plus里的plus.webview.currentWebview()。在uni-app里vue页面直接使用plus.webview.currentWebview()无效
		setTimeout(function() {
			wv = currentWebview.children()[0]
			wv.setStyle({top:150,height:300})
		}, 1000); //如果是页面初始化调用时,需要延时一下
		// #endif
	}
};
</script>

8.uniApp运行在vscode

vue create -p dcloudio/uni-preset-vue uni_vue2_cli创建项目,然后用默认模板

9 .uniApp条件编译

以 #ifdef 或 #ifndef 加 %PLATFORM% 开头,以 #endif 结尾。

#ifdef:if defined 仅在某平台存在
#ifndef:if not defined 除了某平台均存在
%PLATFORM%:平台名称
例如:

条件编译写法说明
#ifdef APP-PLUS需条件编译的代码#endif仅出现在 App 平台下的代码
#ifndef H5需条件编译的代码#endif除了 H5 平台,其它平台均存在的代码(注意if后面有个n)
#ifndef vue2需条件编译的代码#endifuni-app js引擎版用于区分vue2和3
#ifndef vue3需条件编译的代码#endifuni-app js引擎版用于区分vue2和3

十、移动端和小程序

1.请谈谈wxml与标准的html的异同?

wxml 是微信小程序的一种页面渲染语言,类似于 HTML,但也有一些不同之处。

以下是 wxml 与标准的 HTML 的异同:

相同点:
两者都是页面渲染语言,用于描述网页的结构和内容。
两者都使用标签来组织内容。
两者都支持使用 CSS 样式表来控制页面的外观和布局。
两者都支持事件处理,可以通过绑定事件来响应用户的操作。

不同点:
标签不同:wxml 中的标签更多地与微信小程序的 API 相关,比如 、 、 等,而标准的 HTML 则更多地包含一些常见的元素,比如

、 、 等。
属性不同:wxml 的标签属性与 HTML 的标签属性也有一些不同,比如 wxml 中的 wx:if、wx:for、{{}} 表达式 等属性是专门为微信小程序开发设计的,而 HTML 中则没有这些属性。
样式不同:wxml 中的样式是使用 wxss(微信小程序样式表) 来定义的,而 HTML 中则是使用标准的 CSS 样式表来定义的。
盒子模型不同:wxml 中的盒子模型与标准的 HTML 盒子模型也略有不同,主要体现在盒子的宽度和高度的计算方式上。
小程序运行在JS Core内,没有DOM树和windiw对象,小程序中无法使用window对象和document对象。
总的来说,wxml 和 HTML 相似,但也有自己的特点和差异,需要根据具体的开发需求来选择使用哪种语言。

2.请谈谈WXSS和CSS的异同?

WXSS(WeChat Style Sheet)是微信小程序的样式表语言,它与标准的 CSS(Cascading Style Sheets)有以下异同:

相同点:
两者都用于定义页面元素的样式,包括颜色、字体、布局、边框等方面。
两者都支持选择器、属性、值等基本语法。
两者都支持继承、层叠等特性。

不同点:
单位不同:CSS 使用像素(px)、百分比(%)等单位来表示长度或大小,而 WXSS 使用 rpx(微信小程序专用的长度单位),rpx 会根据屏幕宽度进行自适应调整,适应不同设备的屏幕尺寸。
可用属性不同:WXSS 与 CSS 支持的属性并不完全一致,例如 WXSS 中有专门针对微信小程序的一些属性,如 text-decoration-line(下划线)、-webkit-line-clamp(文本行数)等。
属性值不同:WXSS 与 CSS 中某些属性的取值方式有所不同,例如 WXSS 中的 color 属性可以使用全局变量 $color 来表示颜色,而 CSS 中没有这样的机制。
选择器不同:WXSS 中的选择器与 CSS 中的选择器有所不同,例如 WXSS 中的 ::after 伪元素不支持,但是支持一些特定的微信小程序的选择器,如 page、view 等。
总的来说,WXSS 与 CSS 在使用上有一些不同,但是基本的语法和概念都是相似的。如果你已经熟悉了 CSS,那么上手 WXSS 也应该相对容易。

3.bindtap和catchtap的区别?

bind事件绑定不会阻止冒泡事件向上冒泡
catch事件绑定可以阻止冒泡事件向上冒泡
在微信小程序中,bindtap 和 catchtap 都是用来绑定点击事件的属性,它们的主要区别在于事件冒泡和阻止冒泡的处理方式。

bindtap 属性用于绑定一个点击事件处理函数,当点击事件发生时,该处理函数会被触发执行。如果在事件处理函数中调用 event.stopPropagation() 方法来阻止事件冒泡,则该事件不会向父级元素传递。
catchtap 属性也用于绑定一个点击事件处理函数,但与 bindtap 不同的是,当点击事件发生时,该处理函数先于父级元素的事件处理函数执行,如果在事件处理函数中调用 event.stopPropagation() 方法来阻止事件冒泡,则该事件不会向父级元素传递。
因此,bindtap 和 catchtap 的主要区别在于事件冒泡和阻止冒泡的处理方式,如果希望点击事件能够向上冒泡并被父级元素捕获处理,则应该使用 bindtap,如果希望阻止点击事件冒泡,则应该使用 catchtap。

4.小程序和Vue写法的区别?

遍历的时候:小程序 wx:for=“list” ,而Vue是 v-for=“item in list”
调用data模型(赋值)的时候:

小程序:this.data.item // 调用,this.setData({item:1})//赋值

Vue:this.item //调用,this.item=1 //赋值

小程序和Vue在开发模式、语法、组件化等方面有一定的区别,具体如下:

开发模式:小程序需要使用微信开发者工具进行开发和调试,而Vue可以在浏览器中使用webpack等工具进行开发和调试。
语法:小程序使用WXML和WXSS语言,而Vue使用HTML、CSS和JavaScript等技术。小程序的WXML和WXSS语言是为了方便小程序的开发而设计的,它们与HTML、CSS等语言有一些区别,比如标签和属性的命名、样式的定义方式等。
组件化:小程序和Vue都支持组件化的开发方式,但两者的组件化方式有所不同。小程序的组件化主要是通过Component方法进行定义和注册,而Vue则是通过Vue.component方法进行定义和注册。在使用组件时,小程序需要使用组件的名称进行调用,而Vue则是通过组件的标签名称进行调用。
状态管理:在状态管理方面,小程序使用的是原生的数据绑定方式,即通过setData方法进行数据的修改和更新。而Vue使用的是Vue.js提供的响应式数据绑定和Vuex状态管理机制。
总的来说,小程序和Vue在开发模式、语法、组件化等方面有一定的区别,开发者需要根据不同的需求选择合适的技术方案。

5.微信小程序可以进行dom操作吗?

微信小程序中的WXML语言与HTML语言类似,但并不是真正的HTML语言。WXML是一种轻量级的标记语言,它只能用于描述小程序页面的结构,不能进行像HTML一样的DOM操作。

微信小程序的视图层和逻辑层是分离的,WXML负责视图层的渲染,而逻辑层使用JavaScript来处理数据和业务逻辑。逻辑层可以通过setData方法修改视图层中的数据,从而实现动态渲染页面。但是,不能通过JavaScript直接操作DOM元素,因为微信小程序的视图层并没有提供像Web中的document、window等对象,无法直接操作DOM元素。

6.px、rpx、em、rem、rem布局原理

px:Pixel 像素,相对长度单位。像素是相对于显示器分辨率而言。
rpx:单位是微信小程序中css的尺寸单位,rpx可以根据屏幕宽度进行自适应。
em:em是一个相对的长度单位,跟随父级的大小而变化。
rem:rem 也是一个相对的单位,仍是相对大小,但相对的只是HTML根元素。通过rem,既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。

7.em、rem与px间的计算

em:
em和px的之间的相互转换: 任意浏览器的默认字体高都是16px。所有未经调整的浏览器都符合: 1em=16px。那么12px=0.75em,10px=0.625em。 为了使用方便,用em时,我们通常在CSS中的body选择器中声明font-size=62.5%(使em值变为 16px*62.5%=10px), 之后,你只需要将你使用的px值除以10,即可得到em值,如:12px=1.2em, 10px=1em。

rem:
rem是相对单位,它是相对于根元素的字体大小来计算的,这样就意味着,我们只需要在根元素确定一个参考值,这个参考值设置为多少,完全可以根据您自己的需求来定。除了IE8及更早版本外,所有浏览器均已支持rem。
参考设计公式:
html的 font-size= 移动设备 / 设计稿宽度 * 100 = 100px,那么 1rem = 100px

例如你的设计宽度是750,设备页面宽度也是750那你1rem就是100px,html字体font-size就设计为100px。

function rem(){
	//根据设备的宽设置html的字体大小
	// 设备宽度document.documentElement.clientWidth
	// 设计稿宽度designWidth
	document.documentElement.style.fontSize = document.documentElement.clientWidth/designWidth*100 + "px";
}
rem();
//当窗口改变时,调用方法改变rem比例
window.onresize = rem;

换算插件:
1).vscode的cssrem,代码内自动提示换算。
2).插件postcss-pxtorem,直接在vue-config内写入如下,然后写代码的时候,直接用px写,他会在编译的时候自动转换为rem

css: {
    loaderOptions: {
      postcss: {
        postcssOptions: {
          plugins: [
            require('postcss-pxtorem')({ // 把px单位换算成rem单位
              rootValue: 37.5, // vant官方使用的是37.5
              selectorBlackList: ['vant', 'mu'], // 忽略转换正则匹配项
              propList: ['*']
            })
          ]
        }
      }
    }
  }

注意:
1.我看了网上很多关于rem的资料,基本都说浏览器的默认字号就是16px,然后直接定义font-size:62.5%。但是,rem属于css3的属性,有些浏览器的早期版本和一些国内浏览器的默认字号并不是16px,那么上面的10/16换算就不成立,直接给html定义font-size: 62.5%不成立。
2.chrome强制字体最小值为12px,低于12px按12px处理,那上面的1rem=10px就变成1rem=12px,出现偏差(下面给demo)。

解决方案: 将1rem=10px换为1rem=100px(或者其它容易换算的比例值);不要在pc端使用rem。

那么页面根元素样式要改为:
html {font-size: 625%; /100 ÷ 16 × 100% = 625%/}

8.ios的底部回弹(橡皮筋效果)处理

1.用fixed定位来解决
解决思路:这个就是把html,body设置width: 100%; height: 100%; position: fixed;top:0;left:0;
注意问题:测试在微信内生效,嵌入到webview中失效,其他还没测,测好了在更。

2.阻止body的touchmove事件
但是事件是冒泡的,阻止了body的touchmove事件,如果其他有元素需要滑动该怎么办呢?
解决思路:
1).首先给需要滑动的元素加一个touchmove事件,事件触发的时候给event设置一个属性isSCROLL为true;
2).然后给body加touchmove事件,触发事件时判断event的isSCROLL属性是否为true,否的话就禁止默认事件
3).是的话代表点击的事件源为需要滑动的元素,判断其滑动的最高点和最低点加一个限制就ok了,类似碰壁

var ios = navigator.userAgent.indexOf('iphone');//判断是否为ios
 
if(ios == -1){  
    //ios下运行
    var divEl = ....//你需要滑动的dom元素
    iosTrouchFn(divEl);
}
 
function iosTrouchFn(el) {
    //el需要滑动的元素
    el.addEventListener('touchmove',function(e){
        e.isSCROLL = true;
    })
    document.body.addEventListener('touchmove',function(e){
        if(!e.isSCROLL){
            e.preventDefault(); //阻止默认事件(上下滑动)
        }else{
            //需要滑动的区域
            var top = el.scrollTop; //对象最顶端和窗口最顶端之间的距离 
            var scrollH = el.scrollHeight; //含滚动内容的元素大小
            var offsetH = el.offsetHeight; //网页可见区域高
            var cScroll = top + offsetH; //当前滚动的距离
 
            //被滑动到最上方和最下方的时候
            if(top == 0){
                top = 1; //0~1之间的小数会被当成0
            }else if(cScroll === scrollH){
                  el.scrollTop = top - 0.1;
            }
        }
    }, {passive: false}) //passive防止阻止默认事件不生效
}
 

9.移动端手机的底部导航栏遮挡处理并适配

通常来说,IOS 设备在页面顶部和底部都会预留出一定的安全区域,底部安全区域的高度和设备尺寸、系统版本等相关,一般在 34~44px 之间。
解决方案:
1.使用 viewport meta 标签
在 HTML 的 head 标签中添加 viewport meta 标签,以适配不同设备的屏幕大小,并启用安全区域填充模式。

<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">

其中,width=device-width 表示页面宽度等于设备宽度,initial-scale=1.0 表示默认的页面缩放比例为
,viewport-fit=cover 表示页面会填充整个屏幕,包含不同设备的安全区域。
为了兼容不支持 CSS 变量的浏览器,可以使用 fallback 方案,例如:

body {
  padding-top: constant(safe-area-inset-top);  /* iOS */
  padding-top: env(safe-area-inset-top);        /* Android */
  padding-bottom: constant(safe-area-inset-bottom); /* iOS */
  padding-bottom: env(safe-area-inset-bottom);       /* Android */
}

其中,constant 表示兼容 iOS 设备,而 env 表示兼容 Android 设备。

2.通过 document.documentElement.clientHeight 获取浏览器的可视高度,然后使用document.getElementById(‘Top’).style.height 设置总高度。

10.如何解决 Android浏览器查看背景图片模糊的问题?

这个问题是 devicePixelRatio的不同导致的,因为手机分辨率太小,如果按照分辨率来显示网页,字会非常小,所以苹果系统当初就把 iPhone4的960×640像素的分辨率在网页里更改为480×320像素,这样 devicePixelRatio=2。

而 Android的 device PixelRatio比较乱,值有1.5、2和3。

为了在手机里更为清晰地显示图片,必须使用2倍宽高的背景图来代替img标签(一般情况下都使用2倍)。

例如一个div的宽高是100px×100px,背景图必须是200px×200px,然后设置 background-size;contain样式,显示出来的图片就比较清晰了。

11.如何解决移动端 click事件有300ms延迟的问题?

300ms延迟导致用户体验不好。为了解决这个问题,一般在移动端用 touchstart、 touchend、 touchmove、tap(模拟的事件)事件来取代 click事件

12.如何解决移动端HTML5音频标签audio的 autoplay属性失效问题?

因为自动播放网页中的音频或视频会给用户带来一些困扰或者不必要的流量消耗,所以苹果系统和 Android系统通常都会禁止自动播放和使用 JavaScript的触发播放,必须由用户来触发才可以播放。

解决这个问题的代码如下。​​​​​​​

document addEventListener (' touchstart', function( ) {
//播放音频
document .getElementsByTagName ('audio ) [0]. play ( );
//暂停音频
document getElementsByTagName ('audio) [0]. pause ( );
});

13.移动端为什么使用2倍图甚至3倍图?

因为设备像素比的存在
设备像素比:
设备物理像素和设备独立像素之间的比率。设备像素比= 物理像素/ 设备独立像素
我们开发时候的1px 不是一定等于1个物理像素的

例如 在 iPhone8里面 1px 开发像素 = 2个物理像素

  • 设备物理像素: 是一个物理概念,比如设备的分辨率,Phone 5的分辨率640 x 1136px。
  • 设备独立像素dips :
    是一个抽象像素,用于向CSS中的宽度、高度、媒体查询和meta 的viewport
    中的device-width提供信息。通过观察retina和非retina设备之间的区别,可以最好地解释它们。
  • CSS像素:
    是Web编程的概念,指的是CSS中使用的逻辑像素。在CSS规范中,长度单位可以分为两类,绝对(absolute)单位以及相对(relative)单位。px是一个相对单位,相对的是设备物理像素。
    比如iPhone 5使用的是Retina屏幕,使用2px x 2px的设备物理像素,代表 1px x 1px 的
    CSS像素,所以设备物理像素为640 x 1136px,而CSS逻辑像素数为320 x 568px。

正是因为设备像素比这个东西,比如说2倍图 ,1个css逻辑像素面积对应手机屏幕4个像素物理像素的面积,一张图片的width,height分别*2放到手机端为什么就更加清晰呢?

原因:假设在一张图片显示大小由CSS设定为长宽各100x100px,由于设备像素比的存在,实际显示大小是200x200物理像素,那么图片也需要200x200px(两倍图)才能正好对应物理像素。

十一、H5疑难杂点和兼容性问题

1.ios系统iphoneX等机型底部小横条处理

// 1.首先要在meta标签中加上viewport-fit=cover
<meta name="viewport" content="width=device-width, viewport-fit=cover, xxxx">
// 2.整个内容区域
html,body {
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}
// 3.如有底部元素采用固定定位
.fixed{//固定元素类名
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
  background-color: #fff; // 另外background-color要有背景色的,不然会出现透明镂空的情况
}

2.vant登录重定向后出现跳转两次,左滑退出程序后ios出现白屏问题

原因:产生了一个空白页

方法:判断是iOS系统时,利用路由守卫实现一个退一步的操作

3、vant底部导航ios较新机型出现上下浮动问题

解决方法:index.html中设置overflow:hidden,然后在APP.vue中设置overflow:auto

4.点击输入框进入新的一页直接弹出键盘,安卓可以使用input自带属性,ios需模拟点击事件

ios键盘唤起,键盘收起以后页面不归位
给父盒子添加focusout,手动调整页面

   <div class="search between_center" @focusout="inputBlur">
          <input
            type="text"
            v-model="searchValue"
            placeholder="请输入购买平台"
          />
          <span @click="handleSearch"> 搜索</span>
          <van-icon name="search" class="searchIcon" />
        </div>
    /**
     * @method 通过focusout事件解决IOS键盘收起时界面不归位的问题
     */
    inputBlur(e) {
      if (
        e &&
        e.target &&
        e.target.tagName &&
        e.target.tagName.toLowerCase() === "input"
      ) {
        window.scrollTo(0, 0);
      }
    },

5. html5调用安卓或者ios的拨号功能

html5提供了自动调用拨号的标签,只要在a标签的href中添加tel:就可以了。如下:

< a href=" ">400-810-69991034</ a>

拨打手机如下:

< a href="tel:15677776767">点击拨打 15677776767 </ a>

6. 上下拉动滚动条时卡顿、慢

如果你对某个div或模块使用了overflow: scroll属性,在iOS系统的手机上浏览时,则会出现明显的卡顿现象。但是在android系统的手机上则不会出现该问题。以下代码可解决这种卡顿的问题:-webkit-overflow-scrolling: touch;,是因为这行代码启用了硬件加速特性,所以滑动很流畅。这个方法的确可以解决ios5.0、android4.0以后系统的滑动卡顿问题。

body {
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}

7.圆角bug

某些 Android 手机圆角失效

background-clip: padding-box;

或者:

 <style>
     .button {
         width: 50px;
         height: 50px;
         border-radius: 5px;
         -webkit-appearance: none;

     }
 </style>
<input type="button" value="按钮" class="button">

8. ios 设置input 按钮样式会被默认样式覆盖

解决方式如下:

input,textarea {
border: 0;
-webkit-appearance: none;
}

设置默认样式为 none

9.IOS键盘字母输入,默认首字母大写

解决方案,设置如下属性

<input type="text"autocapitalize="off"/>

10.h5底部输入框被键盘遮挡问题

h5页面有个问题是,当输入框在最底部,点击软键盘后输入框会被遮挡。

解决办法:

  1. 由于弹起输入法,会执行onresize 事件,根据窗口变化,将原先是固定定位的元素改为position:static;。当关闭输入法时再切换回position:absolute;。
var getHeight = $(document).height();
$(window).resize(function(){
 if($(document).height() < getHeight) {
  $('#footer').css('position','static');
 }else {
  $('#footer').css('position','absolute');
 }
});
  1. Android 下解决起来比较简单,我们可以借助 Element.scrollIntoView() 这个方法来解决。顾名思义,这个方法的作用就是让当前的元素滚动到浏览器窗口的可视区域内,正是我们想要的效果;IOS下,借助 document.body.scrollTop 来实现,或者直接让它的值为页面高度document.body.scrollHeight就行了
const ua = navigator.userAgent;
const iOS = /iPad|iPhone|iPod/.test(ua);
const input = document.querySelector('#input');

input.addEventListener('focus', () => {
  setTimeout(() => {
    if (iOS) {
 		document.body.scrollTop = document.body.scrollHeight;   
    } else {
        input.scrollIntoView(false);
    }
  }, 300);
});

11.IOS移动端click事件300ms的延迟响应

解决方案:

1、fastclick可以解决在手机上点击事件的300ms延迟

2、zepto的touch模块,tap事件也是为了解决在click的延迟问题

3、触摸事件的响应顺序为touchstart --> touchmove --> touchend --> click,也可以通过绑定ontouchstart事件,加快对事件的响应,解决300ms延迟问题

12.CSS动画页面闪白,动画卡顿

解决方法:

1.尽可能地使用合成属性transform和opacity来设计CSS3动画,不使用position的left和top来定位

2.开启硬件加速

 -webkit-transform: translate3d(0, 0, 0);
 -moz-transform: translate3d(0, 0, 0);
 -ms-transform: translate3d(0, 0, 0);
  transform: translate3d(0, 0, 0);

13.首屏白屏问题

解决方法:

1. 路由懒加载
SPA 项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面打包成一个文件,当用户打开首页时,会一次性加载所有的资源,造成首页加载很慢,降低用户体验

将路由全部改成懒加载

// 通过webpackChunkName设置分割后代码块的名字
const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue");
const MetricGroup = () => import(/* webpackChunkName: "metricGroup" */ "@/views/metricGroup/index.vue");
…………
const routes = [
    {
       path: "/",
       name: "home",
       component: Home
    },
    {
       path: "/metricGroup",
       name: "metricGroup",
       component: MetricGroup
    },
    …………
 ]

2. 组件懒加载
如下:用const xx=()=>{}方式引入

<script>
const dialogInfo = () => import(/* webpackChunkName: "dialogInfo" */ '@/components/dialogInfo');
export default {
  name: 'homeView',
  components: {
    dialogInfo
  }
}
</script>

3. 合理使用 Tree shaking
究其原因,export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效
这也是函数式编程越来越火的原因,因为可以很好利用 tree-shaking 精简项目的体积,也是 vue3 全面拥抱了函数式编程的原因之一

4. 骨架屏优化白屏时长
如以 vue-skeleton-webpack-plugin 插件为例,该插件的亮点是可以给不同的页面设置不同的骨架屏,
5. 长列表虚拟滚动

虚拟滚动——指的是只渲染可视区域的列表项,非可见区域的不渲染,在滚动时动态更新可视区域,该方案在优化大量数据渲染时效果是很明显的

虚拟滚动基本原理:

计算出 totalHeight 列表总高度,并在触发时滚动事件时根据 scrollTop 值不断更新 startIndex 以及 endIndex ,以此从列表数据 listData 中截取对应元素

虚拟滚动的插件有很多,比如 vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized
6. 图片压缩

14.轮播图提前占位

height:0;
font-size:0;
padding-bottom:33%

15.在PC端隐藏html右侧默认滚动条

  1. 在PC端隐藏html右侧默认滚动条
html {
        /*隐藏滚动条,当IE下溢出,仍然可以滚动*/
        -ms-overflow-style:none;
        /*火狐下隐藏滚动条*/
        scrollbar-width: none;
    }
  /*Chrome下隐藏滚动条,溢出可以透明滚动*/
html::-webkit-scrollbar{width:0px}
  1. 移动端隐藏滚动条

1)给滚动条的部分设置宽高为100%, overflow-y: auto;

2)设置滚动条的部分:

::-webkit-scrollbar{
	width: 0;
	display:none;
}

16.在H5 ios端日期数据不支持’-'显示 yyyy-MM-dd HH:mm

解决方式:
yyyy-MM-dd HH:mm 转为 yyyy/MM/dd HH:mm

17.H5开发在微信浏览器上,出现“白屏”问题

大致导致白屏的原因有以下几种:

  1. 重定向次数过多
    原因:重定向次数太多,使用vue的$route.query取参会导致取不到参数。
    解决方案:使用原生js,解析获取参数
	const getURLParameters = url =>
       (url.match(/([^?=&]+)(=([^&]*))/g) || []).reduce((a, v) => {
         a[v.slice(0, v.indexOf('='))] = v.slice(v.indexOf('=') + 1)
         return a
       }, {});`
	  const urlParams = getURLParameters(location.href)
      urlParams.headimgurl = decodeURIComponent(urlParams.headimgurl)
      this.openid = urlParams.openid
      this.headimgurl = urlParams.headimgurl
  1. 微信浏览器缓存机制
    解决方案:在渲染页面前加上一个随机参数
	let timeStamp = new Date()
    window.location.href = window.location.href + '?timeStamp=' + timeStamp.getTime()

十二.webpack和vite

1.什么是webpack? loader是什么?

webpack官方解释:
Webpack 是一个打包模块化 javascript 的工具,在 Webpack 里一切文件皆模块,通过
loader 转换文件,通过 plugin 注入钩子,最后输出由多个模块组合成的文件,Webpack 专注构建
模块化项目,Webpack 可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript 模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript 等),并将其打包为合适的格式以供浏览器使用。(从两点概括这句话即模块和打包)

loader:
Webpack 只能直接处理 JavaScript 代码,任何非 JavaScript 文件,都必须被预先处理为 JavaScript 代码,才可以参与打包
Loader(加载器)就是这样一个代码转换器
它由 Webpack 的 loader runner 执行调用,接收原始资源数据作为参数
当多个加载器联合使用时,上一个 Loader 的结果,会传入下一个 Loader
最终输出 JavaScript 代码(和可选的 source map)给 Webpack 做进一步编译

相同优先级的 Loader 执行顺序为:从右到左,从下到上
举个栗子~
use: [‘loader1’, ‘loader2’, ‘loader3’],执行顺序为 loader3 → loader2 → loader1

webpack如何多环境配置
通过process.env.NODE_ENV,这里我们用cross-env设置,因为他是跨平台的。然后在pack.json里面配置相对应的环境就可以了。

2.什么是webpack多环境打包

1、通过环境变量区分
webpack --env.production

webpack.config.js中判断env

打包命令:webpack --env=production

module.exports = (env, argv) => {
	if(env.production){
      //生产环境的配置
	}else{
	  //开发环境的配置
	}
}

2、通过不同的配置文件区分
cnpm install webpack-merge -D

创建三个文件:webpack.base.config.js、webpack.dev.config.js, webpack.prod.config.js

2.1 webpack.base.config.js
存放基础的配置文件,

2.2 webpack.dev.config.js

// 开发配置文件

const { merge }  = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const devWebpackConfig = merge(baseWebpackConfig, {
  mode: 'development',
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.ejs',
      title: '我是测试的title'
    }),
  ]
})

module.exports = devWebpackConfig

2.3 webpack.prod.config.js

// 生产环境配置文件

const { merge }  = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.config')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

const prodWebpackConfig = merge(baseWebpackConfig, {
  mode: 'production',
  plugins: [ 
    // 压缩css
    new OptimizeCssAssetsPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.ejs',
      title: '我是测试的title',
      minify: {
        collapseWhitespace: true,
        keepClosingSlash: true,
        removeComments: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true
      }
    }),
  ]
})

module.exports = prodWebpackConfig

打包:webpack --config .\webpack.dev.conf.js

2.4 修改package.json

 "scripts": {
    "dev": "webpack serve --mode development",
    "build:dev": "webpack --config ./webpack.dev.conf.js",
    "build:prod": "webpack --config ./webpack.prod.conf.js"
  },

3、Webpack DefinePlugin(内置函数)

为配置注入全局变量,例如:开发环境和生产环境的接口地址不同,我们可以使用webpack-definePlugin,定义不同的变量。

//引入webpack
const webpack = require('webpack')

plugins: [
    new webpack.DefinePlugin({
      // 变量值要求的是一个代码片段
      API_BASE_URL: JSON.stringify('http://XXX.com')
    }),
]

在入口文件中使用:

// eslint-disable-next-line
console.log("==========API_BASE_URL===========", API_BASE_URL)

十三.MySQL和moogodb

1.MongoDB的原理和存储结构?

原理:MongoDB 是一种流行的 NoSQL 数据库,它使用文档模型来存储数据,这些文档是由键值对组成的数据结构,类似于 JSON。

存储结构:MongoDB的存储结构区别于传统的关系型数据库,它由**文档(Document)、集合(Collection)和数据库(Database)**三个主要单元组成。文档是MongoDB中最基本的单元,类似于关系型数据库中的行(Row),而集合则相当于表(Table),数据库则包含多个集合,类似于关系型数据库中的数据库(Database)。

十四.封装

1.Vue——发送http请求详解和封装?

1)vue本身不支持发送AJAX请求,需要使用vue-resource、axios等插件实现。
2) axios是一个基于Promise的HTTP请求客户端,用来发送请求

封装axios进行调用

/****   request.js   ****/
// 导入axios
import axios from 'axios'
// 使用element-ui Message做消息提醒
import { Message} from 'element-ui';

//1. 创建新的axios实例,
const service = axios.create({
  // 公共接口--这里注意后面会讲
  baseURL: '',
  // 超时时间 单位是ms,这里设置了3s的超时时间
  timeout: 3 * 1000
})
// 2.请求拦截器
service.interceptors.request.use(config => {

  //发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等,根据需求去添加
  config.data = JSON.stringify(config.data); //数据转化,也可以使用qs转换
  console.log('请求拦截器中',config)
  config.headers = {
    'Content-Type':'application/x-www-form-urlencoded' //配置请求头
  }
  //注意使用token的时候需要引入cookie方法或者用本地localStorage等方法,推荐js-cookie

  // const token = getCookie('名称');//这里取token之前,你肯定需要先拿到token,存一下

  // if(token){
  //   config.params = {'token':token} //如果要求携带在参数中
  //   config.headers.token= token; //如果要求携带在请求头中
  // }

  return config
}, error => {
  console.log('错误')
  Promise.reject(error)
})

// 3.响应拦截器
service.interceptors.response.use(response => {
  //接收到响应数据并成功后的一些共有的处理,关闭loading等

  return response
}, error => {
  console.log('error',error)
  /***** 接收到异常响应的处理开始 *****/
  if (error && error.response) {
    // 1.公共错误处理
    // 2.根据响应码具体处理
    switch (error.response.status) {
      case 400:
        error.message = '错误请求'
        break;
      case 401:
        error.message = '未授权,请重新登录'
        break;
      case 403:
        error.message = '拒绝访问'
        break;
      case 404:
        error.message = '请求错误,未找到该资源'
        // window.location.href = "/"
        break;
      case 405:
        error.message = '请求方法未允许'
        break;
      case 408:
        error.message = '请求超时'
        break;
      case 500:
        error.message = '服务器端出错'
        break;
      case 501:
        error.message = '网络未实现'
        break;
      case 502:
        error.message = '网络错误'
        break;
      case 503:
        error.message = '服务不可用'
        break;
      case 504:
        error.message = '网络超时'
        break;
      case 505:
        error.message = 'http版本不支持该请求'
        break;
      default:
        error.message = `连接错误${error.response.status}`
    }
  } else {
    // 超时处理
    if (JSON.stringify(error).includes('timeout')) {
      Message.error('服务器响应超时,请刷新当前页')
    }
    Message.error('连接服务器失败')
  }
  Message.error(error.message)
  /***** 处理结束 *****/
  //如果不需要错误处理,以上的处理过程都可省略
  return Promise.resolve(error.response)
})
//4.导入文件
export default service

这里封装请求

/****   http.js   ****/
// 导入封装好的axios实例
import request from './request'


const http ={
  /**
   * methods: 请求
   * @param url 请求地址
   * @param params 请求参数
   */
  get(url,params){
    const config = {
      method: 'get',
      url:url
    }
    if(params) config.params = params
    return request(config)
  },
  post(url,params){
    console.log(url,params)
    const config = {
      method: 'post',
      url:url
    }
    if(params) config.data = params
    return request(config)
  },
  put(url,params){
    const config = {
      method: 'put',
      url:url
    }
    if(params) config.params = params
    return request(config)
  },
  delete(url,params){
    const config = {
      method: 'delete',
      url:url
    }
    if(params) config.params = params
    return request(config)
  }
}
//导出
export default http
import http from './http'
//
/**
 *  @parms resquest 请求地址 例如:http://197.82.15.15:8088/request/...
 *  @param '/testIp'代表vue-cil中config,index.js中配置的代理
 */
// let resquest = ""

// get请求
export function getListAPI(resquest,params){
  return http.get(`${resquest}/getList.json`,params)
}
// post请求
export function postFormAPI(resquest,params){
  console.log('发送post请求')
  return http.post(`${resquest}`,params)
}
// put 请求
export function putSomeAPI(resquest,params){
  return http.put(`${resquest}/putSome.json`,params)
}
// delete 请求
export function deleteListAPI(resquest,params){
  return http.delete(`${resquest}/deleteList.json`,params)
}

解决Vue跨域问题:

解决方法:在脚手架中的config下的index.js中
在 dev 的 proxyTable 对象中添加这些属性

// Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
      "/api":{
        target:"https://xin.yuemei.com/V603/channel/getScreenNew/",//接口域名
        changeOrigin:true,//是否跨域
        pathRewrite:{
          "^/api":""//重写为空,这个时候api就相当于上面target接口基准地址
        }
      }
    },

然后请求这里用axios请求
请求的时候前缀api就相当于基准地址了

2.react封装http请求

/**
 * 网络请求配置
 */
import axios from "axios";

axios.defaults.timeout = 100000;
axios.defaults.baseURL = "http://test.mediastack.cn/";

/**
 * http request 拦截器
 */
axios.interceptors.request.use(
  (config) => {
    config.data = JSON.stringify(config.data);
    config.headers = {
      "Content-Type": "application/json",
    };
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

/**
 * http response 拦截器
 */
axios.interceptors.response.use(
  (response) => {
    if (response.data.errCode === 2) {
      console.log("过期");
    }
    return response;
  },
  (error) => {
    console.log("请求出错:", error);
  }
);

/**
 * 封装get方法
 * @param url  请求url
 * @param params  请求参数
 * @returns {Promise}
 */
export function get(url, params = {}) {
  return new Promise((resolve, reject) => {
    axios.get(url, {
        params: params,
      }).then((response) => {
        landing(url, params, response.data);
        resolve(response.data);
      })
      .catch((error) => {
        reject(error);
      });
  });
}

/**
 * 封装post请求
 * @param url
 * @param data
 * @returns {Promise}
 */

export function post(url, data) {
  return new Promise((resolve, reject) => {
    axios.post(url, data).then(
      (response) => {
        //关闭进度条
        resolve(response.data);
      },
      (err) => {
        reject(err);
      }
    );
  });
}

/**
 * 封装patch请求
 * @param url
 * @param data
 * @returns {Promise}
 */
export function patch(url, data = {}) {
  return new Promise((resolve, reject) => {
    axios.patch(url, data).then(
      (response) => {
        resolve(response.data);
      },
      (err) => {
        msag(err);
        reject(err);
      }
    );
  });
}

/**
 * 封装put请求
 * @param url
 * @param data
 * @returns {Promise}
 */

export function put(url, data = {}) {
  return new Promise((resolve, reject) => {
    axios.put(url, data).then(
      (response) => {
        resolve(response.data);
      },
      (err) => {
        msag(err);
        reject(err);
      }
    );
  });
}

//统一接口处理,返回数据
export default function (fecth, url, param) {
  let _data = "";
  return new Promise((resolve, reject) => {
    switch (fecth) {
      case "get":
        console.log("begin a get request,and url:", url);
        get(url, param)
          .then(function (response) {
            resolve(response);
          })
          .catch(function (error) {
            console.log("get request GET failed.", error);
            reject(error);
          });
        break;
      case "post":
        post(url, param)
          .then(function (response) {
            resolve(response);
          })
          .catch(function (error) {
            console.log("get request POST failed.", error);
            reject(error);
          });
        break;
      default:
        break;
    }
  });
}

//失败提示
function msag(err) {
  if (err && err.response) {
    switch (err.response.status) {
      case 400:
        alert(err.response.data.error.details);
        break;
      case 401:
        alert("未授权,请登录");
        break;

      case 403:
        alert("拒绝访问");
        break;

      case 404:
        alert("请求地址出错");
        break;

      case 408:
        alert("请求超时");
        break;

      case 500:
        alert("服务器内部错误");
        break;

      case 501:
        alert("服务未实现");
        break;

      case 502:
        alert("网关错误");
        break;

      case 503:
        alert("服务不可用");
        break;

      case 504:
        alert("网关超时");
        break;

      case 505:
        alert("HTTP版本不受支持");
        break;
      default:
    }
  }
}

/**
 * 查看返回的数据
 * @param url
 * @param params
 * @param data
 */
function landing(url, params, data) {
  if (data.code === -1) {
  }
}


如上,可通过过对axios请求的拦截实现添加公共请求头,token 验证等操作。

请求隔离

import http from '../utils/http';



/**
 * 获取首页列表
 */
function getArticleList(){
  return new Promise((resolve, reject) => {
    http("get",'/article/home/index').then(res => {
      resolve (res);
    },error => {
      console.log("网络异常~",error);
      reject(error)
    })
  }) 
}

export {
   getArticleList
}


react 组件调用如下:

import React, { Component } from "react";
import { getArticleList } from "~/api/blog";

class Home extends Component {
    constructor(props) {
        super(props);
    } 

    componentDidMount() {
       getArticleList().then(
          (res) => {
              console.log("get article response:", res);
          },
         (error) => {
              console.log("get response failed!");
          }
       );
    }

 ......
}


export default Home;

十五.优化

1.首屏优化

首屏加载慢问题分析
首屏在一些必须的文件都加载成功后才开始进行渲染,首屏加载慢的主要耗时就在加载这些必须的文件上,这些必须的文件是

js/app.d796800d.js
js/chunk-vendors.e95de2cc.js
css/app.69fa25fa.css
css/chunk-vendors.53794358.css

解决方案:解决方案就是加快加载这些必须文件

一、路由懒加载
1、使用 vue-router 懒加载解决首次加载时资源过多导致的速度缓慢问题

路由懒加载是一种按需要加载的方式,即需要时再加载。优化方案是将直接导入js变成按需要导入js。app.js中的页面拆分成单独的js文件,按需加载,加速app.js文件的下载速度从而减少首页加载时间。

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

//写法1:
{
	path: "/base/user",
	name: "user",
	component: resolve => require.ensure([], () => resolve(require('@/modules/base/user/User')), 'User'),
	meta: {
		breadcrumb: ["基本信息", "用户管理"]
	}
},
    
//写法2:
{
    path: "/safety",
    component: Layout,
    redirect: "/safety",
    children: [
      {
        path: "/safety",
        component: (resolve) => require(["@/views/safety/index"], resolve),
        name: "Safety",
        meta: { title: "安全管理", icon: "workplace", affix: true },
      },
    ],
},
    
//写法3:
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})

二、CDN加速
在打包后发现chunk-vendor.js 文件占用内存特别大,这里面主要是使用的一些第三方库,例如 vue-router,axios,elementUI ,echarts等文件。
通过在index.html 中直接引入第三方资源来缓解我们服务器的压力,其原理是将我们的压力分给了其他服务器站点。
推荐外部的库文件使用CDN资源:
bootstrap CDN:https://www.bootcdn.cn
Staticfile CDN:https://www.staticfile.org
jsDelivr CDN:https://www.jsdelivr.com
75 CDN:https://cdn.baomitu.com
UNPKG:https://unpkg.com
cdnjs:https://cdnjs.com

以引入vue、vuex、vue-router为例:
第一步 在index.html中引入第三方库:

<!-- 引入Vue.js -->
<script src="https://cdn.staticfile.org/vue/2.4.3/vue.min.js"></script>
<!-- 引入vuex.js -->
<script src="https://cdn.staticfile.org/vuex/3.0.0/vuex.min.js"></script>
<!-- 引入vue-router -->
<script src="https://cdn.staticfile.org/vue-router/3.0.0/vue-router.min.js"></script>

第二步 去vue-config文件中去配置externals,写上你已经在index.html中引用了cdn的库。
在这里插入图片描述
第三步,将所有的引用去掉:

// import Vue from "vue";
// 引入element 组件
// import ElementUI from "element-ui";
// import "element-ui/lib/theme-chalk/index.css";
// import VueRouter from "vue-router";
// import Vuex from "vuex";
// Vue.use(ElementUI);
// Vue.use(VueRouter);
// Vue.use(Vuex);

三、图片资源压缩以及使用图片懒加载
对于图片较多的页面来说,图片的大小对于页面加载速度的影响十分明显。所以在项目上线之前,一般都要将图片压缩一下

CSS雪碧图 :使用雪碧图可以把多个图片整合到一张图片中,减少HTTP请求次数。但是当整合图片比较大时,一次加载会比较慢,加载失败会导致多个位置的图片无法正常显示。

转换成base64:将图片数据编码成串字符串,来代替原本图像的地址,图片不需要再进行http请求。(缺点:无法缓存;转换后会导致CSS文件体积增大,渲染时长时间出现空白屏幕,影响用户体验,而且只可以转化小图片,图片过大不能转)

SVG矢量图:常用于存储图标,只加载一次,图片不需要再进行http请求。图片放大也不会模糊

大图片压缩网站:TinyPNG

四、防止编译文件中出现map文件
在 config/index.js 文件中将productionSourceMap的值设置为false.

五、gzip压缩
前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。
第一步
命令行执行:npm i compression-webpack-plugin -D
第二步
在webpack的dev开发配置文件中加入如下代码:

const CompressionWebpackPlugin = require('compression-webpack-plugin')
plugins: [
   new CompressionWebpackPlugin()
]

2.懒加载和预加载原理

一、懒加载

懒加载也就是延迟加载。 当访问一个页面的时候,先把img元素或是其他元素的背景图片路径替换成一张大小为1*1px图片的路径(这样就只需请求一次,俗称占位图),只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。
为什么要使用懒加载?
很多页面,内容很丰富,页面很长,图片较多。比如说各种商城页面。这些页面图片数量多,而且比较大,少说百来K,多则上兆。要是页面载入就一次性加载完毕。 影响页面打开速度,用户体验也不好。

懒加载的优点是什么?
页面加载速度快、可以减轻服务器的压力、节约了流量,用户体验好

懒加载的原理是什么?
页面中的img元素,如果没有src属性,浏览器就不会发出请求去下载图片,只有通过javascript设置了图片路径,浏览器才会发送请求。 懒加载的原理就是先在页面中把所有的图片统一使用一张占位图进行占位,把正真的路径存在元素的“data-url”(这个名字起个自己认识好记的就行)属性里,要用的时候就取出来,再设置为图片的真实src.

懒加载的实现步骤?

  1. 首先,不要将图片地址放到src属性中,而是放到其它属性(data-src)中。
  2. 页面加载完成后,根据scrollTop判断图片是否在用户的视野内,如果在,则将data-src属性中的值取出存放到src属性中。
  3. 在滚动事件中重复判断图片是否进入视野,如果进入,则将data-original属性中的值取出存放到src属性中。

实现方式

1. lazyload插件
vue-lazyload提供了一个自定义指令v-lazy,可以在img标签或者任何需要设置背景图片的标签上使用它。

<!-- 懒加载img标签 -->
<img v-lazy="imgUrl" />

<!-- 懒加载背景图片 -->
<div v-lazy:background-image="bgUrl"></div>

v-lazy指令接收一个字符串类型的值,表示图片的地址。如果是背景图片,需要在指令后加上:background-image修饰符(注意冒号)。
当页面滚动时,vue-lazyload会检测元素是否进入可视区域,如果是,则替换元素的src或者style属性,从而实现懒加载。
注意:若图片为循环渲染、分页显示,则必须写key值,不然切换页面后页面视图不刷新

2. IntersectionObserve()
IntersectionObserver是是浏览器原生提供的构造函数,使用它能省到大量的循环和判断。这个构造函数的作用是它能够观察可视窗口与目标元素产生的交叉区域。简单来说就是当用它观察我们的图片时,当图片出现或者消失在可视窗口,它都能知道并且会执行一个特殊的回调函数,就可以利用这个回调函数实现需要的操作。
3. html中的loading=“lazy”

<img src="xxx.png" loading="lazy">

loading 属性支持 3 种属性值:

  • auto 浏览器默认的懒加载策略,和不增加这个属性的表现一样
  • lazy 在资源距当前视窗到了特定距离内后再开始加载
  • eager立即加载,无论资源在页面中什么位置 图片必须声明width和height,不然会看到布局发生移动

4. 方法四 滚动监听+scrollTop+offsetTop+innerHeight
如何判断元素是否到达可视区域

  • window.innerHeight 是浏览器可视区的高度;

  • document.body.scrollTop || document.documentElement.scrollTop是浏览器滚动的过的距离;

  • imgs.offsetTop是元素顶部距离文档顶部的高度(包括滚动条的距离);

  • 内容达到显示区域: img.offsetTop <
    window.innerHeight + document.body.scrollTop;

  • 到达可视区域后,imgs[i].src =
    imgs[i].getAttribute(‘data-src’) as string;将data-src属性值赋值给src,实现懒加载

    但在集成过程中发现,由于img父元素标签没有并且无法设置高度,scrollTop始终为0,无法实现动态

5. 滚动监听+getBoundingClientRect()
API:Element.getBoundingClientRect()

//获取所有img标签
const imgs = document.getElementsByTagName('img');

onMounted(() => {
   //用于首屏加载
  lazyLoad();
  //添加滚动事件监听
  document.addEventListener('scroll', throttle(lazyLoad, 500), true);
});
onUnMounted(() => {
  document.removeEventListener('scroll', throttle(lazyLoad, 500), true);
});

// 节流
const throttle = (fn: { apply: (arg0: any, arg1: any[]) => void }, t: number) => {
  let flag = true;
  const interval = t || 500;
  return function (this: any, ...args: any) {
    if (flag) {
      fn.apply(this, args);
      flag = false;
      setTimeout(() => {
        flag = true;
      }, interval);
    }
  };
};

const lazyLoad = () => {
  const offsetHeight = window.innerHeight || document.documentElement.clientHeight;
  Array.from(imgs).forEach(item => {
    const oBounding = item.getBoundingClientRect(); //返回一个矩形对象,包含上下左右的偏移值
    if (0 <= oBounding.top && oBounding.top <= offsetHeight) {
      //     //性能优化 进行判断 已经加载的不会再进行加载
      if (item.getAttribute('alt') !== 'loaded') {
        item.setAttribute('src', item.getAttribute('data-src') as string);
        item.setAttribute('alt', 'loaded');
      }
    }
  });
};

缺点

  • 挂载时需要立刻调用lazyLoad函数,不然首屏不会加载 若需要图片分页,
  • 切换分页后不会也主动加载,需要调用lazyLoad方法
  • 同样需要给图片设置key值,不然分页图片不刷新

二、预加载

预加载即提前加载图片,当用户需要查看时可直接从本地缓存中渲染

为什么要预加载
在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待的时间。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现为一片空白,直到所有内容加载完毕。图片预先加载到浏览器中,访问者便可顺利地在你的网站上冲浪,并享受到极快的加载速度。这对图片画廊及图片占据很大比例的网站来说十分有利,它保证了图片快速、无缝地发布,也可帮助用户在浏览你网站内容时获得更好的用户体验。

预加载的优点
预加载可以说是牺牲服务器前端性能,换取更好的用户体验,这样可以使用户的操作得到最快的反映。

预加载实现方式

方法一:CSS方式实现预加载,隐藏在css的background的url属性里面

#preload-01 { background: url(http://domain.tld/image-01.png) no-repeat -9999px -9999px; }  
#preload-02 { background: url(http://domain.tld/image-02.png) no-repeat -9999px -9999px; }  
#preload-03 { background: url(http://domain.tld/image-03.png) no-repeat -9999px -9999px; }

方法二:JavaScript实现预加载,通过javascript的Image对象设置实例对象的src属性实现图片的预加载

function preloadImg(url) {
    var img = new Image();
    img.src = url;
    if(img.complete) {
        //接下来可以使用图片了
        //do something here
    } else {
        img.onload = function() {
            //接下来可以使用图片了
            //do something here
        };
    }
}

懒加载和预加载的对比
两者都是提高页面性能有效的办法,两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

十六.打包

1.vue-cli3打包

在vue-cli3的项目中,

npm run serve时会把process.env.NODE_ENV设置为development;
npm run build 时会把process.env.NODE_ENV设置为production;
此时只要根据process.env.NODE_ENV设置不同请求url就可以很简单的区分出本地和线上环境。

vue-cli项目下默认有三种模式:
development:在 vue-cli-service serve 时使用。
production:在 vue-cli-service build 和 vue-cli-service test:e2e 时使用。
test:在 vue-cli-service test:unit 时使用。
对应的 process.env.NODE_ENV 值分别为 development、production、test。

mode是某个模块名,如 在src创建 .env.dev 文件,内容:
在这里插入图片描述
在这里插入图片描述
注意自定义的变量名必须以 VUE_APP_ 开头才能被webpack.DefinePlugin 静态嵌入,通过process.env.VUE_APP_xxx 来访问;执行此文件就相当于“进入”dev环境了。

vue-cli-service 默认会读取 env.development文件 ;
vue-cli-service - -mode dev 指定读取env.dev文件;
上图.env.dev中env后的dev就是对应的NODE_ENV的值,也就是NODE_ENV是一个变量
在这里插入图片描述
在这里插入图片描述
–mode 后跟自定义的环境名 如:dev ,也就是VUE_APP_ ENV后的值

2.vue webpack打包

3.uniApp分环境部署和打包

uni-app 通过在package.json文件中增加uni-app扩展节点,可实现自定义条件编译平台。
注意只能扩展web和小程序平台,不能扩展app打包。
package.json扩展配置用法:

{
    "uni-app": {
        "scripts": {
            "dev-h5": {
                "title": "开发版H5",
                "BROWSER": "chrome",
                "env": {
                    "UNI_PLATFORM": "h5",
                    "UNI_BASE_URL": "/host"
                },
                "define": {
                    "DEV-H5": true
                }
            },
            "pre-h5": {
                "title": "测试版H5",
                "BROWSER": "chrome",
                "env": {
                    "UNI_PLATFORM": "h5",
                    "UNI_BASE_URL": "https://test.XXX.XXX.com"
                },
                "define": {
                    "PRE-H5": true
                }
            },
            "prod-h5": {
                "title": "正式版H5",
                "BROWSER": "chrome",
                "env": {
                    "UNI_PLATFORM": "h5",
                    "UNI_BASE_URL": "https://XXX.XXX.com"
                },
                "define": {
                    "PROD-H5": true
                }
            },
        }
    }
}

NODE_ENV 区分

/* eslint-disable no-undef */
let baseUrl = “”;
if (process.env.NODE_ENV === “development”) {
// 开发环境
baseUrl = “http://development”;
} else if (process.env.NODE_ENV === “test”) {
// 测试环境
baseUrl = “http://test”;
} else if (process.env.NODE_ENV === “pre”) {
// 预发布环境
baseUrl = “http://pre”;
} else if (process.env.NODE_ENV === “production”) {
// 正式环境
}

加了上述配置后,hbuilderx中的运行和发行—自定义发行就会有三个菜单。
在这里插入图片描述
在这里插入图片描述
js文件中引用环境变量
不需要再判断NODE_ENV,直接引用即可。

let baseURL = process.env.VUE_APP_BASE_URL
console.log("baseURL:",baseURL )
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值