前端面试题整合

一、HTML篇

1、简述一下你对HTML语义化的理解?

用正确的标签做正确的事情;
HTML语义化让页面内容结构清晰,便于浏览器、搜索引擎解析;
搜索引擎的爬虫依赖HTML标记来确定上下文和关键字的权重,利于SEO;
便于阅读、维护源代码的用户理解;

2、标签上alt和title属性的区别是什么?

alt是给搜索引擎识别,在图片无法显示时的替代文本;title是关于元素的注释,鼠标悬浮元素会显示title;
在IE中alt起到了title的作用,因此alt和title都应补全;兼容各浏览器;

3、行内元素和块元素举例以及区别;

块元素: div、hr、p、h、table、header、footer、ul、ol、video、audio等;
行内元素: span、img、a、b、i、br、big、small、em、sub、sup、input、button、label等;
默认情况下,行内元素只占据自身宽度空间,块元素独占一整行;

4、href和src

href 指定网络资源位置,建立联系让当前元素链接到目标地址;
src 指向外部资源位置,指向的内容应用到当前标签所在位置;

二、CSS篇;

1、介绍一下css盒子模型;

盒子模型有两种,W3C盒子+IE盒子模型;
content(内容)+padding(填充)+margin(边距)+border(边框)
其中IE盒子模型把边框和填充计算成content

2、css选择器优先级

!important > 行内样式style > ID选择器 > 类选择器 > 标签(div、img等) > *通配符 > 继承 > 浏览器默认继承属性

3、垂直居中几种方式

display:flex;align-items:center;
文字:line-height: height;
图片:vertical-align: middle;
absolute top:50%;left:50%; transform: translate(-50%, -50%);

4、简明说一下css link和import的区别和用法

区别:

(1)加载顺序;link标签在页面加载时同时加载CSS文件,而@import是在页面加载完毕后再加载CSS文件。这意味着使用link标签可以并行加载CSS文件,而@import则会阻塞页面的渲染。

(2)兼容性;link标签兼容性更好,几乎支持所有浏览器。而@import在一些较旧的浏览器中可能不被完全支持。

(3)DOM操作;link标签创建的外部CSS文件可以通过JavaScript动态操作DOM来改变样式,而@import引入的CSS文件不可通过JavaScript动态操作DOM来改变样式。

(4)优先级;link标签引入的CSS文件的样式优先级高于@import引入的CSS文件的样式。这是因为link标签在页面加载时即被加载,而@import在页面加载完成后才被加载,所以link标签的样式表具有更高的优先级。

用法:

在HTML中的header中引入<link rel="stylesheet" type="text/css" href="styles.css">
在css中引入@import url("styles.css");

5、定位有几种?

  • static:默认值,元素在正常的文档流中,不会被特别定位。
  • relative:相对于元素在文档流中的初始位置进行定位的。相对定位
  • absolute:相对于最近的已定位(非static)祖先元素定位。绝对定位;
  • fixed:相对于浏览器窗口定位,即使窗口滚动,元素也会停留在指定位置。固定定位;
  • sticky:基于滚动位置在relativefixed定位之间切换。粘性定位;

6、css布局

Flex布局特别适合以下场景:

垂直居中和水平居中:使用 justify-content: center 和 align-items: center 可以轻松实现。
自适应网格布局:通过组合 flex-wrap 和 flex-basis 可以创建自适应网格。
复杂布局的响应式设计:Flex布局能根据屏幕尺寸自动调整子元素的大小和排列方式。
Flex布局提供了一种强大的布局机制,可以简化许多传统的布局任务,尤其是在响应式设计和复杂布局中。

Grid布局适用于需要二维布局的场景,如:

复杂网页布局:使用Grid布局可以轻松地创建复杂的网页布局,包括多栏布局、卡片布局等。
响应式设计:通过结合媒体查询,可以使用Grid布局创建灵活的响应式设计,适应不同屏幕尺寸。
界面设计:在需要精确控制元素位置的界面设计中,Grid布局是非常有用的工具。
CSS Grid布局提供了一种强大的方式来管理复杂的网页布局,使得设计变得更加灵活和高效。

7、有一个元素需要在pc端显示,在移动端隐藏,应该怎么处理?

visible-md-8 hidden-xs

8、使一个属性为display:flex;的父元素中两个子元素各自单独占一行

设置子元素flex-shark:0;

9、盒模型有几种?

css的盒模型有2种,分别为:1、w3c标准的盒子模型(标准盒模型),width和height指的是内容区域的宽度和高度;2、ie标准的盒子模型(怪异盒模型),width和height指的是内容区域、边框、内边距总的宽度和高度。

/* 标准模型 */
box-sizing:content-box;
 /*IE模型*/
box-sizing:border-box;

10、web标准

通俗点来讲就是html(超文本标记语音)、css(层叠样式表)、javascript(脚本语言)

11、样式优先级

  • ‌!important‌:具有最高优先级,用于强制覆盖其它样式。
  • ‌‌内联样式‌:直接在 ‌HTML 元素的 style 属性中定义的样式,优先级高于后面提到的其它选择器。
  • ‌‌ID 选择器‌:通过 #id 定义的样式,其优先级高于 class 选择器和‌标签选择器
  • ‌‌类选择器、属性选择器、‌伪类选择器‌:通过 .class[attribute] 或 :pseudo 定义的样式,其优先级高于标签选择器和伪元素选择器。
  • ‌标签选择器、伪元素选择器‌:通过 tagname 或 ::pseudo 定义的样式,优先级最低。

具体规则

  • ‌!important 规则‌:当多个规则具有相同的优先级时,CSS 会按照样式表中出现的顺序来决定样式的优先级,越后出现的样式会覆盖前面出现的样式。
  • ‌内联样式和外部/内部样式‌:内联样式的优先级高于外部和内部样式。如果外部样式表的引入位置在内部样式的后面,外部样式表中的样式会覆盖内部样式表中相同元素的样式。
  • ‌选择器的具体计算规则‌:Css选择器优先级计算规则是基于选择器中的不同部分给予不同的权重,例如ID选择器权重为100,类选择器和属性选择器权重为10,标签选择器和伪类选择器权重为1等。最终通过计算权重来确定样式的优先级。

12、css-子元素使用浮动(float)属性,父元素可能会高度塌陷(前端浮动后 不撑开父盒子)

解决办法

(1)使用 clear 属性在父元素的最后一个子元素上使用 clear 属性,以确保它不会浮动到浮动元素的旁边。例如:

<div class="parent">
  <div class="float-child">浮动元素</div>
  <div class="clear-child">清除浮动</div>
</div>

.float-child {
  float: left;
}
 
.clear-child {
  clear: both;
}

(2)使用伪元素 ::after 来清除浮动。这种方法比较简洁,不需要额外的 HTML 元素。 

.parent::after {
  content: "";
  display: table;
  clear: both;
}

(3)在父元素上设置 overflow 属性为 auto 或 hidden,这样父元素会自动包含其浮动的子元素。 

.parent {
  overflow: auto;
}

(4)使用 Flexbox 和grid布局,他们能更好的处理浮动。

三、js

1、实现大数相加

第一种:number最大能够精准到53位,对于更大的整数只能用字符串来表示数字然后逐位相加;

function bigNumberAdd(a, b) {
    let num1 = a.split('');
    let num2 = b.split('');
    let result = '';
    let carry = 0;
 
    while (num1.length || num2.length || carry) {
        let val1 = num1.pop() || '0';
        let val2 = num2.pop() || '0';
        let sum = parseInt(val1) + parseInt(val2) + carry;
        carry = Math.floor(sum / 10);
        result = (sum % 10) + result;
    }
 
    return result.replace(/^0+/, ''); // 移除结果前导0
}
 
// 使用示例
let sum = bigNumberAdd('1234567890123456789', '9876543210987654321');
console.log(sum); // 输出相加结果

第二种:使用bigint,在大数后面使用n相加,例如:

2、图片懒加载的原理

监听图片是否在可视区域内,若在就加载图片,若不在则不加载;实现方案: 自定义属性-将图片真实地址 url 存储在自定义属性中,当监听到图片进入可视区域时,将自定义属性值赋值给 img 的 src 属性。

3、什么是前后端分离以及前后端分离带来的问题?

前端与后端分开,提升前后端开发效率,前端根据确定好的接口文档mock js数据,后台根据postman等接口测试工具做接口测试;项目更新维护变简单,各司其职;提高接口复用率,页面加载变得更快;提升服务器资源利用率;

前后端分离带来的问题:跨域问题,根据不同应用场景,后台在返回的请求header设置即可;单点登录问题;

4、mvvm、mvc、mvp?

(1)MVC(Model-View-Controller)

MVC是比较直观的架构模式,最初生根于服务器端的Web开发,后来渐渐能够胜任客户端Web开发,能够满足其复杂性和丰富性。

1.png

MVC模式将应用程序划分为三个部分:

 ● Model: 模型(用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法)

 ● View: 视图(渲染页面)

 ● Controller: 控制器(M和V之间的连接器,用于控制应用程序的流程,及页面的业务逻辑)

MVC特点:

MVC模式的特点在于实现关注点分离,即应用程序中的数据模型与业务和展示逻辑解耦。在客户端web开发中,就是将模型(M-数据、操作数据)、视图(V-显示数据的HTML元素)之间实现代码分离,松散耦合,使之成为一个更容易开发、维护和测试的客户端应用程序。

用户操作->View(负责接收用户的输入操作)->Controller(业务逻辑处理)->Model(数据持久化)->View(将结果反馈给View):

 1、View 传送指令到 Controller ;

 2、Controller 完成业务逻辑后,要求 Model 改变状态 ;

 3、Model 将新的数据发送到 View,用户得到反馈。

(2)MVP(Model-View-Presenter)

MVP是把MVC中的Controller换成了Presenter(呈现),目的就是为了完全切断View跟Model之间的联系,由Presenter充当桥梁,做到View-Model之间通信的完全隔离方向。

2.png

MVP特点:

 ● M、V、P之间双向通信。

 ● View 与 Model之间不通信,都通过 Presenter 传递。Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。

 ● View 非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

 ● Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,这样就可以重用。不仅如此,还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试–从而不需要使用自动化的测试工具。

(3)MVVM(Model-View-ViewModel)

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。如果说MVP是对MVC的进一步改进,那么MVVM则是思想的完全变革。它是将“数据模型数据双向绑定”的思想作为核心,因此在View和Model之间没有联系,通过ViewModel进行交互,而且Model和ViewModel之间的交互是双向的,因此视图的数据的变化会同时修改数据源,而数据源数据的变化也会立即反应到View上。

3.png

总结:

在MVC中,View会直接从Model中读取数据而不是通过 Controller;View和 Controller之间存在多对一关系。

在MVP中,View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部;View和Presenter之间是一对一关系。 

MVVM 模式基本上与 MVP 模式完全一致,唯一的区别是:MVVM采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

5、git reset和git revert的理解和区别?

reset用于回退版本,可以遗弃不再使用的提交。执行遗弃时,需要根据影响的范围而指定不同的参数,可以指定是否复原索引或工作树内容

git revert在当前提交后面新增一次提交,抵消掉上一次提交导致的所有变化。不会改变过去的历史,主要是用于安全地取消过去发布的提交。

他们之间的区别:git reset用于回滚,且head向后移动到指定位置,git revert是直接删除指定的commit,head继续前进,只是新的commit的内容抵消要被revert的内容;如果回退分支的代码还需要使用就gitrevert,如果分支提错了,且不被记录则使用reset

6、说说你对前端工程化的理解

前端工程化是指将前端开发中的设计、开发、测试和部署等环节进行标准化和自动化,以提高开发效率和代码质量,并降低维护成本。包括模块化、自动化构建、自动化测试、自动化部署、规范化管理;

7、const box = {x:10,y:20} object.freeze(box); const shape = box; shape.x = 100; 打印shape的值?

{x:10,y:20}

freeze使得无法添加、删除或修改对象的属性;因此在严格模式下,shape.x=100可能会抛出typeError异常。

8、防抖和节流的区别?请举例

防抖:debounce;n秒内触发,都会重新计时n秒后触发该事件函数;输入框验证、搜索框输入发起请求等;窗口大小监听改变等;

节流:throttle;在n秒内执行一次,在n秒内,多次触发,只执行一次;比如滚动条监听、下拉刷新;

9、js的数据类型

number、string、null、undefined、Boolean、object、symbol、bigint;

10、js小数精度问题

var two   = 0.2
var one   = 0.1
var eight = 0.8
var six   = 0.6
[two - one == one, eight - six == two]
0.2-0.1 //0.1
0.8-0.6 //0.20000000000000007
//答案是[true,false]

//要想后者为true
(0.8-0.6).toFixed(1)//0.2

JS不能很精确地表示小数。当两个浮点数相加或者相减,将有可能会导致精度丢失问题。

相减的数字之间呈现倍数,即为true,反之false。

JS中的小数采用的是双精度(64位)表示的,由三部分组成:符 + 阶码 + 尾数,在十进制中的 1/10,在十进制中可以简单写为 0.1 ,但在二进制中,他得写成:0.0001100110011001100110011001100110011001100110011001……(后面全是 1001 循环)。因为浮点数只有52位有效数字,从第53位开始,就舍入了。这样就造成了“浮点数精度损失”问题。

通过上面分析,我们知道JS利用二进制存储的,所有的小数都可以用a0*(1/2)+a1*(1/4)+a2*(1/8)+…表示,这里(a0,a1,a2…只能取值0或者1),根据这个表达式我们发现0.1,0.2,0.6和0.8都是无限循环小数,但是0.2-0.1=0.1的原因是0.2是0.1的2倍,二进制乘2或者除2,左移或又移一位就行,所以0.2-0.1=0.1,但是0.8不是0.6的倍数,所以0.8-0.6!=0.2。

11、vite和webpack之间的区别

开发模式的不同

webpack在开发模式下会对所有模块进行打包操作,虽然提供了热更新,但大型项目中,依然可能会出现启动、编译缓慢的问题;而vite采用基于es module开发服务器,只有在有需求时编译对应模块,大幅度提升了开发环境的响应速度;

打包效率不同

webpack在打包的时候会把整个项目打包成一个bundle,这会导致初级加载项目速度较慢;而vite利用了浏览器对es module 的原生支持,只打包和缓存实际改动的模块,从而提高了打包效率;

插件生态不同

webpack插件生态非常丰富,有大量社区和官方的插件提供,覆盖了前端各个方面,而vite的插件生态尽管在不断发展,但是跟webpack比较还是显得稀少。

配置复杂度不同

webpack的配置相对来说比较复杂。对新手不够友好,而vite在设计上注重开箱即用,大部分情况下无需自己写配置文件;

热更新机制不同

webpack热更新需要整个模块链更新替换打包,对于大型项目会有延迟,而vite 的更新只针对改动过的模块,提高了热更新的速度;

webpack常用插件

  • HtmlWebpackPlugin‌:用于生成HTML文件。该插件可以根据webpack的entry配置生成相应的HTML文件,并将入口chunk和提取的CSS样式插入到生成的HTML文件中。用户可以通过配置项指定HTML模板和压缩选项等。
  • CleanWebpackPlugin‌:用于清理bundle文件。该插件可以在每次构建前清理输出目录,确保构建的清洁和高效。
  • MiniCssExtractPlugin‌:用于提取CSS。该插件可以将CSS从JavaScript bundle中提取出来,生成独立的CSS文件,便于缓存和压缩。
  • UglifyJsPlugin‌和‌TerserPlugin‌:用于压缩JavaScript代码。这些插件可以帮助压缩和优化JavaScript代码,减少文件大小,提高加载速度。
  • CompressionWebpackPlugin‌:用于启用gzip压缩。该插件可以帮助减少传输数据的大小,提高网页加载速度。

vite常用插件

  • unplugin-vue-components‌:这个插件支持Vue组件的按需自动导入,可以自动导入UI库中的组件,如Ant Design Vue、Vant等。使用该插件后,开发者无需手动导入组件,直接使用即可‌。
  • unplugin-auto-import‌:该插件用于自动导入Composition API,简化代码编写过程,提高开发效率‌。
  • vite-plugin-restart‌:此插件通过监听文件修改来自动重启Vite服务,避免了反复手动重启的麻烦,特别适用于开发过程中频繁修改配置文件的情况‌。
  • Vite插件模板‌:Vite提供了多种模板,如Vite + Vue 3 + Tailwind CSS的入门脚手架模板、基于Vite的Electron应用程序样板等,这些模板能够帮助快速搭建项目框架‌。

12、VUE的生命周期

vue2:beforecreate created  beforemount mounted  beforeupdate updated beforedestroy destroyed activated(使活动的) deactivated(停止工作) errorcaptured

vue3-options API的周期同vue2

vue3-compents API:setup onbeforemount onmounted onbeforeupdate onupdated onbeforeunmount onunmounted onactivated ondeactivated onerrorcaptured onrendertriggered onrendertracked

13、vue3的hooks和vue2的mixin,以及hooks和utils的区别?

vue3的hooks有点类似vue2中的mixin,hooks封装,可将组件的生命周期和状态体现出来,且数据具有响应式;具备可复用功能,才需要抽离为 hooks ,独立文件函数名/文件名以 use 开头,形如: useXX,引用时将响应式变量或者方法显式解构暴露出来;

示例如下:

const{ nameRef, Fn } = useXX() ;

举例:比如鼠标的位置,我们可以进行封装,在各个页面引入,就可以直接获取鼠标位置,减少代码冗余,提高逻辑复用,提高代码可读性和维护性。

hooks的数据具有响应式,而utils是将特定的函数封装起来,数据不具有响应

14、vuex的重要核心属性有哪些?

四大核心属性和一个子模块管理属性,state存放数据,mutatuons同步修改state里面的数据,actions异步调用mutations里的方法修改数据,getters数据过滤,对数据进行整理;state复杂庞大的时候,modules分割模块,每个模块有自己的四大核心属性。

