史上最全前端八股文来了
引言
由于最近比较忙活没时间学习新东西,现在得空想着能不能好好整理出一些有用的东西,让记忆深刻一点,免得到时候实习找工作面试的时候一问三不知,也希望大家能指正出错误和对大家有点帮助,一起进步,加油奥里给!!!
那么废话不多说直接进入正题,如果觉得可以家人们给个三连😀!!!
正文
HTML+CSS
HTML5的新特性、语义化
语义化指对文本内容的结构化(内容语义化),选择合乎语义的标签(代码语义化),便于开发者阅读,维护和写出更优雅的代码的同时,让浏览器的爬虫和辅助技术更好的解析。通过使用恰当语义的HTML标签,可以有效提高可访问性、可检索性、国际化和互用性。
优点:
- 在没有 CSS 样式的情况下页面的排版结构也很清晰,便于阅读。
- 可以让页面代码结构更清晰,提高互用性,减少网页间的差异性,帮助其他开发者了解网页的结构,方便后期开发和维护。
- 还可以提高可访问性,帮助辅助技术更好地阅读和转译网页,利于无障碍阅读。
- 它们还可以提高国际化,让各国开发者更容易弄懂网页的结构。
新特性:
- 语义化标签:
<header>、<footer>、<nav>、<aside>、<article>和<section>
- 本地存储:localStorage和sessionStorage
- 兼容特性:让网页在不同的浏览器中都能正常显示。
- 2D/3D:让网页具有更丰富的视觉效果。
- 动画/过渡:让网页元素之间的变化更加平滑自然。
- 性能与集成:提高网页的运行速度和稳定性。
- 多媒体标签:
<audio>、<video>
- CSS3 特性:可以让网页样式更加丰富多彩。
- 新的表单控件:required、placeholder、autofocus、autocomplete、multiple
- input类型:color、date、email、month、number、search、tel、time、url、week
CSS3
层叠样式表(Cascading Style Sheets,缩写为 CSS)是一种样式表语言,用来描述 HTML 或 XML(包括如 SVG、MathML 或 XHTML 之类的 XML 分支语言)文档的呈现。CSS 描述了在屏幕、纸质、音频等其他媒体上的元素应该如何被渲染的问题。CSS3 是 CSS(层叠样式表)技术的升级版本,CSS演进的一个主要变化就是W3C决定将CSS3分成一系列模块。
新特性:
- 选择器:新的属性选择器、伪类选择器和伪元素选择器,增强了页面元素的选择范围和对特定元素样式的控制。
- 盒模型:引入了新的盒模型,使得我们可以通过box-sizing属性来定义元素的盒模型是"content-box"(默认值)还是"border-box",深度影响了元素的计算宽度和高度值。
- 版式:新的版式属性包括:多列版式、Flexbox、Grid等,增加了网页版式的灵活性和复杂性。
- 背景图像:使用 background-size 属性,CSS3 允许我们调整背景图片的大小并让其适应我们的容器。
- 过渡和动画:CSS3支持 创建平滑过渡 和 流畅动画 完成CSS的转换效果。
- 渐变:我们可以使用CSS3 gradient属性为元素添加渐变背景。渐变背景可以是线性的或径向(环形)的。
- 字体:新增加字体嵌入@font-face、属性font-stretch、text-shadow等。
- 阴影:强大的 CSS3 模糊阴影效果,我们可以使用box-shadow属性模拟出漂亮的阴影效果,而不需要深入解决重影粒度问题。
- 圆角:CSS3 border-radius 属性是圆角属性,可以使我们轻松地实现不同圆角半径的定制化效果。
- 弹性盒子模型:弹性盒子模型又叫Flexible Box布局模型,可用于彻底改变 Web 页面的布局方式。
常用选择器:
- ID选择器: #id
- 类别选择器: .class
- 元素选择器: element
- 伪类选择器: :link,:visited,:hover,:active,:focus
- 属性选择器: [attribute]
- 后代选择器: A B
- 子元素选择器:A > B
- 兄弟选择器:A + B, A ~ B
- 通用选择器: *
优先级顺序:
- !important >
- 内联样式 >
- ID选择器 >
- 类选择器/属性选择器/伪类选择器 >
- 元素选择器/伪元素选择器 >
- 关系选择器/通用选择器
!important
> 内联样式 > ID选择器 > 类别选择器、属性选择器、伪类选择器 > 元素选择器 > 通配符 > 继承
示例:
// 1. 使用ID选择器:
#main-nav {
background-color: blue;
}
// 2. 元素选择器:
div{
width:100%;
height:100%;
}
// 3. 使用类别选择器:
.button {
background-color: red;
}
// 4. 使用属性选择器:
a[href^="https://"] {
color: green;
}
// 5. 使用伪类选择器:
a:hover {
color: yellow;
}
// 6. 使用后代选择器:
.main-nav ul {
padding: 0;
}
// 7. 使用子元素选择器:
.main-nav > ul {
list-style-type: none;
}
// 8. 使用兄弟选择器:
h1 + p {
font-size: 16px;
}
// 9. 使用通用选择器:
* {
box-sizing: border-box;
}
CSS盒子模型和box-sizing属性
CSS盒子模型(Box Model)是网页布局的基础,可以将页面上所有元素看作一个个矩形的盒子。这些盒子由四个部分组成:内容区(content)、内边距(padding)、边框(border)、外边距(margin)。CSS盒子模型分为标准盒子模型和怪异盒子模型,这两个概念与盒子模型的计算方式有关。
在CSS3中,通过box-sizing
属性可以控制盒子模型的计算方式。CSS3中的 box-sizing 属性有三个值:content-box,border-box和inherit。
- content-box:默认值,模型的宽度和高度只包括内容,不包括边框和内边距。(标准盒子模型)
- border-box:模型的宽度和高度包括内容、内边距和边框,但不包括外边距。(怪异盒子模型)
- inherit:继承父元素的 box-sizing 值。
总结来说,box-sizing属性用于控制盒子模型的计算方式,更改CSS盒子模型的大小计算方式,使得需要计算的尺寸更加精确和方便。
标准盒子模型:
标准盒子模型是CSS2.1规范定义的,也被称为W3C盒子模型。在标准盒子模型下,一个元素的尺寸由其content(内容)的宽度、内边距padding、边框border和外边距margin四个部分组成。其中content的大小可以通过width和height属性进行设置,padding、border和margin的大小可以通过相应的属性进行设置。
标准盒子模型的计算公式如下:
总宽度 =
width + padding-left + padding-right + border-left-width + border-right-width + margin-left + margin-right
总高度 =height + padding-top + padding-bottom + border-top-width + border-bottom-width + margin-top + margin-bottom
示例:
// HTML 代码为:
<div class="box">盒子模型</div>
// CSS 代码为:
.box {
width: 200px;
height: 100px;
padding: 10px;
border: 5px solid #000;
margin: 0 auto;
}
怪异盒子模型
怪异盒子模型也被称为IE盒子模型,是IE5~IE6浏览器采用的盒子模型,由于该模型与标准盒子模型不同,因此被称为怪异盒子模型。
标准盒子模型的计算公式如下:
总宽度 = width + margin-left + margin-right
总高度 = height + margin-top + margin-bottom
也就是说,在怪异盒子模型中,内边距和边框的大小并没有算入元素的总尺寸。(既 width 已经包含了 padding 和 border 值)
示例:
// HTML 代码为:
<div class="box">测试盒子模型</div>
// CSS 代码为:
.box {
width: 200px;
height: 100px;
padding: 10px;
border: 5px solid #000;
margin: 0 auto;
box-sizing: border-box; /* 显示使用IE怪异盒子模型 */
}
解释:当设置一个元素的box-sizing属性为 border-box 时,即可使用怪异盒子模型进行盒子尺寸的计算,而采用其他值(如 content-box)则会使用标准盒子模型进行盒子尺寸的计算。
其实在默认的 content-box 模式下,盒子模型就是标准的盒子模型,元素的宽度和高度仅包含内容,不包括内边距(padding)、边框(border)和外边距(margin)。而使用设置为 border-box 的 box-sizing 属性时,元素的宽度和高度包括了内边距、边框和内容,但不包括外边距。
具体而言,在 content-box 模式下,当我们设置宽度为200px时,它并不包括 padding、border 和 margin 的尺寸,因此该元素的实际宽度可能会比我们期望的要大一些。而在 border-box 模式下,设置的宽度200px已经包含了 padding 和 border 的尺寸,因此该元素的实际宽度也就比较准确了。
总之,box-sizing 属性可以更好地控制元素的尺寸计算方式,让开发者更加方便地实现页面布局效果。在实际开发中,应根据实际需求和页面特点灵活选择使用哪种盒子模型计算方式。
BFC和IFC
BFC和IFC是CSS布局中的概念,他们分别代表“块级格式化上下文”和“内联格式化上下文”。
区别:
BFC是块级格式化上下文,它是一个独立的布局环境,其中块级盒子垂直排列。在BFC中,盒子的垂直边距会发生折叠,浮动元素也会参与高度计算。
IFC是行内格式化上下文,它是一种水平的格式化上下文,其中行内级盒子从左到右水平排列,直到一行被填满,然后换行。在IFC中,盒子的垂直对齐方式由vertical-align属性决定。行高由包含该行内级盒子中最高的盒子决定。
BFC(块级格式化上下文)
BFC(Block Formatting Context),即块级格式化上下文。指的是一个独立的块级渲染区域(布局环境),它具有一定的隔离特性,内部元素的定位、清除浮动、高度塌陷等计算方式与外部元素保持独立。
BFC的原理布局规则:
- 内部的Box会在垂直方向一个接着一个地放置。
- Box垂直方向上的距离由margin决定。属于同一个BFC的两个相邻的Box的margin会发生重叠。
- 每个盒子的左外边框紧挨着包含块的左边框,即使浮动元素也是如此。
- BFC的区域不会与float box重叠。
- 元素的类型和display属性,决定了这个Box的类型。不同类型的Box会参与不同的Formatting Context(一个决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染。
- 计算BFC的高度时,浮动子元素也参与计算。
扩展:Box是CSS布局的对象和基本单位,一个页面是由很多个Box组成的。元素的类型和display属性决定了这个Box的类型。
BFC的生成规则有如下几条:
- 根元素即为一个BFC。
- 浮动元素(float不为none)。
- 绝对定位元素(position为absolute或fixed)。
- display值为inline-block、table-caption、flex、inline-flex、grid、inline-grid的元素。
- overflow值不为visible的块元素。
示例:
// HTML 代码为:
<div class="container">
<div class="box"></div>
<div class="box"></div>
</div>
// CSS 代码为:
.container {
border: 1px solid black;
overflow: hidden;
}
.box {
width: 100px;
height: 100px;
margin: 10px;
float: left;
background-color: lightblue;
}
解释:在这个示例中,我们创建了一个BFC来包裹两个浮动元素,通过设置 overflow:hidden,让它会触发BFC,在BFC中,BFC自适应高度,浮动元素也会参与高度计算,因此解决了浮动元素引发的高度塌陷问题。
应用场景:
- 解决浮动元素引发的高度塌陷问题。
- 防止垂直外边距重叠。
- 创建自适应两栏布局。
- 实现多列文本布局。
扩展:如果页面布局造成了浮动塌陷,除了使用清除浮动(Clearfix)技术强制容器在浮动元素之后换行,还可以为容器设置一个触发BFC的样式,就是上面那个例子中为 container 设置了 overflow: hidde 的样式。
IFC(内联格式化上下文)
IFC指的是一个内联元素渲染区域,它是一种水平的格式化上下文,具有一定的隔离特性,同一个IFC内部的元素在渲染时互相影响,但与外部元素不产生任何影响。在IFC中,盒子从左到右水平排列,直到一行被填满,然后换行。行内级盒子的垂直对齐方式由 vertical-align 属性决定。行高由包含该行内级盒子中最高的盒子决定。
IFC中的布局规则包括:
- 行内级盒子从左到右水平排列。
- 盒子的垂直对齐方式由vertical-align属性决定。
- 行高由包含该行内级盒子中最高的盒子决定。
- 当一行被填满时,盒子会换行。
IFC的生成规则有如下几条:
- 根元素即为一个IFC。
- inline-block元素。
- 表格单元格(table-cell)。
- display值为inline-flex、inline-grid的元素。
- img元素、input元素、textarea元素。
示例:
// HTML 代码为:
<div class="container">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
// CSS 代码为:
.container {
border: 1px solid black;
width: 300px;
}
.box {
display: inline-block;
width: 100px;
height: 100px;
margin: 10px;
background-color: lightblue;
}
解释:这里没有用flex布局,有兴趣的可以自己试一试噢!在这个例子中,我们有一个包含三个盒子的容器。盒子被设置为display: inline-block,这使它们成为行内块级元素。由于容器的宽度只有300像素,所以第三个盒子会换行。
应用场景:
- 内联元素的居中对齐。
- 解决内联元素导致的空隙问题。
- 禁止文本被浮动元素覆盖。
- 实现多行文本的两端对齐布局。
总而言之,BFC和IFC在CSS布局中扮演了至关重要的角色,可以解决很多常见的布局问题,对于理解CSS的渲染流程、排版规则有很大帮助。
页面布局
在我的第一篇博客文章中有介绍了前端常见的十种布局方式,所以这里就不再详细介绍了,大家可以去看看,我就简单提一下就好了:
- 静态布局:常见于pc端,是给页面设定固定的宽高且居中布局,web网站开发的单位一般用px。
- 浮动布局:浮动布局是调用浮动属性来使得元素向左或者向右移动从而共享一行,直到碰到包含框或者另一个浮动框。浮动元素是脱离文档流的,不占据页面空间,但不脱离文本流,且浮动会将行内元素和行内块元素转化为块元素。
- 定位布局:定位布局是给元素设置 position 属性从而控制元素显示在不规则的位置,偏向于单个元素定位操作。
- 栅格布局:栅格布局也被称为网格布局,它是一种新兴的布局方式,常用的有瀑布流等。它的布局很简单,就是把一个区域划分为一个个的格子排列好,再把需要的元素填充进去。
- table布局:table 布局是在父元素使用 display:table; 子元素使用 display:table-row或 display:table-cell; 子元素会默认自动平均划分父元素的空间。
- 弹性(flex)布局:flexible 模型又被称为 flexbox,它不像栅格布局可以同时处理行跟列,只能处理单行或者当列,是一维的布局模型。
- 圣杯布局:圣杯布局跟双飞翼的布局区别在于中间是否有包括两边的区域,圣杯布局是没有的,两边或者一边非主要部分填充父元素的 padding;而双飞翼布局是有的,但多了一层 dom 节点,非主要部分用的是 center 部分的 margin 空间。
- 自适应布局:总结的来说就是创建多个静态布局,每个布局对应一个屏幕的分辨率范围,每个静态布局页面的元素大小不会因为窗口的改变而变化,除非从一个静态布局变到另外一个布局,不然在同一设备下还是固定的布局。常用的方式有使用 CSS 的 @media 媒体查询,也有高成本的 JS 进行设计开发,或者使用第三方开源框架 bootstrap,这个能够很好的支持多个浏览器。
- 流式布局:流式布局也叫百分比布局(也有叫非固定像素布局),是页面中的元素根据屏幕分辨率自动进行适配调整,页面元素大小会发生变化,但是整体布局不会发生变化,始终都是满屏显示。它使用的是百分比定义宽,但高一般会被固定住,这种布局在早期是为了适应不同尺寸的PC屏幕,但现在在移动端比较常见。
- 响应式布局:响应式通过检测视口分辨率判断是pc端、平板还是手机,针对不同的客户端在客户端做处理,来展示不同的布局和内容从而达到令人满意的效果,屏幕大小的变化会导致元素的位置和大小都改变,可以说是流式布局和自适应布局的结合体,一套界面布局即可适应所有不同的尺寸和终端,可想而知设计考虑的比自适应复杂的多。
其他
伪类和伪元素
伪类和伪元素都是CSS选择器,它们用来选择文档树以外的元素,或者选择文档树中无法用简单选择器表示的状态。但它们之间有一些重要的区别。
伪类用来选择元素的特殊状态。例如,:hover伪类用来选择鼠标悬停在其上的元素,:focus伪类用来选择获得焦点的元素。伪类通常用于添加一些特殊的样式,以反映元素的状态。
伪元素用来创建一些不在文档树中的元素,并为其添加样式。例如,::before伪元素用来在一个元素之前插入内容,::after伪元素用来在一个元素之后插入内容。伪元素通常用于添加装饰性内容。
总之,伪类和伪元素的主要区别在于它们的作用对象不同。伪类作用于已经存在的元素,而伪元素创建新的元素。
长度单位px、em和rem
px、em和rem都是CSS中的长度单位,但它们之间有一些重要的区别。
px(像素)是一个绝对长度单位,它表示屏幕上的一个物理像素。由于不同设备的屏幕分辨率不同,所以1px在不同设备上可能表示不同的物理尺寸。
em是一个相对长度单位,它相对于当前元素的字体大小。例如,如果一个元素的字体大小为16px,那么1em就等于16px。em单位常用于设置元素的字体大小、边距和填充等属性。
rem(root em)也是一个相对长度单位,它相对于根元素(元素)的字体大小。例如,如果根元素的字体大小为16px,那么1rem就等于16px。rem单位常用于实现响应式布局。
总之,px、em和rem的主要区别在于它们的参考系不同。px是绝对长度单位,而em和rem是相对长度单位。
position属性
- static:默认值。元素按照正常文档流进行定位。
- relative:元素按照正常文档流进行定位,但可以通过top、right、bottom和left属性相对于其正常位置进行偏移。
- absolute:元素脱离正常文档流,相对于最近的非static定位祖先元素进行定位。如果没有非static定位的祖先元素,则相对于初始包含块进行定位。
- fixed:元素脱离正常文档流,相对于浏览器窗口进行定位。
- sticky:元素在正常文档流中,但可以根据用户的滚动固定在指定位置。
想要了解更多或者详细一点可以看我第一篇文章前端常见的十种布局方式中的定位布局。
让一个元素水平垂直居中
常见方法:
- 使用flex布局:可以在父元素上设置 display: flex;,并且使用 align-items: center; 和 justify-content: center; 来实现水平垂直居中。
- 使用绝对定位和负边距:可以在父元素上设置 position: relative;,然后在子元素上设置 position: absolute;,并且使用 top: 50%;、left: 50%; 和负边距(例如 margin-top: -10px; margin-left: -10px;)来实现水平垂直居中。
- 使用绝对定位和transform:可以在父元素上设置 position: relative;,然后在子元素上设置 position: absolute;,并且使用 top: 50%;、left: 50%; 和 transform: translate(-50%, -50%); 来实现水平垂直居中。
- 使用表格布局:可以在父元素上设置 display: table-cell; vertical-align: middle; text-align:center; 来实现水平垂直居中。
- 使用网格布局:可以在父元素上设置 display: grid;,并且使用 place-items: center; 来实现水平垂直居中。
- 使用行内块元素和文本对齐:可以在父元素上设置 text-align: center; 和 line-height: 200px;(其中200px是父元素的高度),然后在子元素上设置 display: inline-block; vertical-align: middle; 来实现水平垂直居中。
隐藏页面中某个元素
常见方法:
- 使用 display: none;:这会将元素从页面布局中完全移除,就像它从未存在过一样。
- 使用 visibility: hidden;:这会将元素隐藏,但它仍然占据页面布局中的空间。
- 使用 opacity: 0;:这会将元素的透明度设置为0,使其完全透明,但它仍然占据页面布局中的空间,并且仍然可以与用户交互(例如,可以点击)。
- 使用 position: absolute; 和 left: -9999px;:这会将元素移出屏幕外,使其不可见。
JS、ES6
ES6,全称 ECMAScript 6.0,是 JavaScript 语言的下一代标准,于 2015 年 6 月正式发布。它为 JavaScript 带来了许多新的语法特性和功能,使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ES6 的一些主要新语法特性包括:
- 新的原始类型和变量声明:let 和 const 关键字用于声明块级作用域的变量和常量。
- 箭头函数:使用 => 符号定义函数,可以更简洁地编写函数。
- 模板字符串:使用反引号(`)定义字符串,可以在字符串中嵌入表达式。
- 解构赋值:允许从数组或对象中提取值并赋值给变量。
- 类:使用 class 关键字定义类,支持继承、构造函数、静态方法等面向对象编程特性。
- 模块化:使用 import 和 export 关键字导入和导出模块。
- Promise:用于处理异步操作的结果。
- 迭代器和生成器:支持迭代器和生成器,可以更方便地遍历数据结构。
- Set 和 Map 数据结构:新增了 Set 和 Map 数据结构,用于存储唯一值和键值对。
迭代器和生成器的简单示例:
// 简单的迭代器示例,它实现了一个next()方法,用于遍历数组中的元素:
function makeIterator(array) {
let nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
}
let it = makeIterator(['a', 'b', 'c']);
console.log(it.next().value); // 'a'
console.log(it.next().value); // 'b'
console.log(it.next().value); // 'c'
console.log(it.next().done); // true
// 简单的生成器示例,它使用yield表达式来暂停函数执行并返回一个值:
function* idMaker() {
let index = 0;
while (true)
yield index++;
}
let gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
数据类型
分为两大类:包括值类型(基本对象类型)和引用类型(复杂对象类型)
值类型:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol和BigInt。其中,Symbol是ES6引入的一种新的原始数据类型,表示独一无二的值。
引用数据类型:对象(Object)、数组(Array)和函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)。
示例:
// 值类型
let myString = 'Hello, World!'; // 字符串
let myNumber = 3.14; // 数字
let myBoolean = true; // 布尔
let myNull = null; // 空
let myUndefined = undefined; // 未定义
let mySymbol = Symbol(); // Symbol
let myBigInt = 123n; // BigInt
// 引用数据类型
let myObject = {name: '幼儿园技术家', age: 25}; // 对象
let myArray = [1, 2, 3]; // 数组
let myFunction = function() {console.log('Hello, World!')}; // 函数
let myRegExp = /hello/i; // 正则表达式
let myDate = new Date(); // 日期
我相信大家很少见过 symbol 和 Bigint 吧,如果面试问到估计只有少部分大佬能聊出来(反正我不行)。
先详细解释一下吧:
- Symbol是ES6中新增的一种基本数据类型,它表示独一无二的值。每个通过Symbol()生成的值都是唯一的。Symbol可以用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。
- BigInt是ES10中新增的一种基本数据类型,它提供了一种方法来表示大于2^53-1的整数。BigInt可以表示任意大的整数。
示例:
let mySymbol = Symbol('mySymbol');
let obj = {};
obj[mySymbol] = 'Hello, World!';
console.log(obj[mySymbol]); // 输出'Hello, World!'
let myBigInt = 1234567890123456789012345678901234567890n;
console.log(myBigInt * 2n); // 输出2469135780246913578024691357802469135780n
好处:
Symbol的好处在于它能够创建独一无二的值,这样就可以避免属性名冲突的问题。例如,当你想要给一个对象添加一个新属性时,你可以使用Symbol来创建一个唯一的属性名,这样就不用担心这个属性名会与对象中已有的属性名冲突。
BigInt的好处在于它能够表示任意大的整数,这样就可以避免整数溢出的问题。例如,在对大整数进行数学运算时,以任意精度表示整数的能力尤为重要。有了BigInt,整数溢出将不再是一个问题。此外,你可以安全地使用高精度时间戳、大整数ID等,而不必使用任何变通方法。
数据类型常用检测方法
1. typeof:typeof操作符可以返回一个字符串,表示未经计算的操作数的类型。优点在于它简单易用,可以快速检测基本数据类型。但它也有一些缺点,例如它无法区分Object、Array和Null,因为都会返回"object"。
示例:
console.log(typeof 'Hello, World!'); // 输出'string'
console.log(typeof 3.14); // 输出'number'
console.log(typeof true); // 输出'boolean'
console.log(typeof undefined); // 输出'undefined'
console.log(typeof null); // 输出'object'
console.log(typeof Symbol()); // 输出'symbol'
console.log(typeof 123n); // 输出'bigint'
console.log(typeof {}); // 输出'object'
console.log(typeof []); // 输出'object'
console.log(typeof function() {}); // 输出'function'
2. instanceof:instanceof操作符主要用于检测引用数据类型,它用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。因此,它并不适用于检测所有数据类型。优点在于它可以检测引用数据类型,判断一个实例是否属于某个类。但它也有一些缺点,例如它无法检测基本数据类型。
示例:
console.log([] instanceof Array); // 输出true
console.log({} instanceof Object); // 输出true
console.log(function() {} instanceof Function); // 输出true
3. Object.prototype.toString.call():这种方法可以用来检测对象的类型。优点在于它可以准确地检测所有数据类型,包括基本数据类型和引用数据类型。但它也有一些缺点,例如使用起来比较麻烦,需要调用Object.prototype.toString.call()方法,并传入要检测的值作为参数。
示例:
console.log(Object.prototype.toString.call('Hello, World!')); // 输出'[object String]'
console.log(Object.prototype.toString.call(3.14)); // 输出'[object Number]'
console.log(Object.prototype.toString.call(true)); // 输出'[object Boolean]'
console.log(Object.prototype.toString.call(undefined)); // 输出'[object Undefined]'
console.log(Object.prototype.toString.call(null)); // 输出'[object Null]'
console.log(Object.prototype.toString.call(Symbol())); // 输出'[object Symbol]'
console.log(Object.prototype.toString.call(123n)); // 输出'[object BigInt]'
console.log(Object.prototype.toString.call({})); // 输出'[object Object]'
console.log(Object.prototype.toString.call([])); // 输出'[object Array]'
console.log(Object.prototype.toString.call(function() {})); // 输出'[object Function]'
数据类型转换方法
在JavaScript中,数据类型转换分为两种:隐式类型转换和显式类型转换。
隐式类型转换:指在运算过程中,JavaScript会自动将一种数据类型转换为另一种数据类型,以便进行运算。例如,在字符串和数字相加时,数字会被自动转换为字符串,然后进行字符串拼接。
示例:
let x = '3' + 4; // x的值为'34'
let y = '3' - 4; // y的值为-1
显式类型转换:指通过调用特定的函数或方法来手动进行数据类型转换。例如,可以使用Number()函数将字符串转换为数字,或使用String()函数将数字转换为字符串。
示例:
// 使用Number()函数将字符串转换为整数
let a = Number('3') + 4; // a的值为7
// 使用String()函数将整数转换为字符串
let b = String(3) + 4; // b的值为'34'
// 使用一元加号运算符将字符串转换为数字
let x = +'3'; // x的值为3
// 使用一元减号运算符将字符串转换为数字
let y = -'3'; // y的值为-3
// 使用parseInt()函数将字符串转换为整数
let a = parseInt('3.14'); // a的值为3
// 使用parseFloat()函数将字符串转换为浮点数
let b = parseFloat('3.14'); // b的值为3.14
// 使用toString()方法将数字转换为字符串
let c = (3).toString(); // c的值为'3'
深拷贝和浅拷贝
深拷贝和浅拷贝是针对引用数据类型(如Object和Array)的概念。浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
实现方法
- 浅拷贝可以通过多种方法实现。例如,可以使用Object.assign()方法进行浅拷贝,也可以使用扩展运算符...进行浅拷贝。此外,还可以使用Array.prototype.concat()和Array.prototype.slice()方法对数组进行浅拷贝。
示例:
// 使用Object.assign()进行浅拷贝:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = Object.assign({}, obj1);
obj1.b.c = 3;
console.log(obj2.b.c); // 输出3,因为obj2.b和obj1.b指向同一个对象
// 使用扩展运算符...进行浅拷贝:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = {...obj1};
obj1.b.c = 3;
console.log(obj2.b.c); // 输出3,因为obj2.b和obj1.b指向同一个对象
// 使用Array.prototype.concat()对数组进行浅拷贝:
let arr1 = [1, 2, { a: 3 }];
let arr2 = arr1.concat();
arr1[2].a = 4;
console.log(arr2[2].a); // 输出4,因为arr2[2]和arr1[2]指向同一个对象
// 使用Array.prototype.slice()对数组进行浅拷贝:
let arr1 = [1, 2, { a: 3 }];
let arr2 = arr1.slice();
arr1[2].a = 4;
console.log(arr2[2].a); // 输出4,因为arr2[2]和arr1[2]指向同一个对象
- 深拷贝可以通过多种方法实现。例如,可以使用递归的方式实现深拷贝,也可以通过JSON对象实现深拷贝,即先使用JSON.stringify()将对象转换为JSON字符串,再使用JSON.parse()将字符串解析成新的对象。
示例:
// 使用递归实现深拷贝:
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key]);
}
}
return result;
}
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = deepClone(obj1);
obj1.b.c = 3;
console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
// 使用JSON.stringify()和JSON.parse()实现深拷贝:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.b.c = 3;
console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
此外,还可以通过jQuery的extend方法实现深浅拷贝: extend()方法的第一个参数是一个布尔值,用来指定是否进行深拷贝。如果该参数为true,则进行深拷贝;否则进行浅拷贝。
示例:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = jQuery.extend(true, {}, obj1);
obj1.b.c = 3;
console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
作用域链和闭包
作用域链
作用域链是指在JavaScript中,变量的查找机制。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。这个作用域链保证了对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端是当前执行环境的变量对象,如果这个执行环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
其实作用域链的理解比较简单,就是当查找变量时,会从作用域链的前端开始,逐级向后查找,直到找到为止。如果在整个作用域链中都没有找到该变量,则该变量未定义。
示例1(查找成功):
let x = 1;
function outer() {
let y = 2;
console.log(x + y);
}
outer(); // 输出 3
示例2(查找失败):
function outer() {
let y = 2;
console.log(x + y);
}
outer(); // 报错:ReferenceError: x is not defined
闭包
闭包是指一个函数能够访问其定义时的词法作用域,即使这个函数在其定义时的作用域之外执行。闭包可以让你从内部函数访问外部函数作用域。
在JavaScript中,函数在创建时会保存一个指向其定义时的词法作用域的引用。当这个函数被调用时,它会使用这个引用来确定其外部变量的值。这就是闭包。
优点:
- 封装:闭包可以用来封装私有变量,防止外部访问。
- 记忆:闭包可以用来记忆函数的状态,例如计数器。
- 柯里化:闭包可以用来实现柯里化,即将一个多参数函数转换为一系列单参数函数。
缺点:
- 内存占用:由于闭包会引用外部函数的变量,因此它会占用更多的内存。如果不需要使用闭包,应该及时释放内存。
- 性能问题:由于闭包需要在作用域链中查找变量,因此它的性能可能不如直接访问全局变量。
避免闭包导致的内存泄漏:
- 及时释放不再使用的闭包,以便垃圾回收器可以回收它们占用的内存。
- 避免在闭包中捕获不必要的变量,尽量只捕获必要的变量。
- 注意循环引用,避免在闭包中捕获会导致循环引用的变量。
我们常常使用的定时器、事件处理、Ajax请求等常用于异步操作用了回调函数,但是回调函数其实是可以使用闭包也可以不使用闭包的,并不是说回调一定是在使用闭包。
回调示例1(使用闭包):
let x = 1;
function doSomething(callback) {
// 执行一些操作
let result = x + 1;
// 调用回调函数
callback(result);
}
doSomething(function(result) {
console.log(result); // 输出 2
});
<!-- 在这个例子中,我们定义了一个全局变量x和一个函数doSomething。
doSomething函数接受一个回调函数作为参数。
当我们调用doSomething时,它会执行一些操作,然后调用回调函数,并将结果作为参数传递给回调函数。 -->
回调示例2(不使用闭包):
function doSomething(callback) {
// 执行一些操作
let result = 1 + 1;
// 调用回调函数
callback(result);
}
doSomething(function(result) {
console.log(result); // 输出 2
});
<!-- 在这个例子中,我们定义了一个函数doSomething,它接受一个回调函数作为参数。
当我们调用doSomething时,它会执行一些操作,然后调用回调函数,并将结果作为参数传递给回调函数。 -->
那么闭包中定义的变量怎么回收呢?
在JavaScript中,内存管理是自动进行的。当一个变量不再被引用时,它所占用的内存就会被垃圾回收器回收。
在闭包中定义的变量也是如此。当闭包不再被引用时,它所引用的外部变量也就不再被引用,因此它们所占用的内存就会被垃圾回收器回收。
所以有两种情况:
- 第一是当全局变量作为闭包变量的时候,那么闭包变量就会因为上下文的存在(一直被引用)而保存到页面关闭。
- 第二是当局部变量作为闭包变量的时候,其一是引用完毕立即回收(可以赋予null),其二是可以一直引用依然保存在内存中直到不再被引用则会回收。
第二种情况示例1(立即回收):
function fn() {
let x = 1;
return function() {
console.log(x);
}
}
for (let i = 0; i < 10; i++) {
fn()(); // 输出10次1
}
<!-- 在这个例子中,fn是一个函数,它返回一个匿名函数。
当我们在循环中调用fn()时,它每次都会返回一个新的匿名函数,并立即执行这个匿名函数。
由于这些匿名函数在执行完毕后就不再被引用,因此它们所占用的内存就会被垃圾回收器回收。 -->
第二种情况示例2(等到不再引用则回收):
function fn() {
let x = 1;
return function() {
console.log(x++);
}
}
let closure = fn();
for (let i = 0; i < 10; i++) {
closure(); // 输出 1,2,3,...,10
}
closure = null; // 释放对闭包的引用
<!-- 在这个例子中,fn是一个函数,它返回一个匿名函数。当我们调用fn()时,它返回inner函数,并将其赋值给closure变量。
当我们在循环中调用closure()时,它每次都会输出递增的值,即fn函数内部定义的变量x的值。
由于x在每次调用闭包时都会自增1,因此每次输出的都是递增的值。在循环结束后,我们将closure变量赋值为null,这样就释放了对闭包的引用。 -->
经典面试题
涉及for循环和闭包:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 输出什么?
data[1](); // 输出什么?
data[2](); // 输出什么?
// 连续输出3个3
<!-- 原因:在这段代码中,i 是全局变量,共用一个作用域。当函数被执行的时候,此时的 i 已经变成了3,导致输出的结果都是3。 -->
如果预期输出1、2、3,使用闭包改善:
var data = [];
for (var i = 0; i < 3; i++) {
(function (j) {
data[j] = function () {
console.log(j);
};
})(i);
}
data[0](); // 输出1
data[1](); // 输出2
data[2](); // 输出3
<!-- 原因:在这个例子中,我们使用了一个自执行函数和闭包来创建3个互不干扰的私有作用域。
这样,每次循环时都会创建一个新的闭包,并将当前的 i 值传递给闭包,使得每个闭包都有自己独立的 j 值。
因此,当我们调用 data[0]()、data[1]() 和 data[2]() 时,它们分别输出1、2和3。 -->
原型和原型链
原型(prototype)是一个对象,它是用来创建其他对象的模板。每个函数都有一个 prototype 属性,它指向该函数的原型对象。
原型链是由一系列原型对象组成的链条。每个对象都有一个原型对象与之关联,这个原型对象也是一个普通对象,它也有自己的原型对象,这样层层递进,就形成了一个链条,这个链条就是原型链。
原型链的作用是实现继承。当访问一个对象的属性时,如果该属性不存在于该对象中,则会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端。
原型关系: 指的是对象与其原型对象之间的关系。每个对象都有一个内部属性 [[Prototype]],它指向该对象的原型对象。在 JavaScript 中,可以通过 __proto__ 属性来访问这个内部属性。
示例:
// 假设我们有一个构造函数 Person 和一个实例对象 p:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
var p = new Person('Tom');
// 在这个例子中,p 的原型对象就是 Person.prototype。我们可以通过 p.__proto__ 来访问它:
console.log(p.__proto__ === Person.prototype); // true
ES6新语法特性:let && const
ES6之前创建变量用的是var,之后创建变量用的是let/const,当然也会用var,那么区别在哪呢?
var,let和const都是用来声明变量的,但它们之间有一些区别。var声明的变量属于函数作用域,而let和const声明的变量属于块级作用域。此外,var声明的变量存在变量提升现象,而let和const没有。在同一块级作用域中,let变量不能重新声明,而const常量不能修改。简单的来说就是,var定义全局变量且可以覆盖,let定义块级作用域变量且不能再一次进行声明({}),const定义不允许修改的块级作用域常量。
示例:
function exampleVar() {
var x = 1;
if (true) {
var x = 2;
console.log(x); // 输出2
}
console.log(x); // 输出2
}
function exampleLet() {
let x = 1;
if (true) {
let x = 2;
console.log(x); // 输出2
}
console.log(x); // 输出1
}
function exampleConst() {
const x = 1;
if (true) {
const x = 2;
console.log(x); // 输出2
}
console.log(x); // 输出1
}
解释:
在exampleVar函数中,由于var声明的变量属于函数作用域,所以在if语句块中重新声明的变量x会覆盖函数作用域中的变量x。
而在exampleLet和exampleConst函数中,由于let和const声明的变量属于块级作用域,所以在if语句块中声明的变量x不会影响到外部作用域中的变量x。
this指向问题
在JavaScript中,this关键字指向函数执行时的当前对象。this的指向取决于函数调用的方式,而不是函数定义的位置。
- 在全局作用域中,this指向全局对象(在浏览器中是window对象,在Node.js中是global对象)。
- 在函数调用中,如果函数不是作为对象的方法被调用,那么this指向全局对象。
- 在作为对象方法调用时,this指向调用该方法的对象。
- 在构造函数中,this指向新创建的对象。
- 在事件处理程序中,this指向触发事件的元素。
此外,可以使用call()、apply()和bind()方法显式地设置函数调用时的this值。
示例:
// 1.在全局作用域中,this指向全局对象:
console.log(this === window); // 输出true(在浏览器中)
// 2.在函数调用中,如果函数不是作为对象的方法被调用,那么this指向全局对象:
function foo() {
console.log(this === window); // 输出true(在浏览器中)
}
foo();
// 3.在作为对象方法调用时,this指向调用该方法的对象:
let obj = {
myMethod: function() {
console.log(this === obj); // 输出true
}
};
obj.myMethod();
// 4.在构造函数中,this指向新创建的对象:
function MyConstructor() {
this.myProperty = 'Hello World!';
console.log(this instanceof MyConstructor); // 输出true
}
let myInstance = new MyConstructor();
// 5.在事件处理程序中,this指向触发事件的元素:
<button id="myButton">点击!</button>
<script>
let button = document.getElementById('myButton');
button.onclick = function() {
console.log(this === button); // 输出true
};
</script>
// 6.使用call()、apply()和bind()方法显式地设置函数调用时的this值:
function foo() {
console.log(this);
}
let obj = { a: 1 };
foo.call(obj); // 输出{ a: 1 }
foo.apply(obj); // 输出{ a: 1 }
let bar = foo.bind(obj);
bar(); // 输出{ a: 1 }
此外还有一些特殊情况会影响this的指向问题:
- 在严格模式下,如果函数不是作为对象的方法被调用,那么this的值为undefined。
- 在DOM事件处理程序中,如果使用addEventListener()方法添加事件处理程序,那么事件处理程序中的this指向触发事件的元素。但是,如果使用attachEvent()方法(仅在旧版本的IE中可用),那么事件处理程序中的this指向全局对象。
- 在回调函数中,this的指向取决于回调函数被调用的方式。例如,在setTimeout()和setInterval()中,回调函数中的this指向全局对象。在数组方法(如forEach()、map()、filter()等)中,回调函数中的this指向全局对象,除非显式地设置了thisArg参数。
- 在箭头函数中,this的值取决于箭头函数定义时所在的上下文。箭头函数不会创建自己的this值,而是从外层作用域继承this值。
- 如果使用了ES6的类语法,那么类中的方法默认是在严格模式下执行的,因此类方法中的this指向取决于方法调用的方式。
示例:
// 1.在严格模式下,函数调用中的this指向undefined:
'use strict';
function foo() {
console.log(this);
}
foo(); // 输出undefined
// 2.在DOM事件处理程序中,使用addEventListener()方法添加事件处理程序,事件处理程序中的this指向触发事件的元素:
<button id="myButton">Click me!</button>
<script>
let button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this); // 输出<button id="myButton">Click me!</button>
});
</script>
// 3.在回调函数中,this的指向取决于回调函数被调用的方式:
// 在setTimeout()中,回调函数中的this指向全局对象
setTimeout(function() {
console.log(this === window); // 输出true(在浏览器中)
}, 1000);
// 在数组方法中,回调函数中的this指向全局对象,除非显式地设置了thisArg参数
let arr = [1, 2, 3];
arr.forEach(function() {
console.log(this === window); // 输出true(在浏览器中)
});
arr.forEach(function() {
console.log(this === obj);
}, obj); // 输出true
// 4.在箭头函数中,this的值取决于箭头函数定义时所在的上下文:
let obj = {
myMethod: function() {
let arrowFunction = () => {
console.log(this === obj); // 输出true
};
arrowFunction();
}
};
obj.myMethod();
// 5.在类方法中,this指向取决于方法调用的方式:
class MyClass {
myMethod() {
console.log(this);
}
}
let myInstance = new MyClass();
myInstance.myMethod(); // 输出MyClass实例
let myMethod = myInstance.myMethod;
myMethod(); // 输出undefined(在严格模式下)或全局对象(在非严格模式下)
EventLoop 事件循环
EventLoop 即 事件循环,是指浏览器或 Node 的一种解决 javaScript 单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。在事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。
这个模型与其他语言中的模型截然不同,比如 C 和 Java。它永不阻塞,处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其他事情,比如用户输入。
宏任务和微任务
在 JavaScript 引擎中,任务分为两种类型:微任务(microtask)和宏任务(macrotask)。微任务是指在当前任务执行结束后立即执行的任务,它可以看作是在当前任务的“尾巴”添加的任务。常见的微任务包括 Promise 回调和 process.nextTick。宏任务是指在下一轮事件循环中执行的任务。常见的宏任务包括 setTimeout、setInterval、setImmediate、requestAnimationFrame 等。
微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。
宏任务和微任务与事件循环有着密切的关系。在事件循环中,每个宏任务执行完后,都会检查微任务队列并执行队列中的所有微任务,然后再执行下一个宏任务。这个过程会一直重复,直到队列中没有消息为止。
示例:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// 输出结果:
script start
script end
promise1
promise2
setTimeout
解释:首先,同步代码 console.log('script start') 和 console.log('script end') 被执行。然后,setTimeout 被添加到宏任务队列中。接着,Promise.resolve().then 中的回调被添加到微任务队列中。当同步代码执行完后,事件循环检查微任务队列并执行队列中的所有微任务,即 console.log('promise1') 和 console.log('promise2')。最后,事件循环执行下一个宏任务,即 setTimeout 中的回调。
setTimeout Promise Async/Await 的区别
- setTimeout 是 JavaScript 中的一个异步函数,用于在指定的时间间隔后执行一段代码。它属于延迟方法,会被放到最后,也就是主线程空闲的时候才会触发。
- Promise 是 JavaScript 中的一种对象,用于处理异步操作的结果。它本身是同步的立即执行函数,当在执行体中执行 resolve() 或者 reject() 的时候,此时是异步操作,会先执行 then/catch 等,等主栈完成后,才会去执行 resolve()/reject() 中的方法。
- Async/Await 是 JavaScript 中的一种语法,用于处理异步操作,使代码看起来像同步代码一样。async 用于定义一个异步函数,await 用于等待异步操作的结果。当遇到 await 的时候,会让出主线程,阻塞后面的代码的执行。async 函数需要等待 await 后的函数执行完成并且有了返回结果(Promise 对象)之后,才能继续执行下面的代码。
优先级:
Promise 的回调属于微任务,所以它会在当前宏任务执行完后立即执行。
setTimeout 属于宏任务,所以它会在下一轮事件循环中执行。
Async/Await 是基于 Promise 的语法糖,它能实现的效果都能用 then 链来实现。当遇到 await 的时候,会让出主线程,阻塞后面的代码的执行。所以 await 后面的代码相当于 promise.then() 里面的代码。
示例:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1();
console.log('script end');
// 输出结果:
script start
async1 start
async2
script end
promise1
promise2
async1 end
setTimeout
解释: 首先,同步代码 console.log('script start')、console.log('async1 start')、console.log('async2') 和 console.log('script end') 被执行。然后,setTimeout 被添加到宏任务队列中。接着,Promise.resolve().then 中的回调被添加到微任务队列中。当同步代码执行完后,事件循环检查微任务队列并执行队列中的所有微任务,即 console.log('promise1') 和 console.log('promise2')。最后,事件循环执行下一个宏任务,即 setTimeout 中的回调。
节流&&触底加载 防抖&&实时搜索
节流
节流(Throttle)是一种控制函数执行频率的技术。当事件被频繁触发时,节流函数会按照一定的频率来执行函数。它可以保证在一段时间内,不管事件触发了多少次,函数都只会执行一次,且是最先被触发调用的那次。
举个例子,假设你正在滚动一个页面,每滚动一段距离就会触发一个事件。如果这个事件被频繁触发,可能会导致页面卡顿。这时候,你可以使用节流来控制事件的执行频率,让它每隔一段时间才执行一次。
节流通常用于优化性能,避免因为事件触发过于频繁而导致的页面卡顿或浏览器崩溃。
场景:
- 滚动事件:当用户滚动页面时,可以使用节流来控制滚动事件的执行频率,让它每隔一段时间才执行一次。
- 窗口大小调整:当用户调整浏览器窗口大小时,可以使用节流来控制调整事件的执行频率,让它每隔一段时间才执行一次。
- 鼠标移动:当用户移动鼠标时,可以使用节流来控制鼠标移动事件的执行频率,让它每隔一段时间才执行一次。
滚动事件当然是 触底加载 比较多了。现在用这个作为示例:
// 节流函数
function throttle(fn, delay) {
let timer = null;
return function() {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
}
}
// 加载函数
function loadMore() {
// 加载更多内容
console.log('Loading more content...');
}
// 监听滚动事件
window.addEventListener('scroll', throttle(function() {
// 滚动到页面底部时触发加载函数
if (document.documentElement.scrollTop + window.innerHeight === document.documentElement.scrollHeight) {
loadMore();
}
}, 500));
解释: 在这个例子中,我们定义了一个节流函数 throttle,它接受两个参数:一个是要执行的函数 fn,另一个是延迟时间 delay。当事件被触发时,节流函数会按照指定的频率来执行函数。然后,我们定义了一个加载函数 loadMore,用来加载更多内容。接着,我们监听了滚动事件,并使用节流函数来控制加载函数的执行频率。当滚动到页面底部时,会触发加载函数。
防抖
防抖(Debounce)是一种控制函数执行频率的技术。当事件被频繁触发时,防抖函数会推迟执行函数。只有当等待一段时间后也没有再次触发该事件,那么才会真正执行函数。
举个例子,假设你正在输入一个搜索关键词,每输入一个字符就会触发一个搜索事件。如果这个事件被频繁触发,可能会导致页面卡顿或浏览器崩溃。这时候,你可以使用防抖来控制搜索事件的执行频率,让它在用户停止输入一段时间后才执行。
防抖通常用于优化性能,避免因为事件触发过于频繁而导致的页面卡顿或浏览器崩溃。
场景:
- 输入框实时搜索:当用户在输入框中输入内容时,可以使用防抖来控制搜索事件的执行频率,让它在用户停止输入一段时间后才执行。
- 窗口大小调整:当用户调整浏览器窗口大小时,可以使用防抖来控制调整事件的执行频率,让它在用户停止调整一段时间后才执行。
- 按钮点击:当用户点击一个按钮时,可以使用防抖来防止用户连续点击,避免重复提交表单。
那么就用 实时搜索 作为示例:
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
}
}
// 搜索函数
function search(keyword) {
// 执行搜索操作
console.log(`Searching for ${keyword}...`);
}
// 获取输入框元素
const input = document.querySelector('input');
// 监听输入事件
input.addEventListener('input', debounce(function(event) {
// 获取输入框的值
const keyword = event.target.value;
// 执行搜索操作
search(keyword);
}, 500));
解释: 在这个例子中,我们定义了一个防抖函数 debounce,它接受两个参数:一个是要执行的函数 fn,另一个是延迟时间 delay。当事件被触发时,防抖函数会推迟执行函数。如果在等待时间内再次触发该事件,那么会重新计算等待时间。然后,我们定义了一个搜索函数 search,用来执行搜索操作。接着,我们获取了输入框元素,并监听了输入事件。当用户在输入框中输入内容时,会触发输入事件。我们使用防抖函数来控制搜索函数的执行频率,让它在用户停止输入一段时间后才执行。
垃圾回收机制
JavaScript 的垃圾回收机制是用来防止内存泄漏的。内存泄漏指的是当已经不需要某块内存时,这块内存还存在着。在项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。垃圾回收机制就是间歇性地、不定期地寻找到不再使用的变量,并释放掉它们所指向的内存。
JavaScript 的垃圾回收算法主要有两种:引用计数(reference counting)和标记清除(mark-and-sweep)。
引用计数算法通过跟踪每个值被引用的次数来工作。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因此就可以将其占用的内存空间回收回来。
标记清除算法将“不再使用的变量”定义为“无法访问到这个变量”。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量即为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
其他
new
过程:
- 首先,创建一个全新的对象。然后,将这个对象的原型链(proto)指向函数的 .prototype。
- 接着,将这个对象绑定到函数中的 this,然后执行函数,函数内部可以借助 this 给这个对象添加属性。
- 最后,如果这个函数没有返回其他对象的话,new 操作符就会将上面步骤创建的对象返回出去。但如果该函数最后返回了一个其他对象的话,new 操作符就会把这个函数返回的对象返回出去。也就是判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
示例:
function Person(name, age) {
this.name = name;
this.age = age;
}
var person1 = new Person('幼儿园技术家', 25);
console.log(person1.name); // 输出: 幼儿园技术家
console.log(person1.age); // 输出: 25
三种常用方法实现继承
- 使用原型链。
示例:
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('Woof!');
}
let dog = new Dog('Max', 'German Shepherd');
dog.sayName(); // Max
dog.bark(); // Woof!
- 使用 class 关键字来定义类,并使用 extends 关键字来实现继承。
示例:
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log('Woof!');
}
}
let dog = new Dog('Max', 'German Shepherd');
dog.sayName(); // Max
dog.bark(); // Woof!
- 使用混入(Mixin)。
示例:
let Animal = {
sayName: function() {
console.log(this.name);
}
}
function Dog(name, breed) {
this.name = name;
this.breed = breed;
}
Object.assign(Dog.prototype, Animal);
Dog.prototype.bark = function() {
console.log('Woof!');
}
let dog = new Dog('Max', 'German Shepherd');
dog.sayName(); // Max
dog.bark(); // Woof!
手写bind方法
// 可以通过在 Function.prototype 上添加一个新方法来手写实现 bind 方法
Function.prototype.myBind = function(context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
var obj = {
name: '幼儿园技术家'
}
function sayName(age) {
console.log(this.name);
console.log(age);
}
var boundSayName = sayName.myBind(obj, 25);
boundSayName(); // 输出: 幼儿园技术家 \n 25
解释:在上面的示例中,我们定义了一个 myBind 方法,它接受一个参数 context,表示绑定的上下文对象。然后我们使用 apply 方法将函数的执行上下文绑定到指定的对象上,并传入相应的参数。最后我们可以调用绑定后的函数。
CmmonJS和ESM
CommonJS和ESM是两种不同的JavaScript模块化规范。CommonJS主要用于服务器端,比如Node.js,而ESM是ECMAScript 6中引入的模块化标准,它既可以用于前端,也可以用于后端。
CommonJS和ESM之间有一些主要区别:
首先,它们的语法不同。CommonJS使用 require 和 module.exports 来导入和导出模块,而ESM使用 import 和 export 关键字。
其次,CommonJS模块是运行时加载的,而ESM模块是编译时输出接口的。此外,CommonJS是同步加载模块的,而ESM支持异步加载。
示例:
// CommonJS
var foo = require('foo');
module.exports = foo;
// ESM
import foo from 'foo';
export default foo;
柯里化
在上面的闭包中我们有提到柯里化,那么这里简单介绍一下。要思考柯里化是什么?有什么用?怎么实现?
柯里化(Currying)是一种处理多元函数的方法,它是指将一个多参数的函数转化为单参数函数的方法。它是数学家柯里(Haskell Curry)提出的。
柯里化的主要作用是将一个复杂的函数拆分成多个简单的函数,使得每个函数只接受一个参数。这样做可以让我们更灵活地使用这些函数,比如可以将它们组合起来,或者将它们作为参数传递给其他函数。
示例:
function add(x, y) {
return x + y;
}
function curriedAdd(x) {
return function(y) {
return add(x, y);
}
}
var add5 = curriedAdd(5);
console.log(add5(3)); // 输出: 8
call bind apply
在解决this指向问题中提到了call、apply 和 bind,那么现在来介绍一下。
call、apply 和 bind 都是JavaScript中的函数方法,它们都可以用来改变函数的执行上下文(即函数内部的 this 指向)。
call 和 apply 的作用相似,它们都可以用来立即调用一个函数,并指定函数内部的 this 指向。它们的区别在于传递参数的方式不同:call 方法接受若干个参数,第一个参数是 this 指向的对象,后面的参数依次传递给函数;而 apply 方法接受两个参数,第一个参数是 this 指向的对象,第二个参数是一个数组,数组中的元素依次传递给函数。
bind 方法与 call 和 apply 不同,它不会立即调用函数,而是返回一个新的函数。这个新函数与原函数具有相同的行为,但是它内部的 this 指向被绑定到了 bind 方法的第一个参数上。除了第一个参数外,bind 方法还可以接受若干个参数,这些参数会被预先传递给新函数。
示例:
function sayName(greeting) {
console.log(`${greeting}, my name is ${this.name}`);
}
var obj = {
name: '幼儿园技术家'
}
sayName.call(obj, 'Hello'); // 输出: Hello, my name is 幼儿园技术家
sayName.apply(obj, ['Hello']); // 输出: Hello, my name is 幼儿园技术家
var boundSayName = sayName.bind(obj);
boundSayName('Hello'); // 输出: Hello, my name is 幼儿园技术家
Vue
Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。
Vue 是一个典型的 MVVM 模型的框架。MVVM 是 Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对 View 和 ViewModel 的双向数据绑定。这使得 ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。
优点:
- 易于学习和使用:Vue提供了一个平滑的学习曲线,使其适用于初学者和专业开发人员。它有着丰富的文档和教程,并且有着庞大的社区支持。
- 高性能:Vue使用虚拟DOM来提高应用的性能和渲染效率。虚拟DOM是一个轻量级的JavaScript对象,它是真实DOM的抽象表示。当组件的状态发生变化时,Vue会根据新的状态创建一个新的虚拟DOM树。然后,Vue会使用一个高效的算法来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM。
- 灵活性:Vue非常灵活,可以与现有项目无缝集成。它提供了许多高级功能,如计算属性、侦听器和过渡效果等,可以帮助开发人员更快地构建复杂的应用程序。
缺点:
- 生态系统不够成熟:相比其他前端框架,Vue的生态系统不够成熟。它缺少一些高质量的插件和工具,这可能会影响开发人员的工作效率。
- 文档不足:尽管Vue有着丰富的文档和教程,但由于其快速发展,有时文档可能不够完整或过时。
- 学习曲线陡峭:尽管Vue相对容易学习,但要真正掌握它并开发复杂的应用程序仍然需要一定的时间和精力。
既然提到了 mvvm,那么就简单说一下 MVC 以及 MVVM 和 MVC 之间的区别:
MVC 和 MVVM 都是一种设计模式,它们都旨在将应用程序分成不同的部分,以便更好地管理和维护。
MVC
MVC 是 Model-View-Controller 的缩写,它将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,Controller 负责接收用户输入并更新 Model 和 View。在 MVC 模式中,View 和 Model 是相互独立的,它们之间通过 Controller 来进行通信。
优点:
- 耦合度低:MVC 的三个部件(Model、View 和 Controller)是相互独立的,改变其中一个不会影响其他两个。
- 重用性高:多个视图可以使用同一个模型。
- 可维护性高:由于各个部件之间的分离,MVC 模式下的应用程序更容易维护。
缺点:
- 不适合小型项目开发。
- 视图与控制器联系过于紧密,妨碍了它们的独立重用。
MVVM
MVVM 是 Model-View-ViewModel 的缩写,它也将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,ViewModel 则负责连接 View 和 Model。与 MVC 不同的是,在 MVVM 模式中,View 和 ViewModel 之间有着双向数据绑定的联系。这意味着当 ViewModel 中的数据发生变化时,View 会自动更新;而当 View 中的数据发生变化时,ViewModel 也会自动更新。
优点:
- 低耦合:视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上。当 View 变化时,Model 可以不变化;当 Model 变化时,View 也可以不变。
- 可重用性:你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑。
- 独立开发:双向数据绑定的模式实现了 View 和 Model 的自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要一直操作 DOM。
缺点:
- 增加了代码复杂度,并且对于简单的应用来说可能会显得过于繁琐。
- 由于 MVVM 模式依赖于双向数据绑定,因此它也可能会带来一些性能问题。
总之,MVC 和 MVVM 的主要区别在于它们对 View 和 Model 之间通信方式的不同处理。MVC 通过 Controller 来进行通信,而 MVVM 则通过双向数据绑定来实现通信。这两种模式各有优缺点,具体使用哪种模式取决于具体的应用场景。
底层实现原理
Vue 的底层实现原理主要包括数据双向绑定和虚拟 DOM两部分。
数据双向绑定是指当数据发生变化时,视图会自动更新;而当视图发生变化时,数据也会自动更新。Vue 实现数据双向绑定的方式是通过数据劫持
和发布订阅模式
相结合。
- 数据劫持:Vue 会拦截 data 对象中所有属性的读取和写入操作。在 Vue 2.x 版本中,数据劫持是通过 Object.defineProperty() 方法实现的;而在 Vue 3.x 版本中,数据劫持则是通过 Proxy 对象实现的。
- 发布订阅模式:当我们修改 data 中的某个属性时,Vue 会通知所有订阅了该属性变化的观察者(Watcher),并执行相应的回调函数。这些回调函数通常会更新视图,以保证视图与数据保持同步。
虚拟 DOM 是一种用 JavaScript 对象表示 DOM 的技术。它可以让我们在不直接操作 DOM 的情况下更新视图。Vue 在更新视图时会先生成一个新的虚拟 DOM 树,然后将新旧虚拟 DOM 树进行对比,找出它们之间的差异。最后,Vue 会根据这些差异来更新真实的 DOM 树。这个过程被称为“patching”。
使用虚拟DOM有以下几个好处:
- 提高渲染性能:直接操作真实DOM通常是非常慢的,因为浏览器需要执行很多额外的工作,如样式计算、布局和重绘。使用虚拟DOM可以减少对真实DOM的操作次数,从而提高渲染性能。
- 跨平台:虚拟DOM是一个抽象层,它可以运行在任何支持JavaScript的平台上。这意味着你可以使用Vue来构建跨平台应用,如桌面应用、移动应用和Web应用。
- 更容易测试:由于虚拟DOM是一个纯粹的数据结构,它更容易进行测试和调试。
相对于手动操作真实DOM,使用虚拟DOM通常可以获得更好的性能。但这并不是绝对的,因为虚拟DOM也有一些开销,如创建虚拟DOM树和计算差异。在某些情况下,手动操作真实DOM可能会更快。但总体来说,使用虚拟DOM可以让我们更容易地构建高性能和跨平台的应用。
生命周期
Vue 的生命周期指的是 Vue 实例从创建到销毁的整个过程。在这个过程中,Vue 实例会经历一系列的生命周期钩子函数,这些钩子函数可以让我们在特定的时刻执行特定的操作。
- beforeCreate:在实例初始化之后,数据观测和事件配置之前被调用。
created:在实例创建完成后被立即调用。此时,实例已完成以下配置:数据观测、属性和方法的运算、watch/event 事件回调。但是,挂载阶段还没开始,$el 属性目前不可见。 - beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用。
mounted:在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。如果根实例挂载了一个文档内元素,当 mounted 被调用时,vm.$el 也在文档内。 - beforeUpdate:在数据更新之前调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM。
updated:在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。 - beforeDestroy:在实例销毁之前调用。此时实例仍然完全可用。
destroyed:在实例销毁之后调用。此时,所有的指令绑定都被解除,所有的事件监听器都被移除,所有的子实例也都被销毁。
Vuex
Vuex是一个专为Vue.js应用程序开发的状态管理模式+库。使用Vuex时,每一个Vuex应用的核心就是store(仓库)。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它可以帮助我们管理共享状态,解决多组件数据通信问题。
简单来说,Vuex就像一个容器,它包含了你的应用中大部分的状态。当Vue组件从store中读取状态时,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
你可以通过store.state
来获取状态对象,并通过store.commit
方法触发状态变更。在Vue组件中,可以通过this.$store
访问store实例,但不能直接改变store中的状态。改变store中的状态的唯一途径就是显式地提交mutation
,而非直接改变store.state.count
。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
Vuex主要包括以下几个核心模块:
- State:Vuex使用单一状态树,用一个对象就包含了全部的应用层级状态。每个应用将仅仅包含一个store实例。单一状态树让我们能够直接定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
- Getter:有时候我们需要从store中的state中派生出一些状态,例如对列表进行过滤并计数。Vuex允许我们在store中定义getter(可以认为是store的计算属性)。就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- Mutation:更改Vuex的store中的状态的唯一方法是提交mutation。Vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数。
- Action:Action类似于mutation,不同在于:Action提交的是mutation,而不是直接变更状态;Action可以包含任意异步操作。Action函数接受一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.state和context.getters来获取state和getters。
- Module:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。为了解决这个问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块。
一些常见的Vuex使用场景包括:用户的个人信息管理模块、电商项目的购物车模块、我的订单模块(订单列表中点击取消订单,然后更新对应的订单列表)、在订单结算页获取需要的优惠券并更新订单优惠信息等。
示例:
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
}
})
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
el: '#app',
store,
render: h => h(App)
})
// App.vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count
}
},
methods: {
increment() {
this.$store.dispatch('increment')
}
}
}
</script>
组件之间的通信方式
除了以上说的 Vuex 进行组件之间的通讯外,常见的组件通讯还有以下几种方式:
- props / $emit:父组件通过props向子组件传递数据,子组件通过$emit向父组件传递数据。
示例:
// 父组件
<template>
<div>
<child :msg="msg" @changeMsg="changeMsg"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
data() {
return {
msg: 'Hello'
}
},
methods: {
changeMsg(newMsg) {
this.msg = newMsg
}
}
}
</script>
// 子组件
<template>
<div>
<p>{{ msg }}</p>
<button @click="changeMsg">Change Msg</button>
</div>
</template>
<script>
export default {
props: ['msg'],
methods: {
changeMsg() {
this.$emit('changeMsg', 'Hi')
}
}
}
</script>
- ref / $refs:父组件可以通过$refs获取子组件的实例,从而调用子组件的方法或访问子组件的数据。
示例:
// 父组件
<template>
<div>
<child ref="child"></child>
<button @click="getChildMsg">Get Child Msg</button>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
methods: {
getChildMsg() {
console.log(this.$refs.child.msg)
}
}
}
</script>
// 子组件
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello'
}
}
}
</script>
- eventBus事件总线($emit / $on):可以创建一个空的Vue实例作为事件总线,在组件中通过$emit触发事件,在另一个组件中通过$on监听事件,从而实现组件间通信。
示例:
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()
// 组件A
<template>
<div>
<button @click="emitEvent">Emit Event</button>
</div>
</template>
<script>
import { eventBus } from './eventBus.js'
export default {
methods: {
emitEvent() {
eventBus.$emit('myEvent', 'Hello')
}
}
}
</script>
// 组件B
<template>
<div>{{ msg }}</div>
</template>
<script>
import { eventBus } from './eventBus.js'
export default {
data() {
return {
msg: ''
}
},
mounted() {
eventBus.$on('myEvent', (data) => {
this.msg = data
})
}
}
</script>
- $parent / $children:子组件可以通过$parent访问父组件实例,父组件可以通过$children访问子组件实例。
示例:
// 父组件
<template>
<div>
<child></child>
<button @click="getChildMsg">Get Child Msg</button>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
methods: {
getChildMsg() {
console.log(this.$children[0].msg)
}
}
}
</script>
// 子组件
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello'
}
}
}
</script>
- $attrs/ $listeners:$attrs包含了父组件中不作为prop被识别且获取的特性绑定,$listeners包含了父组件中的v-on事件监听器。
示例:
// 父组件
<template>
<div id="app">
<middle :msg="msg" @changeMsg="changeMsg"></middle>
</div>
</template>
<script>
import Middle from './Middle.vue'
export default {
components: { Middle },
data() {
return {
msg: 'Hello'
}
},
methods: {
changeMsg(newMsg) {
this.msg = newMsg
}
}
}
</script>
// 中间组件
<template>
<div>
<child v-bind="$attrs" v-on="$listeners"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
inheritAttrs: false // 不继承父组件的属性,避免将属性绑定到根元素上。
}
</script>
// 子组件
<template>
<div>
<p>{{ msg }}</p>
<button @click="changeMsg">Change Msg</button>
</div>
</template>
<script>
export default {
props: ['msg'],
methods: {
changeMsg() {
this.$emit('changeMsg', 'Hi')
}
}
}
</script>
- provide/inject:祖先组件通过provide提供变量,然后在子孙组件中通过inject来注入变量。
示例:
// 祖先组件
<template>
<div>
<child></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
provide() {
return {
msg: 'Hello'
}
}
}
</script>
// 子孙组件
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
inject: ['msg']
}
</script>
computed与watch
computed和watch都是Vue实例的选项,用来监听数据变化并执行相应的操作。
computed
computed:计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要相关依赖没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。计算属性默认只有getter,不过在需要时你也可以提供一个setter。
示例:
new Vue({
el: '#app',
data: {
message: 'Hello'
},
computed: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
})
watch
watch:当你需要在数据变化时执行异步或开销较大的操作时,可以使用watch。watch选项允许我们执行异步操作(访问一个API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。
示例:
new Vue({
el: '#app',
data: {
message: 'Hello'
},
watch: {
message: function (newVal, oldVal) {
console.log('message changed from', oldVal, 'to', newVal)
}
}
})
区别:
- 计算属性是基于它们的依赖进行缓存的。只有在相关依赖发生改变时,计算属性才会重新求值。这意味着只要相关依赖没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。相比之下,watch选项中的函数每次都会执行。
- 计算属性通常用来计算一个值,这个值是基于它的依赖进行计算的。当你需要根据数据变化来改变数据时,可以使用计算属性。相比之下,watch选项通常用来执行异步操作或开销较大的操作。
- 计算属性是响应式的,当它们的依赖发生改变时,它们会自动更新。相比之下,watch选项需要手动设置监听的数据。
总之,当你需要根据数据变化来改变数据时,可以使用计算属性;当你需要根据数据变化来执行异步操作或开销较大的操作时,可以使用watch。
其他
v-if和v-for同时使用在一个元素上的问题
不建议在同一元素上同时使用v-for和v-if。当它们同时存在时,v-for的优先级比v-if更高,这意味着v-if将分别重复运行于每个循环的项上。这可能会导致性能问题,因为在渲染列表时会进行更多的计算。
场景一:如果你想根据条件过滤列表并渲染过滤后的结果,可以将过滤后的结果计算为一个计算属性,然后在v-for中使用这个计算属性:
<template>
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1', show: true },
{ id: 2, text: 'Item 2', show: false },
{ id: 3, text: 'Item 3', show: true }
]
}
},
computed: {
filteredItems() {
return this.items.filter(item => item.show)
}
}
}
</script>
场景二:如果你的目的是有条件地跳过循环的执行,那么可以将v-if放置在外层元素(如<template>
)或包装元素上:
<template>
<ul v-if="shouldShowItems">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
],
shouldShowItems: true
}
}
}
</script>
Vue.nextTick的原理及实现
Vue.nextTick是一个全局API,用于在下一次DOM更新循环结束之后延迟执行一个回调函数。它的实现依赖于JavaScript的事件循环和微任务队列。
- 在Vue 2.x中,Vue.nextTick的实现使用了一个异步队列来存储所有等待执行的回调函数。当一个回调函数被传递给Vue.nextTick时,它会被推入这个异步队列中。然后,Vue会使用一个内部函数来异步刷新这个队列,以便在下一次DOM更新循环结束之后执行所有等待的回调函数。
为了异步刷新队列,Vue会尝试使用原生的Promise.then、MutationObserver或setImmediate来实现异步延迟。如果这些方法都不可用,它会退而使用setTimeout(fn, 0)。
- 在Vue 3.x中,Vue.nextTick的实现类似于Vue 2.x,但使用了更现代的API来实现异步延迟。它首先尝试使用原生的Promise.then,如果不可用则退而使用setTimeout(fn, 0)。
示例:
<template>
<div>
<p ref="message">{{ message }}</p>
<button @click="updateMessage">Update</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello'
}
},
methods: {
updateMessage() {
this.message = 'Updated'
console.log(this.$refs.message.textContent) // => 'Hello'
this.$nextTick(() => {
console.log(this.$refs.message.textContent) // => 'Updated'
})
}
}
}
</script>
总之,Vue.nextTick的实现依赖于JavaScript的事件循环和微任务队列。它使用一个异步队列来存储所有等待执行的回调函数,并使用原生API或setTimeout来异步刷新这个队列。
组件中data是一个函数的原因
在Vue组件中,data必须是一个函数,而不是一个对象。这是因为当一个组件被多次使用时,每个实例都应该维护一份被返回对象的独立的拷贝。
如果data是一个对象,那么所有组件实例将共享同一个数据对象。这意味着当一个组件实例改变了数据对象时,其他组件实例的数据也会受到影响。
为了避免这个问题,Vue要求组件的data选项必须是一个函数。当一个组件被实例化时,Vue会调用这个函数来获取组件的初始数据。由于每个组件实例都会调用这个函数来获取自己的数据,所以每个组件实例都会维护一份独立的数据拷贝。
前端路由
前端路由是指在单页应用(SPA)中,通过改变URL并不向服务器发送请求,而是通过JavaScript来控制页面内容的切换。这种方式可以让用户在不离开当前页面的情况下,浏览不同的内容。
前端路由通常有两种实现方式:hash模式和history模式。
- hash模式:在这种模式下,URL中的hash(即#符号后面的部分)用来表示路由状态。当hash发生变化时,浏览器不会向服务器发送请求,而是触发hashchange事件。我们可以监听这个事件,并根据新的hash值来更新页面内容。这种方式兼容性好,但URL中会多出一个#符号,可能会影响美观。
- history模式:在这种模式下,我们使用HTML5的History API来控制URL的变化。当URL发生变化时,浏览器不会向服务器发送请求,而是触发popstate事件。我们可以监听这个事件,并根据新的URL来更新页面内容。这种方式可以让URL看起来更像传统的URL,但需要服务器端的支持。
Vue diff算法
Vue的diff算法是用来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM的过程。它采用了深度优先遍历和双端比较的策略来优化比较过程,是Vue虚拟DOM实现的核心部分。
Vue的diff算法基于两个假设:
- 两个相同标签的元素会产生类似的DOM结构。
- 同一层级的一组子节点,它们可以通过唯一的id进行区分。
基于这两个假设,Vue的diff算法采用了深度优先遍历和双端比较的策略来比较新旧虚拟DOM树。
在比较过程中,Vue会从新旧虚拟DOM树的根节点开始,逐层进行比较。当遇到不同类型的节点时,Vue会直接替换整个节点及其子节点;当遇到相同类型但属性不同的节点时,Vue会更新节点的属性;当遇到相同类型且属性相同但子节点不同的节点时,Vue会递归地比较子节点。
在比较子节点时,Vue会使用双端比较的策略来优化比较过程。它会同时从新旧虚拟DOM树的两端开始比较,如果发现两端的节点相同,则直接移动节点;如果发现两端的节点不同,则继续比较中间部分。这种策略可以有效地减少需要比较的节点数量,从而提高diff算法的性能。
那么我们常常在for循环中要绑定一个key属性值,有什么作用呢?
其实在Vue中,key
是一个特殊的属性,用于标识列表渲染中每个节点的唯一性。这是因为在列表渲染中,列表数据可能会发生变化,导致列表项的顺序、数量或内容发生变化。如果没有key属性,Vue将无法准确地确定新旧虚拟DOM树中的节点是否相同,从而无法快速地更新虚拟DOM树。所以它可以帮助Vue更快地更新虚拟DOM树,从而提高应用的性能。
当Vue进行列表渲染时,它需要一种方式来确定新旧虚拟DOM树中的节点是否相同。如果没有key属性,Vue会默认使用“就地更新”的策略,即直接复用旧虚拟DOM树中的节点来更新新虚拟DOM树中的节点。这种方式简单快速,但在某些情况下可能会导致问题。
为了避免这些问题,我们可以使用key属性来为每个节点指定一个唯一的标识。当Vue进行列表渲染时,它会根据key属性来确定新旧虚拟DOM树中的节点是否相同。这样,Vue就可以更快地更新虚拟DOM树,从而提高应用的性能。
keep-alive使用及原理。
keep-alive
是Vue的一个内置组件,用于保留组件状态或避免重新渲染。它可以将其包裹的组件缓存起来,当组件切换时不会销毁,而是保留在内存中,以便下次切换回来时可以直接使用。
实现原理:是通过一个缓存对象来存储被缓存的组件实例。当一个组件被切换出去时,它不会被销毁,而是被保存在缓存对象中;当一个组件被切换回来时,keep-alive会先检查缓存对象中是否有这个组件的实例,如果有,则直接使用缓存的实例;如果没有,则创建一个新的实例。
示例:
<template>
<div>
<button @click="toggle">Toggle</button>
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
</div>
</template>
<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: {
Foo,
Bar
},
data() {
return {
currentView: 'Foo'
}
},
methods: {
toggle() {
this.currentView = this.currentView === 'Foo' ? 'Bar' : 'Foo'
}
}
}
</script>
插槽
插槽(Slot)是Vue的一个功能,用于实现组件的内容分发。它允许你在父组件中定义一些内容,然后将这些内容分发到子组件的指定位置。
默认插槽,具名插槽和匿名插槽:
- 默认插槽用于分发没有指定名称的内容
- 具名插槽用于分发指定名称的内容。
- 匿名插槽是指没有被
元素包裹的内容。
示例:
// 子组件
Vue.component('my-component', {
template: `
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
})
// 父组件
new Vue({
el: '#app',
template: `
<my-component>
<template v-slot:header>
<h1>Header</h1>
</template>
<p>Content</p>
<template v-slot:footer>
<h1>Footer</h1>
</template>
</my-component>
`
})
React
React是一个由Facebook创建的JavaScript库,用于构建用户界面。它是一个用于构建UI组件的工具。
React是一个前端框架,它允许开发人员使用组件化的方式来构建复杂的用户界面。React组件是独立的、可复用的代码块,它们可以接收输入并返回React元素来描述应该在页面上显示什么。
React的核心思想是声明式编程
。这意味着开发人员只需要描述应用程序应该呈现什么样子,而不需要关心如何实现它。React会负责计算出如何高效地更新用户界面,以便它始终与最新的状态保持一致。
优点:
- 高性能:React使用虚拟DOM来提高应用的性能和渲染效率。虚拟DOM是一个轻量级的JavaScript对象,它是真实DOM的抽象表示。当组件的状态发生变化时,React会根据新的状态创建一个新的虚拟DOM树。然后,React会使用一个高效的算法来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM。
- 组件化架构:React采用了组件化的架构来构建复杂的用户界面。在React中,一个组件是一个独立的、可复用的代码块,它可以接收输入并返回React元素来描述应该在页面上显示什么。开发人员可以使用组件来封装各种UI功能,并将它们组合起来构建复杂的用户界面。
- 易于学习:相比其他前端框架,React相对容易学习。它有着丰富的文档和教程,并且有着庞大的社区支持。
缺点:
- 开发速度:由于React不断更新和改进,开发人员需要不断学习新知识才能跟上React的发展步伐。
- 文档不足:尽管React有着丰富的文档和教程,但由于其快速发展,有时文档可能不够完整或过时。
- 学习曲线陡峭:尽管React相对容易学习,但要真正掌握它并开发复杂的应用程序仍然需要一定的时间和精力。
底层实现原理
React是一个JavaScript库,用于构建用户界面。它的底层实现原理包括虚拟DOM、组件化架构和响应式更新等。
- 虚拟DOM:React使用虚拟DOM来提高应用的性能和渲染效率。虚拟DOM是一个轻量级的JavaScript对象,它是真实DOM的抽象表示。当组件的状态发生变化时,React会根据新的状态创建一个新的虚拟DOM树。然后,React会使用一个高效的算法来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM。这个过程被称为“reconciliation”。
- 组件化架构:React采用了组件化的架构来构建复杂的用户界面。在React中,一个组件是一个独立的、可复用的代码块,它可以接收输入并返回React元素来描述应该在页面上显示什么。开发人员可以使用组件来封装各种UI功能,并将它们组合起来构建复杂的用户界面。
- 响应式更新:React采用了响应式的方式来更新用户界面。当组件的状态发生变化时,React会自动计算出需要更新的部分,并高效地更新真实DOM。这样,开发人员只需要关心如何描述应用程序应该呈现什么样子,而不需要关心如何实现它。
生命周期
React组件的生命周期可分成三个状态:Mounting(挂载)、Updating(更新)和Unmounting(卸载)。
Mounting(挂载):当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor(): 在 React 组件挂载之前,会调用它的构造函数。
- getDerivedStateFromProps(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
- render(): render() 方法是 class 组件中唯一必须实现的方法。
- componentDidMount(): 在组件挂载后(插入 DOM 树中)立即调用。
Updating(更新):每当组件的 state 或 props 发生变化时,组件就会更新。当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- getDerivedStateFromProps(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
- shouldComponentUpdate(): 当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。
- render(): render() 方法是 class 组件中唯一必须实现的方法。
- getSnapshotBeforeUpdate(): 在最近一次渲染输出(提交到 DOM 节点)之前调用。
- componentDidUpdate(): 在更新后会被立即调用。
Unmounting(卸载):当组件从 DOM 中移除时会调用如下方法:
- componentWillUnmount(): 在组件卸载及销毁之前直接调用。
React-fiber
React-fiber是对React核心算法的一次重新实现。它能让React中的同步渲染进行中断,并将渲染的控制权让回浏览器,从而达到不阻塞浏览器渲染的目的。Fiber能够将渲染工作分割成块并将其分散到多个帧中。同时加入了在新的更新进入时暂停,中止或重复工作的能力和为不同类型的更新分配优先级的能力。
在Fiber诞生之前,React处理一次setState(首次渲染)时会有两个阶段:调度阶段(Reconciler)和渲染阶段(Renderer)。调度阶段React用新数据生成新的Virtual DOM,遍历Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去。渲染阶段React根据所在的渲染环境,遍历更新队列,将对应元素更新。在浏览器中,就是更新对应的DOM元素。
这种设计看似合理,但是对于复杂组件,需要大量的diff计算,会严重影响到页面的交互性。例如,假设更新一个组件需要1ms,如果有500个组件要更新,那就需要500ms,在这500ms的更新过程中,浏览器唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。这就是所谓的界面卡顿。
React-fiber就是为了解决渲染复杂组件时严重影响用户和浏览器交互的问题。实现原理可以简单分为以下几个步骤:
- 将一次任务拆解成单元。
- 以划分时间片的方式,按照Fiber的自己的调度方法,根据任务单元优先级,分批处理或吊起任务。
- 将一次更新分散在多次时间片中。
- 在浏览器空闲的时候,也可以继续去执行未完成的任务。
这样,React Fiber就能够充分利用浏览器每一帧的工作特性,避免渲染复杂组件时严重影响用户和浏览器交互的问题。
组件
React组件是组成React应用程序的可重复利用的模块。它们是用于构建Web和原生交互界面的库。
React组件可以分为两种类型:函数组件和类组件。
- 函数组件是一个接受props作为参数并返回一个React元素的函数。它们通常用于简单的、无状态的组件。
- 类组件是一个继承自React.Component的类,它包含一个render方法,该方法返回一个React元素。类组件通常用于更复杂的、有状态的组件。
主要的区别:
- 语法不同:函数组件是一个简单的JavaScript函数,而类组件是一个继承自React.Component的类。
- 功能不同:函数组件通常用于简单的、无状态的组件,而类组件通常用于更复杂的、有状态的组件。
- 状态管理不同:函数组件没有自己的状态和生命周期方法,而类组件具有自己的状态和生命周期方法。
- 更新控制不同:类组件可以使用一些特殊的方法来控制组件的更新过程,而函数组件则无法使用这些方法。
示例:
// 函数组件
function Greeting(props) {
return <h1>Hello, {props.name}</h1>;
}
const Free = props =>{
return <h1>Hello, {props.name}</h1>;
}
// 类组件
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
}
通讯方式
- 父子组件之间:父向子,可以通过props的方式传递。子组件可以通过props对象访问这些数据。子向父,子组件可以通过调用父组件传递给它的回调函数来向父组件传递数据。
// 父向子传
function Parent() {
const message = "来自父组件的问候";
return <Child message={message} />;
}
function Child(props) {
return <p>{props.message}</p>;
}
// 子向父传
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "" };
this.handleMessage = this.handleMessage.bind(this);
}
handleMessage(newMessage) {
this.setState({ message: newMessage });
}
render() {
return (
<>
<Child onMessage={this.handleMessage} />
<p>{this.state.message}</p>
</>
);
}
}
function Child(props) {
function handleClick() {
props.onMessage("来自幼儿园技术家的问候");
}
return <button onClick={handleClick}>发送消息</button>;
}
- 兄弟组件之间:兄弟组件之间的数据传递,可以利用组件的Props以及Props回调函数来进行,而这种使用方法通信的前提是:必须要有共同的父组件。父组件可以维护一个状态,并将状态作为props传递给兄弟组件。同时,父组件还可以定义一个回调函数,用于更新状态,并将该回调函数作为props传递给兄弟组件。这样,兄弟组件就可以通过调用回调函数来更新状态,从而实现兄弟组件之间的通信。
示例:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "" };
this.handleMessage = this.handleMessage.bind(this);
}
handleMessage(newMessage) {
this.setState({ message: newMessage });
}
render() {
return (
<>
<ChildA onMessage={this.handleMessage} />
<ChildB message={this.state.message} />
</>
);
}
}
function ChildA(props) {
function handleClick() {
props.onMessage("Hello from ChildA");
}
return <button onClick={handleClick}>Send Message</button>;
}
function ChildB(props) {
return <p>{props.message}</p>;
}
- 跨组件层级:可以使用Context API来实现跨组件层级的通信。使用
createContext
方法创建一个Context对象,然后使用Provider
组件包裹根组件,并通过value属性提供要共享的数据。在任意后代组件中,使用Consumer
组件包裹整个组件,就可以获取到共享的数据。
示例:
const MessageContext = React.createContext();
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "Hello from Parent" };
}
render() {
return (
<MessageContext.Provider value={this.state.message}>
<Child />
</MessageContext.Provider>
);
}
}
function Child() {
return (
<>
<Grandchild />
</>
);
}
function Grandchild() {
return (
<MessageContext.Consumer>
{(message) => <p>{message}</p>}
</MessageContext.Consumer>
);
}
- 全局状态管理:对于非嵌套关系的组件通信,可以使用全局状态管理库,如Redux或MobX。这些库可以在应用程序的顶层维护一个全局状态,并允许组件订阅状态变化并更新其自身。这样,即使组件之间没有直接的嵌套关系,它们也可以共享状态并进行通信。
示例:
import { createStore } from "redux";
// Redux store
const initialState = { message: "" };
function reducer(state = initialState, action) {
switch (action.type) {
case "SET_MESSAGE":
return { message: action.message };
default:
return state;
}
}
const store = createStore(reducer);
// Parent component
class Parent extends React.Component {
render() {
return (
<>
<ChildA />
<ChildB />
</>
);
}
}
// ChildA component
function ChildA() {
function handleClick() {
store.dispatch({ type: "SET_MESSAGE", message: "Hello from ChildA" });
}
return <button onClick={handleClick}>Send Message</button>;
}
// ChildB component
class ChildB extends React.Component {
constructor(props) {
super(props);
this.state = { message: "" };
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const state = store.getState();
this.setState({ message: state.message });
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return <p>{this.state.message}</p>;
}
}
复用方式
React组件复用可以提高开发效率,减少Bug和程序体积。设计接口时,可以把通用的设计元素(按钮,表单框,布局组件等)拆成接口良好定义的可复用的组件。这样,下次开发相同界面程序时就可以写更少的代码。
复用方式有以下几种:
- Props:通过props将数据和回调函数传递给子组件,可以实现组件的复用。
示例:
function Greeting(props) {
return <h1>Hello, {props.name}</h1>;
}
- 高阶组件(HOC):高阶组件是一种用于复用组件逻辑的高级技巧。它是一个接受组件作为参数并返回一个新组件的函数。
示例:
function withGreeting(WrappedComponent) {
return function(props) {
return (
<>
<Greeting name={props.name} />
<WrappedComponent {...props} />
</>
);
};
}
- Render Props:Render Props是一种在React组件之间使用一个值为函数的prop共享代码的简单技术。
示例:
function Greeting(props) {
return props.children("Hello");
}
function App() {
return (
<Greeting>
{greeting => (
<>
<h1>{greeting}, World</h1>
<h2>{greeting}, React</h2>
</>
)}
</Greeting>
);
}
React Hooks
React Hooks是一种新的API,它允许你在函数组件中使用状态和其他React特性。
常用的钩子有:
- useState(状态钩子):
useState
是一个允许你在函数组件中添加状态的Hook。它返回一个状态变量和一个更新该状态变量的函数。
示例:
import { useState } from "react";
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>你点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
}
- useEffect(副作用钩子):
useEffect
是一个允许你在函数组件中执行副作用的Hook。它接受一个函数作为参数,该函数将在组件渲染后执行。
示例:
import { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `你点击了 ${count} 次`;
});
return (
<div>
<p>你点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
}
- useContext(共享状态钩子):
useContext
是一个允许你在函数组件中访问上下文的Hook。它接受一个上下文对象作为参数,并返回该上下文的当前值。
示例:
import { useContext } from "react";
const ThemeContext = React.createContext("light");
function Example() {
const theme = useContext(ThemeContext);
return <p>Current theme: {theme}</p>;
}
- useReducer(action 钩子):
useReducer
是一个允许你在函数组件中使用类似于Redux的状态管理模式的Hook。它接受一个reducer函数和初始状态作为参数,并返回当前状态和一个dispatch函数。
示例:
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Example() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</>
);
}
其他钩子函数:useCallback(记忆函数),useMemo(记忆组件)和useRef(保存引用值)等。
其他
React diff算法
React的diff算法是一种高效的算法,它用来计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面,从而提高了页面渲染效率。简单来说,diff算法就是通过最小代价将旧的fiber树转换为新的fiber树。
React的每次更新,都会将新的ReactElement内容与旧的fiber树作对比,比较出它们的差异后,构建新的fiber树,将差异点放入更新队列之中,从而对真实dom进行render。
diff算法在React中处于主导地位,是React V-dom和渲染的性能保证,这也是React最有魅力、最吸引人的地方。React一个很大一个的设计有点就是将diff
和V-dom
的完美结合,而高效的diff算法可以让用户更加自由的刷新页面,让开发者也能远离原生dom操作。
setState
setState()是React中用来更新组件状态的方法。当你调用setState()时,React会将你提供的对象与当前状态合并。例如,你的状态可能包含几个独立的变量:constructor(props) {super(props);this.state = {posts: [], comments: []};} 。
那么 setState 到底是同步还是异步的呢?
React中的setState()并不是真正意义上的异步,而是一个伪异步
或者称为延迟执行
。它的执行顺序在同步代码后、异步代码前。这种现象得益于React的合成事件,React的批处理更新也得益于合成事件。
注意:setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)中的callback拿到更新后的结果。
而 setState 关于同异步也可以分两种情况讨论:
- 在React事件处理程序和生命周期方法中,setState()是异步的,这意味着在调用setState()后,state不会立即更新。
示例:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的是更新前的值
}
render() {
return (
<div>
<p>你点击了 {this.state.count} 次</p>
<button onClick={this.handleClick}>
Click me
</button>
</div>
);
}
}
- 在setTimeout事件或者自定义的DOM事件中,setState()是同步的,这意味着在调用setState()后,state会立即更新。
示例:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的是更新后的值
}, 0);
}
render() {
return (
<div>
<p>你点击了 {this.state.count} 次</p>
</div>
);
}
}
事件绑定原理
React事件绑定的原理与传统的DOM事件绑定有所不同。在传统的DOM事件中,我们通常会将事件处理程序直接绑定到DOM元素上。但是,在React中,事件处理程序并不是直接绑定到真实的DOM元素上,而是在document处监听所有支持的事件。当事件发生并冒泡到document处时,React会将事件内容封装并交由真正的处理函数运行。
React中的事件都是合成事件,不是把每一个dom的事件绑定在dom上,而是把事件统一绑定到document中,触发时通过事件冒泡到document进行触发合成事件,因为是合成事件,所以我们无法去使用e.stopPropagation去阻止,而是使用e.preventDefault
去阻止。
这种设计可以提高性能,因为它避免了在每个DOM元素上都绑定事件处理程序。此外,它还使得React能够更好地控制事件的传播和处理。
React key的作用
在React中,key是一个特殊的字符串属性,它可以帮助React识别哪些元素发生了变化。当你渲染一个列表时,你应该给每个列表项分配一个稳定的、唯一的key。这样,当列表项的顺序发生变化时,React就能够正确地更新列表。
key的作用是帮助React确定哪些元素需要被重新渲染。当组件更新时,React会比较新旧两个Virtual DOM树,找出它们之间的差异。如果两个元素具有不同的key,React就会认为它们是不同的元素,并重新渲染它们。
工程化
前端工程化是一种将软件工程的方法和思想应用于前端开发的过程。它主要指从前端项目开始开发到部署线上再到后期迭代维护的整个过程,从工程的角度管理前端开发,形成前端开发流程的一整套开发规范或解决方案,提高前端开发效率。
前端工程化可以提升开发体验、提高开发效率和质量、提升应用的访问性能。一切以提高效率、降低成本、质量保证为目的的手段都属于工程化。
webpack
构建流程
Webpack的构建流程大致如下:
- 初始化参数:根据命令行和配置文件收集参数,形成最终的配置结果。
- 开始编译:传入参数,创建compiler实例,注册所有配置的插件,插件监听Webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译。
- 确定入口:从配置文件中指定的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去。
- 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译并输出:递归完后,得到每个文件结果,包含每个模块以及它们之间的依赖关系,根据entry配置生成代码块chunk。输出所有的chunk到文件系统。
优化Webpack的构建速度
优化Webpack的构建速度有很多方法,可以从以下几个方面入手:
- 使用新版本:升级到最新版本的Webpack可以带来性能提升,因为每个版本的Webpack都会进行一些性能优化。
- 使用缓存:可以使用缓存来加快构建速度,例如使用
babel-loader
、ts-loader
等loader
的缓存选项。 - 多线程编译:可以使用多线程来加快编译速度,例如使用
thread-loader
或happypack
。 - 速度分析:可以使用
speed-measure-webpack-plugin
来分析Webpack构建期间各个阶段花费的时间,从而快速定位到可以优化的地方。
优化Webpack的构建速度可以带来许多好处。随着项目涉及到的页面越来越多,功能和业务代码也会越来越多,相应的Webpack的构建时间也会越来越久。这个时候我们就不得不考虑性能优化的事情了。
因为这个构建时间与我们的日常开发是密切相关,当我们本地开发启动devServer或者build的时候,如果时间过长,会大大降低我们的工作效率。试想一下,我们突然碰到一个紧急bug,项目启动需要花费几分钟,改完后项目build上线也要几分钟,换谁估计都得有暴脾气了...
优化 Webpack 的打包体积
优化Webpack的打包体积可以带来许多好处。打包体积越小,应用程序的加载速度就越快,用户体验就越好。下面是一些优化Webpack打包体积的方法:
- 提取公共代码:在多入口情况下,可以使用
CommonsChunkPlugin
来提取公共代码。 - 提取常用库:可以通过
externals
配置来提取常用库。 - 预编译资源模块:可以利用
DllPlugin
和DllReferencePlugin
预编译资源模块。 - 剔除多余代码:可以使用
Tree-shaking
和Scope Hoisting
来剔除多余代码。例如:如果你在开发项目时将整个组件库都引入了,那么在使用Webpack打包时,可以使用Tree-shaking来自动删除没有引用的组件,从而减小打包体积。
扩展:Tree-shaking
Tree-shaking是一种通过静态分析代码,删除未引用代码的技术。它可以帮助开发人员减小打包体积,提高应用程序的加载速度。
Tree-shaking的原理是基于ES6模块的静态结构特性。由于ES6模块的导入和导出是在编译时确定的,而不是在运行时确定的,因此Webpack可以在构建过程中静态分析代码,找出未被引用的模块,并将它们从最终的打包文件中删除。
不过需要注意的是,Tree-shaking只能删除未被引用的模块,而不能删除未被执行的代码。因此,如果你想要使用Tree-shaking来优化打包体积,需要注意代码组织方式,尽量避免在一个模块中混合使用被引用和未被引用的代码。
性能优化
Webpack性能优化有很多方法,可以从以下几个方面入手:
- 减少模块解析:可以通过配置
resolve.alias
来减少模块解析的时间。 - 优化loader性能:可以通过限制
loader
的应用范围来提高构建速度。 - 使用热替换(HMR):虽然热替换并不能降低构建时间,但它可以降低代码改动到效果呈现的时间。
- 手动分包:可以通过手动分包来减少打包时间。
- 使用新版本:使用新版本的Webpack可以带来性能提升,因为每个版本的Webpack都会进行一些性能优化。
loader && plugin
Webpack的loader和plugin是两种不同的扩展机制,它们都可以帮助开发人员定制Webpack的构建过程。
- Loader:用于对模块的源代码进行转换。它们可以将非JavaScript文件(如CSS、图片等)转换为Webpack能够处理的模块。常用的loader有:
babel-loader
(用于将ES6+代码转换为ES5代码)、css-loader
(用于加载CSS文件)、file-loader
(用于加载文件)等。 - Plugin:用于扩展Webpack的功能。它们可以在构建过程中执行各种任务,如优化输出文件、管理资源等。常用的plugin有:
HtmlWebpackPlugin
(用于生成HTML文件)、MiniCssExtractPlugin
(用于提取CSS文件)、UglifyJsPlugin
(用于压缩JavaScript代码)等。
git
Git是一种分布式版本控制系统,它可以帮助开发人员管理和协作代码。Git可以跟踪代码的变化历史,帮助开发人员查看每次修改的内容,并在出现问题时快速恢复到之前的状态。
Git支持分支和合并,可以帮助开发人员在不同的分支上并行开发功能,然后将它们合并到主分支上。这样,开发人员可以更好地协作,并更快地完成项目。
常用的git命令:
- git init:初始化本地git仓库(创建新仓库)
- git add:添加文件到暂存区
- git commit:提交暂存区到本地仓库
- git status:查看当前版本状态(是否修改)
- git push:将当前分支push到远程分支
- git pull:获取远程分支并merge到当前分支
- git clone:clone远程仓库
- git branch:显示本地分支
- git branch -d:删除分支
- git checkout:检出已存在的分支或文件
- git checkout -b:创建+切换分支
- git merge:合并分支
不同类型的分支:
- master分支:通常用于线上发布使用。它应该始终保持稳定,并且只接受来自其他分支经过充分测试和验证的更改。
- dev分支:用于平常的开发和测试。它通常包含最新的开发进度,并且可以接受来自功能分支或修复分支的更改。
- Feature branches(功能分支):用于开发新功能。每个功能分支都应该专注于一个特定的功能,并且在功能完成后合并到dev分支。
- Hotfix branches(修复分支):用于修复bug。每个修复分支都应该专注于修复一个特定的bug,并且在修复完成后合并到dev分支。
- Release branches(发布分支):用于准备新版本的发布。它通常从dev分支中创建,并且只接受用于修复bug和准备发布的更改。当新版本准备好发布时,发布分支会被合并到master分支和dev分支。
ESLint
ESLint是一个用于识别和报告ECMAScript/JavaScript代码中模式的工具,旨在使代码更加一致并避免错误。它是完全可插拔的,每个规则都是一个插件,你可以在运行时添加更多。你还可以添加社区插件、配置和解析器来扩展ESLint的功能。
ESLint可以帮助你快速找到代码中的问题。它内置于大多数文本编辑器中,你可以将ESLint作为持续集成管道的一部分运行。许多ESLint发现的问题都可以自动修复。ESLint修复是语法感知的,因此你不会遇到传统查找和替换算法引入的错误。
作用:
- 快速找到代码中的问题:ESLint静态分析你的代码以快速找到问题。
- 自动修复问题:许多ESLint发现的问题都可以自动修复。ESLint修复是语法感知的,因此你不会遇到传统查找和替换算法引入的错误。
- 遵循编码规范:你可以配置ESLint来强制执行特定的编码规范,以确保你的代码风格一致;如果是团队协作开发,它可以帮助团队成员遵循统一的编码规范,确保代码风格一致,便于阅读和理解彼此的代码。
- 提高代码质量:通过识别和修复潜在的问题,ESLint可以帮助你提高代码质量并减少错误。
其他
补充说明一些常见的前端工程化工具和技术:
- 模块化工具:如CommonJS、AMD和ES6模块等,用于将复杂的前端代码分解为更易于管理的模块。
- 构建工具:如Grunt、Gulp和Webpack等,用于自动化执行常见的前端开发任务,如压缩、合并和转换代码等。
- 代码检查工具:如ESLint和JSHint等,用于检查代码质量并确保遵循编码规范。
- 单元测试工具:如Jasmine和Mocha等,用于编写和运行单元测试,以确保代码的正确性。
项目优化
性能优化
前端页面性能优化是一个复杂的过程,可以从多个方面进行优化。一些常见的优化方法包括:
- 减少请求数量:通过合并文件、使用雪碧图、使用字体图标等方法来减少HTTP请求数量。
- 减小资源大小:通过压缩代码、压缩图片、使用Gzip等方法来减小资源大小。
- 优化网络连接:通过使用CDN、DNS预解析、持久连接等方法来优化网络连接。
- 优化资源加载:通过优化资源加载位置、使用异步加载、使用懒加载等方法来优化资源加载。
- 减少重绘回流:通过避免使用CSS表达式、避免使用table布局、避免频繁操作样式等方法来减少重绘回流。
- 使用性能更好的API:通过使用requestAnimationFrame、使用Web Workers、使用Service Workers等方法来使用性能更好的API。
重绘回流
回流(reflow)和重绘(repaint)是浏览器渲染过程中的两个步骤。它们都会影响页面的渲染性能,因此应尽量避免。
- 回流:当页面中的元素的布局或几何属性发生变化时,浏览器需要重新计算元素的位置和大小,这个过程称为回流。回流会影响到页面中所有元素的位置和大小,因此它是一个非常耗时的过程。
- 重绘:当页面中的元素的外观(如颜色、背景等)发生变化,但不影响布局时,浏览器会重新绘制这些元素,这个过程称为重绘。重绘不会影响到页面中其他元素的布局,因此它比回流更快。
避免回流和重绘的一些方法包括:
- 避免使用CSS表达式:CSS表达式会在每次页面渲染时重新计算,这会导致大量的回流和重绘。
- 避免使用table布局:table布局会导致大量的回流和重绘,应尽量避免使用。
- 避免频繁操作样式:频繁地修改元素的样式会导致大量的回流和重绘。可以考虑使用类名来修改样式,或者将多次修改样式的操作合并为一次。
- 避免频繁操作DOM:频繁地操作DOM会导致大量的回流和重绘。可以考虑使用文档片段(DocumentFragment)来减少对DOM的操作。
主要方法
- 代码优化:可以通过压缩、合并和混淆代码来减小文件大小,提高页面加载速度。例如,可以使用
UglifyJS
等工具来压缩JavaScript代码,使用CSSNano
等工具来压缩CSS代码。此外,还可以使用Webpack
等构建工具来合并多个文件为一个文件,减少HTTP请求数量。 - 资源优化:可以通过使用
CDN
、启用Gzip压缩、使用浏览器缓存等方法来优化资源加载,提高页面加载速度。例如,可以将静态资源部署到CDN上,以加快资源加载速度;可以在服务器端启用Gzip压缩,以减小传输文件的大小;可以合理设置HTTP缓存头,以利用浏览器缓存加快页面加载速度。 - 页面结构优化:可以通过合理安排页面结构,避免使用嵌套过深的HTML标签,减少DOM操作等方法来优化页面结构,提高页面渲染速度。例如,可以避免在HTML中使用过多的嵌套标签;可以
尽量减少对DOM的操作
,避免触发浏览器的回流和重绘。 - 交互优化:可以通过使用
懒加载
、预加载
、按需加载
等技术来优化用户交互,提升用户体验。例如,可以使用懒加载技术来延迟加载页面中不可见的图片;可以使用预加载技术来预先加载页面中即将需要的资源;可以使用按需加载技术来动态加载页面中需要的JavaScript模块。 - 可维护性优化:可以通过使用
模块化
、组件化
、设计模式
等技术来提高代码的可维护性,降低维护成本。例如,可以使用CommonJS或ES6模块化语法来组织代码;可以使用React或Vue等框架来构建可复用的组件;可以使用设计模式来编写可扩展、可维护的代码。
其他一些措施:
- 移除生产环境的控制台打印:在生产环境中,应该移除所有不必要的控制台打印,以减少对性能的影响。
- 第三方库的按需加载:可以使用按需加载技术来动态加载第三方库中需要的部分,以减小文件大小和加快页面加载速度。
- 降低请求成本:可以通过使用HTTP/2、使用Service Workers等技术来降低请求成本,提高页面加载速度。
- 减少请求数:可以通过合并文件、使用雪碧图、使用字体图标等方法来减少HTTP请求数量。
- 减小传输体积:可以通过压缩代码、压缩图片、启用Gzip压缩等方法来减小传输文件的大小。
如果各位大佬还有其他什么方法措施打在评论区吧,我会加上去的!
其他
浏览器渲染一帧都做了什么?
浏览器渲染一帧的过程包括以下几个步骤:
- 处理用户输入:浏览器会处理用户的输入事件,如鼠标点击、键盘输入等。
- JavaScript执行:浏览器会执行页面中的JavaScript代码。
- 请求动画帧回调:浏览器会执行requestAnimationFrame回调函数。
- 样式计算:浏览器会计算元素的最终样式。
- 布局:浏览器会根据元素的样式和大小计算它们在页面中的位置。
- 绘制:浏览器会根据元素的样式和位置绘制它们。
- 合成:浏览器会将多个图层合并为一张图像,并显示在屏幕上。
这些步骤是浏览器渲染一帧的基本过程。不同的浏览器可能会有一些细微的差别,但总体流程是相同的。
计算机基础与网络通信
HTTP和HTTPS的基本概念
HTTP(超文本传输协议)和HTTPS(超文本传输安全协议)都是用于在Web浏览器和网站服务器之间传输信息的协议。它们的主要区别在于安全性。
-
HTTP是一种明文传输协议,它不提供任何加密机制。这意味着,如果攻击者截获了HTTP传输的数据,他们可以直接读取其中的内容。因此,HTTP不适合用于传输敏感信息,如信用卡号、密码等。
- 优点:
- 简单快速:HTTP协议简单,通信速度快。
- 灵活:HTTP允许传输任意类型的数据对象,传输类型由Content-Type加以标记。
- 缺点:
- 不安全:HTTP是明文传输,数据都是未加密的,容易被窃听截取。
- 数据完整性未校验:HTTP传输的数据完整性未校验,容易被篡改。
工作原理:
- 客户端发起HTTP请求:客户端在浏览器中输入一个HTTP网址,然后连接到服务器的80端口。
- 服务器处理请求:服务器收到客户端的请求后,会根据请求的内容进行处理。
- 服务器返回响应:服务器处理完客户端的请求后,会返回一个响应,包括状态码、响应头和响应正文。
- 客户端处理响应:客户端收到服务器的响应后,会根据状态码和响应头进行相应的处理。如果状态码为200,表示请求成功,客户端会渲染响应正文中的内容。
- 优点:
-
HTTPS则是HTTP的安全版本。它在HTTP的基础上使用了SSL/TLS协议来加密数据。HTTPS 开发的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。整个过程中,客户端和服务器之间传输的数据都是经过加密的,这意味着即使攻击者截获了HTTPS传输的数据,他们也无法读取其中的内容,除非能够破解加密算法。
- 优点:
- 安全:HTTPS通过SSL/TLS协议对数据进行加密,保护了数据的安全性。
- 身份认证:HTTPS提供了对网站服务器的身份认证,防止了“中间人攻击”。
- 缺点:
- 相对较慢:由于HTTPS需要进行加密和解密操作,因此它比HTTP相对较慢。
- 需要证书:使用HTTPS协议需要申请数字证书,可能需要一定的费用。
工作原理:
- 客户端发起HTTPS请求:客户端在浏览器中输入一个HTTPS网址,然后连接到服务器的443端口。
- 服务器发送证书:服务器收到客户端的请求后,会将网站支持的证书信息(包括公钥)发送给客户端。
- 客户端验证证书:客户端收到证书后,会验证证书的有效性。如果证书有效,客户端会生成一个随机值,并使用公钥对该随机值进行加密。
- 客户端发送加密信息:客户端将加密后的随机值发送给服务器,以便服务器获取该随机值。
- 服务器解密信息:服务器使用私钥解密客户端发送的加密信息,从而获得客户端生成的随机值。
- 服务器发送加密数据:服务器使用该随机值对数据进行对称加密,并将加密后的数据发送给客户端。
- 客户端解密数据:客户端使用之前生成的随机值对服务器发送的加密数据进行解密,从而获得原始数据。
怎么保证安全性的?
HTTPS 通过使用对称加密、非对称加密、签名算法和证书机制来保证数据安全。在 HTTPS 请求过程中,客户端首先会请求服务端的数字证书,并生成一个随机数 R1,将随机数和自己支持的加密算法告诉服务端。服务端接收到客户端的请求后,会将自己的数字证书发送给客户端,并生成一个随机数 R2,然后根据客户端支持的加密算法选择一种加密算法,并将 R2 和加密算法告诉客户端。客户端接收到服务端的响应后,会验证服务端的数字证书是否有效,如果有效,则根据 R1、R2 和服务端选择的加密算法生成一个对称加密的密钥,并使用该密钥对后续通信进行加密。这样,HTTPS 协议就能够保证所有信息都是加密传播,第三方无法窃听;具有校验机制,一旦被篡改,通信双方会立刻发现;配备身份证书,防止身份被冒充1。
- 优点:
另外,HTTP和HTTPS还有一些其他区别。例如,它们使用的端口不同:HTTP默认使用80
端口,而HTTPS默认使用443
端口。且由于HTTPS需要进行加密
和解密
操作,因此它比HTTP更耗费服务器资源。
TCP
TCP(传输控制协议)是一种面向连接的协议,它使用三次握手来建立连接,并使用四次挥手来断开连接。
三次握手
过程:
- 第一次握手:客户端向服务器发送一个SYN报文,请求建立连接。报文中包含客户端的初始序列号。
- 第二次握手:服务器收到SYN报文后,如果同意建立连接,则向客户端发送一个SYN+ACK报文,表示确认客户端的SYN报文。报文中包含服务器的初始序列号和对客户端初始序列号的确认。
- 第三次握手:客户端收到SYN+ACK报文后,向服务器发送一个ACK报文,表示确认服务器的SYN+ACK报文。此时,TCP连接建立完成。
四次挥手
过程:
- 第一次挥手:当客户端没有更多数据要发送时,它会向服务器发送一个FIN报文,请求断开连接。
- 第二次挥手:服务器收到FIN报文后,向客户端发送一个ACK报文,表示确认客户端的FIN报文。
- 第三次挥手:当服务器没有更多数据要发送时,它会向客户端发送一个FIN报文,请求断开连接。
- 第四次挥手:客户端收到FIN报文后,向服务器发送一个ACK报文,表示确认服务器的FIN报文。此时,TCP连接断开完成。
那么为什么要进行三次握手和四次挥手呢?
- 三次握手的目的是为了防止失效的连接请求报文突然又传送到了服务器,从而产生错误。通过三次握手,可以确保双方都准备好建立连接。
- 四次挥手的目的是为了确保双方都能够完整地发送和接收数据。由于TCP是
全双工通信协议
,因此每个方向都需要单独进行关闭。这就需要四次挥手来完成。
TCP/IP 怎么保证数据包传输的有序可靠?
首先,TCP使用序列号和确认应答机制来保证数据包的有序传输。每个发送的数据包都会被分配一个序列号,接收方收到数据包后会发送一个确认应答报文,其中包含对发送方序列号的确认。这样,发送方就能够知道哪些数据包已经被接收,哪些数据包需要重传。
其次,TCP使用检验和来保证数据包的完整性。发送方在发送数据包时会计算一个检验和,并将其附加到数据包中。接收方收到数据包后会重新计算检验和并与发送方的检验和进行比较。如果两者不一致,则说明数据包在传输过程中发生了错误,接收方会丢弃该数据包并请求重传。
此外,TCP还使用超时重传机制来保证数据包的可靠传输。当发送方发送一个数据包后,它会启动一个定时器。如果在定时器超时之前没有收到接收方的确认应答报文,则认为该数据包丢失,并进行重传。
最后,TCP还使用流量控制和拥塞控制机制来保证网络的稳定性。流量控制通过调整发送窗口的大小来控制发送方的发送速率,防止接收方缓冲区溢出。拥塞控制则通过调整拥塞窗口的大小来控制网络拥塞程度,防止网络拥塞。
TCP和UDP的区别
- TCP是一种面向连接的协议,它在传输数据之前需要建立连接。
- UDP是一种无连接的协议,它不需要建立连接就可以直接发送数据。
- TCP提供了可靠的数据传输,它通过序列号、确认应答、重传等机制来保证数据包的有序可靠传输。而UDP不提供可靠性保证,它只负责将数据包发送出去,不保证数据包能够到达目的地。
- TCP提供了流量控制和拥塞控制机制来保证网络的稳定性。而UDP没有这些控制机制。
总之,TCP和UDP各有优缺点。TCP提供了可靠的数据传输,但速度相对较慢;而UDP速度快,但不提供可靠性保证。选择哪种协议取决于应用程序的需求。
解决跨域问题
跨域是指浏览器为了安全起见,限制了脚本内发起的跨源HTTP请求。这种限制被称为同源策略。同源策略规定,只有当协议
、域名
和端口
都相同时,两个页面才被认为是同源的。如果两个页面不同源,那么它们之间就不能进行跨域请求。
例如,运行在 https://api.example-a.com 的 JavaScript 代码使用 XMLHttpRequest 来发起一个到 https://api.example-b.com/data.json 的请求。这由于域名的不同所以不同源,这就是一个跨域请求。
但是,有时候我们需要在不同源之间进行通信。为了解决这个问题,出现了一些解决跨域问题的方法,如JSONP
、CORS
、代理
和postMessage
等。这些方法都是通过绕过浏览器的同源策略限制来实现跨域请求的。
-
JSONP:JSONP是一种通过动态创建
<script>
标签来实现跨域请求的方法。它利用了<script>
标签的src属性不受同源策略限制的特点,可以获取到其他域下的数据。
原理:- 首先,在页面中定义一个回调函数,用来处理获取到的数据。
- 然后,动态创建一个
<script>
标签,将其src属性指向目标服务器上的一个接口,并在URL中添加一个callback参数,用来指定回调函数的名称。 - 当
<script>
标签被插入页面后,浏览器会自动发起一个GET请求,获取目标服务器上的数据。目标服务器在接收到请求后,会将数据包装在回调函数中返回。 - 当数据返回到页面后,浏览器会自动执行回调函数,并将数据作为参数传入。这样,我们就可以在回调函数中处理获取到的数据了。
示例:
<!DOCTYPE html> <html> <head> <title>JSONP Example</title> </head> <body> <h1>JSONP Example</h1> <p id="output"></p> <script> // 定义回调函数 function handleResponse(data) { // 在回调函数中处理获取到的数据 document.getElementById('output').innerHTML = data.message; } // 动态创建<script>标签 var script = document.createElement('script'); // 设置src属性,指定回调函数的名称 script.src = 'https://api.example.com/getData?callback=handleResponse'; // 将<script>标签插入页面 document.body.appendChild(script); </script> </body> </html>
缺点:
- 有安全风险,如果目标服务器返回数据中包含了恶意代码,那么这些代码将被执行。
- JSONP只支持GET请求,不支持POST、PUT、DELETE等其他类型的HTTP请求。
- JSONP 需要后端配合返回指定格式的数据。
- 需要服务器端兼容这种跨域请求。
- 不容易调试,如果返回错误浏览器不会返回具体的错误,只能在控制台中查看。
-
CORS:CORS(Cross-Origin Resource Sharing,跨源资源共享)是一种基于HTTP头的机制,它允许服务器标识除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。服务器可以通过设置响应头中的Access-Control-Allow-Origin字段来指定哪些源可以访问它的资源。
-
代理:可以在服务器端设置一个代理,将前端发出的请求转发到目标服务器上,然后再将目标服务器返回的数据转发回前端。这样,前端就可以绕过浏览器的同源策略限制,实现跨域请求。
-
postMessage:postMessage是HTML5中新增的一个API,它允许不同源之间的窗口进行通信。可以通过监听message事件来接收其他窗口发送过来的消息。
浏览器
从输入url到页面展示出来的整个过程
- 浏览器会检查缓存,如果请求的资源在缓存中并且新鲜,就会直接使用缓存中的资源。如果资源未缓存或缓存不够新鲜,浏览器会发起新请求。
- 浏览器会解析URL,获取协议、主机、端口和路径。
- 浏览器会通过DNS查询获取主机的IP地址。
- 浏览器会与目标IP地址和端口建立TCP连接。
- TCP连接建立后,浏览器会发送HTTP请求。
- 服务器接收到请求并进行处理,然后返回HTTP响应。
- 浏览器接收到HTTP响应,并根据情况选择关闭TCP连接或保留重用。
- 浏览器检查响应状态码,并根据资源类型决定如何处理。如果资源是HTML文档,浏览器会解析HTML文档,构建DOM树,下载资源,构建CSSOM树,执行js脚本等操作。
浏览器缓存机制
浏览器提供了多种缓存机制,包括HTTP缓存、Cookie、Web Storage(包括localStorage和sessionStorage)和IndexedDB等。
- HTTP缓存:HTTP缓存是一种通过重复使用之前获取的资源来提高网站性能的机制。它通过设置HTTP响应头中的缓存控制字段来实现。
- Cookie:Cookie是一种用于在客户端存储少量数据的机制。它通常用于保存用户的登录状态、偏好设置等信息。Cookie的大小有限制,通常不能超过4KB。
- Web Storage:Web Storage包括localStorage和sessionStorage两种类型。它们都提供了一种在客户端存储键值对数据的机制。localStorage和sessionStorage的区别在于,localStorage中存储的数据会持久保存在客户端,即使浏览器关闭也不会丢失;而sessionStorage中存储的数据只在当前会话期间有效,当浏览器关闭时会被清除。
- IndexedDB:IndexedDB是一种在客户端存储大量结构化数据的机制。它提供了丰富的查询功能,并支持事务。
- 此外,还有一种名为session的机制,它用于在服务器端存储
用户会话信息
。session与上述几种缓存机制不同,它不是在浏览器中实现的,而是在服务器端实现的。
Cookie、sessionStorage、localStorage 的区别
- 生命周期:Cookie的生命周期可以通过设置过期时间来控制,过期后会被自动删除。sessionStorage中存储的数据只在当前会话期间有效,当浏览器关闭时会被清除。localStorage中存储的数据会持久保存在客户端,即使浏览器关闭也不会丢失。
- 存储容量:Cookie的大小有限制,通常不能超过4KB。sessionStorage和localStorage的存储容量要大得多,一般可以达到5MB或更多。
- 作用域:Cookie在同一个域名下是共享的,可以在不同页面之间共享数据。sessionStorage和localStorage的作用域是以窗口或标签页为单位的,它们只能在同一个窗口或标签页中共享数据。
- 数据传输:Cookie会随着每次HTTP请求一起发送到服务器端,这会增加额外的网络流量。而sessionStorage和localStorage中存储的数据只存在于客户端,不会被发送到服务器端。
浏览器缓存
浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。
通常浏览器缓存策略分为两种:强缓存(Expires,cache-control)和协商缓存(Last-modified ,Etag),并且缓存策略都是通过设置 HTTP Header 来实现的。
- 强缓存是指在缓存期间不需要请求,state code为200。它可以通过Expires和Cache-Control来实现。Expires是HTTP/1.0的产物,表示资源的过期时间,受限于本地时间。如果修改了本地时间,可能会造成缓存失效。Cache-Control是HTTP/1.1的产物,用来控制资源在本地缓存的有效期。它的max-age属性可以指定资源的最大生命周期。
- 协商缓存是指在强缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。其中协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的。它可以通过Last-Modified/If-Modified-Since和Etag/If-None-Match来实现。
当浏览器请求资源时,首先会检查资源的Expires和Cache-Control。如果命中强缓存,状态仍然返回200,但不会请求数据,在浏览器中能明显看到from cache字样。如果强缓存失效,则会携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存。如果命中协商缓存,则返回304状态码,并且不会返回数据。
304协商缓存
协商缓存是一种服务端的缓存策略,即通过服务端来判断某件事情是不是可以被缓存。服务端判断客户端的资源,是否和服务端资源一致,如果一致则返回304,反之返回200和最新的资源。
当网页不是首次加载时,如果设置了强缓存,数据则会从缓存中读取,不请求服务端。如果强缓存时间过期,则会请求服务端,服务端判断是否命中协商缓存,如果协商缓存时间或者哈希没变,则返回304。如果协商缓存时间对比不一样或资源变化,则数据重新被获取,返回200。
进程、线程、协程
进程、线程和协程是计算机程序执行的三种不同方式。
- 进程是操作系统分配资源和调度的基本单位,它拥有独立的内存空间和系统资源。一个进程可以包含多个线程,线程共享进程的资源。
- 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程共享进程的内存空间和系统资源,但拥有独立的运行栈和程序计数器。
- 协程是一种轻量级的线程,它避免了无意义的调度,由此可以提高性能。但也因此,程序员必须自己承担调度的责任。协程也失去了标准线程使用多CPU的能力。
进程和线程的区别与联系
进程和线程是两个密切相关但又不同的概念。
进程和线程之间的区别主要在于它们是不同的操作系统资源管理方式。
- 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
- 而线程只是一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间。
- 同一进程的所有线程共享该进程的所有资源,一个线程死掉就等于整个进程死掉。
- 一个线程只能属于一个进程,而一个进程可以有多个线程,且至少有一个线程。
网络攻击
网络攻击是指损害网络系统安全属性的危害行为,危害行为导致网络系统的机密性、完整性、可控性、真实性、抗抵赖性等受到不同程度的破坏。
常见的网络攻击类型包括:
- 恶意软件:一种可以执行各种恶意任务的应用程序。
- 网络钓鱼攻击:攻击者试图诱骗毫无戒心的受害者交出有价值的信息,例如密码、信用卡详细信息、知识产权等。
- 中间人攻击(MITM):攻击者拦截两方之间的通信以试图监视受害者、窃取个人信息或凭据,或者可能以某种方式改变对话。
- 分布式拒绝服务(DDoS)攻击:攻击者实质上用大量流量淹没目标服务器,以试图破坏甚至关闭目标。
- SQL注入攻击:一种特定于 SQL 数据库的攻击类型。SQL 数据库使用 SQL 语句来查询数据,这些语句通常通过网页上的 HTML 表单执行。如果未正确设置数据库权限,攻击者可能会利用 HTML 表单执行查询,从而创建、读取、修改或删除存储在数据库中的数据。
- 零日漏洞利用:网络犯罪分子获悉在某些广泛使用的软件应用程序和操作系统中发现的漏洞,然后将使用该软件的组织作为目标,以便在修复程序可用之前利用该漏洞。
- 还有其他的例如:DNS隧道攻击、商业电子邮件攻击 (BEC) 和加密劫持等。
部分攻击的防御措施:
- 防御网络攻击的方法有很多,具体取决于所面临的威胁类型。例如,防止恶意软件攻击需要安装最新最好的反恶意软件/垃圾邮件保护软件,确保员工接受过识别恶意电子邮件和网站的培训,拥有强大的密码策略,并尽可能使用多因素身份验证,为所有软件打补丁并保持最新,仅在绝对必要时才使用管理员账户,控制对系统和数据的访问,并严格遵守最小权限模型,监控网络是否存在恶意活动,包括可疑文件加密、入站/出站网络流量、性能问题等。
- 防止网络钓鱼攻击需要员工接受过充分的培训,能够识别可疑的电子邮件、链接和网站,并且知道不要输入信息或从他们不信任的网站下载文件。下载任何可以帮助识别恶意网站的附加组件也是一个好主意。
- 防止 MITM 攻击需要使用 VPN(虚拟专用网络),尤其是当从公共 Wi-Fi 热点连接时。当心虚假网站、侵入性弹出窗口和无效证书,并在每个 URL 的开头查找“HTTPS”。
- 防止 DDoS 攻击需要使用下一代防火墙或入侵防御系统 (IPS) 将实时了解任何流量不一致、网络性能问题、间歇性网络崩溃等。将服务器放在不同的数据中心也是一个好主意,因为如果当前服务器发生故障,可以切换到另一台服务器。在许多方面,保护网络免受 DDoS 攻击的最佳方法是制定一个久经考验的响应计划,这将能够尽快让系统恢复在线并维持业务运营。
- 防止 SQL 注入攻击,可以采用参数化语句并加强对用户输入的验证等。
其他
get和post的区别
GET和POST是两种HTTP请求方法,它们之间有一些区别。
- GET方法用于从指定的资源请求数据。它将参数显示在URL上,例如:/test/demo_form.php?name1=value1&name2=value2。GET方法提交的数据量有限制,因为它是通过URL添加数据来发送的,而URL的长度是受限制的。GET方法只允许ASCII字符。此外,GET请求可被缓存、保留在浏览器历史记录中、收藏为书签。但是,GET请求不应在处理敏感数据时使用。
- POST方法用于向指定的资源提交要被处理的数据。它通过表单提交不会显示在URL上,因此POST方法更具隐蔽性。POST方法可以传输大量的数据,对数据长度没有要求。POST方法没有限制,也允许二进制数据。此外,POST请求不会被缓存、不会保留在浏览器历史记录中、不能被收藏为书签。
http常见状态码
HTTP状态码用来表明特定HTTP请求是否成功完成。响应被归为以下五大类:
- 信息响应 (100–199)
- 成功响应 (200–299)
- 重定向消息 (300–399)
- 客户端错误响应 (400–499)
- 服务端错误响应 (500–599)
一些常见的HTTP状态码包括:
- 200 OK:请求成功。
- 301 Moved Permanently:请求的资源的URL已永久更改。
- 302 Found:请求的资源的URI已暂时更改。
- 304 Not Modified:所请求的资源未修改,客户端可以继续使用相同的缓存版本的响应。
- 400 Bad Request:客户端请求的语法错误,服务器无法理解。
- 401 Unauthorized:请求要求用户的身份认证。
- 403 Forbidden:客户端没有访问内容的权限。
- 404 Not Found:服务器找不到请求的资源。
- 500 Internal Server Error:服务器内部错误,无法完成请求。
CDN
CDN是Content Delivery Network的缩写,即内容分发网络。它的基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。
CDN的工作原理就是将源站的资源缓存到CDN各个节点上,当请求命中了某个节点的资源缓存时,立即返回客户端,避免每个请求的资源都通过源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验。
简单来说,CDN通过在全球范围内部署大量节点服务器,将网站内容缓存在这些节点服务器上,当用户访问网站时,CDN会根据用户的地理位置和网络状况等因素,智能调度用户访问离其最近的节点服务器,从而加快网站访问速度、提高网站可用性。
主要作用:
- 加快网站访问速度:通过将网站内容缓存在全球范围内的节点服务器上,CDN能够让用户就近访问所需内容,从而加快网站访问速度。
- 提高网站可用性:CDN能够通过智能调度、负载均衡等技术,保证用户能够快速、稳定地访问网站,提高网站的可用性。
- 缓解源站压力:CDN能够将大量用户请求分流到各个节点服务器上,从而减轻源站的压力,保证源站能够稳定运行。
- 节省带宽成本:CDN能够通过缓存技术、数据压缩技术等手段,有效减少数据传输量,从而节省源站的带宽成本。
算法
时间复杂度和空间复杂度:衡量算法的优劣。 时间复杂度和空间复杂度是用来衡量算法的优劣的两个指标。时间复杂度表示算法执行时间与数据规模之间的关系,常用大O表示法来表示。空间复杂度表示算法在运行过程中临时占用存储空间大小的量度,也常用大O表示法来表示。
常见的算法类型包括:
- 排序算法:快速排序、归并排序、计数排序。
- 搜索算法:回溯、递归、剪枝技巧。
- 图论:最短路、最小生成树、网络流建模。
- 动态规划:背包问题、最长子序列、计数问题。
- 基础技巧:分治、倍增、二分、贪心。
常见的考察内容包括:
- 数组与链表:单/双向链表、跳舞链。
- 栈与队列。
- 树与图:最近公共祖先、并查集。
- 哈希表。
- 堆:大/小根堆、可并堆。
- 字符串:字典树、后缀树。
例子:
- 爬楼梯:每次可以选择爬1个或2个台阶,问有多少种爬楼梯的方法。
var climbStairs = function(n) {
if (n === 1) {
return 1;
}
let dp = new Array(n + 1);
dp[1] = 1;
dp[2] = 2;
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
- 有效的括号:判断括号是否正确闭合。
var isValid = function(s) {
let stack = [];
let map = {
'(': ')',
'[': ']',
'{': '}'
}
for (let i = 0; i < s.length; i++) {
if (map[s[i]]) {
stack.push(s[i]);
} else if (s[i] !== map[stack.pop()]) {
return false;
}
}
return stack.length === 0;
};
- 整数转化英文表示:将整数转化为英文表示。
const singles = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"];
const teens = ["Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
const tens = ["", "Ten", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
const thousands = ["", "Thousand", "Million", "Billion"];
var numberToWords = function(num) {
if (num === 0) {
return 'Zero';
}
let sb = '';
let unit = 1000000000;
for (let i = 3; i >= 0; i--) {
let curNum = Math.floor(num / unit);
if (curNum !== 0) {
num -= curNum * unit;
sb += toEnglish(curNum) + thousands[i] + ' ';
}
unit /= 1000;
}
return sb.trim();
};
function toEnglish(num) {
let curr = '';
let hundred = Math.floor(num / 100);
num %= 100;
if (hundred !== 0 && num !== 0) {
curr += singles[hundred] + ' Hundred ';
} else if (hundred !== 0 && num === 0) {
curr += singles[hundred] + ' Hundred';
}
let ten = Math.floor(num / 10);
if (ten >= 2 && num % 10 !== 0) {
curr += tens[ten] + ' ';
num %= 10;
} else if (ten >= 2 && num % 10 === 0) {
curr += tens[ten];
num %= 10;
}
if (num < 10 && num > 0) {
curr += singles[num] + ' ';
} else if (num >=10 && num <20) {
curr += teens[num -10] + ' ';
}
return curr;
}
- Z 字形变换:将字符串按照Z字形排列后输出。
var convert = function(s, numRows) {
if (numRows === 1 || s.length < numRows) return s;
let rows = [];
let converted = '';
let reverse = false;
let count = 0;
for (let i=0; i<numRows; i++) rows[i] = [];
for (let i=0; i<s.length; i++) {
rows[count].push(s[i]);
if (count === numRows-1 || count === 0) reverse=!reverse;
reverse ? count++ : count--;
}
for(let row of rows){
converted+=row.join('');
}
return converted;
};
- 两数之和:在数组中找出和为目标值的两个数。
var twoSum = function(nums, target) {
let map = new Map();
for (let i = 0; i < nums.length; i++) {
let complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
};
- 最长回文子串:找出字符串中最长的回文子串。
var longestPalindrome = function(s) {
if (s.length < 2) return s;
let start = 0;
let maxLength = 1;
function expandAroundCenter(left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
if (right - left + 1 > maxLength) {
maxLength = right - left + 1;
start = left;
}
left--;
right++;
}
}
for (let i = 0; i < s.length; i++) {
expandAroundCenter(i - 1, i + 1);
expandAroundCenter(i, i + 1);
}
return s.substring(start, start + maxLength);
};
总结
以上就是目前所学的知识进行收集和整理的一些前端八股文,如若有错误请大佬们指正我会马上进行更改,若有欠缺请兄弟们补充一下我都会加上去!
最好的学习还是要把所学的知识进行整理理解,其实我也是小白,以上还有很多不大懂的地方需要进行理解学习再融会贯通,希望与各位一起进步,加油!!!
参考文献
https://juejin.cn/post/6994617237793406990
https://juejin.cn/post/7016593221815910408
https://cn.vuejs.org/guide/introduction.html
https://react.docschina.org/learn
创作不易,若需转载请备注出处!