vuex的重要核心属性包括state、getters、mutations、actions、modules。

  • state‌:在Vuex中,state用于定义和管理应用的状态数据,包括数组、对象、字符串等。它是单一状态树,所有组件共享的数据都存储在这里。

  • getters‌:getters类似于Vue的计算属性,用于从state中派生出一些状态,可以理解为state的计算属性。Getters可以接收state作为第一个参数,并且其返回值会根据依赖被缓存,只有当依赖的值发生变化时才会重新计算。

  • mutations‌:mutations是唯一改变state中状态的方法,类似于事件。每个mutation都有一个字符串类型的事件类型和一个回调函数,通过执行store.commit来改变state的值。

  • actions‌:actions可以提交mutations,并且可以包含任何异步操作。在页面中调用action时,需要执行store.dispatch

  • modules‌:当state变得复杂和庞大时,modules可以将store分割成模块,每个模块拥有自己的state、mutations、actions和getters,有助于更好地组织和管理代码。

Vuex通过这些核心属性,提供了一个集中式的管理方式来管理应用的状态数据,使得状态的变化更加可预测和可维护,特别是在构建中大型单页应用时,Vuex成为了一个不可或缺的工具‌

15、pinia是什么?

Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。

  • dev-tools 支持
    • 跟踪动作、突变的时间线
    • Store 出现在使用它们的组件中
    • time travel 和 更容易的调试
  • 热模块更换
    • 在不重新加载页面的情况下修改您的 Store
    • 在开发时保持任何现有状态
  • 插件:使用插件扩展 Pinia 功能
  • 为 JS 用户提供适当的 TypeScript 支持或 autocompletion
  • 服务器端渲染支持

 Pina 和 Vuex 区别如下:

目标:Pina 是一个轻量级的状态管理库,它专注于状态的管理,而 Vuex 是一个完整的状态管理解决方案,它包括了状态管理、副作用处理、模块化等功能。

模块化:Pina 提供了模块化管理状态的功能,可以将状态分解为多个模块,每个模块都可以独立管理状态。而 Vuex 提供了更强大的模块化功能,它允许你将应用分解为多个模块,每个模块都可以独立管理状态。

副作用处理:Vuex 提供了副作用处理的功能,可以处理应用中产生的副作用。Pina 不支持副作用处理。

状态存储:Pina 使用对象来存储状态,而 Vuex 使用对象和数组来存储状态。

API 设计:Pina 的 API 设计更加简洁,它只提供了一些基本的 API 功能,而 Vuex 的 API 设计更加复杂,它提供了更多的 API 功能。

社区支持:Vuex 的社区支持更加广泛,有更多的开发者在使用它,有更多的资源和文档可以参考。Pina 的社区支持相对较小。

        总的来说,Pina 更适合小型应用和简单的状态管理需求,而 Vuex 更适合大型应用和复杂的状态管理需求。

16、vue3性能提升主要通过那几个方面?

(1)Composition API(组合式API)

vue3引入了Composition API,相较于vue2的options API(选项式),使得组件更易于阅读维护和重用。开发者可以根据功能或逻辑相关性将代码组织在一起而不是按照生命周期钩子函数的顺序。

(2)响应式系统的改进

vue3使用了proxy作为底层实现,代替了vue2中的object.defineProperty,提供了更高效更可靠的响应式数据追踪机制。

小tips—监听数组和对象有什么不同:

vue2:defineproperty只能监听某个属性,不能全属性监听,vue2通过重写数组的原型方法来监听数组的变化(无法监控数组下标的变化,导致通过数组下标添加元素不能实时响应)。

vue3:proxy可以对整个对象进行监听,可以拦截对象所有的操作,可以检测到数组内部的变化。

(3)更好的性能

通过优化虚拟dom(静态标记)和模板编译,提升了组件的渲染性能。vue3还引入了静态提升(Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用)、动态内容标记和基于proxy的响应式系统,进一步提高了应用程序的性能。

(4)更小的体积

vue3通过模块化的设计,将其核心库分割成了多个小模块,使得开发者可以按需引入所需功能,从而减少了应用程序的体积,这在移动端和网络条件较差的情况下更有竞争力。

(5)更好的typeScript支持

vue3对ts的支持更加完善,代码库中使用了更多的ts类型注解,提供了更好的类型推导和错误检查。

(6)更好的懒加载支持

vue3默认开启了懒加载机制,可以极大地提升页面加载速度和性能。

(7)更好的开发工具支持

vue3提供了全新的Devtools,具有更多的功能和改进,可以更好的帮助开发者调试和分析应用程序。

17、vue的语法糖以及双向绑定的原理是什么?

vue语法糖:

  • v-model‌:用于实现数据的双向绑定,例如<input type="text" v-model="name">可以简化为<input type="text" v-bind:value="name" v-on:input="name=$event.target.value">
  • v-bind‌:用于动态绑定HTML元素的属性,例如<div :title="title">可以简化为<div v-bind:title="title">
  • v-on‌:用于绑定事件监听器,例如<button @click="btn">可以简化为<button v-on:click="btn">
  • v-if和‌v-show‌:用于条件渲染,v-if根据条件判断显示或隐藏元素,v-show通过CSS的display属性控制显示与隐藏。
  • v-for‌:用于列表渲染,例如v-for="(item, index) in list"可以简写为(item, index) of list

详细解释每种语法糖的具体用法和作用

  • v-model‌:通过绑定input事件实现数据的双向绑定,例如在输入框中输入内容时,会自动更新绑定的数据。
  • v-bind‌:用于动态绑定HTML元素的属性,例如src、href、class、style、title等。
  • v-on‌:用于绑定事件监听器,简化事件监听的写法,例如点击事件可以简写为@click="handleClick"
  • v-if和v-show‌:v-if根据条件判断显示或隐藏元素,v-show通过修改CSS的display属性控制显示与隐藏。
  • v-for‌:用于遍历数组或对象,生成对应的DOM元素。

这些语法糖使得代码更加简洁、易读,提高了开发效率。

双向绑定原理:Vue的双向绑定通过数据劫持和发布-订阅者模式实现,涉及Observer、Watcher和Compiler的协同工作。Observer监控数据变化,Watcher负责更新视图,Compiler解析指令。数据变化时,setter触发更新,通知订阅者,最终更新视图。

数据劫持:Vue组件初始化时,对 data 中的 item 进行递归遍历,会通过Object.defineProperty()方法来劫持对象的属性,这个方法可以对一个对象的属性进行定义或修改,包括定义getter和setter。这样,当这些被劫持的属性被读取或赋值时,就会触发相应的getter或setter函数。

依赖收集:在编译器编译模板的过程中,如果模板中出现了某个数据对象的属性,那么Vue会为这个属性添加一个订阅者,这个过程就是依赖收集。这样,当数据变化时,就可以通知所有依赖于这个数据的订阅者进行更新。

派发更新:当数据发生变化时,会触发setter函数,进而通知所有的订阅者。订阅者收到通知后,就会执行对应的更新函数,从而更新视图。

视图更新:最后,Vue会根据更新函数的指示,对DOM进行操作,从而实现视图的更新。

具体步骤:

第一步: 需要Observer(数据劫持)对数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

第二步: Compiler(订阅者)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

第三步: Watcher(观察者)是Observer和Compiler之间通信的桥梁,主要做的事情是:

1、在自身实例化时往属性订阅器(dep)里面添加自己

2、自身必须有一个update()方法

3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

第四步: MVVM作为数据绑定的入口,整合Observer、Compiler和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

v-model是v-on和v-bind的语法糖

  • v-on用于事件绑定,给当前组件绑定一个input事件。
  • v-bind用于属性绑定,给当前组件绑定一个value属性。

v-model双向绑定的实现:

  • 在父组件内给子组件添加v-model,相当于给组件添加了个value 属性,传递的值就是绑定的值。和绑定了一个此绑定值的input事件。
  • 子组件中使用prop属性接受这个value值,名字必须是value。
  • 子组件内部更改value值的时候,必须通过$emit()事件派发个input事件,并携带最新的值。
  • v-model绑定的input事件会自动监听这个input事件,把接收到的最近新值赋值给v-model绑定的变量。

18、vue3的语法糖

(1)setup语法糖起初 Vue3.0 暴露变量必须 return 出来,template中才能使用。现在只需在script标签中添加setup,组件只需引入不用注册,属性和方法也不用返回,也不用写setup函数,也不用写export default ,甚至是自定义指令也可以在我们的template中自动获得。

(2)使用setup组件自动注册

在 script setup 中,引入的组件可以直接使用,无需再通过components进行注册,并且无法指定当前组件的名字,它会自动以文件名为主,也就是不用再写name属性了

<template>
    <zi-hello></zi-hello>
</template>
​
<script setup>
  import ziHello from './ziHello'
</script

(3)使用setup后新增API

因为没有了setup函数,Setup script语法糖提供了新的API来供我们获取props、emit。

defineProps:用来接收父组件传来的 props;

<template>
 <div class="die">
  <h3>我是父组件</h3>
  <zi-hello :name="name"></zi-hello>
 </div>
</template>
<script setup>  
import ziHello from './ziHello'    
import {ref} from 'vue'  
let name = ref('赵小磊========')
</script>

子组件代码
<template>
 <div>
  我是子组件{{name}} // 赵小磊========
 </div>
</template>
<script setup>  import {defineProps} from 'vue'
 defineProps({
  name:{
   type:String,
   default:'我是默认值'
  }
 })

// 在js中使用则需要用变量接收
let obj = defineProps({
  msg: {
    type: String,
    default: "默认值",
  },
});
onMounted(() => {
  console.log(obj.msg);
});
</script>

defineEmits:子组件向父组件事件传递。

示例:子组件

<template>
    <h2>语法糖子组件</h2>
    <h3>{{msg}}</h3>
    <button @click="tofa">向父级传参</button>
</template>
<script setup>  
import {defineEmits} from 'vue'
​
 //自定义函数,父组件可以触发
 const em = defineEmits(['up'])
const tofa = () => {
    em('up', '我是sugarson') // 通过up方法向父组件传递
}
</script>

父组件

<template>
    <div>
        <h2>语法糖</h2>
       // <son-view :uname="uname" xlj="肖丽君" @up="up"></son-view>
    </div>
</template>
<script setup>  
// 需要在组件中引用子组件中的方法
import SonView from './SonView.vue'    
const up = val => {
    console.log(val) // 子组件传递过来的值
}
</script>

defineExpose:组件暴露出自己的属性,在父组件中可以拿到。

示例:子组件

<template>
 <div>
  我是子组件
 </div>
</template>
​
<script setup>  
import {defineExpose,reactive,ref} from 'vue'  
let ziage=ref(18)  
let ziname=reactive({    name:'赵小磊'  })  
//暴露出去的变量  
defineExpose({    ziage,    ziname  })
</script>

父组件

<template>
    <div>
        <h2>语法糖</h2>
        <h3>{{ myName }}</h3>
        <button @click="fn">提交</button>
        <!-- <check-all-vue></check-all-vue> -->
        <son-view :uname="uname" xlj="肖丽君" @up="up" ref="sonview"></son-view>
    </div>
</template>
 
<script setup>  
import ziHello from './ziHello'  
import {ref} from 'vue'  
const sonview = ref() // 给组件定义一个ref名称, 查找组件sonview  通过sonview找到子组件抛出的值
onMounted(() => {
    console.log('挂载后')
    console.log('接收ref暴漏出来的值',sonview.value.ziage)
  console.log('接收reactive暴漏出来的值',sonview.value.ziname.name)
})

(4)语法糖生命周期

<script setup>
import {onMounted,onUnmounted} from 'vue'
//所有的生命周期用法均为回调函数
onMounted(()=>{
  console.log('我创建了');
})
// 销毁实例变为onUnmounted,与vue2的destroy作用一致
onUnmounted(()=>{
  console.log('我销毁了');
})
</script>

(5)ref获取dom节点

<script setup>
import { ref ,getCurrentInstance,nextTick} from 'vue'
import Header from './Header.vue'
​
const {proxy} = getCurrentInstance()
const rename = ref(null)//声明一个和ref一样的名字,直接获取它
nextTick(()=>{
  console.log(rename.value)
  //console.log(proxy.$refs.rename) 或者直接这样获取
    //要获取子组件的dom节点,通过绑定ref在子组件标签上,子组件获取自己的dom节点,通过defineExpose暴露给父组件
})
</script>
​
<template>
  <div ref="rename"></div>
  <Header/>
  <div></div>
</template>

19、vue3中ref和reactive以及shallowRef和shallowReactive

        相同:均是声明响应式对象。且声明的响应式对象是深层的

  • 数据类型不同:ref用于包装JavaScript基本类型的数据(如字符串、数字、布尔值等),而reactive可以用于包装JavaScript对象和数组等复杂类型的数据。
  • 访问方式不同:对于通过ref函数创建的响应式数据,我们可以通过.value属性来访问其实际值;而对于通过reactive函数创建的响应式对象,我们可以直接访问其属性或调用其方法。
  • 设计理念不同:ref主要是为了解决单一元素/数据的响应式问题,而reactive则是为了解决JavaScript对象和数组等复杂数据结构的响应式问题

shallowRef和shallowReactive是Vue3中定义浅层次响应式数据的方式

// 注意:监听不到变化的数据,在页面中其它数据变化被监听到时,会同步改变

// shallowRef shallowReactive
// 修改浅层数据 正常监听 正常监听
// 修改深层数据 无法监听 无法监听
// 修改对象数据自身 正常监听 无法监听
// 修改数组对象自身 正常监听 正常监听

// 1、shallowRef
// ① 定义基础数据
let sum = shallowRef(0)
sum.value++ // 数据正常监听

// ② 定义对象、数组
// 1、定义对象
let personRef = shallowRef({
    name:'李四',
    age:18,
})
// 修改深层数据
personRef.value.name = '小猪佩奇'; // 监听不到

// 修改浅层数据(自身)
personRef.value = {name:'小猪佩奇',age:2}; // 数据可正常监听

// 2、定义数组
let listRef = shallowRef([1,2,3,4])
// 修改深层数据
listRef.value[0] += 2; // 监听不到
// 修改浅层数据(自身)
listRef.value = [9,99,999]; // 数据可正常监听

// 2、shallowReactive
// ①、定义对象
let personRea = shallowReactive({
    name:'张三',
    age:25,
    children:{
        name:'张XX',
        age:1
    }
})
// 修改浅层次数据
personRea.name = '张思锐'; // 可正常监听
personRea.children = {name:'张Y',age:1.5}; // 可正常监听

// 修改深层次数据
personRea.children.name = '张YY'; // 数据监听不到

// 修改数据本身
Object.assign(personRea,{
    name:'张三三',
    age:26,
    children:{
    name:'张XX',
    age:2
}

let listRea = shallowReactive([
    {a:1},
    {a:2}
])
// 修改浅层次数据
listRea[0] = {a:99}; // 可正常监听

// 修改深层次数据
listRea[0].a += 2; // 监听不到
// 修改数据本身

triggerRef
配合shallowRef一起使用;
可以使用triggerRef(xxx)强制使括号内属性进行同步(即触发一次响应)

import { triggerRef , shallowRef} from 'vue'
let message = shallowRef({
  tags: 'helloworld'
})
const changeTag = () => {
  message.value.tags = 'goodbyeworld'
  triggerRef(message)   // 强制触发响应
}

20、vue组件传值的几种方法

  • Props / $emit:父子组件之间通过props传递数据,通过$emit触发事件来传递数据;
  • vuex:通过Vuex状态管理传递数据,适用于跨多个组件的状态管理。
  • Provide / Inject:父组件提供数据,子组件注入使用,适用于跨层级的组件通信。
  • Event Bus:创建一个事件总线,通过事件触发来传递数据,适用于任意组件间的通信。

数据单向绑定和数据双向

<template>
  <div class="test-v-model">
    <p>使用v-model</p>
    <input v-model="msg" placeholder="edit me" />
    <p>{{ msg }}</p>

    <p>不使用v-model</p>
    <input :value="msg1" @input="handleInput" placeholder="edit me" />
    <p>{{ msg1 }}</p>
  </div>
</template>

<script>
export default {
  name: 'test-v-model',
  data() {
    return {
      msg: '',
      msg1: ''
    }
  },
  methods: {
    handleInput(e) {
      this.msg1 = e.target.value
    }
  }
}
</script>
  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;

  • select 字段将 value 作为 prop 并将 change 作为事件。

在vue中是通过指令 v-model 来实现双向绑定(视图数据变化跟随),通过 v-bind 来实现单向数据绑定(数据变化,视图同步更新,视图变化,数据不会更新)

<template>
  <div>
    <p>双向绑定</p>
    <input v-model="msg" placeholder="edit me" />
    <p>{{ msg }}</p>

    <p>单向数据绑定</p>
    <input v-bind:value="msg1" placeholder="edit me" />
    <p>{{ msg1 }}</p>
  </div>
</template>

<script>
export default {
  name: 'test-v-model',
  data() {
    return {
      msg: '',
      msg1: ''
    }
  }
}
</script>

21、vue2和vue3的知识点

21.1vue2和vue3的区别

Vue 2:

  • 基于Object.defineProperty的响应式系统。
  • 使用虚拟DOM进行高效的diff操作。
  • 使用组件概念构建用户界面。
  • 使用单一的全局状态管理模式。

Vue 3:

  • 基于Proxy的响应式系统,提供了更多的响应式能力。
  • 使用Proxy代替Vue 2中的defineProperty,提供了更广泛的响应式API。
  • Composition API(组合式API),提供了更多灵活性和函数式编程的工具。
  • 引入了Fragment、Teleport、Suspense等组件,提升了组件模板的灵活性。
  • 服务端渲染(SSR)改进,使用Teleport和Suspense提高了非同步内容的渲染质量。
  • 提供了更好的TypeScript支持。

21.2 vue渲染过程

  • 创建Vue实例,提供选项,如datamethodscomputed属性等。
  • Vue实例开始监控data中的数据变化。
  • 使用templaterender函数定义组件的HTML结构。
  • Vue编译模板为render函数,可以是v-node树的形式。
  • render函数产生真实的DOM,并挂载到HTML上。
  • 当数据变化时,Vue追踪变化,重新执行render函数,计算出变化的部分,实现高效更新DOM。

21.3 vue中遇到的算法

  • 数组过滤(Array Filtering)

问题:给定一个数组和一个条件,要求返回满足条件的新数组。解决方案:使用JavaScript的filter方法。

  • 数组映射(Array Mapping)

问题:给定一个数组,对每个元素应用一个函数,返回一个新数组。解决方案:使用JavaScript的map方法。

  • 排序(Sorting)

问题:给定一个数组,对元素进行排序。解决方案:使用JavaScript的sort方法。

  • 搜索(Searching)

问题:在数组中查找特定元素。解决方案:使用indexOffind方法。

  • 动画(Animation)

问题:在Vue中实现组件的进入、离开动画。解决方案:使用<transition>元素和CSS过渡效果。

  • 递归(Recursion)

问题:解决一个问题需要反复调用自身解决更小的子问题。解决方案:确保递归有适当的终止条件,并且不会导致无限循环

21.4 vue-router

问:vue-router 是什么?它有哪些组件?

答:vue-router 是 Vue.js 官方的路由管理器,它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过度系统的视图过渡效果
  • 颗粒度的导航控制
  • 带有自动激活的 CSS class 的连接
  • history模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

vue-router 组件:

  • < router-link to=""> 路由的路径
  • < router-link :to="{name:’‘l路由名’}"> 命名路由
  • < router-view> 路由的显示

问:active-class 是哪个组件的属性?

答:active-class 属于vue-router的样式方法,当routerlink标签被点击时将会应用这个样式。

使用方法一:routerLink标签内使用

<router-link to='/' active-class="active" >首页</router-link>

使用方法二:在路由js文件,配置active-class

<script>
    const router = new VueRouter({
        routes,
        linkActiveClass: 'active'
    });
</script>

在使用时会有一个bug:首页的active会一直被应用

为了解决上面的问题,还需加入一个属性exact,也有两种方式。

方式一:在router-link中写入exact

<router-link to='/' active-class="active" exact>首页</router-link>

方式二:在路由js文件,配置active-class

<script>
    const router = new VueRouter({
        routes,
        linkExactActiveClass: 'active'
    });
</script>

问:怎么定义 vue-router 的动态路由? 怎么获取传过来的值?

可以通过query ,param两种方式,区别:query通过url传参,刷新页面参数还在,params刷新页面参数不在了。

param的类型:

  • 配置路由格式:/router/:id
  • 传递的方式:在path后面跟上对应的值
  • 传递后形成的路径:/router/123
<!-- 动态路由-params -->
 
//在APP.vue中
    <router-link :to="'/user/'+userId" replace>用户</router-link>    
 
//在index.js
     {
    path: '/user/:userid',
    component: User,
    },

跳转方法:

// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link>
// 方法2:
this.$router.push({name:'users',params:{uname:wade}})
// 方法3:
this.$router.push('/user/' + wade)

通过$route.params.参数名 获取你所传递的值

query的类型:

  • 配置路由格式:/router,也就是普通配置
  • 传递的方式:对象中使用query的key作为传递方式
  • 传递后形成的路径:/route?id=123
<!--动态路由-query -->
//01-直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
/*
    02-或者写成按钮以点击事件形式
    <button @click='profileClick'>我的</button>    
*/
 
 //点击事件
 profileClick(){
   this.$router.push({
        path: "/profile",
        query: {
          name: "kobi",
          age: "28",
          height: 198
        }
      });
 }

跳转方法:

// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)

通过$route.query 获取你所传递的值

问:vue-router 有哪几种导航钩子(导航守卫)?

答:第一种:全局导航钩子

(1)前置钩子

//单独设置每个路由的属性:
meta: { may: true }
router.beforeEach((to, from, next) => {
    if (to.matched.some(item => item.meta.may)) {
        let id = window.localStorage.getItem("id")
        if (id) {
            next()
        } else {
            next({ name: "login" })
        }
    } else {
        next()
    }
})
注意:next 方法必须要调用,否则钩子函数无法 resolved

(2)后置钩子

router.afterEach((to,from) => {
	if(to.meta && to.meta.title){
		document.title = to.meta.title
	}else{
		document.title = "666"
	}
})

第二种:单独路由独享钩子

{
    path: '/home',
    name: 'home',
    component: Home,
    beforeEnter(to, from, next) {
        if (window.localStorage.getItem("id")) {
            next()
        } else {
            next({ name: "login" })
        }
    }
}

第三种:组件内的钩子

beforeRouteEnter(to, from, next) {
    // do someting
    // 在渲染该组件的对应路由被 confirm 前调用
},
beforeRouteUpdate(to, from, next) {
    // do someting
    // 在当前路由改变,但是依然渲染该组件是调用
},
beforeRouteLeave(to, from ,next) {
    // do someting
    // 导航离开该组件的对应路由时被调用
}

全局解析守卫

router.beforeResolve 注册一个全局守卫,和 router.beforeEach 类似

可以在src目录下新建一个permission.js文件

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'

NProgress.configure({ showSpinner: false })

const whiteList = ['/login', '/auth-redirect', '/bind', '/register']

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(res => {
          // 拉取user_info
          const roles = res.roles
          store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {
          // 测试 默认静态页面
          // store.dispatch('permission/generateRoutes', { roles }).then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            router.addRoutes(accessRoutes) // 动态添加可访问路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        })
          .catch(err => {
            store.dispatch('FedLogOut').then(() => {
              Message.error(err)
              next({ path: '/' })
            })
          })
      } else {
        next()
        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
        // if (hasPermission(store.getters.roles, to.meta.roles)) {
        //   next()
        // } else {
        //   next({ path: '/401', replace: true, query: { noGoBack: true }})
        // }
        // 可删 ↑
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

nprogress是页面跳转时出现在浏览器顶部的进度条

问:route和router 的区别

答:$route对象:$route对象表示当前的路由信息,包含了当前 URL 解析得到的信息。包含当前的路径,参数,query对象等。

  •  $route.path 字符串,对应当前路由的路径,总是解析为绝对路径,如"/foo/bar"。
  • $route.params 一个 key/value 对象,包含了 动态片段 和 全匹配片段, 如果没有路由参数,就是一个空对象。
  • $route.query 一个 key/value 对象,表示 URL 查询参数。 例如,对于路径 /foo?user=1,则有$route.query.user == 1, 如果没有查询参数,则是个空对象。
  • $route.hash 当前路由的hash值 (不带#) ,如果没有 hash 值,则为空字符串。锚点*
  • $route.fullPath 完成解析后的 URL,包含查询参数和hash的完整路径。
  • $route.matched 数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
  • $route.name 当前路径名字
  • $route.meta 路由元信息

$router对象:$router对象是全局路由的实例,是router构造方法的实例。

实例方法:

(1)push

  • 字符串this.$router.push('home')
  • 对象this.$router.push({path:'home'})
  • 命名的路由this.$router.push({name:'user',params:{userId:123}})
  • 带查询参数,变成 /register?plan=123this.$router.push({path:'register',query:{plan:'123'}})

push方法其实和<router-link :to="...">是等同的。

注意:push方法的跳转会向 history 栈添加一个新的记录,当我们点击浏览器的返回按钮时可以看到之前的页面。
(2)go
页面路由跳转
前进或者后退this.$router.go(-1) // 后退
(3)replace
push方法会向 history 栈添加一个新的记录,而replace方法是替换当前的页面,
不会向 history 栈添加一个新的记录

问:vue-router响应路由参数的变化?

答:当使用路由参数时,例如从 /user/aside导航到 /user/foo,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。

注意:

(1)从同一个组件跳转到同一个组件。

(2)生命周期钩子created和mounted都不会调用。

beforeRouteUpdate(to,from,next){
//在这个钩子函数中:to表示将要跳转的路由对象,from表示从哪个路由跳转过来,next多数就是需要调用
//created和mounted不调用,无法拿到需要的动态值,就通过to.path,to.params等
//可以在这个函数中打印to,具体看to对象有什么可以使用的属性
}

添加watch监听

watch: {
 // 方法1 //监听路由是否变化
  '$route' (to, from) {
   if(to.query.id !== from.query.id){
            this.id = to.query.id;
            this.init();//重新加载数据
        }
  }
}
//方法 2  设置路径变化时的处理函数
watch: {
'$route': {
    handler: 'init',
    immediate: true
  }
}

为了实现这样的效果可以给router-view添加一个不同的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。

<router-view :key="$route.fullpath"></router-view>

问:vue项目实现路由按需加载(路由懒加载)的3种方式

vue异步组件技术 ==== 异步加载

vue-router配置路由 , 使用vue的异步组件技术 , 可以实现按需加载 .

但是,这种情况下一个组件生成一个js文件

/* vue异步组件技术 */
{
  path: '/home',
  name: 'home',
  component: resolve => require(['@/components/home'],resolve)
},{
  path: '/index',
  name: 'Index',
  component: resolve => require(['@/components/index'],resolve)
},{
  path: '/about',
  name: 'about',
  component: resolve => require(['@/components/about'],resolve)
} 

路由懒加载(使用import)

// 下面2行代码,没有指定webpackChunkName,每个组件打包成一个js文件。
/* const Home = () => import('@/components/home')
const Index = () => import('@/components/index')
const About = () => import('@/components/about') */
// 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。 把组件按组分块
const Home =  () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home')
const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index')
const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about')

{
  path: '/about',
  component: About
}, {
  path: '/index',
  component: Index
}, {
  path: '/home',
  component: Home
}

webpack提供的require.ensure()
vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。
这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。

/* 组件懒加载方案三: webpack提供的require.ensure() */
{
  path: '/home',
  name: 'home',
  component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
}, {
  path: '/index',
  name: 'Index',
  component: r => require.ensure([], () => r(require('@/components/index')), 'demo')
}, {
  path: '/about',
  name: 'about',
  component: r => require.ensure([], () => r(require('@/components/about')), 'demo-01')
}

21.5 vue-route

Vue Route 是指当前激活的路由信息对象。每个 Vue Route 对象包含了当前路由的各种信息,如路径、参数、查询参数等。Vue Route 是 Vue Router 的一个属性,通过 this.$route 访问。
Vue Route 对象包含以下常用属性:

  • path: 当前路由的路径。
  • name: 当前路由的名称(如果有的话)。
  • params: 当前路由的参数对象。
  • query: 当前路由的查询参数对象。
  • hash: 当前路由的哈希值。
  • fullPath: 包含查询参数和哈希值的完整路径。
  • matched: 包含当前路由的所有匹配记录。

在 Vue 组件中,可以通过 this.$route 访问当前路由信息。

<template>
  <div>
    <h1>Home Page</h1>
    <p>Current Path: {{ $route.path }}</p>
    <p>Query Params: {{ $route.query }}</p>
  </div>
</template>
 
<script>
export default {
  mounted() {
    console.log(this.$route); // 当前路由信息
  }
};
</script>

Vue Router: 是 Vue.js 官方的路由管理器,提供了路由的定义、导航、匹配等功能。
Vue Route: 是指当前激活的路由信息对象,包含了当前路由的各种信息,如路径、参数、查询参数等。
简单来说,Vue Router 是管理路由的工具,而 Vue Route 是当前路由的信息。

21.6 vue2和vue3中router的区别

vue2路由和vue3路由区别及原理_vue3实现动态路由 和 vue2有什么不同-CSDN博客

21.7 vue3中ref和reactive的原理

原理上,refreactive都是通过ProxyReflect实现的,使得数据是响应式的,并且能够触发视图的更新。

当你读取ref对象的value属性时,Vue会追踪这个ref值的依赖,当你修改ref对象的value属性时,Vue会更新依赖于这个ref值的所有视图。

对于reactive对象,Vue会追踪对象属性的依赖,当对象的属性被访问或修改时,Vue会更新依赖于这些属性的所有视图。

这些操作都是通过Proxygetset trap实现的,使得对象属性的读取和设置可以被Vue拦截并追踪依赖。

21.8 vue3创建虚拟dom节点

import { h } from 'vue';
 
// 创建一个简单的文本虚拟节点
const vNode = h('div', 'Hello, Vue 3!');
 
// 渲染虚拟节点到DOM容器中
const app = createApp({
  render() {
    return vNode;
  }
});
app.mount('#app');

21.9 vue自定义组件传值 props 和emit更新父组件的值

父组件

<!-- ParentComponent.vue -->
<template>
  <CustomInput :value="parentValue" @input="updateParentValue" />
</template>
 
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
 
const parentValue = ref('');
 
function updateParentValue(value) {
  parentValue.value = value;
}
</script>

子组件

<!-- CustomInput.vue -->
<template>
  <input :value="value" @input="$emit('input', $event.target.value)">
</template>
 
<script setup>
import { defineProps } from 'vue';
 
const props = defineProps({
  value: String
});
</script>

22、

23、v-if、v-show、v-for

v-if 在不需要频繁切换的地方使用,v-if显示隐藏是将dom元素整个添加或删除。

v-show使用在频繁切换的时候,隐藏则是为该元素添加css--display:nonedom元素依旧还在。

v-for当使用v-for指令进行循环渲染数组或对象时,绑定key是一个重要的最佳实践。key是用于识别VNode(虚拟DOM节点)的特殊属性,它有以下几个重要的作用:绑定key可以帮助Vue跟踪元素的身份,优化DOM更新,提高渲染性能,并确保在循环渲染时组件状态的正确性。因此,尽量遵循这个最佳实践,为v-for循环中的元素提供合适的唯一标识。通常,可以使用循环项的唯一ID或其他稳定的唯一属性作为key。

v-for和v-if在vue2中的优先级是v-for,意思就是会先循环渲染,然后根据if判断是否显示;但是在vue3中 v-if的优先级高于v-for,但是vue3文档不推荐v-if和v-for同时出现同一节点出现,可以使用template标签先循环,在内部使用v-if。

24、computed和watch:Vue中的响应式双胞胎

(1)computed适用场景:

  • 当你需要根据现有数据计算一个新的值,并且这个值在多个地方被用到时。
  • 当计算过程可能较为复杂,不希望在多个地方重复相同的逻辑时。
  • 当派生数据的实时性要求不高,且希望避免不必要的重复计算以优化性能时。

实例:
假设有一个购物网站的购物车页面,其中每个商品都有一个price(价格)和一个quantity(数量)。你想要计算所有商品的totalPrice(总价),这可以通过computed属性实现。

(2)watch适用场景:

  • 当数据变化时需要需要触发复杂的业务逻辑或副作用时。
  • 当需要在数据变化时执行异步操作,如发起API请求时。
  • 当需要对数据变化做出条件性响应,如根据用户输入切换不同的视图或状态时。

实例:
考虑一个具有实时搜索功能的应用,用户在搜索框中输入文本时,应用需要动态更新搜索结果。

computed关键点:

  • computed属性用于创建派生数据,这些数据是基于响应式依赖自动计算的。
  • 它们提供了缓存机制,只有当依赖项变化时,计算属性才会重新计算。
  • computed适合于声明性地描述数据如何从其他数据派生,常用于视图渲染优化。

watch关键点:

  • watch用于侦听响应式数据的变化,并在变化发生时执行定义的逻辑。
  • 它不具备缓存机制,每次数据变化都会触发回调函数。
  • watch适合于执行复杂的业务逻辑,如异步请求、DOM操作,或者在数据变化时执行条件性响应。

25、

26、可适配表单

响应式设计:表单能够根据屏幕大小和分辨率自动调整其布局和元素大小。

输入优化:针对不同的设备,表单的输入字段(如文本框、选择框等)会进行优化,例如在触屏设备上提供更大的点击区域。

简化交互:在移动设备上,可能会隐藏或简化某些表单元素,以减少用户的输入负担,提高填写效率。

上下文感知:表单可以根据用户的上下文(如地理位置、时间等)显示或隐藏某些字段。

动态内容:表单可以根据用户的输入动态显示或隐藏相关的内容或字段。

易于访问性:可适配表单考虑到了不同用户的需求,包括那些使用辅助技术的用户,确保表单对所有用户都是可访问的。

性能优化:在移动设备上,表单通常会减少资源加载,以加快加载速度。

27、使用cookie、session维持登录状态的原理是什么?

cookie存贮在客户端,web服务器通过传送HTTP包头中的set-cookie把一个cookie发送到用户的浏览器中,如果不设置过期时间则表示这个cookie的生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失。如果设置了过期时间,浏览器会把cookie保存在硬盘上,关闭后再次打开浏览器,cookie过期时间还在设置范围内就还能继续使用,反之则不能;

session机制是一种服务器端的机制。当客户端第一次请求服务端的时候,服务端会检查客户端请求头中携带cookie中是否存在sessionid。如果有则会检索sessionid对应session是否存在;如果不存在则会创建对应会话信息,生成对应session,并将对应sessionid返回给客户端,客户端接受到这个sessionid,把它存贮起来,下一次发送请求的时候,附带把这个session一起发送给服务端,服务端只要根据这个sessionid就知道是谁,而这个sessionid就是这次会话生命周期的凭证,服务端可以设置session过期时间,一但客户端丢失sessionid或者是服务端设置了失效时间,那这次会话结束。

28、Cookie、Session、LocalStorage 和 SessionStorage 的区别详解

(1)Cookie 和 Session 的区别

Cookie 是由服务器生成并发送到客户端的一小段数据,客户端会将其存储并在后续请求中携带,帮助服务器识别用户。Cookie 主要用于以下场景:

  • 用户身份认证(如记住登录状态)
  • 存储用户偏好设置
  • 跟踪用户行为(如广告点击记录)
// 设置 Cookie
document.cookie = "username=John Doe; expires=Fri, 31 Dec 2024 12:00:00 UTC; path=/";
// 读取 Cookie
console.log(document.cookie);

Session 是存储在服务器端的用户会话信息。每个用户会话都有一个唯一的 Session ID,服务器通过这个 ID 来识别不同的用户。客户端通过 Cookie 将 Session ID 发送给服务器,从而实现用户识别。

// 使用 Express 和 express-session 中间件
const express = require('express');
const session = require('express-session');
const app = express();
 
app.use(session({
  secret: 'my secret',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}));
 
app.get('/', (req, res) => {
  if (req.session.views) {
    req.session.views++;
    res.send(`Number of views: ${req.session.views}`);
  } else {
    req.session.views = 1;
    res.send('Welcome to the session demo. Refresh!');
  }
});
 
app.listen(3000);

区别总结
存放位置不同:Cookie 存放在客户端,Session 存放在服务器端。
安全性不同:Cookie 存储在浏览器中,容易被篡改和截取;Session 存放在服务器端,安全性更高。
对服务器的压力不同:Cookie 存放在客户端,对服务器没有压力;Session 存放在服务器端,会占用服务器资源。
有效期不同:Cookie 可以设置过期时间,分为会话 Cookie 和持久 Cookie;Session 一般在用户关闭浏览器或会话超时后失效。
跨域支持:Cookie 支持跨域名访问,可以通过设置 domain 值实现;Session 不支持跨域。
安全性分析
Cookie:由于 Cookie 存储在客户端,可能会被用户篡改或盗取,导致安全风险。例如,攻击者可以通过 XSS 攻击获取 Cookie。
Session:Session ID 存储在服务器端,通过加密的方式传输和验证,安全性较高。即使攻击者获取了 Session ID 也无法直接篡改服务器上的会话数据。

(2)Web Storage:LocalStorage 和 SessionStorage

Web Storage 是 HTML5 引入的一种新的本地存储方案,主要包括 LocalStorage 和 SessionStorage。它们相比 Cookie 有以下优点:

存储空间更大,通常为 5MB。
API 简单易用,封装了常用方法(如 setItem、getItem、removeItem 和 clear)。
数据只存储在客户端,不会随每次请求发送到服务器。

LocalStorage:LocalStorage 是持久化的本地存储方式,数据除非手动清除,否则会一直保留,即使浏览器关闭和重启也不会丢失。

// 设置 LocalStorage
localStorage.setItem('username', 'John Doe');
 
// 读取 LocalStorage
console.log(localStorage.getItem('username'));
 
// 删除 LocalStorage
localStorage.removeItem('username');
 
// 清空 LocalStorage
localStorage.clear();

SessionStorage:SessionStorage 是会话级别的存储方式,数据只在当前会话中有效,一旦用户关闭浏览器窗口或标签页,数据就会被清除。

// 设置 SessionStorage
sessionStorage.setItem('username', 'John Doe');
 
// 读取 SessionStorage
console.log(sessionStorage.getItem('username'));
 
// 删除 SessionStorage
sessionStorage.removeItem('username');
 
// 清空 SessionStorage
sessionStorage.clear();

有效期不同:LocalStorage 是持久化存储,除非手动清除;SessionStorage 是会话级别存储,浏览器关闭后数据失效。
应用场景不同:LocalStorage 适用于长期存储数据,如用户偏好设置;SessionStorage 适用于临时存储数据,如页面状态、表单数据等。

(3)四者的详细对比

存储位置
Cookie:存储在客户端,浏览器内。
Session:存储在服务器端。
LocalStorage:存储在客户端,浏览器内。
SessionStorage:存储在客户端,浏览器内。
有效期
Cookie:可以设置为持久性(通过 expires 或 max-age)或会话级别(浏览器关闭后失效)。
Session:会话级别,浏览器关闭或会话超时后失效。
LocalStorage:持久性存储,除非手动删除,否则永久有效。
SessionStorage:会话级别,浏览器关闭或标签页关闭后失效。
存储大小
Cookie:每个 Cookie 大小约为 4KB,浏览器对单个域名下的 Cookie 总数有限制。
Session:服务器端存储,大小取决于服务器配置。
LocalStorage:一般为 5MB,各浏览器可能不同。
SessionStorage:一般为 5MB,各浏览器可能不同。
传输数据
Cookie:每次请求都会携带 Cookie 数据,影响性能。
Session:仅在初始会话时传输 Session ID,后续请求不再携带全部会话数据。
LocalStorage:不随请求发送,仅在客户端存储和访问。
SessionStorage:不随请求发送,仅在客户端存储和访问。
安全性
Cookie:容易被窃取和篡改,需注意 XSS 攻击。
Session:相对安全,通过加密的 Session ID 进行识别和验证。
LocalStorage:易受 XSS 攻击,数据存储在客户端。
SessionStorage:易受 XSS 攻击,数据存储在客户端。

(4)应用场景

Cookie
用户登录状态保持:用于记住用户的登录状态,实现自动登录。
用户偏好设置:存储用户的语言、主题等偏好信息。
广告跟踪:用于记录用户的广告点击行为,实现精准广告投放。
Session
用户会话管理:用于存储用户的会话信息,如购物车数据、用户权限等。
高安全性场景:适用于需要保护敏感数据的场景,如在线银行、支付系统等。
 LocalStorage
长期数据存储:适用于存储长期有效的数据,如用户偏好设置、浏览历史等。
前端缓存:用于缓存大量数据,提高应用性能和用户体验。
SessionStorage
临时数据存储:适用于存储会话级别的数据,如表单数据、页面状态等。
多标签页数据隔离:在同一域名下的不同标签页之间实现数据隔离,防止数据污染。

(5)总结

Cookie、Session、LocalStorage 和 SessionStorage 各有优缺点,适用于不同的场景。开发者应根据实际需求选择合适的存储方案:

Cookie:适用于需要跨域访问或持久化存储少量数据的场景。
Session:适用于需要高安全性、临时存储用户会话数据的场景。
LocalStorage:适用于需要持久化存储较大数据的场景。
SessionStorage:适用于需要临时存储较大数据且不需要跨页面的数据的场景。

29、单点登录的流程

用户输入登录信息点击登录,发送请求接口,服务器校验生成一个sessionID或者token存贮在服务器数据库,接口的response的header中存入cookie中返回,客户端接收到这个信息会存放在application的cookie中,同域名的系统就能根据这个application的cookie在下次请求的时候,在request的header中cookie自动带入对应sessionID或者token和数据库存贮的sessionID或者token进行对比,如果失效或者验证不通过,会返回对应状态码,前端根据对应状态码在请求接口的HTTPresponse函数中进行判断重定向到登录界面,重新验证。

30、在浏览器地址栏输入URL,按下回车发生了什么

  • URL解析:浏览器首先会解析输入的URL。URL通常由协议(如HTTP、HTTPS)、域名(或IP地址)、端口号(如果未指定,默认为协议的默认端口)、路径(指定服务器上的资源位置)、查询参数和片段标识符组成。浏览器会将这些部分分解并提取出来,以便后续的操作。
  • DNS解析:如果输入的URL中包含了域名而非IP地址,浏览器会进行DNS解析,将域名解析成相应的IP地址。DNS解析通过向域名服务器发送查询请求,并接收服务器返回的IP地址来完成。一旦浏览器获取了目标服务器的IP地址,它就可以通过该地址与服务器建立连接。
  • 建立TCP连接:浏览器使用HTTP协议或HTTPS协议与服务器通信。如果是HTTP协议,浏览器会尝试与服务器的默认HTTP端口(通常是80)建立TCP连接;如果是HTTPS协议,浏览器会尝试与服务器的默认HTTPS端口(通常是443)建立加密的TLS连接。这个过程通常涉及“三次握手”,即浏览器向服务器发送一个连接请求,服务器确认请求并回复,最后浏览器再次确认服务器的回复。
  • 发送HTTP请求:一旦TCP连接建立完成,浏览器会向服务器发送HTTP请求。这个请求包含了之前解析得到的URL、请求方法(GET、POST等)、请求头部(包含浏览器和客户端的信息、所需的数据格式等)以及请求体(对于POST请求,通常包含用户提交的数据)。
  • 服务器处理请求并返回响应:服务器收到浏览器发送的请求后,会根据请求的内容进行相应的处理。这可能涉及到从服务器上获取请求的资源(如HTML文件、图片、视频等),执行数据库查询、处理用户提交的数据等操作。处理完成后,服务器会生成一个HTTP响应,包含了响应状态码(指示请求的成功或失败)、响应头部(包含服务器信息、内容类型、缓存控制等)以及响应体(所请求资源的实际内容)。
  • 接收并渲染响应:浏览器接收到服务器返回的HTTP响应后,会根据响应的内容进行相应的处理。如果响应的内容是HTML,浏览器会解析HTML并构建DOM树,然后根据CSS样式信息构建渲染树,最终将DOM树和渲染树结合起来,展示给用户。如果响应的内容是其他资源(如图片、视频、Java文件等),浏览器会根据其内容类型进行相应的处理,并将其展示在页面上或执行相应的操作。
  • 断开连接:一旦浏览器完成了对响应的处理,它会关闭与服务器的TCP连接。在HTTP/1.1中,连接通常会保持一段时间以便于后续的请求,这被称为“持久连接”。在HTTP/2及更新的版本中,多个请求可以通过同一个连接并行处理,以提高性能。

31、一个城市下拉选择器,在选择的时候,有几种方案保证等每次城市选择后出现对应的数据?

(1)接口请求时禁用选择器,等数据响应后赋值数据且选择器状态设置为启用。

(2)使用防抖,比如五秒内点击多次,都会重新计时,以最后一次点击生效去处理对应参数请求。这种方法跟后台数据的响应时间和前端视图数据渲染时间有关。可能不是最优解。

(3)请求多次的时候,对应取消上一次的请求。始终保留这个功能模块只有一次请求。例如fetch:使用AbortController;axios中的cancelToken。

32、whistle和fiddler区别 

Whistle

Whistle 是一个开源的网络调试代理工具,它主要用于开发者调试和分析网络请求和响应。Whistle 提供了一个图形用户界面(GUI),使得用户可以直观地查看和修改网络数据。Whistle 支持多种协议,包括 HTTP、HTTPS、WebSocket 等。

Fiddler

Fiddler 是一个更为知名的网络调试工具,它由微软开发,主要用于捕获和分析 HTTP 和 HTTPS 网络流量。Fiddler 提供了强大的过滤、分析和修改功能,并且支持代码注入和脚本编写,使得用户可以更深入地控制和调试网络流量。

区别

用户群体:Fiddler 更倾向于开发者,因为它提供了更复杂的调试功能和更强的代码控制能力。而 Whistle 更倾向于那些需要快速分析和调试网络问题的用户。

功能:Fiddler 提供更多的调试功能,如代码注入、脚本编写等。Whistle 则提供了更简单的图形界面和更直观的数据展示。

兼容性:Fiddler 通常需要更多的配置来捕获 HTTPS 流量,而 Whistle 则内置了捕获 HTTPS 流量的功能。

更新和维护:Fiddler 是由微软开发的,因此它的更新和维护可能会更稳定。而 Whistle 是一个开源项目,其更新和维护可能会受到社区活跃度的影响。

价格:Whistle 是免费的,而 Fiddler 则提供了一个免费的社区版和一个付费的商业版。

33、equals和==

对象类型不同

(1)equals():是超类Object中的方法。

(2)==:是操作符。

比较的对象不同

(1)equals():equals是Object中的方法,在Object中equals方法实际"ruturn (this==obj)",用到的还是"==",说明如果对象不重写equals方法,实际该对象的equals和"=="作用是一样的,都是比较的地址值(因为"=="比较的就是地址值),但是大部分类都会重写父类的equals方法,用来检测两个对象是否相等,即两个对象的内容是否相等,例如String就重写了equals方法,用来比较两个字符串内容是否相同。

(2)==:用于比较引用和比较基本数据类型时具有不同的功能,比较引用数据类型时,如果该对象没有重写equals方法,则比较的是地址值,如果重写了,就按重写的规则来比较两个对象;基本数据类型只能用"=="比较两个值是否相同,不能用equals(因为基本数据类型不是类,不存在方法)。

以下是延伸==和===:

  • ==:称为‌相等运算符,比较两个值是否相等,会自动进行类型转换。如果两个值的类型不同,运算符会尝试将它们转换为相同的类型,然后再进行比较。‌
  • ===‌:称为‌严格相等运算符,比较两个值是否严格相等,不会进行类型转换。只有当两个值的类型和值都相等时,=运算符才返回true。

== 运算符的具体行为

  • 如果两个值都是null或undefined,==认为它们相等。
  • 如果一个是字符串,一个是数值,==会将字符串转换成数值后再进行比较。
  • 如果一个是整数,一个是布尔类型的数值(1为true,0为false),==认为它们相等。
  • ==运算符在进行比较时会进行隐式类型转换,例如将布尔类型转换为数值类型。‌23

=== 运算符的具体行为

  • 如果类型不同,===认为它们不相等。
  • 如果两个都是数值,并且是同一个值,=认为它们相等。如果其中至少一个是NaN,=认为它们不相等。
  • 如果两个都是字符串,每个位置的字符都一样,===认为它们相等,否则不等。
  • 如果两个值都是true或false,===认为它们相等。
  • 如果两个值都引用同一个对象或是函数,===认为它们相等,否则不相等。‌24

在不同类型比较时的具体规则

  • 对于基础类型(如string, number, boolean),会进行类型转换,而===不会。例如,字符串和数字比较时,会将字符串转换为数字进行比较,而===会认为它们不相等。
  • 对于高级类型(如Array, Object),和==没有区别,因为===会将高级类型转换为基本类型进行比较。‌

34、js事件循环机制

JavaScript单线程指的是在同一时刻只能执行一个任务,任务只有在前一个任务执行完毕后才能开始执行。但是JavaScript通过事件循环机制实现了异步。

JavaScript事件循环机制指的是JavaScript运行时环境(ECMAScript规范定义的)按照一定的规则处理代码中的异步操作和事件的机制。JavaScript事件循环机制包含任务队列(Task)、微任务队列(Microtask)和宏任务队列(Macrotask)。

当JavaScript代码执行到一个异步操作或事件时,它并不会立即执行,而是将其放入对应的任务队列中。当当前任务执行完成后,在下一个事件循环的开始,JavaScript会从任务队列中取出一个任务,执行该任务。当任务执行时,可能会产生新的异步操作和事件,这些新的操作也会被放入任务队列中等待执行。

在JavaScript事件循环机制中,任务分为宏任务和微任务。
宏任务包括setTimeout、setInterval、I/O操作等;
微任务包括Promise、MutationObserver、process.nextTick等。
在每次事件循环开始时,JavaScript会先执行所有的微任务队列中的任务,然后再执行宏任务队列中的任务。

一个完整的事件循环流程包括以下步骤:

1. 从宏任务队列中取出一个任务执行;
2. 如果该任务中产生了微任务,将它们放入微任务队列中;
3. 执行微任务队列中的所有任务;
4. 检查是否需要重新渲染页面;
5. 重复执行上述步骤,直到任务队列和微任务队列中都没有任务;

事件循环机制是指:

1、代码执行时,先执行同步任务,然后将异步任务放入任务队列中,等待执行。
2、当所有同步任务执行完毕后,JavaScript引擎会去读取任务队列中的任务。
3、将队列中的第一个任务压入执行栈中执行,执行完毕后将其出栈。
4、如此循环执行,直到任务队列中的所有任务都执行完毕。

这就是JavaScript实现异步的基本原理,通过将异步任务放到任务队列中,并通过事件循环机制来实现异步执行。

总的来说,JavaScript通过事件循环机制来实现异步操作,将异步任务放到任务队列中,然后在任务队列中等待执行,直到JavaScript引擎空闲,再将任务队列中的任务拿出来执行。

以下代码输出什么?

async function async1() {
    console.log('async1');
}
async function async2() {
    console.log('async2 start');
    await async1();
    console.log('async2 end')
}
console.log('start');
setTimeout(() => {
    console.log('setTimeout')
}, 0);
async2();
new Promise(function(resolve){
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2')
})
console.log('end')

答案是start ==> async2 start ==> async1= => promise1 ==> end ==> async2 end ==> promise2 ==> setTimeout

执行代码,首先打印start,遇到settimeout宏任务,放入宏任务队列排队,继续执行async2,打印出async2 start,遇到await async1 打印出async1,await下的代码变成异步微任务,放到微任务队列中排队,走到promise打印promise1,then下的代码异步,放到微任务队列中排队,继续执行打印日志end,这时候同步任务执行完毕,执行微任务队列,第一个await下的异步,打印出async2 end,第二个异步微任务promise then 打印出promise2,微任务异步结束,执行宏任务队列 ,setTimeout打印出来。

35、promise构造函数详解

从功能上来说,Promise 对象用于封装一个异步操作,并获取其成功/ 失败的结果值。

let blog = true;
new Promise((resolve,reject) =>{
    console.log('promise 1')
    if (blog){
        resolve(1);
        console.log('promise 2')
    } else {
        reject();
    }
}).then((res) => {
    console.log('then',res);
    reject();
}).catch(()=> {
    console.log('catch');
}).finally(()=> {
    console.log('finally');
})

输出promise 1 ==》promise 2 ==》 then 1 ==》 catch ==》 finally

状态定义‌

  • Pending(等待态)‌:初始状态,既没有被兑现(resolve),也没有被拒绝(reject)。
  • Fulfilled(履行态)‌:意味着操作成功完成。
  • Rejected(拒绝态)‌:意味着操作失败。

状态变化规则

  • Promise对象的状态一旦改变,就不能再变。即状态一旦从pending变为fulfilled或rejected,就会保持这个状态,不会再变。
  • 状态变化只有两种途径:从pending变为fulfilled或从pending变为rejected。

状态的应用场景

  • Fulfilled‌:当异步操作成功时,Promise对象的状态会变为fulfilled,此时可以通过.then()方法获取异步操作的结果。
  • Rejected‌:当异步操作失败时,Promise对象的状态会变为rejected,此时可以通过.catch()方法处理错误。
  • Finally‌:无论Promise成功还是失败,.finally()方法都会执行,用于进行清理工作,比如关闭文件流等。

promise  遇到的函数:

Promise构造函数:Promise构造函数接受一个函数作为参数,该函数有两个参数,分别是resolve和reject。resolve函数用于将Promise的状态从pending变为fulfilled,并将结果传递给后续的then函数。reject函数用于将Promise的状态从pending变为rejected,并将错误信息传递给后续的catch函数。

then函数:then函数是Promise对象的方法,用于指定Promise状态改变时的回调函数。then函数接受两个参数,分别是成功时的回调函数和失败时的回调函数。成功时的回调函数会接收到Promise的返回值作为参数,而失败时的回调函数会接收到错误信息作为参数。

catch函数:catch函数是Promise对象的方法,用于指定Promise发生错误时的回调函数。catch函数接受一个参数,即错误时的回调函数。它相当于调用then函数的第二个参数。

finally函数:finally函数是Promise对象的方法,用于指定Promise无论成功还是失败都会执行的回调函数。finally函数不接受任何参数。

all函数:all函数是Promise对象的静态方法,用于将多个Promise对象包装成一个新的Promise对象。当所有的Promise都变为fulfilled状态时,新的Promise对象才会变为fulfilled状态,并将所有Promise的返回值作为一个数组传递给then函数。如果任意一个Promise变为rejected状态,则新的Promise对象会立即变为rejected状态,并将第一个被rejected的Promise的错误信息传递给catch函数。

race函数:race函数是Promise对象的静态方法,和all函数类似,它也是将多个Promise对象包装成一个新的Promise对象。但是不同的是,race函数只要有一个Promise变为fulfilled或rejected状态,新的Promise对象就会变为相应的状态,并将第一个完成的Promise的返回值或错误信息传递给后续的then或catch函数。

 36、async和wait

async和await的概念
1)async 函数是 Generator 函数的语法糖,使用 关键字 async 来表示,在函数内部使用 await 来表示异步
2)ES7 提出的async 函数,终于让 JavaScript 对于异步操作有了终极解决方案
3)async 作为一个关键字放到函数的前面,用于表示函数是一个异步函数,该函数的执行不会阻塞后面代码的执行
4)await是等待,只能放到async函数里面,在后面放一个返回promise对象的表达式
5)async和await是为了解决大量复杂不易读的Promise异步的问题。


  const test = async()=>{
        let message = "hello1";
        let result = await message;
        console.log(result);
        console.log("我是hello2");
        return result;
    };
    test().then(result =>{
        console.log("输出",result);
    });

37、前端提升网站性能

(1)资源加载优化。图片压缩后放到cdn;图片视频使用懒加载,需要的时候再去加载;减少HTTP资源请求。

(2)js执行优化;频繁处理的事件使用防抖节流;JavaScript 中的内存泄漏可能会导致页面变慢甚至崩溃。常见的内存泄漏包括意外的全局变量、被遗忘的定时器和闭包等。

(3)css优化;多个小图标的可以合并到一个图中,通过background-position显示;避免使用@import引入css,使用link;

(4)服务器优化;Gzip 压缩可以显著减少传输的数据量,加快页面加载速度;合理利用浏览器缓存可以减少不必要的网络请求。

38、谈谈闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。

闭包产生的条件:函数嵌套;内部函数引用了外部函数的数据(变量/函数)。

PS:还有一个条件是外部函数被调用,内部函数被声明。比如:

    function fn1() {
        var a = 2
        var b = 'abc'

        function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数)
            console.log(a)
        }

    }
    fn1();
    function fn3() {
        var a = 3
        var fun4 = function () {  //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前
            console.log(a)
        }
    }
    fn3();

缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。

闭包的作用

  • 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)

  • 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

39、怎么处理跨域

前端处理跨域的九种方式

  • 通过jsonp跨域
  • document.domain + iframe跨域
  • location.hash + iframe
  • window.name + iframe跨域
  • postMessage跨域
  • 跨域资源共享(CORS)
  • nginx代理跨域
  • nodejs中间件代理跨域
  • WebSocket协议跨域

40、es6新特性

包括let和const关键字、箭头函数、模板字符串(`字符串${变量}`)、解构赋值、类和继承、模块化等。

41、Map和object的区别

  • Object是JavaScript的内置对象,用于存储键值对。
  • Object的键必须是字符串或符号,值可以是任意类型。
  • Map是ES6引入的新数据结构,用于存储键值对。
  • Map的键可以是任意类型,值也可以是任意类型。

键的类型:Map的键可以是任意类型,包括对象、函数、基本类型等;而Object的键只能是字符串或符号。

键值对的顺序:Map中的键值对是有序的,即按照插入顺序进行迭代;而Object中的键值对是无序的,迭代顺序不确定。

键值对的个数:Map中的键值对个数可以通过size属性获取;而Object的键值对个数需要手动计算。

迭代方式:Map可以直接使用for...of循环进行迭代,或者使用forEach方法;而Object需要先获取键的数组,然后再进行迭代。

键的唯一性:Map中的键是唯一的,不会存在重复的键;而Object中如果使用相同的键进行多次赋值,后面的值会覆盖前面的值。

性能:在频繁增删键值对的场景下,Map的性能通常会更好,而在静态数据的场景下,Object的性能可能更好。

// 使用Object存储键值对
const obj = {
    key1: 'value1',
    key2: 'value2',
    key3: 'value3'
}
console.log(obj.key1) // 输出:"value1"

// 使用Map存储键值对。可以使用set、get、has、delete
const map = new Map()
map.set('key1', 'value1');
map.set('key2', 'value2');
map.set('key3', 'value3');
// 输出:"value1" 3 true false
console.log(map.get('key1'), map.size, map.has('key1'), map.has('key4')); 

42、js的set

Set 是一个集合,类似于数组,但是成员的值都是唯一的,不能重复。主要用于快速查找、去重等场景。

属性:

constructor:构造函数,默认就是 Set 函数
size:返回 Set 实例的成员总数

操作方法:
add(value) 添加某个值,返回 Set 实例本身
delete(value) 删除某个值,返回一个布尔值,表示删除是否成功
has(value) 查询该值是否是 Set 实例的成员,返回一个布尔值
clear() 清除所有成员,没有返回值
遍历方法:
keys() 返回键名的遍历器
values() 返回键值的遍历器
entries() 返回键值对的遍历器
forEach() 使用回调函数遍历每个成员
集合运算方法:
intersection(other) 交集
union(other) 并集
difference(other) 差集
symmetricDifference(other):对称差集
isSubsetOf(other):判断是否为子集
isSupersetOf(other):判断是否为超集
isDisjointFrom(other):判断是否不相交

43、http和https的区别

安全方面:http是一种超文本传输协议,它以明文方式发送内容,不提供任何方式的数据加密。这就意味着在传输过程中,数据可能会被别有用心的人窃取、篡改或监听,存在着较大的安全风险。https是在http的基础上使用SSL或者TLS对数据进行加密处理,此外https还通过数字证书进行身份认证。

传输性能:http在传输市由于不需要加密、解密等操作,通常速度回你https快一些。https因为需要加密、解密等工作,在一定程度上会影响网页的加载速度。

连接方式:http,客户端向服务器发送请求,服务器接到请求后会返回数据,整个过程不需要额外的安全验证。https,在连接时,需要SSL/TLS的握手,因此客户端和服务器在交换过程中比较复杂,但这也极大程度的提高了连接的安全性。

44、箭头函数

箭头函数提供了一个更加简洁的函数书写方式。

箭头函数不绑定this,使用上层作用域的this; 箭头函数没有arguments,使用上层的arguments; 箭头函数不可new,因为没有this,new要把函数的prototype赋值给this的__proto__; 箭头函数不会函数提升; 不能用call和apply绑定。

var f = (a,b ) => a+b;
var f1 = () => {
    return 'a'
}
console.log(f(1,2),f1())

45、改变this指向

全局作用域或者普通函数中this指向全局对象window ,如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window(注意定时器里面的this指向window)

function Hello(){
    var user = "你好";
    console.log(this.user); //undefined 因为window对象中没有user属性
    console.log(this); //Window
}
Hello(); //相当于window.Hello();
//this最终指向的是调用它的对象,这里的函数Hello实际是被Window对象所点出来的

如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。绑定事件函数this指向的是函数的调用者

// 案例一
var person = {
    user:"你好",
    fn:function(){
        console.log(this.user); //你好  因为fn函数被person所调用,所以this指向person
        console.log(this); //输出person对象 {user: "你好", fn: ƒ}
    }
}
person.fn(); //this执行时被它的上一级对象person{user: "你好", fn: ƒ}调用

// 案例二
var btn = document.querySelector('button');
btn.onclick=funcion(){
    console.log(this);//this指向的是btn这个按钮对象
};
btn.addEventListener('click',function(){
    console.log(this);//this指向的是btn这个按钮对象
});

如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

var numberObj = {
    a:10,
    b:{
        fn:function(){
            console.log(this.a); //undefind  有两个对象b和numberObj ,所以此this.a指向它的上一级,因为b对象中没有a属性 所以输出undefind
        }
    },
    fn1:function(){
        console.log(this.a);  //10 
    }
}
numberObj.fn1(); // fn1函数被numberObj 所调用,所以this指向numberObj对象
numberObj.b.fn(); // 尽管fn函数是被外层numberObj对象所调用,但是fn里面的this还是指向他的此处的上一级b对象

当this遇到return---- 如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。

// 案例一
function fn()  
{  
    this.user = '张三';  
    return {}; 
    // return function(){}; //这种写法跟上行代码一样
}
var a = new fn;  
console.log(a.user); //undefined

// 案例二
function fn()  
{  
    this.user = '张三';  
    return 1; 
    // return null; // 返回一样
}
var a = new fn;  
console.log(a.user); //张三  因为 new关键字改变this指向 

this的指向可以通过多种方法进行改变,主要包括以下几种方法‌:

  • 使用new关键字
//构造函数 this
function Fn(){
    this.user = "您好";
}
var a = new Fn();
console.log(a.user); //您好
  • 使用call()方法:fun.call(thisArg,arg1,arg2,…)‌

var a = {
    user:"您好",
    fn:function(a, b){
        console.log(this.user, a + b); //您好 3
    }
}
var b = a.fn;
b.call(a, 1, 2);  //若不用call,想当与b() 也就是window.b()执行后this指的是Window对象
  • 使用apply()方法:fun.apply(thisArg,[argsArray]) , apply方法和call方法有些相似,它也可以改变this的指向,也可以有多个参数,但是不同的是,第二个参数必须是一个数组

var a = {
    user:"您好",
    fn:function(e,ee){
        console.log(this.user); //您好
        console.log(e+ee); //11
    }
}
var b = a.fn;
b.apply(a,[10,1]);

注意:如果call和apply的第一个参数写的是null,那么this指向的是window对象

  • 使用bind()方法:fun.bind(thisArg,arg1,arg2,…)
    不会调用函数,可以改变函数内部this指向,返回的是原函数改变this之后产生的新函数 (如果有的函数我们不需要立即调用,但是又想改变this指向此时用bind)

var a = {
    user:"张三",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
b.bind(a);  //代码没有被打印

var a = {
    user:"里斯",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
var c = b.bind(a);
console.log(c); // ƒ (){ console.log(this.user);}

var a = {
    user:"王五",
    fn:function(){
        console.log(this.user); //王五
    }
}
var b = a.fn;
var c = b.bind(a);
c();  // 因为bind返回的是一个函数,要输出必须要调用

  • 使用箭头函数

  • 使用构造函数

  • 使用闭包‌:闭包可以捕获其外部作用域中的变量和函数,包括this。通过闭包,可以在外部作用域中定义一个变量来捕获当前的this值,然后在闭包内部使用这个变量‌。

46、typescript和js的区别

TypeScript和‌JavaScript的主要区别在于TypeScript引入了‌静态类型检查,提供了‌面向对象编程的支持,以及更好的开发工具支持,而JavaScript是一种动态类型语言,支持解释性执行。

TypeScript是JavaScript的一个超集,它扩展了JavaScript的语法,增加了静态类型检查、类和接口等特性,使得代码更加安全、可维护和可扩展。JavaScript则是一种动态类型语言,变量的类型在运行时确定,没有静态类型检查,这可能导致运行时错误和调试困难。此外,TypeScript支持面向对象编程,包括类、接口和继承等概念,而JavaScript虽然也支持面向对象编程,但TypeScript在这方面提供了更严格和丰富的语法支持。

在开发工具方面,TypeScript由于其静态类型检查和编译过程,能够与各种开发工具更好地集成,提供代码提示、自动补全和错误检查等功能,从而提高开发效率和代码质量。而JavaScript虽然也有丰富的生态系统,但TypeScript可以无缝地与现有的JavaScript库和框架配合使用。

总的来说,TypeScript适合用于大型项目、需要严格类型检查和高代码质量的场景,而JavaScript则适用于快速原型设计和简单的客户端脚本编写。‌

(1)定义类型范围:interface只能定义对象类型,type可定义任何类型,基础类型、交叉类型、联合类型;

(2)可扩展性:interface可以extends、implements;从而扩展多个接口或类;类型没有扩展功能;

(3)合并声明:interface可以声明两个相同名称的接口,会进行合并;但类型会出现异常;(4)type可以使用typeof获取实例类型

ts快速入门学习

(1)类型注解

let age: number = 10
let username: string = '刘狄威'

(2)类型概述
可以将 TS 中的常用基础类型细分为两类:JS 已有类型和ts新增类型

JS 已有类型

2.1原始数据类型( number/string/boolean/null/undefined )

let age: number = 18
let myName: string = '老师'
let isLoading: boolean = false

2.2复杂数据类型(数组,对象,函数等)

// 写法一(推荐):
let numbers: number[] = [1, 3, 5]
// 写法二:
let strings: Array<string> = ['a', 'b', 'c']

TS 新增类型

2.3联合类型,通过联合类型将多个类型组合成一个类型。

如果数组中既有 number 类型,又有 string 类型,那么这个数组的类型应该如何写?

  let arr: (number | string)[] = [1, 2, 3, 'abc']
  // 注意事项: | 的优先级较低, 需要用 () 包裹提升优先级
  // 一旦使用联合类型, 说明 arr 中存储的既可能是 number 也可能是 string
  // 所以会丢失一部分提示信息 (只能提示共有的方法和属性)
  let timerId: number | null = null

| (竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种
注意:这是 TS 中联合类型的语法,只有一根竖线,不要与 JS 中的或(||)混淆了。

2.4自定义类型(类型别名)使用类型别名给类型起别名,简化类型的使用。

当同一类型(复杂)被多次使用时,可以通过类型别名,简化该类型的使用

// 将一组类型存储到「变量」里, 用 type 来声明这个特殊的「变量」
type CustomArray = (number | string)[]
let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]

2.5函数类型

函数的类型实际上指的是: 函数参数 和 返回值 的类型

  • 单独指定参数、返回值的类型
  // 函数声明
  // function 函数名(参数1: 参数1类型, 参数2: 参数2类型): 返回值类型 { 函数体 }
   function add(a: number, b: number): number {
     return a + b
   }
  // 函数表达式
   const fn = function(a: number, b: number): number {
     return a + b
   }
  // 箭头函数
  // 注意事项: 以前箭头函数如果只有一个参数, 则可省略小括号, ts不行
  // ts箭头函数必须要有小括号
   const sub = (a: number): number => {
     return a
   }
   const sub = (a: number, b: number): number => {
     return a - b
   }
  • 同时指定参数、返回值的类型(函数的类型别名)
type AddFn = (num1: number, num2: number) => number
const add: AddFn = (num1, num2) => {
    return num1 + num2
}

这种形式只适用于函数表达式

2.6 void类型

如果函数没有返回值,那么,函数返回值类型为: void;无返

// 如果什么都不写,此时,add 函数的返回值类型为: void
const add = () => {}
// 这种写法是明确指定函数返回值类型为 void,与上面不指定返回值类型相同
const add = (): void => {}
// 但如果指定返回值类型为 undefined 时,函数体中必须显式的 return undefined 才可以
const add = (): undefined => {
// 此处,返回的 undefined 是 JS 中的一个值
return undefined
}

2.7 可选参数;使用?给函数指定可选参数类型

  // 注意事项: 必选参数不能在可选参数后(可选参数只能出现在参数列表的最后)
  const print = (name?: string, gender?: string): void => {
    if (name && gender) {
      console.log(name, gender)
    }
  }

2.8 对象类型

JS 中的对象是由属性和方法构成的,而 TS 对象的类型就是在描述对象的结构(有什么类型的属性和方法)

let person: {
  name: string
  sayHi(): void
} = {
  name: 'jack',
  sayHi(content: string) {}
}

也可以用类型别名进行简化

  type Person = {
    name: string,
    age: number,
    girlFriend?: string, // ? 表示可选属性
    // sayHi: (content: string) => void
    sayHi(content: string): void
  }

  // ts 就像在写注释, 以前写的注释是给程序员看的, ts 写的类型是给编辑器看的, 程序员也可以看
  let obj1: Person = {
    name: 'james',
    age: 39,
    sayHi(content) {
      console.log(content)
    }
  }

2.9 接口类型

当一个对象类型被多次使用时,一般会使用接口( interface )来描述对象的类型,达到复用的目的

interface IPerson {
  name: string
  age: number
  sayHi(): void
}
let person: IPerson = {
  name: 'jack',
  age: 19,
  sayHi() {}
}

interface(接口)和 type(类型别名)的对比:

  • 相同点:都可以给对象指定类型
  • 不同点:接口只能为对象指定类型;类型别名不仅可以为对象指定类型,实际上可以为任意类型指定别名

推荐:能使用 type 就是用 type

2.9.1 接口继承

接口继承: 可以实现一个接口使用另一个接口的类型约束, 实现接口的复用

  interface IPerson {
    username: string
    age: number
    gender: string
    sayHi: () => void
  }
  // 接口继承: IStudent 具备 IPerson 的所有约束规则
  interface IStudent extends IPerson {
    score: number
    sleep: () => void
  }

  const s1: IStudent = {
    username: 'james',
    age: 19,
    gender: '男',
    sayHi() {
      console.log('詹姆斯')
    },
    score: 59,
    sleep() {
      console.log('詹姆斯正在睡觉...')
    },
  }

使用type也能实现类似继承的效果

// 使用 type 实现和 interface 类似继承的效果
  type Person = {
    username: string
    age: number
    gender: string
    sayHi: () => void
  }

  // & 与连接符: 既要满足前面的也要满足后面的
  // | 或连接符: 满足其中一个即可
  type Student = {
    score: number
    sleep: () => void
  } & Person
  
  const s2: Student = {
    username: 'james',
    age: 28,
    gender: '未知',
    sayHi() {
      console.log('hello!')
    },
    score: 80,
    sleep() {
      console.log('我在睡觉')
    },
  }

如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用

2.10 元组类型

元组类型是另一种类型的数组,它确切地知道包含多少个元素,以及特定索引对应的类型

let position: [number, number] = [39.5427, 116.2317]
  • 元组类型可以确切地标记出有多少个元素,以及每个元素的类型
  • 该示例中,元素有两个元素,每个元素的类型都是 number

使用 number[] 的缺点:不严谨,因为该类型的数组中可以出现任意多个数字

2.11 类型推论

在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型

// 变量 age 的类型被自动推断为:number
let age = 18
// 函数返回值的类型被自动推断为:number
function add(num1: number, num2: number): number {
return num1 + num2
}

发生类型推论的 2 种常见场景:

  • 声明变量并初始化时
  • 决定函数返回值时

2.12 字面量类型

let str1 = 'Hello TS'
const str2 = 'Hello TS'

通过 TS 类型推论机制:

  • 变量 str1 的类型为:string
  • 变量 str2 的类型为:‘Hello TS’

此处的 ‘Hello TS’,就是一个字面量类型,也就是说某个特定的字符串也可以作为 TS 中的类型

// 使用自定义类型:
type Direction = 'up' | 'down' | 'left' | 'right'
function changeDirection(direction: Direction) {
console.log(direction)
}
// 调用函数时,会有类型提示:
changeDirection('up')
  • 解释:参数 direction 的值只能是 up/down/left/right 中的任意一个
  • 优势:相比于 string 类型,使用字面量类型更加精确、严谨

字面量类型就是把字面量当做类型来用

2.13 枚举类型

枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个

// 创建枚举
enum Direction { Up, Down, Left, Right }
// 使用枚举类型
function changeDirection(direction: Direction) {
    console.log(direction)
}
// 调用函数时,需要应该传入:枚举 Direction 成员的任意一个
// 类似于 JS 中的对象,直接通过 点(.)语法 访问枚举的成员
changeDirection(Direction.Up)
  • 数字枚举:默认为从 0 开始自增的数值
// Down -> 11、Left -> 12、Right -> 13
enum Direction { Up = 10, Down, Left, Right }
  • 字符串枚举:字符串枚举的每个成员必须有初始值(没有自增长行为)
enum Direction {
	Up = 'UP',
	Down = 'DOWN',
	Left = 'LEFT',
	Right = 'RIGHT'
}
  • 枚举是 TS 为数不多的非 JavaScript 类型级扩展(不仅仅是类型)的特性之一
    因为:其他类型仅仅被当做类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)
    也就是说,其他的类型会在编译为 JS 代码时自动移除。但是,枚举类型会被编译为 JS 代码
enum Direction {
	Up = 'UP',
	Down = 'DOWN',
	Left = 'LEFT',
	Right = 'RIGHT'
}
// 会被编译为以下 JS 代码:
var Direction;
(function (Direction) {
	Direction['Up'] = 'UP'
	Direction['Down'] = 'DOWN'
	Direction['Left'] = 'LEFT'
	Direction['Right'] = 'RIGHT'
})(Direction || Direction = {})

2.14 any类型

这会让 TypeScript 变为 “AnyScript”(失去 TS 类型保护的优势)
当值的类型为 any 时,可以对该值进行任意操作,并且不会有代码提示

  • 其他隐式具有 any 类型的情况
  1. 声明变量不提供类型也不提供默认值
  2. 函数参数不加类型

注意:因为不推荐使用 any,所以,这两种情况下都应该提供类型

  function fn(a, b) {}
  let b

2.15 类型断言

使用类型断言来指定更具体的类型

const aLink = document.getElementById('link')
  • 该方法返回值的类型是 HTMLElement,此类型只包含所有标签公共的属性或方法,不包含 a
    标签特有的 href 等属性,导致无法操作 href 等 a 标签特有的属性或方法(这种情况下就需要使用类型断言指定更加具体的类型)
const aLink = document.getElementById('link') as HTMLAnchorElement
  • 关键字 as 后面的类型是一个更加具体的类型(HTMLAnchorElement 是 HTMLElement 的子类型)
// 此语法不常用,知道即可:
const aLink = <HTMLAnchorElement>document.getElementById('link')

2.16 TypeScript泛型

  • 泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用
  • 在 C# 和 Java 等编程语言中,泛型都是用来实现可复用组件功能的主要工具之一
//定义泛型函数
function id<Type>(value: Type): Type { return value }
function id<T>(value: T): T { return value }
  • 该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)
// 调用泛型函数
const num = id<number>(10)
const str = id<string>('a')

实现了复用的同时保证了类型安全

泛型函数的调用可简化=>省略 <类型> 来简化泛型函数的调用,利用TS内部的类型参数推断

// 省略 <number> 调用函数
let num = id(10)
let str = id('a')

当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数

2.17 泛型约束

为什么需要泛型约束呢,举例一个场景:

function id<Type>(value: Type): Type {
	console.log(value.length)
	return value
}
id(2)
  • Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length

此时,就需要为泛型添加约束来收缩类型 (缩窄类型取值范围)
添加泛型约束收缩类型,主要有以下两种方式:1 .指定更加具体的类型 2. 添加约束

1、指定更加具体的类型

function id<Type>(value: Type[]): Type[] {
	console.log(value.length)
	return value
}

将类型修改为 Type[] (Type 类型的数组),只要是数组就一定存在 length 属性

2、添加约束

// 创建一个接口
interface ILength { length: number }
// Type extends ILength 添加泛型约束
// 解释:表示传入的类型必须满足 ILength 接口的要求才行,也就是得有一个 number 类型的 length 属性
function id<Type extends ILength>(value: Type): Type {
	console.log(value.length)
	return value
}
  • 创建描述约束的接口 ILength,该接口要求提供 length 属性
  • 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束
  • 该约束表示:传入的类型必须具有 length 属性
    此方法,传入的实参(比如,数组)只要有 length 属性即可(类型兼容性)

2.18 泛型约束——多个类型变量

泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)
比如,创建一个函数来获取对象中属性的值:

function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
	return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')
  • 两个类型变量之间的使用,用逗号分隔。
  • keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。
  • 本示例中 keyof Type 实际上获取的是 person 对象所有键的联合类型,也就是: ‘name’ | ‘age’
  • 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性
function getProperty<Type extends object, Key extends keyof Type>(obj: Type, key: Key)
{
	return obj[key]
}

2.19 泛型接口

泛型接口:接口也可以配合泛型来使用,以增加其灵活性,增强其复用性

interface IdFunc<Type> {
	id: (value: Type) => Type
	ids: () => Type[]
}
let obj: IdFunc<number> = {
	id(value) { return value },
	ids() { return [1, 3, 5] }
}

在接口名称的后面添加 <类型变量> ,那么这个接口就变成了泛型接口。

 47、join()、 push()、pop()、shift()、unshift()、slice()、splice()、sort()、substring()、substr()、indexOf()、includes()常用函数

join():将数组中元素 组成字符串 ,需要传个参数作为连接符,不传的话默认就是逗号。

var a = ['a', 'b', 'c'];
var b = a.join();
console.log(a); //[ 'a', 'b', 'c' ],不改变原数组
console.log(b); // a,b,c
var c = a.join('');
var d = a.join(`'`);
console.log(c); // abc
console.log(d); // a'b'c

push():在数组 尾部逐个添加 元素,返回结果数组的长度,能接收任意数量参数,修改原数组。

var a = ['a', 'b', 'c'];
var b = a.push(1); 
console.log(a); //['a', 'b', 'c', 1]
console.log(b); // 4
var c = a.push(2, 3); 
console.log(a); //['a', 'b', 'c', 1, 2, 3]
console.log(c); // 6

pop():移除数组最后一项,返回的是被移除项。修改原数组

var a = ['a', 'b', 'c'];
var b = a.pop(); 
console.log(a); //['a', 'b']
console.log(b) // c

shift():删除数组的第一项元素,返回被删除的元素, 修改原数组

var a = [1, 2, 3, 4, 5];
var b = a.shift();
console.log(a) // [ 2, 3, 4, 5 ]
console.log(b); // 1

unshift():向数组的头部添加元素,返回的是结果数组的长度,修改原数组

var a = [1, 2, 3, 4, 5];
var b = a.unshift('a', 'b', 'c');
console.log(a); // [ 'a', 'b', 'c', 1, 2, 3, 4, 5 ]
console.log(b); //8

slice():/slaɪs/  slice() 方法可从已有的数组中返回选定的元素。并不会修改原数组。

arrayObject.slice(start,end)

参数

start——必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始倒推选取。比如,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。如果不传参数,则当0处理。

end——可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

返回值: 返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。

说明:
请注意,该方法并不会修改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该使用方法 Array.splice()。

提示和注释:

  • 可使用负值从数组的尾部选取元素。
  • 如果 end 未被规定,那么 slice() 方法会选取从 start 到数组结尾的所有元素。
  • 如果start是正数且大于arrayObject.length,则返回[]空数组。如果start是负数且绝对值大于arrayObject.length,则当0处理。

并不会改变原字符串和数组。

// 只有start,参数为正值
var a = [1, 2, 3, 4, 5];
var b = a.slice(2);
console.log(a); // 原数组不变[ 1, 2, 3, 4, 5 ]
console.log(b); // 得到从下标2开始到数组结束的所有参数[ 3, 4, 5 ]

// 只有start,参数为负值
var c = a.slice(-2);
console.log(c); // 得到从下标数组倒数第2个开始到数组结束的所有参数[ 4, 5 ]

// start、end
var d = a.slice(1, 4); //从下标1开始,选到下标4(不包含4)的数据
console.log(d); // [ 2, 3, 4]

// start、end 负值
var e = a.slice(-3, -1); //从下标倒数第三个到倒数第一个(不包含)的值
console.log(e); // [ 3, 4 ]

splice():/splaɪs/ 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

Array.splice(index, howmany,  item, ...,  itemX)

参数:

index —— 必需。整数,规定添加/删除项目的位置。使用负数可从数组结尾处规定位置。

howmany —— 必需。要删除的项目数量。包含该index项在内。如果设置为 0,则不会删除项目。

item1, …, itemX —— 可选。向数组添加的新项目。在index项之前添加。

返回值:通常为删除掉的数组元素。

原数组:被改变,原数组经过删除或增加之后的。

// 一个参数
var a = [1, 2, 3, 4, 5];
var b = a.splice(2); // a从下标2开始包括2删除后面所有的参数
console.log(a); // [ 1, 2 ]
console.log(b); // [ 3, 4, 5 ]

// 两个参数
var c = [1, 2, 3, 4, 5, 6]; // c从下标3开始,长度为2删除,c剩余[ 1, 2, 3, 6 ],b得到[ 4, 5 ]
var d = c.splice(3, 2); 
console.log(c); // [ 1, 2, 3, 6 ]
console.log(d); // [ 4, 5 ]

// 大于三个参数
var e = [1, 2, 3, 4, 5, 6];
var f = e.splice(2, 3, 'a', 'b'); // e从下标2开始,长度为3删除,且在删除位置后面插入'a', 'b'
console.log(e); // [ 1, 2, 'a', 'b', 6 ]
console.log(f) // [ 3, 4, 5 ]

sort():将数组按照从小到大的顺序排列, 修改原数组 。

没有参数会按照数组元素对应的ASCII码进行比较和排序,sort()无法对由两位数以上的数组元素构成的数组进行合理排序,因此需要用到比较参数。

// 没有参数,按照ASCII码比较
var a = ['e', 'b', 'c', 'a'];
var b = a.sort(); // 原数组也会改变
console.log(a); // [ 'a', 'b', 'c', 'e' ]
console.log(b); // [ 'a', 'b', 'c', 'e' ]

// 没有参数,大于两位的数值不好比较
var c = [11, 1, 25, 5, 4, 6, 7];
console.log(c.sort()); // [1, 11, 25, 4, 5,  6,  7]

// 没有参数, 尽可能的使用比较参数
var d = [11, 1, 25, 5, 4, 6, 7];
var e = d.sort((val1, val2) => val1 - val2);
console.log(e); // [1,  4,  5, 6, 7, 11, 25]

substring():用于从字符串中提取指定范围的子串,并返回一个新的字符串。

substring(start, end);

参数:

start——表示起始位置(包含);

end ——表示结束位置(不包含)。
如果省略 end 参数,则提取从 start 位置到字符串末尾的所有字符。这两个参数传入的任意负值都当作0处理。
并不会改变原字符串。

var a = 'Hello,world!';
var b = a.substring(2,5); //从下标2开始到下标5(不包含5)的字符
console.log(a); // 'Hello,world!'
console.log(b); // 'llo'
var c =  a.substring(1); //从下标1开始字符串结尾的字符
console.log(c); // ello,world!
console.log(a.substring(-1)); //传入负数当0处理 Hello,world!
console.log(a.substring(3, 1)); //从下标3到下标1(不包含1)的字符 el
console.log(a.substring(3, -2)); //从下标3到下标0的字符,传入负数当作0处理 Hel

substr():可在字符串中抽取从 start 下标开始的指定数目的字符。

参数:

参数start:必需。要抽取的子串的起始下标。必须是数值。如果是负数,那么该参数声明从字符串的尾部开始算起的位置。也就是说,-1 指字符串中最后一个字符,-2 指倒数第二个字符,以此类推。如果不传参数,则当0处理。
参数length:可选。子串中的字符数。必须是数值。如果省略了该参数,那么返回从 stringObject 的开始位置到结尾的字串。

返回值:

返回一个新的字符串,包含从 stringObject 的 start(包括 start 所指的字符) 处开始的 length 个字符。如果没有指定 length,那么返回的字符串包含从 start 到 stringObject 的结尾的字符。

提示和注释

注释:substr() 的参数指定的是子串的开始位置和长度,因此它可以替代 substring() 和 slice() 来使用。

重要事项:ECMAscript 没有对该方法进行标准化,因此反对使用它。在 IE 4 中,参数 start 的值无效。在这个 BUG 中,start 规定的是第 0 个字符的位置。在之后的版本中,此 BUG 已被修正。

var a = 'Hello,world!';
var b = a.substr(2);
console.log(a); // 不改变原字符串
console.log(b); // llo,world! 从下标2开始到最后字符串截取
console.log(a.substr(2, 3)); // llo 从下标2开始长度为3的字符串截取
console.log(a.substr(-3, 2)); // ld 从下标-3开始长度为2的字符串截取
console.log(a.substr(-3, -1)); // 空字符串
console.log(a.substr(0)); // Hello,world! 

区分

在传递给这些方法的参数是负值的情况下,它们的行为就不尽相同了。其中,
slice()方法会将传入的负值与字符串的长度相加;
substr()方法将负的第一个参数加上字符串的长度,而将负的第二个参数转换为 0;
substring()方法会把所有负值参数都转换为0。

indexOf:查找字符串中某个字符或字符串下表的位置,找到返回第一次出现的下标, 未找到返回-1

// 不传递第二个参数: 默认从0开始
[1,2,3,4,5].indexOf(3);  // 查找字符3,找到返回3所在的下标,即为2

// 第一个参数要查找的, 第二个参数从哪个索引开始
[1,2,3,4,5].indexOf(3, 1);  // 查找字符3,从索引1开始找直到最后,  字符3在索引2的位置,所以能找到3, 返回索引值2
 
 
//从索引值为2的地方开始查找值为1,找不到返回-1,找到返回索引值
[1,2,3,4,5].indexOf(2,1);// -1
 
//'-'代表需要从后往前  在这里:-1指向字符4、-2->3......以此类推
//从4(索引值为-1)的位置开始寻找2
[1,2,3,4].indexOf(2,-1);// -1

includes:判断一个字符串是否包含另外一个字符串,包含返回true, 否则false

 // 第一个参数:需要查找的元素,  第二个参数查找的起始索引,不写默认从0开始
[1,2,3,4,5].includes(2); // true

 // 判断字符串2,是否在数组里,从索引为1开始向右查找直到最后,字符串2在索引1的位置
[1,2,3,4,5].includes(2,1);// true
 
 //从5(索引值为-1)的位置开始向右寻找2,找不到,返回false
 [1,2,3,4,5].includes(2,-1);// false
 
 //从索引值为-3的位置开始寻找字符c, 索引-3对应数组字符串c的位置。e-> -1, d-> -2, c-> -3
 [a,b,c,d,e].includes(c,-3);// true

 NaN、undefined 的处理:
indexoOf():不能匹配到数组中的NaN
includes():能匹配到数组中的NaN

[1,2,NaN].indexOf(NaN);// -1
[1,2,NaN].includes(NaN);// true
 
 
[1,2,undefined].indexOf(undefined);// -1
[1,2,undefined].includes(undefined);// true

相同点:
方法的基本调用:第一个参数:要查找的元素;第二个参数:position从哪个索引值开始查找

不同点:

数据类型转换:

        字符串在执行匹配的时候:进行类型转换

        数组在执行匹配的时候:严格相等匹配(===)

'12345'.indexOf(2);   // 1  会进行数据类型转换
'12345'.includes(2);  // true
 
 
['1','2','3','4','5'].indexOf(2);   // -1   不会进行数据类型转换
['1','2','3','4','5'].includes(2);  // false
 

split():str.split(separator, limit)将一个字符串分割成数组

  • separator(可选):指定用于分割字符串的分隔符。可以是字符串、正则表达式或空字符串。如果省略该参数,则默认将整个字符串拆分成单个字符的数组。
  • limit(可选):指定返回的数组的最大长度。如果提供了该参数,则最多只返回指定数量的元素。
console.log("abcd".split('')); // [ 'a', 'b', 'c', 'd' ]
console.log("ab,cd".split(',')); // [ 'ab', 'cd' ]
console.log("ab,cd,efg,hj".split(',', 3)); // [ 'ab', 'cd', 'efg' ]

[...str]也可以把字符串分割成数组

console.log([...'123']) //[ '1', '2', '3' ]

join():Array.join(separator) 将数组中的所有元素拼接成字符串

  • separator(可选):指定一个字符串来分隔数组的每个元素。如果省略该参数,则使用逗号(,)作为分隔符。
console.log(['a','b', '1'].join(',')); // a,b,1
console.log(['a','b', '1'].join('-'));// a-b-1

reverse(): 数组反转

[1,2,3,4].reverse()    //[4, 3, 2, 1]

trim():去除字符串两端的空格

' 123    '.trim(); //123

toLowerCase(): 将字符串转为小写字符  string.toLowerCase();

toUpperCase():将字符串转为大写字符  string.toUpperCase();

console.log('Wqw12VBBB'.toLowerCase()) // wqw12vbbb

console.log('Wqw12VBBB'.toUpperCase()) // WQW12VBBB

 49、常用的一些字符数组方法

字符串倒转:

function reverseString(str) {
    return str.split('').reverse().join('');
}
 
// 示例使用
var originalString = "Hello, World!";
var reversedString = reverseString(originalString);
console.log(reversedString); // 输出: "!dlroW ,olleH"

验证回文串 倒转是否相等

var isPalindrome = function(s) {
    //先替换掉所有非字母和数字
    //再替换掉所有的空格
    //然后后reverse()方法颠倒顺序
    //最后两者进行对比
     s=s.replace(/[^a-zA-Z0-9]/g,"").replace(/\s/g,"").toLowerCase();
     return s===[...s].reverse().join("")
}

多个空格替换成一个空格,头尾空格去除使用trim()

function replaceMultipleSpacesWithSingle(str) {
  return str.replace(/\s+/g, ' ');
}

var originalString = "这  是  一  个  测  试  字  符  串";
var newString = replaceMultipleSpacesWithSingle(originalString);
console.log(newString); // 输出: "这 是 一 个 测 试 字 符 串"

字符串脱敏处理

function replaceStar(str, frontLen, endLen, repStr) {
    // 计算需要替换的字符数量
    const replaceLength = str.length - frontLen - endLen;
    // 如果计算出的长度小于0,则不进行替换
    if (replaceLength < 0) return str;
    // 使用repStr重复replaceLength次来构建替换字符串
    const star = repStr.repeat(replaceLength);
    // 返回拼接后的字符串
    return str.substring(0, frontLen) + star + str.substring(str.length - endLen);
}
console.log(replaceStar('湖北省恩施市某某乡', 2, 2, '*')); // 输出: 湖北*****某乡

字符串数组根据首字母分类

const group = (arr: Array<string>): Record<string, string[]> => {
    const obj: Record<string, string[]> = {};
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        // 确保元素不为空且是字符串
        if (typeof element === 'string' && element.length > 0) {
            // 首字母转为小写收集
            const firstLetter = element[0].toLocaleLowerCase();
            if (!obj[firstLetter]) obj[firstLetter] = [];
            obj[firstLetter].push(element);
        }
    }
    return obj;
}

console.log(group(['c11', 'a22', 'b111', 'b222', 'B222']));

根据id把一维数组变成二维数组


const arrTree = (data) => {
    const tree = [];
    const treeMap = {};
    // id键值对
    data.forEach(item => {
       treeMap[item.id] = {...item, chrild: []}
    });
    data.forEach(item => {
        const parentId = item.parent_id;
        if (parentId === null) {
            tree.push(treeMap[item.id])
        } else {
            if (treeMap[parentId]) {
                // treeMap的值变动,也会改变tree中已push的值
                treeMap[parentId].chrild.push(treeMap[item.id]);
            }
        }
    });
    return tree;
}

const data = [
    { id: 0, name: '0', parent_id: 1 },
    { id: 1, name: '1', parent_id: null },
    { id: 2, name: '2', parent_id: 1 },
    { id: 3, name: '3', parent_id: 2 },
    { id: 4, name: '4', parent_id: null }
];

console.log(arrTree(data))

打印等腰圣诞树

function printIsoscelesTriangle(height) {
    for (let i = 0; i < height; i++) {
        // 空格
        let spaces = ' '.repeat(height - i - 1);     
        // star 1 3 5 7 9 
        let stars = '*'.repeat(i * 2 + 1 );
        console.log(spaces + stars)
    }
}
// 设定三角形的高度为5
printIsoscelesTriangle(5);

49、for、for in 、 for of 、forEach、while、map6种循环

50、冒泡排序、二分查找排序、sort

冒泡排序根据数组长度进行for循环,利用内层循环从数组的第一个往后比较,大则后移,一次循环以后,最大的就在最后面了,每轮依次获取并修改当前最大值,这样只需要再套一层循环让内层for循环的可以每次循环减少1个比较长度直到最前面两个最小的也比较完成即可

let arr = [11, 10, 8, 7, 9];
const quickSort = (arr) =>{
    for (let i = 0; i < arr.length - 1; i++) {
        for (let j = 0; j < arr.length - i - 1; j++) {
            console.log(arr[i], arr[j]);
            // 从小到大 > 从大到小 <
            if (arr[j] > arr[j+1]) {
                let oldVal = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = oldVal;
            }
        } 
    }
    return arr;
}
quickSort(arr);
console.log(arr);

二分查找排序

let arr = [11, 10, 12, 8, 6, 9, 7];
const binarySort = (arr) => {
    if(arr.length <= 1) return arr;
    let middleVal = arr.splice(Math.floor(arr.length / 2), 1); \\splice取arr长度一半的整数位置的长度为1的数组值,middleVal 是个数组
    console.log(middleVal,middleVal[0]);
    let leftArr = [], rightArr = [];
    for (let index = 0; index < arr.length; index++) {
        // 从小到大<; 从大到小 >
        if(arr[index] < middleVal[0]) {
            leftArr.push(arr[index]);
        } else {
            rightArr.push(arr[index]);
        }
    }
    return binarySort(leftArr).concat(middleVal, binarySort(rightArr));
}
console.log(binarySort(arr));

sort排序

// sort 排序 b - a从大到小; a - b 从小到大
let arr = [11, 10, 8, 7, 9];
arr.sort((a, b) => b - a);
console.log(arr);

50、交集补集差集合集

var a = [1,2,3,4,5,6];
var b = [2,3,7];
// 数组转换成Set,Set转换成数组使用Array.from(setA)或者解构赋值[...setA]
var setA = new Set(a);
var setB = new Set(b);

// 并集
var unionArr = Array.from(new Set(a.concat(b)));
console.log(unionArr);//[ 1, 2, 3, 4, 5, 6, 7 ]

// 交集第一种
var intersectionArr1 = Array.from(a.filter(value => setB.has(value)));
// 交集第二种
var intersectionArr2 = a.filter(value => b.includes(value));
console.log(intersectionArr1, intersectionArr2);//[ 2, 3 ]

//第一个数组中有且第二个数组中没有的元素
// 差集第一种
var minusArr1 = Array.from(a.filter(value => !setB.has(value)));
// 差集第二种
var minusArr2 = a.filter(value => !b.includes(value));
console.log(minusArr1, minusArr2);//[ 1, 4, 5, 6 ]

// 补集
var complementArr1 = [...a.filter(value => !b.includes(value)), ...b.filter(value => !a.includes(value))];
var complementArr2 = [...Array.from(a.filter(value => !setB.has(value))), ...Array.from(b.filter(value => !setA.has(value)))];
console.log(complementArr1, complementArr2); //[ 1, 4, 5, 6, 7 ]

51、前端安全性能问题

(1)xss攻击:通过输入一些恶意指令代码攻击,前端在输入的时候进行一些输入限制过滤或者转义,但攻击者可能通过工具绕过前端的输入限制,因此还需后台服务器在接收到数据后,对特殊危险字符进行过滤或者转义,再存储到数据库。

(2)CSRF 跨站请求伪造:这种会在攻击网站放置请求,如果刚好用户访问网站信息未过期后浏览了攻击网站,这个攻击请求就会成功。在一些敏感操作的页面(如账户交易),增加验证流程(比如指纹密码短信验证码等),尽量使用POST,限制GET,服务器检查检查Referer字段,前后端新增每次请求增加token验证。

(3)sql注入:一般是后台针对数据做多层验证或者权限管理等。

除了上述几种攻击方式,前端安全还涉及其他问题,如点击劫持资源篡改未授权访问等。防范措施包括设置X-Frame-Options头、使用iframe sandbox、加密敏感信息、使用HTTPS等‌2。

防范措施和最佳实践

为了增强前端安全性,可以采取以下措施:

  1. 过滤和验证输入数据‌:确保所有输入数据进行适当的过滤和验证,防止恶意代码注入。
  2. 使用内容安全策略(CSP)‌:限制页面可以加载的资源类型,防止XSS攻击。
  3. 实施双重验证‌:增加额外的验证步骤,提高账户安全性。
  4. 使用HTTPS‌:加密用户与服务器之间的通信,防止中间人攻击。
  5. 定期安全审计‌:定期对应用程序进行安全审计,及时发现并修复漏洞‌12。

52、深拷贝和浅拷贝

浅拷贝仅复制对象的第一层属性。对于复杂(嵌套)对象,浅拷贝仅复制对象的引用,而不是实际内容。例如,Object.assign、数组的slice方法、展开运算符...以及一个a对象直接复制给b对象都是浅拷贝。浅拷贝可以理解为改变目标对象的值得同时改变了源对象。

//展开运算符
let obj = {a: 1, b: {c: 2}};
let shallowCopy = {...obj};
//Object.assign
let obj = {a: 1, b: {c: 2}};
let shallowCopy = Object.assign({}, obj);

考考你:这道题会输出什么?

let obj = {a:1, b: {c:1}};
let obj2 = {...obj};
obj.a = 2;
obj.b.c = 2;
console.log(obj);
console.log(obj2)

obj 被修改为 {a: 2, b: {c: 2}}obj2 被赋值为 {a: 1, b: {c: 2}}

这是因为 obj2 = {...obj} 实际上是一个浅拷贝。这意味着 obj2 中的 a 属性是独立于 obj 的,但 b 属性是一个对象的引用,所以当你修改 obj.b.c 时,这个改变也会反映在 obj2 中。

深拷贝则是复制对象的所有层级的属性,不管是基本类型还是复杂类型,目标对象都与源对象完全独立。这意味着修改目标对象不会影响源对象,反之亦然。JSON 的 stringify 和 parse、浏览器新特性structuredClone(obj, options)可以实现深拷贝。

//JSON 的 stringify 和 parse
let obj = {a: 1, b: {c: 2}};
let deepCopy = JSON.parse(JSON.stringify(obj));


//structuredClone
//基本对象的深拷贝:
const obj = { a: 1, b: { c: 2 } };
const clone = structuredClone(obj);

console.log(clone); // { a: 1, b: { c: 2 } }
console.log(clone.b !== obj.b); // true,b 是不同的对象


//数组的深拷贝
const arr = [1, 2, [3, 4]];
const cloneArr = structuredClone(arr);

console.log(cloneArr); // [1, 2, [3, 4]]
console.log(cloneArr[2] !== arr[2]); // true,内部数组是不同的引用

//日期对象的深拷贝:
const date = new Date();
const cloneDate = structuredClone(date);

console.log(cloneDate); // 输出与原日期相同的日期
console.log(cloneDate instanceof Date); // true

//Map 和 Set 的深拷贝:
const map = new Map([[1, 'one'], [2, 'two']]);
const cloneMap = structuredClone(map);

console.log(cloneMap); // Map(2) { 1 => 'one', 2 => 'two' }
console.log(cloneMap !== map); // true,map 是不同的引用

const set = new Set([1, 2, 3]);
const cloneSet = structuredClone(set);

console.log(cloneSet); // Set(3) { 1, 2, 3 }
console.log(cloneSet !== set); // true,set 是不同的引用

53、js事件委托机制

事件委托是一种高效且实用的事件处理方式,特别适用于动态元素和大量子元素的场景。通过利用事件冒泡机制,事件委托能够简化代码结构、提升性能,并且为动态内容提供灵活的处理方式。

事件委托是一种优化DOM事件处理的技术,它允许你将事件监听器绑定到父元素,而不是每个子元素单独设置。当事件发生时,浏览器会检查事件的目标是否匹配该监听器,如果是,则执行回调函数。

// html
<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
</ul>
// js
// 获取父元素
const list = document.getElementById('myList');
// 事件监听器绑定在父元素上
list.addEventListener('click', function(event) {
  // 检查事件是否来自列表中的一个子元素
  if (event.target && event.target.nodeName === 'LI') {
    // 如果是,执行操作
    console.log('List item clicked:', event.target.textContent);
  }
});

在这个例子中,我们将事件监听器绑定到了<ul>元素上,而不是每个<li>元素上。当点击列表中的任何一个项时,事件会冒泡到父元素,然后通过检查event.targetnodeName来判断是否为列表项。如果是,则执行相应的操作。

事件冒泡:是指当一个元素上的事件被触发后,事件会从该元素开始沿着 DOM 树向上冒泡到更高层次的父元素,直至达到根节点。这意味着如果一个子元素上的事件被触发,其父元素上绑定的相同事件也会被触发。事件冒泡是默认的事件传播方式。

事件捕获:事件捕获是事件冒泡的另一种模式。在事件捕获中,事件会从根节点开始,依次向下沿着 DOM 树传播,直至达到事件的目标元素。然后,事件才会在目标元素上触发。

54、js垃圾回收机制

JavaScript垃圾回收机制的基本概念和作用‌

JavaScript的垃圾回收机制(Garbage Collection, GC)是自动管理内存的一种机制,用于检测和回收不再使用的内存,以释放资源并提高性能。当变量、函数、对象等不再被使用时,垃圾回收机制会自动释放它们所占用的内存,防止内存泄漏和溢出。

垃圾回收机制的工作原理

垃圾回收机制主要通过两种策略工作:标记清除和‌引用计数。标记清除算法通过标记活动对象并清除未标记的对象来回收内存。引用计数算法通过记录每个对象的引用次数,当引用次数为零时,对象被视为不再使用并可以回收。

垃圾回收机制的触发条件和性能问题

垃圾回收的触发条件通常与内存使用情况有关,当内存使用达到一定阈值时,垃圾回收机制会被触发。然而,频繁的垃圾回收可能会影响应用程序的性能。例如,‌IE浏览器在早期版本中根据分配的变量数量触发垃圾回收,这可能导致性能问题。现代浏览器通常采用更智能的触发策略,以减少对性能的影响。

55、重绘和回流

  • 回流(reflow):当DOM的结构发生改变或者某个元素的样式发生变化时,浏览器需要重新计算并重新布局(layout)页面中的元素,这个过程就称为回流。回流会导致浏览器重新计算元素的位置和大小,然后重新绘制到屏幕上,是一种相对耗费资源的操作。
  • 重绘(repaint):当页面元素的样式(如颜色、背景等)发生变,但并不影响其布局时,浏览器只需要重新绘制(repaint)这些元素,而无需重新计算元素的布局,这个过程称为重绘。重绘的性能开销较小,因只是简单地更新元素的样式。

回流和重绘都会带来性能消耗,因此在前端开发中,要尽可能减少回流和重绘的次数,以提高页面的渲染性能。重绘和回流是前端性能优化中需要格外关注的问题。通过使用 class 替代 style,使用文档片段,使用 transform 替代 top/left,以及使用虚拟 DOM 等技巧和方法,我们可以显著减少页面的重绘和回流操作,提升页面的性能。在实际开发中,建议开发者时刻关注页面的性能,并遵循上述的技巧与方法进行优化。

56、熟悉主流浏览器的兼容性问题处理

一、认识浏览器
四大内核: Blink、Gecko、WebKit、Trident (不再活跃)

主流浏览器:
IE(Trident内核)、Firefox(火狐:Gecko内核)、Safari(苹果:webkit内核)、Google Chrome(谷歌:Blink内核)、Opera(欧朋:Blink内核)

(1)css兼容问题

  • 使用ormalize.css 抹平差异,同时可以定制自己的 reset.css,例如通过通配符选择器或者标签选择器(推荐),全局重置样式
  • CSS3兼容前缀表示

.box{ 
  height: 40px; 
  background-color: red; 
  color: #fff;
  border-radius: 5px;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
}

可以使用 Autoprefixer 是一个用于自动添加 CSS 浏览器前缀的工具,以确保你的样式在不同浏览器中正确地显示。它可以根据 Can I Use 数据库来确定需要添加哪些前缀,以满足特定的浏览器兼容性要求。

  • 图片加a标签在IE9中会有边框
img{border:none}
  • IE9以下浏览器不能使用opacity
Firefox/Chrome/Safari/Opera浏览器使用opacity;IE浏览器使用filter
opacity: 0.7; /*FF chrome safari opera*/ 
filter: alpha(opacity:70); /*用了ie滤镜,可以兼容ie*/
  • cursor兼容问题

统一使用 {cursor:pointer}

  • a标签css状态的顺序

按照link–visited–hover–active 的顺序编写

  • 在Chrome中字体不能小于10px

(2)js兼容问题

  • vue项目在IE11下一片空白

原因:
IE10浏览器解析不了es6的语法,需要我们使用babel(Babel是一种工具链,主要用于将ECMAScript 2015+代码转换为当前和旧版浏览器或环境中的向后兼容版本的JavaScript)。但是Babel 默认只转换新的 JavaScript 语法(syntax),而不转换新的 API ,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。

解决方法1:babel-polyfill
在vue项目中安装babel-polyfill,polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。

npm install --save-dev babel-polyfill

然后在mian.js中引入babel-polyfill(要在引入vue,vuex,elementui 之前引入,直接写在第一句)
import ‘babel-polyfill’;
注意: vue不持之ie8 以及之前的版本

这里介绍的是babel-polyfill的完整导入,如果为了性能考虑,你还可以针对ie的报错信息进行自动按需导入,如promise之类。
解决方法2:core-js
你可能听过’babel-polyfill’,babel-polyfill 融合了 core-js 和 regenerator-runtime,因此’babel-polyfill’ 本质就是’corejs’。
core-js 是 babel-polyfill 的底层依赖,通过各种奇技淫巧,用 ES3 实现了大部分的 ES2017 原生标准库,同时还要严格遵循规范。

npm i --save core-js

在main.js入口文件中引入:

'core-js' 导入几种形式:
// 导入所有新提案api
import "core-js";
// 唯一稳定的'core js'功能-es和web标准
import "core-js/stable";
// 仅支持稳定的ES功能
import "core-js/es";
// 只导入指定api
import "core-js/features/set";

import "core-js/stable/set";
import "core-js/es/set";

总结:简单使用,不太考虑优化: 1.npm i --save core-js 2.main.js入口文件中导入import “core-js”;
推荐使用:兼容性推荐使用core-js,因为core-js包含了babel-polyfill,且babel-polyfill已经逐步放弃使用了。

  • addEventListener 与 attachEvent 区别
attachEvent ——兼容:IE7、IE8;不兼容 firefox、chrome、IE9、IE10、IE11、safari、opera。
addEventListener——兼容:firefox、chrome、IE、safari、opera;不兼容 IE7、IE8

function addEvent(elm, evType, fn, useCapture) {
  if (elm.addEventListener) { // W3C标准
    elm.addEventListener(evType, fn, useCapture);
    return true;
  } else if (elm.attachEvent) { // IE
    var r = elm.attachEvent('on' + evType, fn); // IE5+
    return r;
  } else {
    elm['on' + evType] = fn; // DOM事件
  }
}

事件对象的兼容
    e = ev || window.event

  • 滚动事件的兼容

scrollTop = document.documentElement.scrollTop || document.body.scrollTop

  • 阻止冒泡的兼容
if (e.stopPropagation) { 
    e.stopPropagation()
} else {
    e.cancelBubble = true
 }
  • 阻止默认行为的兼容
link.onclick = function(event) {
  // 执行自定义逻辑
  if (event.preventDefault) {
    event.preventDefault();
  } else {
    event.returnValue = false;
  }
  return false;
};
  • const 问题

Firefox下,可以使用 const 关键字或 var 关键字来定义常量;IE下,只能使用 var 关键字来定义常量。
解决方案:统一使用 var 关键字来定义常量。

  • event.x 与 event.y 问题

IE下,event 对象有 x、y 属性,但是没有 pageX、pageY属性;Firefox下,event 对象有pageX、pageY属性,但是没有 x、y属性。

var myX = event.x ? event.x : event.pageX; 
var myY = event.y ? event.y : event.pageY;
  • 禁止选取网页内容

在Firefox下需要用CSS禁止选取网页内容,在IE用JS禁止

-moz-user-select: none; // Firefox
obj.onselectstart = function {return false;} // IE

(3)移动端兼容

  • 禁止iOS弹出各种操作窗口
-webkit-touch-callout:none
  • 禁止iOS和Android用户选中文字

-webkit-user-select:none

  • iOS下取消input在输入的时候英文首字母的默认大写

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

  • Android下取消输入语音按钮

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

  • 在移动端修改难看的点击的高亮效果,iOS和安卓下都有效

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

  • 在Android上placeholder文字设置行高会偏上

input有placeholder情况下不要设置行高

  • overflow: scroll或auto;在iOS上滑动卡顿的问题

-webkit-overflow-scrolling: touch;

  • iOS中日期如:2022-02-22 00:00:00格式的时间转时间戳不成功

需要将时间中的’00:00:00去除之后才能转为时间戳

57、for和foreach怎么跳出循环

(1)for循环使用return 、 break,是跳出了整个循环。 

for(var i = 0; i < 5; i++) {
    console.log(i)
    if (i == 3) {
        return;// 跳出整个循环
        // break; // 跳出整个循环
        // continue; // 跳出当前循环
    }
}

(2) foreach循环使用return跳出当前循环, 使用break语法错误。使用try catch跳出整个循环。

let arr = [1, 2, 3, 4, 5]
arr.forEach(item => {
	console.log(item)
	if (item == 3) {
		console.log('item')
		return;
		// break; // 语法报错
		console.log('return');
	}
})

那么如何在forEach 跳出整个循环,可以使用try ,然后主动抛出一个错误

let arr = [1, 2, 3, 4, 5];
try {
	arr.forEach(item => {
		console.log(item);
		if (item == 3) {
			console.log('item');
			throw new error // 主动去抛出一个错误
			// return;
			// break; // 语法报错
			console.log('return');
		}
	})
} catch {
	console.log('跳出来了');
}

(3)lodash中的forEach测试循环,和foreach一致

(4)for--of循环和for类型

let arr = [1, 2, 3, 4, 5];
for (const value of arr) { // 支持数组,类数组对象,Dom节点对象等
	console.log(value, '依次打印');
	if (value === 3) {
		let a = 0; // a为null或者undefined时,才为false
		let obj = {
			name: 'ceshi'
		}
		let b = a ?? '777';
		let c = a || '777';
		let d = obj?.name;
		console.log('item', b, c, d);
		// return;
		// break; //  跳出整个循环
		continue; // 结束后面的操作
		console.log('return');
	}
}

58、js对象中引用出现的问题

var arr1 = [1,2,3,4];
var arr2 = [1,2,3,4];
console.log(arr1 == arr2);//false

var arr3 = arr1;
arr3.push(5);
console.log(arr1); //[1,2,3,4,5];这里并没有修改arr1,arr1怎么改变了
arr3 = [1,2,3]
console.log(arr1); //[1,2,3,4,5];//这里又开始困惑了,不说说改了arr3,arr1怎么值不变了
arr3 = [1,2,3]重新赋值一个对象给目标数组arr3,

arr3是数组对象的另一个对象实例
(只要程序中出现赋值,那就是重新生成,分配了新的存贮空间),
原数组arr1和目标数组arr3就没关系了,如果用数组对象的方法,
改动目标数组的值,此时原数组和目标数组的存贮空间一致,就会一起改动

var obj1 = {name:'李四',age:20};
var obj2 = {name:'李四',age:20};
console.log(obj1 == obj2);//false

var n1 = 3;
var n2 = 3;
console.log(n1 == n2); //true

为啥这个demo中答案不一致了,所有的一切,总结出来一句话:

在js中,基本类型数据,在内存中存的就是个数据

n1 | 5

n2 | 5

n1,n2值相同,就是n1 == n2

但是对象不一样,在内存中,对象存的是个地址,a和b这两个对象相同的话,需要值和地址都相同才可以。

obj1 | 内存指针1

obj2 | 内存指针1

这样obj1和obj2才相同。

在面向对象程序中,如果用这种方式创建对象,如下

function CreatePerson(name){
    this.name = name;
    this.test = function(){
        console.log(this.name);
    }
}

var my_obj1 = new CreatePerson('张三');
var my_obj2 = new CreatePerson('李四');

console.log(my_obj1.test == my_obj2.test); //false
//虽然代码上是一样的,但是my_obj1和my_obj2是两个对象实例,他们各自在内存中开辟一个地方,放置test方法,如果对象实例很多,就比较占内存了。

所以,要更改下,用prototype,在对象中,不同的地方,如属性,写在构造函数里,相同的地方,如方法,用原型来写。如下:

function CreatePerson(name){
    this.name = name;
}

CreatePerson.prototype.test = function(){
    console.log(this.name);
}

var my_obj1 = new CreatePerson('张三');
var my_obj2 = new CreatePerson('李四');

console.log(my_obj1.test == my_obj2.test); //true,方法一致,指向一个原型
my_obj1.test(); // 张三
my_obj2.test(); // 李四

59、哪些会造成内存泄漏

(1)意外的全局变量:在函数内部忘记使用varlet, 或 const 声明变量,导致创建了一个全局变量,而这个变量会一直保留在内存中。

function leakMemory() {
    globalVar = "I am a memory leak!"; // 未使用var,创建了一个全局变量
}

(2)未解除的事件监听器:给一个DOM元素添加了事件监听器,但在移除该元素之前没有移除监听器,导致该元素和相关的函数无法被垃圾收集器回收。

const element = document.getElementById('myElement');
 
element.addEventListener('click', function leakListener() {
    // 这个函数永远不会被调用完毕,因此会造成内存泄漏
});
 
// 解决方法:显示移除事件监听器
element.removeEventListener('click', leakListener);

(3)闭包:如果一个函数内部返回了一个函数,并且这个返回的函数访问了外部函数的变量,那么这个返回的函数就是一个闭包。如果闭包持续存在于作用域链中,它会使得闭包内的变量无法释放。

function createLeakyClosure() {
    const memory = new Array(10000);
    return function leakClosure() {
        // 这个闭包会一直持有memory变量
    };
}
 
// 解决方法:断开闭包对外部变量的引用
const leakClosure = createLeakyClosure();
leakClosure = null;

(4)定时器或回调函数:设置了一个setIntervalsetTimeout,但是在不需要它们执行的时候没有清除。

function leakSetInterval() {
    setInterval(function() {
        // 这个匿名函数会导致内存泄漏
    }, 1000);
}
 
// 解决方法:清除定时器
const intervalId = setInterval(function() { /* ... */ }, 1000);
clearInterval(intervalId);

(5)单向链引用:如果一个对象被另一个对象引用,但是没有任何外部引用指向它,那么它将永远不会被垃圾收集器回收。

function createMemoryLeak() {
    var leak = {};
    leak = new Array(10000); // leak指向的对象变得不可达,但仍然占用内存
}
 
// 解决方法:确保没有单向链引用到无用对象
createMemoryLeak = null;

避免内存泄漏:

  • 使用varlet, 或 const声明变量。
  • 显式移除事件监听器和定时器。
  • 避免闭包中的循环引用。
  • 确保不再需要的对象被标记为nullundefined以断开引用。

60、如何定位前端线上问题

在不使用插件的前提下,我们会使用 window.onerror 去处理。

61、单页应用的性能优化

单页应用(Single Page Application,SPA)是一种通过JavaScript动态更新页面内容的Web应用程序,它在加载时通常只需要加载一次HTML、CSS和JavaScript资源,之后的页面更新通过AJAX和DOM操作完成。尽管单页应用提供了良好的用户体验,但在首屏加载方面可能会遇到以下挑战:

首次加载时间长,白屏时间长:首屏加载需要下载整个JavaScript应用程序以及所需的依赖项,这可能导致较长的加载时间,特别是在网络较慢的情况下。用户可能会在等待页面加载时看到一个空白的屏幕,这会给用户带来不好的体验,甚至让用户误以为页面出现了问题。

  • 减小入口文件体积
  • 代码压缩
  • 入口文件分包处理
  • 防止组件重复打包
  • 静态资源本地缓存
  • 后端返回资源问题:
  • 采用HTTP缓存,设置Cache-Control,Last-Modified,Etag等响应头
  • 采用Service Worker离线缓存
  • 前端合理利用localStorage,sessionStorage等本地缓存
  • 图片资源压缩
  • 图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
  • 对于所有的图片资源,我们可以进行适当的压缩
  • 对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。
  • CDN加速(CDN存放静态资源)
  • 减少http请求数量(静态资源合并,合并相关的ajax请求,小的图片使用base64格式)
  • 合理利用缓存策略
  • 组件动态导入及使用路由懒加载
  • 开启GZip压缩
  • 使用SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
  • 第三方工具包及UI框架按需加载

62、浏览器缓存

浏览器缓存是为了减少网络请求,减少服务器的压力,提高页面加载的速度,提高网络的性能。

前端缓存有一下几种方式

  • Service Workers 【可以缓存资源的请求,参考】
  • web Storage【LocalStorage/SessionStorage】
  • IndexedDB【这个相当于浏览器中的数据库,请看官网】
  • Cache API【这个我没用过,可以看官网,是实验性技术】
  • Http 缓存头

 缓存机制之协商缓存与强缓存

62.1、基本原理
  • 1)浏览器在加载资源时,根据请求头的expirescache-control判断是否命中强缓存,是直接从缓存读取资源,不会发请求到服务器。
  • 2)如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过last-modifiedetag验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源
  • 3)如果前面两者都没有命中,直接从服务器加载资源
62.2、相同点

如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据;

62.3、不同点

强缓存不发请求到服务器,协商缓存会发请求到服务器。

强缓存

强缓存通过ExpiresCache-Control两种响应头实现

  • Expires

Expires是http1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回。
Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效

Expires: Wed, 11 May 2018 07:20:00 GMT
  • Cache-Control

Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires ,表示的是相对时间

Cache-Control: max-age=315360000

题外tips
Cache-Control: no-cache不会缓存数据到本地的说法是错误的,详情《HTTP权威指南》P18

Cache-Control: no-store才是真正的不缓存数据到本地
Cache-Control: public可以被所有用户缓存(多用户共享),包括终端和CDN等中间代理服务器
Cache-Control: private只能被终端浏览器缓存(而且是私有缓存),不允许中继缓存服务器进行缓存

协商缓存

当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串

协商缓存是利用的是【Last-Modified,If-Modified-Since】【ETag、If-None-Match】这两对Header来管理的

  • Last-Modified,If-Modified-Since

Last-Modified 表示本地文件最后修改日期,浏览器会在request header加上If-Modified-Since(上次返回的Last-Modified的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来

但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag

  • ETag、If-None-Match

Etag就像一个指纹,资源变化都会导致ETag变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的

If-None-Match的header会将上次返回的Etag发送给服务器,询问该资源的Etag是否有更新,有变动就会发送新的资源回来

ETag的优先级比Last-Modified更高

具体为什么要用ETag,主要出于下面几种情况考虑:

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  • 某些服务器不能精确的得到文件的最后修改时间。

几种状态码的区别

  • 200:强缓Expires/Cache-Control存失效时,返回新的资源文件
  • 200(from cache): 强缓Expires/Cache-Control两者都存在,未过期,Cache-Control优先Expires时,浏览器从本地获取资源成功
  • 304(Not Modified ):协商缓存Last-modified/Etag没有过期时,服务端返回状态码304

但是!但是!
现在的200(from cache)已经变成了from disk cache(磁盘缓存)from memory cache(内存缓存)两种
打开chrome控制台看一下网络请求就知道了
具体两者的区别,暂时没有去深究,有兴趣的同学可以自己去研究

如何选择合适的缓存

大致的顺序

  • Cache-Control —— 请求服务器之前
  • Expires —— 请求服务器之前
  • If-None-Match (Etag) —— 请求服务器
  • If-Modified-Since (Last-Modified) —— 请求服务器

协商缓存需要配合强缓存使用,如果不启用强缓存的话,协商缓存根本没有意义

大部分web服务器都默认开启协商缓存,而且是同时启用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】

但是下面的场景需要注意:

  • 分布式系统里多台机器间文件的Last-Modified必须保持一致,以免负载均衡到不同机器导致比对失败;
  • 分布式系统尽量关闭掉ETag(每台机器生成的ETag都会不一样);

63、跨浏览器状态共享

SharedWorker

SharedWorker允许在多个浏览器窗口或标签页之间共享数据,通过SharedWorker实例,不同的页面可以相互发送和接收消息,实现信息的实时同步和交互。SharedWorker在主线程之外运行,执行一些耗时的任务而不会阻塞用户界面,适合用于后台任务处理和资源共享‌。

WebSocket

WebSocket是一种网络通信协议,提供在单个TCP连接上进行全双工通信的能力。它被设计用于建立一个持久性的连接,允许客户端和服务器之间进行实时数据交互。虽然WebSocket功能强大,但它需要服务器支持‌。

LocalStorage和sessionStorage

LocalStorage和sessionStorage是Web Storage API的一部分,它们可以在同一域名下的不同标签页之间进行数据共享。LocalStorage用于长期存储数据,而sessionStorage用于临时存储页面会话期间的数据。通过监听storage事件,可以实现跨标签页通信‌。

跨浏览器状态共享的原理和实现方式

跨浏览器状态共享的原理主要基于浏览器的同源策略和跨文档消息机制。同源策略限制了不同源的站点读取当前站点的数据,而跨文档消息机制(如window.postMessage)允许不同源的DOM进行通信。通过这些机制,可以实现跨浏览器状态共享‌。

64、原型和原型链

01 显式原型

每一个类(构造函数)都有一个显示原型prototype(本质就是个对象)

02 隐式原型

每一个实例都有一个隐式原型__proto__

03 显式原型与隐式原型的关系

类显式原型的prototype等于其创建的实例的隐式原型__proto__

var arr = [];
arr.__proto__ === Array.prototype
04 原型链

查找对象实例的方法和属性时,先在自身找,找不到则沿着__proto__向上查找,我们把__proto__形成的链条关系称原型链(实现了js继承)

解析: var arr = [1,2,3]
arr.toString()在arr.__proto__有这个方法
arr.hasOwnProperty()在arr.proto.__proto__上面有这个方法
arr.proto === Array.prototype
arr继承了Array的prototype上所有方法
Aray .prototype.proto === Object.prototype
Array 继承Object的protype所有方法
arr.proto.proto === Object.prototype
arr继承了Object 的prototype上的所有方法

四、小程序

1、生命周期

onLoad : 页面加载时触发。一个页面只会调用一次,可以在 onLoad的参数中获取打开当前页面路径中的参数
onShow : 页面显示 / 切入前台时触发调用。
onReady : 页面初次渲染完成时触发,一个页面只会调用一次。
onHide : 页面隐藏 / 切入后台时触发,如 navigateTo 或底部 tab切换到其他页面,小程序切入后台等
onUnload : 页面卸载时触发。如 redirectTo或 navigateBack 到其他页面时

2、微信小程序面试题汇总-CSDN博客

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值