# 前端经典面试题 by 爽爽学编程
# 有哪些常用的 HTML 标签?
<html>
:定义整个 HTML 文档的根元素
<head>
:包含了文档的元数据,如<title>
、<meta>
、<link>
、<script>
和<style>
等
<title>
:定义文档的标题,显示在浏览器的标题栏或标签页上
<body>
:定义文档的主体,包含了网页的可见内容
<h1>
到 <h6>
:定义标题,<h1>
是最高级别的标题,<h6>
是最低级别的标题
<p>
:定义段落
<br>
:定义换行
<hr>
:定义水平线
<strong>
或 <b>
:定义加粗文本
<em>
或 <i>
:定义斜体文本
<mark>
:定义标记或高亮文本
<small>
:定义小型文本
<del>
:定义删除文本
<ins>
:定义插入文本
<sub>
:定义下标文本
<sup>
:定义上标文本
<blockquote>
:定义引用的文本
<pre>
:定义预格式化的文本
<code>
:定义计算机代码文本
<samp>
:定义计算机程序的输出
<kbd>
:定义键盘文本
<var>
:定义变量名
<abbr>
:定义缩写或首字母缩写
<address>
:定义作者或拥有者的联系信息
<bdo>
:定义文本方向
<cite>
:定义引用的来源
<q>
:定义短的引用
<a>
:定义超链接
<img>
:定义图像
<figure>
和 <figcaption>
:定义图像及其标题
<iframe>
:定义内联框架
<embed>
:定义外部应用或交互内容
<object>
:定义内嵌对象
<param>
:定义对象的参数
<map>
和 <area>
:定义图像映射
<canvas>
:定义图形
<svg>
:定义 SVG(Scalable Vector Graphics)
<audio>
和 <video>
:定义音频和视频
<source>
:定义媒体资源
<track>
:定义媒体的字幕
<form>
:定义表单
<input>
:定义输入字段
<textarea>
:定义多行的文本输入控件
<button>
:定义按钮
<label>
:定义表单控件的标签
<select>
和 <option>
:定义下拉选择框和选项
<fieldset>
和 <legend>
:定义表单控件的分组和分组标题
<table>
、<tr>
、<td>
、<th>
:定义表格、行、单元格和表头单元格
<thead>
、<tbody>
、<tfoot>
:定义表格的头部、主体和脚部
<colgroup>
和 <col>
:定义表格的列组和列
# HTML5 有哪些新特性?
语义化标签:HTML5 引入了新的语义化标签,如 <article>
、<section>
、<nav>
、<aside>
、<header>
、<footer>
等,这些标签提供了更清晰的内容结构,有助于搜索引擎优化(SEO)和屏幕阅读器
图形和多媒体:
<canvas>
:用于绘制图形,支持复杂的图形和动画<svg>
:用于嵌入可伸缩矢量图形<audio>
和<video>
:用于嵌入音频和视频内容,无需插件
表单控件:HTML5 引入了新的表单控件和属性,如 <input type="email">
、<input type="url">
、<input type="number">
、<input type="range">
、<input type="date">
等,提供了更好的表单验证和用户体验
本地存储:
Web Storage:包括
localStorage
和sessionStorage
,提供了一种在用户浏览器中存储数据的方式IndexedDB:一种低级 API,用于存储大量结构化数据
Web Workers:允许在后台线程中运行脚本,不干扰用户界面
WebSockets:提供了一种在用户和服务器之间建立全双工通信渠道的方式
地理位置定位:通过
navigator.geolocation
API,可以获取用户的地理位置拖放 API:允许用户通过拖放操作与网页交互
跨文档消息传输:允许来自不同源的文档进行通信
Web Components:允许开发者创建可重用的自定义元素,包括
<template>
、<content>
、<shadow>
等响应式图片:通过
srcset
和sizes
属性,可以根据不同的屏幕尺寸和分辨率加载不同的图片资源新的 API 和功能:如
requestAnimationFrame
用于平滑动画,history.pushState
和history.replaceState
用于操作浏览器历史记录等安全性:HTML5 提供了更多的安全特性,如对内容的安全加载、沙箱 iframe 等
跨浏览器兼容性:HTML5 设计时考虑了跨浏览器兼容性,使得在不同浏览器上的表现更加一致
# iframe 标签的作用是什么?有哪些优缺点?
- 内容嵌入:可以在网页中嵌入来自不同源的内容,如视频、地图、文档等
- 广告展示:常用于展示广告内容
- 多页面浏览:允许用户在一个页面上浏览多个页面的内容
- 隔离内容:
<iframe>
可以隔离加载的内容,防止其与主页面的样式或脚本相互影响
优点:
跨域通信:允许不同源的页面嵌入,可以绕过浏览器的同源策略
内容隔离:嵌入的内容在视觉上和功能上都与主页面分离,不会影响主页面的布局和样式
安全性:通过沙箱属性(sandbox),可以限制
<iframe>
的行为,提高页面的安全性SEO 优化:通过使用
<iframe>
,可以将第三方内容嵌入到页面中,有助于搜索引擎优化用户体验:可以提供丰富的多媒体内容和交互体验
缺点:
性能问题:嵌入的页面可能会加载缓慢,影响整体页面的加载时间
SEO 影响:由于搜索引擎可能无法完全索引
<iframe>
中的内容,这可能对网站的搜索引擎优化不利可访问性问题:屏幕阅读器等辅助技术可能难以正确读取
<iframe>
中的内容样式问题:嵌入的内容可能难以与主页面的样式保持一致
安全性风险:如果嵌入的内容不受信任,可能会带来跨站脚本(XSS)等安全风险
交互限制:
<iframe>
中的内容通常不能与主页面进行交互,除非使用postMessage
API
# 什么是 HTML 语义化?为什么要语义化?
HTML 语义化是指使用具有明确语义含义的 HTML 标签来构建网页内容,使得网页的结构更加清晰和易于理解。语义化标签能够告诉浏览器、搜索引擎和辅助技术(如屏幕阅读器)网页内容的结构和意义
- 提高可访问性:语义化的标签有助于屏幕阅读器等辅助技术更好地理解网页内容,从而为视障用户等提供更好的访问体验
- 改善搜索引擎优化(SEO):搜索引擎能够更好地解析语义化的 HTML 结构,从而更准确地理解网页内容,有助于提高网页在搜索结果中的排名
- 增强代码的可读性和维护性:使用语义化的标签可以让开发者更容易理解网页的结构和内容,便于团队协作和代码维护
- 促进内容的重用:语义化的标签有助于内容在不同设备和平台上的重用,例如,通过样式表(CSS)或脚本(JavaScript)可以轻松地改变页面的布局或行为,而不需要修改 HTML 结构
- 支持更好的内容导航:用户和开发者可以通过语义化的标签更直观地导航网页内容,例如,使用
<header>
、<nav>
、<section>
、<article>
、<aside>
和<footer>
等标签可以清晰地标识页面的不同部分 - 适应性:语义化的 HTML 结构有助于网页在不同设备和屏幕尺寸上的自适应显示,提高用户体验
- 减少对样式和脚本的依赖:当 HTML 结构清晰且语义化时,可以减少对 CSS 和 JavaScript 的依赖,因为页面的基本布局和功能已经通过 HTML 标签本身定义
- 提高网页的可维护性:随着网站的发展,语义化的 HTML 结构使得添加新内容或修改现有内容变得更加容易,因为开发者可以快速理解页面的组织方式
# CSS 选择器有哪些?优先级分别是什么?
元素选择器(Type selector):根据元素类型选择元素,例如
p
选择所有<p>
元素类选择器(Class selector):根据类名选择元素,使用点
.
表示,例如.myclass
选择所有具有class="myclass"
的元素ID 选择器(ID selector):根据 ID 选择元素,使用井号
#
表示,例如#myid
选择id="myid"
的元素。属性选择器(Attribute selector):根据元素的属性或属性值选择元素,例如
[type="text"]
选择所有type
属性为text
的元素后代选择器(Descendant selector):选择作为某元素后代的所有元素,例如
div p
选择所有在<div>
内部的<p>
元素子选择器(Child selector):选择作为某元素直接子元素的所有元素,使用
>
表示,例如div > p
选择所有直接在<div>
内部的<p>
元素相邻兄弟选择器(Adjacent sibling selector):选择紧接在某元素后的同级元素,使用
+
表示,例如h1 + p
选择紧接在<h1>
后的<p>
元素通用兄弟选择器(General sibling selector):选择所有紧接在某元素后的同级元素,使用
~
表示,例如h1 ~ p
选择所有在<h1>
后的<p>
元素伪类选择器(Pseudo-class selector):选择元素的特定状态或行为,例如
:hover
、:focus
、:nth-child()
等伪元素选择器(Pseudo-element selector):选择元素的特定部分,使用双冒号
::
表示,例如::before
和::after
优先级
CSS 选择器的优先级决定了当多个选择器影响同一元素时,哪个选择器的样式将被应用。优先级由四个组成部分决定,通常表示为一个四位的数字(例如:0,0,0,1),每个部分代表不同的选择器类型:
- 行内样式:如果样式直接在 HTML 元素上使用
style
属性定义,则具有最高的优先级,可以覆盖其他所有样式 - ID 选择器:每个 ID 选择器贡献 1 点
- 类选择器、属性选择器和伪类选择器:每个类、属性或伪类选择器贡献 0.1 点
- 元素选择器和伪元素选择器:每个元素或伪元素选择器贡献 0.01 点
# 有哪些常见的 CSS 布局?
盒模型布局(Box Model Layout):
- 使用 CSS 的盒模型属性(如
width
、height
、padding
、border
、margin
)来控制元素的大小和位置
浮动布局(Float Layout):
- 使用
float
属性使元素向左或向右浮动,从而实现元素的并排排列
定位布局(Position Layout):
- 使用
position
属性(如static
、relative
、absolute
、fixed
、sticky
)来控制元素的定位方式
Flexbox 布局(Flexible Box Layout):
- 一种现代的布局方法,使用
display: flex;
来创建灵活的容器,可以轻松地对齐和分布容器内的项目
Grid 布局(Grid Layout):
另一种现代布局方法,使用 display: grid;
来创建网格容器,可以精确控制行和列的布局
表格布局(Table Layout):
- 使用 HTML 表格元素(如
<table>
、<tr>
、<td>
)来创建行列布局,通常用于展示数据
响应式布局(Responsive Layout):
- 使用媒体查询(Media Queries)来根据不同的屏幕尺寸和设备特性应用不同的样式规则
框架布局(Framework Layout):
- 使用 CSS 框架(如 Bootstrap、Foundation)提供的预定义样式和组件来快速构建布局
多列布局(Multi-column Layout):
- 使用
column-count
、column-width
、column-gap
等属性来创建多列布局
居中布局(Centering Layout):
- 使用各种 CSS 技巧(如 Flexbox、Grid、绝对定位等)来实现元素的水平和垂直居中
圣杯布局(Holy Grail Layout):
- 一种经典的三栏布局,通常包括头部、主体和底部,主体部分又分为左侧边栏和内容区域
CSS 框架布局:
- 使用如 Tailwind CSS、Bulma 等 CSS 框架提供的布局类来快速实现复杂的布局结构
# CSS 中的 1 像素问题是什么?有哪些解决方案?
CSS 中的 "1 像素问题" 主要出现在移动设备和高清显示设备上,特别是在使用视网膜(Retina)显示屏的设备上。这个问题指的是在这些设备上,CSS 中设置的 1 像素的边框或线条在视觉上看起来比实际的 1 像素要粗,通常是因为设备使用了像素倍增技术(如 2x、3x 分辨率)
原因:
- 视网膜显示屏拥有比传统显示屏更高的像素密度,这意味着在物理尺寸相同的情况下,屏幕可以容纳更多的像素点
- 当 CSS 中设置 1 像素的边框时,由于屏幕的高像素密度,这 1 像素在屏幕上被放大显示,导致看起来比 1 像素宽
解决方案:
- 媒体查询和缩放: 使用媒体查询结合
-webkit-device-pixel-ratio
属性来为高分辨率设备提供不同的样式。例如:
@media only screen and (-webkit-min-device-pixel-ratio: 2) {
.border {
border: 0.5px solid #000;
}
}
2
3
4
5
这种方法通过在高分辨率设备上使用 0.5 像素的边框来模拟 1 像素的视觉效果
- 使用矢量图形: 使用 SVG(Scalable Vector Graphics)来代替 CSS 边框,因为 SVG 可以保持清晰度,不受屏幕分辨率的影响
- 使用图片: 创建一个 1 像素的图片,并使用 CSS 将其作为边框或背景。这种方法在不同分辨率的设备上都能保持一致的视觉效果
- CSS 3D 变换: 利用 CSS 的 3D 变换特性来实现 1 像素的边框效果,例如使用
transform: scaleY(0.5);
来实现垂直方向上的缩放 - 伪元素和线性渐变: 使用伪元素(如
::before
或::after
)和线性渐变来创建 1 像素的视觉效果 - 使用
border-image
属性:border-image
属性允许你使用图片来定义边框,你可以创建一个非常小的图片,其中包含 1 像素的边框,然后应用到元素上 - 硬件加速: 在某些情况下,启用硬件加速可以改善 1 像素问题,例如使用
transform: translate3d(0, 0, 0);
- 使用 CSS 变量: 定义一个 CSS 变量来控制边框宽度,然后在不同的媒体查询中调整这个变量的值,以适应不同分辨率的设备
- 避免使用 1 像素: 在设计时尽量避免使用 1 像素的边框,因为这个问题主要是由于 1 像素在高分辨率设备上的表现不佳
# 什么是 CSS 盒子模型?
CSS 盒子模型(Box Model)是 CSS 布局的核心概念之一,用于定义 HTML 元素在页面上所占的空间大小。每个元素可以看作是一个盒子,这个盒子由以下几个部分组成:
内容区域(Content Area):
- 这是盒子的核心部分,用来展示元素的实际内容,由
width
和height
属性定义
- 这是盒子的核心部分,用来展示元素的实际内容,由
内边距(Padding):
- 位于内容区域的周围,是内容与边框之间的空间。内边距是透明的,不会显示背景颜色或背景图片。由
padding-top
、padding-right
、padding-bottom
、padding-left
属性定义,或者使用简写属性padding
- 位于内容区域的周围,是内容与边框之间的空间。内边距是透明的,不会显示背景颜色或背景图片。由
边框(Border):
- 位于内边距的外围,可以设置宽度(
border-width
)、样式(border-style
)和颜色(border-color
)
- 位于内边距的外围,可以设置宽度(
外边距(Margin):
- 位于边框的外围,是元素与其他元素之间的空间。外边距可以是正数,也可以是负数。由
margin-top
、margin-right
、margin-bottom
、margin-left
属性定义,或者使用简写属性margin
- 位于边框的外围,是元素与其他元素之间的空间。外边距可以是正数,也可以是负数。由
CSS 盒子模型有两种模式:
- 标准盒子模型(Standard Box Model):
- 在这种模式下,元素的总宽度和高度是内容区域、内边距和边框的总和。也就是说,如果你设置一个元素的宽度为 100px,那么这个宽度包括了内容区域、内边距和边框的宽度
- IE 盒子模型(IE Box Model,也称为怪异模式):
- 在这种模式下,元素的宽度和高度只包括内容区域的宽度和高度,不包括内边距和边框。如果你设置一个元素的宽度为 100px,这个宽度只应用在内容区域,内边距和边框会在内容区域的宽度之外增加
CSS3 引入了 box-sizing
属性,允许开发者在这两种盒子模型之间进行选择。默认情况下,大多数现代浏览器使用标准盒子模型,但可以通过设置 box-sizing: border-box;
来让元素的宽度和高度包括内边距和边框
# 哪些 CSS 属性可以继承?
在 CSS 中,某些属性可以被元素的子元素继承,这意味着如果父元素设置了这些属性,那么它的子元素也会应用这些样式,除非子元素有更具体的样式规则覆盖
以下是一些常见的可以继承的 CSS 属性:
字体相关属性:
font-family
:字体族font-size
:字体大小font-weight
:字体粗细font-style
:字体风格(如斜体)line-height
:行高
文本相关属性:
color
:文本颜色text-align
:文本对齐方式text-indent
:文本缩进text-transform
:文本转换(如大写、小写)letter-spacing
:字符间距word-spacing
:单词间距white-space
:空白处理direction
:文本方向(如从左到右或从右到左)
列表相关属性:
list-style-type
:列表项的标记类型(如圆点、方框)list-style-position
:列表项标记的位置list-style-image
:列表项的自定义标记图像
光标属性:
cursor
:光标类型
可见性属性:
visibility
:元素的可见性
表格相关属性:
border-collapse
:表格边框合并border-spacing
:表格边框间距caption-side
:表格标题的位置empty-cells
:空单元格的显示
引用和引用源属性:
quotes
:引用标记
内容属性:
content
:伪元素内容counter-reset
和counter-increment
:计数器属性
伪元素相关属性:
:before
和:after
伪元素的样式可以继承,但它们的内容(content
属性)除外
其他属性:
table-layout
:表格布局算法(如自动或固定)
# 什么是响应式设计?响应式设计的基本原理是什么?如何进行实现?
响应式设计(Responsive Design)是一种网页设计方法,它使网页能够适应不同的屏幕尺寸和设备类型,从而提供最佳的用户体验。响应式设计的核心目标是确保网页在各种设备上都能够正确显示,无论是桌面显示器、笔记本电脑、平板电脑还是智能手机
响应式设计的基本原理:
流体布局(Fluid Grids):
- 使用相对单位(如百分比)而不是固定单位(如像素)来定义元素的宽度,使得布局可以根据视口大小灵活调整
灵活的图片和媒体:
- 图片和其他媒体元素应该能够根据容器的大小进行缩放,通常使用
max-width: 100%;
属性实现
- 图片和其他媒体元素应该能够根据容器的大小进行缩放,通常使用
CSS 媒体查询(Media Queries):
- 根据设备的特定特征(如屏幕宽度、分辨率等)应用不同的 CSS 样式规则,以适应不同的屏幕尺寸
可变字体大小:
- 使用相对单位(如
em
或rem
)设置字体大小,使得文本大小可以根据屏幕大小和用户缩放设置进行调整
- 使用相对单位(如
灵活的导航:
- 导航元素应该能够适应不同的屏幕尺寸,例如在小屏幕上使用折叠式菜单(汉堡菜单)
测试和优化:
- 在多种设备和屏幕尺寸上测试网页,确保布局、功能和性能达到最佳状态
如何实现响应式设计:
- 使用 CSS 媒体查询:
- 定义不同的断点(breakpoints),并为不同的屏幕尺寸应用不同的样式
@media (max-width: 600px) {
/* 对于最大宽度为600px的设备应用的样式 */
}
2
3
使用流体布局:
- 利用百分比宽度和自动边距(如
margin: 0 auto;
)来创建流体布局
- 利用百分比宽度和自动边距(如
优化图片和媒体:
- 确保图片和视频元素能够适应容器大小,使用
max-width: 100%;
并考虑使用srcset
和sizes
属性来提供不同分辨率的图片
- 确保图片和视频元素能够适应容器大小,使用
使用可变字体大小:
- 利用
em
、rem
或vw
(视口宽度单位)等相对单位来设置字体大小
- 利用
优化导航:
- 为小屏幕设备设计适合的导航方式,如使用下拉菜单或侧边栏菜单
使用框架和库:
- 利用响应式设计框架(如 Bootstrap、Foundation)和 CSS 预处理器(如 Sass、Less)来简化响应式设计的开发
进行跨设备测试:
- 使用不同的设备和浏览器进行测试,确保网页在各种环境下都能正常工作
优化性能:
- 确保响应式设计不会影响网页的加载速度和性能,例如通过压缩图片和使用异步加载技术
考虑触摸操作:
- 优化元素的大小和间距,以适应触摸屏幕操作
# CSS3 新增了哪些特性?
CSS3 是 CSS 语言的最新标准,它引入了许多新特性来增强网页设计的能力和用户体验。以下是一些 CSS3 的主要新特性:
选择器的扩展:
- 新增了属性选择器、伪类选择器等,如
:nth-child()
、:nth-of-type()
、:last-child
等
- 新增了属性选择器、伪类选择器等,如
颜色和渐变:
- 引入了 RGBA 颜色模式,允许设置透明度
- 新增了渐变背景,包括线性渐变和径向渐变
边框和阴影:
- 增加了边框样式,如
border-image
属性 - 新增了
box-shadow
属性,用于添加阴影效果
- 增加了边框样式,如
文本效果:
- 新增了
text-shadow
属性,用于为文本添加阴影 - 引入了
word-wrap
、text-overflow
属性,用于处理文本溢出
- 新增了
Web 字体:
- 通过
@font-face
规则,可以加载自定义字体
- 通过
多列布局:
- 引入了多列布局模块,使用
column-count
和column-width
属性
- 引入了多列布局模块,使用
弹性盒子(Flexbox):
- 提供了一种更有效的方式来布局、对齐和分配容器内项目的空间,即使它们的大小未知或是动态变化的
网格布局(Grid):
- 引入了 CSS 网格布局,允许开发者使用行和列创建复杂的二维布局
媒体查询:
- 增强了媒体查询的功能,允许更细致地控制不同设备和条件下的样式
响应式图片:
- 通过
srcset
和sizes
属性,可以根据不同的屏幕尺寸和分辨率加载不同的图片资源
- 通过
CSS 动画:
- 引入了
@keyframes
规则和动画相关的属性,允许创建复杂的动画效果
- 引入了
CSS 过渡:
- 增强了过渡效果,可以平滑地在属性值之间进行过渡
CSS 变量(Custom Properties):
- 允许在 CSS 中定义变量,可以在样式表中重复使用
CSS 计数器:
- 增强了计数器系统,可以创建更复杂的计数器
形状:
- 引入了
clip-path
和shape-outside
属性,可以创建复杂的形状
- 引入了
滤镜效果:
- 通过
filter
属性,可以应用各种视觉效果,如模糊、亮度调整等
- 通过
会话历史管理:
- 通过
history
接口,可以在不重新加载页面的情况下操作浏览器的历史记录
- 通过
全屏和画面模式:
- 引入了全屏 API 和画面模式,允许网页或元素进入全屏状态
CSS 条件规则:
- 允许根据浏览器支持的特性来应用不同的样式
# 怎么用 CSS 实现一个宽高自适应的正方形?
方法 1:使用 width
和 padding
这种方法是设置元素的宽度,并使用百分比的 padding-top
来创建正方形的假象
.square {
width: 100%; /* 或者固定宽度,如 width: 200px; */
padding-top: 100%; /* 宽度的100%作为padding-top,创建正方形比例 */
position: relative;
}
/* 可选:如果你需要在正方形内放置内容 */
.square-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
方法 2:使用 vw
单位
视口宽度(vw
)单位允许元素的尺寸基于视口宽度的百分比进行缩放
.square {
width: 20vw; /* 根据需要调整vw的值 */
height: 0;
padding-top: 20vw; /* 保持宽高比 */
}
2
3
4
5
方法 3:使用 Flexbox
使用 Flexbox 布局,设置容器为 display: flex;
并使用 align-items
和 justify-content
来居中内容
.square {
display: flex;
width: 100%; /* 或者固定宽度 */
height: 0;
padding-top: 100%; /* 宽度的100%作为padding-top,创建正方形比例 */
align-items: center;
justify-content: center;
}
/* 正方形内的子元素 */
.square-content {
width: 80%; /* 或者其他百分比,小于100%以适应正方形 */
height: 80%; /* 同上 */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# CSS 中有哪些方式可以隐藏页面元素?有什么区别?
display: none;
- 元素不会在页面布局中占据任何空间,就像它根本不存在一样
- 元素及其所有子元素都不会显示
- 常用于通过 CSS 或 JavaScript 动态显示和隐藏元素
visibility: hidden;
- 元素在页面布局中仍然占据空间,但不会被显示出来
- 元素的子元素也会被隐藏,即使它们单独设置
visibility
为visible
- 适用于临时隐藏元素,而不改变页面布局
opacity: 0;
- 将元素的透明度设置为 0,使其完全透明,看起来像是隐藏了
- 元素在布局中仍然占据空间
- 子元素的透明度不会受到影响,除非它们也被设置为
opacity: 0;
height: 0; width: 0;
- 将元素的高度和宽度设置为 0,使元素在页面上不可见
- 元素在布局中不占据空间,但可以通过
overflow
属性影响其父元素 - 子元素也会被隐藏,但可以通过设置
overflow: visible;
使其可见
max-height: 0; max-width: 0;
- 将元素的最大高度和宽度设置为 0,使元素在页面上不可见
- 元素在布局中不占据空间,但可以通过
overflow
属性影响其父元素 - 与
height
和width
方法不同,这种方法允许元素在内容变化时动态显示
clip-path
或 shape-outside
- 使用 CSS 形状或剪辑路径将元素的可见区域限制为零
- 元素在布局中占据空间,但内容被裁剪掉,看起来像是隐藏了
position: absolute;
或 fixed;
与 left
/top
负值
- 将元素定位到视口之外,使其不在可视区域内显示
- 元素在布局中仍然占据空间,但可以通过绝对定位将其移出视口
transform: scale(0);
- 使用变换将元素缩放到 0,使其在页面上不可见
- 元素在布局中占据空间,但被缩放至不可见
# 怎么使用 CSS3 来实现动画?
CSS 动画(@keyframes
)
CSS 动画是通过 @keyframes
规则来定义的,它允许你创建动画序列,然后在选择器中应用这些动画
- 定义关键帧:使用
@keyframes
规则来定义动画的起始状态和结束状态,以及中间状态(如果有)
@keyframes example {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
2
3
4
5
6
7
8
- 应用动画:将定义好的动画应用到选择器上,设置动画的持续时间、时间函数、延迟、迭代次数等
.rotate {
animation-name: example;
animation-duration: 2s;
animation-timing-function: linear;
animation-delay: 1s;
animation-iteration-count: infinite;
animation-direction: normal;
animation-fill-mode: none;
}
2
3
4
5
6
7
8
9
CSS 过渡(Transitions)
CSS 过渡是另一种实现动画的方法,适用于当属性值改变时发生的平滑变化
- 设置过渡属性:在元素上设置
transition
属性,指定需要过渡的属性、持续时间和时间函数
.transition-box {
width: 100px;
height: 100px;
background-color: red;
transition: background-color 2s, transform 2s;
}
2
3
4
5
6
- 触发过渡:当元素的状态改变时(如鼠标悬停、类更改等),过渡效果将自动触发
.transition-box:hover {
background-color: blue;
transform: scale(1.5);
}
2
3
4
# 有哪些 CSS 性能优化的操作或技巧?
减少选择器复杂性:
- 避免使用过于复杂或具体的选择器,它们会增加浏览器的匹配成本
避免使用通配符(*
):
- 通配符选择器会匹配页面上的所有元素,这可能导致性能问题
使用类选择器:
- 类选择器(
.className
)比元素选择器(element
)和属性选择器([attr]
)更高效
利用继承:
- 利用 CSS 的继承机制,避免重复定义相同的样式
压缩 CSS 文件:
- 使用工具压缩 CSS,移除空格、注释和其他不必要的字符,减少文件大小
合并 CSS 文件:
- 将多个 CSS 文件合并为一个,减少 HTTP 请求的数量
使用 CSS 预处理器:
- 利用 Sass、Less 等预处理器的变量、混合(mixins)、函数等特性来提高 CSS 的可维护性和性能
避免使用过于复杂的 CSS 属性:
- 某些 CSS 属性(如
box-shadow
、border-radius
和linear-gradient
)可能会影响渲染性能
利用硬件加速:
- 使用
transform: translate3d(0,0,0);
可以利用 GPU 硬件加速,提高动画性能
避免使用 CSS 表达式:
- CSS 表达式(使用
expression()
)已被废弃,因为它们会导致性能问题
使用媒体查询加载特定样式:
- 根据设备特性加载适当的样式,避免所有设备加载相同的大型样式表
避免使用 !important
:
- 过度使用
!important
会使得样式的优先级难以控制,影响性能
优化动画性能:
- 使用
transform
和opacity
属性进行动画,因为它们可以实现更好的性能
缓存 CSS:
- 利用浏览器缓存,避免重复下载相同的 CSS 文件
避免使用过多的浮层:
- 浮层(
float
)可能导致额外的渲染工作,尤其是在复杂的布局中
使用 will-change
属性:
- 适当使用
will-change
属性可以告知浏览器哪些属性可能会改变,从而优化性能
优化 CSS 选择器规则:
- 将最常用的选择器放在 CSS 文件的顶部,因为浏览器会从上到下解析 CSS
使用 CSS 变量:
- 使用 CSS 变量(Custom Properties)来减少重复的代码和提高可维护性
避免使用内联样式:
- 内联样式(
style
属性)的优先级很高,而且会降低样式的可维护性
使用浏览器开发者工具:
- 利用浏览器的开发者工具分析和识别性能瓶颈
# JavaScript 中如何中止网络请求?
XMLHttpRequest (XHR)
对于使用 XMLHttpRequest
发起的请求,可以使用 abort()
方法来中止请求:
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://example.com/data");
// 设置请求完成和错误处理的回调函数
xhr.onload = function () {
if (xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.onerror = function () {
console.error("请求出错");
};
// 发送请求
xhr.send();
// 中止请求
xhr.abort();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Fetch API
Fetch API 本身没有内置的方法来取消请求。但是,可以通过 AbortController
来实现:
// 创建一个 AbortController
const controller = new AbortController();
const signal = controller.signal;
fetch("https://example.com/data", { signal })
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.text();
})
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Fetch aborted");
} else {
console.error("Fetch error:", error);
}
});
// 中止 fetch 请求
controller.abort();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
jQuery.ajax
如果你使用的是 jQuery 的 $.ajax
方法,可以通过传递一个 AbortSignal
来中止请求:
const controller = new AbortController();
const signal = controller.signal;
$.ajax({
url: "https://example.com/data",
signal: signal,
success: function (data) {
console.log(data);
},
error: function (xhr, status, error) {
if (status === "abort") {
console.log("AJAX request aborted");
}
},
});
// 中止 AJAX 请求
controller.abort();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
注意事项
- 中止请求后,
XMLHttpRequest
的readyState
会保持不变,但status
会设置为 0 - 使用
fetch
API 时,如果请求已经被中止,Promise
会被拒绝,并抛出一个AbortError
- 中止请求并不总是立即生效,特别是如果请求已经开始接收数据
# 什么是 BOM 和 DOM?分别列举一些它们的函数
BOM(Browser Object Model,浏览器对象模型)和 DOM(Document Object Model,文档对象模型)是 Web 开发中的两个重要概念,它们都是 JavaScript 操作网页的基础
BOM
BOM 提供了与浏览器交互的接口和操作浏览器窗口的方法。BOM 允许 JavaScript 与浏览器进行多种交互,例如:
- 打开新窗口或标签页
- 操纵浏览器历史记录
- 控制浏览器的滚动条
- 处理浏览器的导航请求
以下是一些 BOM 的常见函数和属性:
window.open(url, [windowName], [windowFeatures])
:打开一个新窗口window.close()
:关闭当前窗口window.location
:包含当前页面的 URL 的对象window.history
:包含浏览器历史记录的相关信息window.scroll(x-coord, y-coord)
或window.scrollTo(x-coord, y-coord)
:滚动到页面的指定位置window.alert(message)
:显示带有指定消息的警告框window.confirm(message)
:显示带有指定消息的确认框window.prompt(message, [default])
:显示带有输入字段的提示框window.addEventListener(type, listener, [options])
:注册事件监听器到窗口上window.removeEventListener(type, listener, [options])
:从窗口上移除事件监听器
DOM
DOM 是 HTML 和 XML 文档的编程接口,它将文档表示为对象树,每棵树由对象节点组成,这些节点可以是元素、属性、甚至是文本或注释。DOM 提供了对文档结构化表示形式的修改功能。以下是一些 DOM 的常见函数和方法:
document.getElementById(id)
:通过元素的 ID 获取元素document.getElementsByTagName(name)
:通过标签名获取元素的集合document.getElementsByClassName(names)
:通过类名获取元素的集合document.querySelector(selector)
:根据 CSS 选择器获取第一个匹配的元素document.querySelectorAll(selector)
:根据 CSS 选择器获取所有匹配的元素集合element.setAttribute(name, value)
:为元素设置属性element.getAttribute(name)
:获取元素的属性值element.style
:设置元素的内联样式element.innerHTML
:获取或设置元素的 HTML 内容element.textContent
:获取或设置元素的文本内容element.appendChild(node)
:将一个节点添加到元素的子节点列表末尾element.removeChild(node)
:移除元素的子节点element.addEventListener(type, listener, [options])
:注册事件监听器到元素上element.removeEventListener(type, listener, [options])
:从元素上移除事件监听器
# 深拷贝和浅拷贝有什么区别?JS 怎么实现深拷贝?
在 JavaScript 中,浅拷贝(Shallow Copy)和深拷贝(Deep Copy)是两种不同的对象复制方式,它们在处理对象和数组时表现出不同的行为:
浅拷贝
浅拷贝只会复制对象的第一层属性。如果属性是基本数据类型,那么会复制其值;如果属性是引用类型(如数组、对象等),则只复制引用的地址,不会复制引用的对象本身。这意味着,如果原对象的属性是对象或数组,拷贝后的对象与原对象共享同一个引用
常见的浅拷贝方法:
- 使用对象的
Object.assign()
方法 - 使用展开运算符(Spread Operator)
...
- 使用
Array.prototype.slice()
方法来复制数组
// 使用 Object.assign() 实现浅拷贝
let shallowCopyObj = Object.assign({}, originalObj);
// 使用展开运算符实现浅拷贝
let shallowCopyArray = [...originalArray];
// 使用 slice() 方法实现数组的浅拷贝
let shallowCopyArray2 = originalArray.slice();
2
3
4
5
6
7
8
深拷贝
深拷贝会创建一个完全独立的新对象,新对象的所有属性都是原对象属性的副本,不论属性是基本类型还是引用类型。这意味着,即使原对象的属性包含对象或数组,深拷贝也会递归地复制这些属性,使得新对象与原对象不共享任何引用
实现深拷贝的方法:
使用 JSON 方法:通过 JSON.stringify()
将对象序列化为字符串,然后使用 JSON.parse()
将字符串解析为新对象。但这种方法无法复制函数、undefined、循环引用的对象等
let deepCopyObj = JSON.parse(JSON.stringify(originalObj));
使用第三方库:例如 Lodash 库提供的 _.cloneDeep()
方法。
let deepCopyObj = _.cloneDeep(originalObj);
自定义深拷贝函数:通过递归遍历对象的所有可枚举属性,并递归地复制它们
function deepCopy(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
let temp = obj.constructor(); // 对于数组使用 Array(), 对于对象使用 Object()
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = deepCopy(obj[key]);
}
}
return temp;
}
let deepCopyObj = deepCopy(originalObj);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 如何使用 JavaScript 来判断用户设备类型?比如判断是 PC 端还是移动端访问?
在 JavaScript 中,可以通过检查 navigator
对象的 userAgent
字符串来判断用户的设备类型。userAgent
字符串包含了用户正在使用的浏览器和设备的相关信息。以下是一些基本的方法来判断用户设备类型:
检查 userAgent
字符串
function detectDevice() {
const userAgent = navigator.userAgent;
if (/Mobi|Android/i.test(userAgent)) {
return "mobile";
} else if (/iPad/i.test(userAgent)) {
return "tablet";
} else {
return "desktop";
}
}
console.log(detectDevice());
2
3
4
5
6
7
8
9
10
11
12
13
检查屏幕宽度
这种方法假设屏幕宽度较小的设备是移动设备,但这不是一个完全可靠的方法,因为一些桌面设备也有较小的屏幕
function detectDeviceByScreen() {
if (screen.width <= 768) {
// 可以根据实际需要调整这个值
return "mobile";
} else if (screen.width <= 1024) {
return "tablet";
} else {
return "desktop";
}
}
console.log(detectDeviceByScreen());
2
3
4
5
6
7
8
9
10
11
12
检查触摸支持
移动设备通常支持触摸事件,而桌面设备则不一定
function detectDeviceByTouchSupport() {
if ("ontouchstart" in window || navigator.msMaxTouchPoints) {
return "mobile";
} else {
return "desktop";
}
}
console.log(detectDeviceByTouchSupport());
2
3
4
5
6
7
8
9
# JS 中数组是如何在内存中存储的?
连续内存空间: 数组通常在内存中分配一块连续的空间来存储其元素。这种连续性使得数组可以通过索引快速访问元素
动态大小: JavaScript 数组是动态的,可以根据需要自动调整大小。当向数组添加元素时,如果当前分配的内存空间不足,JavaScript 引擎会分配一块新的更大的内存空间,并将原有元素复制到新空间,然后添加新元素
元素类型不固定: JavaScript 数组可以存储不同类型的元素,如数字、字符串、对象等。这意味着数组在内存中存储的是元素的引用或值,而不是元素类型
稀疏数组: 当数组中存在删除操作或从未被赋值的索引时,数组成为稀疏数组。在稀疏数组中,只有实际存储数据的元素会占用内存空间,而不存在的元素则不占用空间
原型链: JavaScript 对象(包括数组)在内存中存储时,除了实际的数据外,还会包含一个指向其原型的引用。这是 JavaScript 继承机制的一部分
隐藏类和优化: 在一些 JavaScript 引擎(如 V8)中,会使用隐藏类来优化对象(包括数组)的存储和访问。如果创建的数组模式相似,引擎可以利用这些模式进行内存分配和访问速度的优化
内存分配: 数组在内存中的分配可能涉及到额外的内存,用于存储数组的长度(length 属性)和其他元数据
垃圾回收: JavaScript 引擎使用垃圾回收机制来回收不再使用的数组占用的内存。当数组没有被任何变量引用时,它将变成垃圾回收的候选对象
# JS 中 Map 和 WeakMap 有什么区别?
引用的强度:
Map
存储的是普通的引用,键和值都是以强引用的方式存储的WeakMap
存储的是弱引用,键必须是对象,而且这些对象作为 WeakMap 的键时,不会阻止垃圾回收器回收这些键所引用的对象
键的范围:
Map
可以使用任何类型的值作为键,包括对象、基本类型或null
WeakMap
的键只能是对象,不能使用基本类型或null
垃圾回收:
Map
中的键值对即使没有被其他地方引用,也不会被垃圾回收,因为Map
内部保持着对它们的引用WeakMap
允许垃圾回收器在没有其他地方引用键所指向的对象时,回收这些键值对
性能:
WeakMap
可能提供比Map
更好的性能,因为它不需要跟踪键的引用,这减少了内存占用
迭代:
Map
是可迭代的,可以使用for...of
循环或Map
的迭代器方法(如entries()
、keys()
、values()
)来遍历WeakMap
不可迭代,没有提供迭代器方法,因此不能使用for...of
循环或类似Map
的迭代方法
大小:
Map
有size
属性,可以获取Map
中元素的数量WeakMap
没有size
属性,因为其内部的元素数量可能会随着垃圾回收而变化
方法:
Map
提供了丰富的方法,如set()
、get()
、has()
、delete()
、clear()
等WeakMap
只提供了get()
、set()
、has()
和delete()
方法,且没有clear()
方法
用途:
Map
适用于需要存储键值对且键的引用需要保持的场景WeakMap
适用于需要跟踪对象,但又希望在没有其他地方引用这些对象时允许它们被回收的场景
# 用 CSS 和 JS 来实现动画分别有哪些优缺点?
使用 CSS 实现动画的优缺点:
优点:
- 简单快捷:对于简单的动画效果,CSS 可以实现快速开发和部署
- 性能优越:CSS 动画可以由浏览器硬件加速,通常比 JavaScript 动画性能更好
- 易于维护:CSS 代码通常更简洁,易于理解和维护
- 兼容性好:大多数现代浏览器都支持 CSS 动画,跨浏览器兼容性好
- 无需 JavaScript:在用户禁用 JavaScript 的情况下,CSS 动画仍然可以工作
缺点:
- 功能有限:CSS 动画主要支持简单的变换和过渡效果,对于复杂的动画序列和逻辑控制能力有限
- 难以实现复杂的交互:对于需要复杂交互和逻辑判断的动画,CSS 难以实现
- 动画控制能力弱:CSS 动画的控制不如 JavaScript 灵活,例如动态调整动画参数或响应复杂的用户事件
使用 JavaScript 实现动画的优缺点:
优点:
- 功能强大:JavaScript 提供了几乎无限的动画能力,可以创建任何复杂度的动画效果
- 逻辑控制:JavaScript 可以处理复杂的逻辑和交互,使动画能够根据用户行为或其他事件动态变化
- 动画控制:JavaScript 可以精确控制动画的开始、暂停、停止和速度等
- 状态管理:对于需要维护复杂状态的动画,JavaScript 提供了更好的状态管理能力
缺点:
- 性能问题:JavaScript 动画可能不如 CSS 动画性能好,特别是在动画大量元素或执行复杂计算时
- 依赖 JavaScript:如果用户禁用了 JavaScript,动画将无法工作
- 代码复杂性:实现相同效果的 JavaScript 代码通常比 CSS 代码更复杂,增加了维护难度
- 浏览器兼容性:需要考虑不同浏览器对 JavaScript 动画的支持程度
总结:
- 对于简单的效果,如颜色渐变、元素淡入淡出、简单移动等,推荐使用 CSS 动画
- 对于需要复杂交互、逻辑控制或状态管理的动画,JavaScript 是更好的选择
- 在实际开发中,CSS 和 JavaScript 动画经常结合使用,利用 CSS 处理视觉和布局相关的动画,用 JavaScript 处理更复杂的逻辑和交互
# JS 中怎么阻止事件冒泡和事件默认行为?
阻止事件冒泡(Event Bubbling)
事件冒泡是指事件从目标元素开始,逐级向上传播到文档的根元素。要阻止事件继续向上冒泡,可以在事件处理函数中调用 event.stopPropagation()
方法
element.addEventListener("click", function (event) {
event.stopPropagation(); // 阻止事件冒泡
// 处理点击事件
});
2
3
4
阻止事件默认行为(Default Action)
每个事件都有一个默认行为,例如点击按钮会提交表单,点击链接会导航到新页面。要阻止这个默认行为,可以在事件处理函数中调用 event.preventDefault()
方法
element.addEventListener("click", function (event) {
event.preventDefault(); // 阻止默认行为
// 处理点击事件,但不会执行默认行为
});
2
3
4
阻止表单提交的默认行为
对于表单提交事件,通常使用 submit
事件的 preventDefault()
方法来阻止表单的提交
form.addEventListener("submit", function (event) {
event.preventDefault();
// 执行 AJAX 请求或其他逻辑
});
2
3
4
阻止链接的默认导航行为
对于链接元素 <a>
,当使用 href
属性时,点击链接会触发页面导航。使用 event.preventDefault()
可以阻止这种导航
link.addEventListener("click", function (event) {
event.preventDefault(); // 阻止链接的默认导航行为
// 执行自定义逻辑
});
2
3
4
阻止输入框的默认行为
对于输入框等元素,可能需要阻止用户的输入,例如在输入框中输入特定字符
input.addEventListener("input", function (event) {
if (/[^0-9]/.test(event.target.value)) {
event.preventDefault();
// 可以进一步处理,比如清除输入框或显示提示
}
});
2
3
4
5
6
# 什么是防抖和节流?如何用 JS 编码实现?
防抖(Debounce)
防抖技术确保函数在指定的时间间隔结束后才执行,如果在该间隔内再次触发事件,则重新开始计时。这适用于搜索框输入、按钮提交等场景
防抖的 JS 实现:
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 使用
const handleInput = debounce(function (event) {
console.log(event.target.value);
}, 300);
inputElement.addEventListener("input", handleInput);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
节流(Throttle)
节流技术确保函数在指定的时间间隔内最多只执行一次,不管事件触发了多少次。这同样适用于滚动、窗口调整大小等频繁触发的事件
节流的 JS 实现:
function throttle(func, limit) {
let inThrottle;
return function () {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// 使用
const handleScroll = throttle(function () {
console.log("Scrolled");
}, 100);
window.addEventListener("scroll", handleScroll);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
两者的区别:
- 防抖:无论事件触发多少次,只有在事件停止触发后等待指定的时间间隔,函数才会执行一次
- 节流:在指定的时间间隔内,无论事件触发多少次,函数最多只执行一次
使用场景:
- 当你想要限制事件处理函数的执行频率,以避免性能问题或不必要的计算时,可以使用这两种技术
- 防抖适用于最后一次操作最重要时,如表单验证或搜索框输入
- 节流适用于希望在事件持续发生时保持一定频率的处理,如滚动事件或窗口调整大小事件
# 前端有哪些实现跨页面通信的方法?
URL 参数: 通过在 URL 中附加参数来传递信息,接收页面通过解析 URL 获取参数值
锚点(Fragment)标识: 使用 URL 的锚点(#
)来触发当前页面或另一个页面中的某些行为
Web Storage(localStorage 和 sessionStorage)
localStorage
提供了一个在所有同源页面之间共享数据的方式,数据没有时间限制sessionStorage
类似于localStorage
,但数据只在浏览器会话期间有效
Cookie: Cookie 可以存储在用户的浏览器上,通过设置合适的域(domain),可以实现不同页面间的信息传递
HTML5 WebSockets: 使用 WebSockets 可以在浏览器和服务器之间建立一个全双工通信渠道,实现实时的跨页面通信
Window.postMessage: window.postMessage
方法可以安全地实现跨源通信,允许你向另一个窗口发送消息
BroadcastChannel API: BroadcastChannel
提供了一个在相同源(origin)的不同浏览器标签页之间进行简单通信的方式
SharedWorker: SharedWorker
可以在多个浏览器标签页之间共享,通过它可以实现不同页面之间的数据共享和通信
服务器端存储: 通过服务器端的数据库或文件存储,不同的页面可以通过读写服务器上的数据来交换信息
自定义 URL 协议: 自定义 URL 协议允许应用程序定义自己的协议,并在浏览器中注册处理程序,实现跨页面通信
二维码: 生成包含信息的二维码,用户扫描后可以在另一个设备或应用中打开,实现信息传递
文件下载: 在页面上提供一个下载链接,下载的文件可以存储需要传递的信息
二维码和 NFC 标签: 使用二维码或 NFC 标签存储信息,用户通过手机扫描后可以在另一个应用中读取信息
第三方授权服务: 使用如 OAuth 这样的第三方授权服务,在不同应用或服务之间传递认证信息
跨文档消息传递: 对于同源下的多个 iframe,可以使用window.frames
或者window.parent
等属性实现通信
# 什么是虚拟 DOM?使用虚拟 DOM 一定更快吗?
虚拟 DOM(Virtual DOM)是一种编程概念,用于高效地更新和管理真实 DOM(Document Object Model)。虚拟 DOM 实际上是真实 DOM 的一个轻量级的 JavaScript 对象模型表示
虚拟 DOM 的工作原理:
- 渲染阶段:当应用程序的状态发生变化时,UI 也需要相应地更新。虚拟 DOM 通过比较新旧状态的虚拟 DOM 树来计算出最小的更新操作
- 差异比较(Diffing):虚拟 DOM 算法会比较新旧两棵树的差异,确定哪些组件或元素需要更新
- 更新真实 DOM:根据差异比较的结果,虚拟 DOM 只更新真实 DOM 中实际变化的部分,而不是重新渲染整个页面
使用虚拟 DOM 的优势:
- 性能:通过减少对真实 DOM 的操作次数,虚拟 DOM 可以提高性能,尤其是在大型应用程序中
- 跨平台:虚拟 DOM 允许相同的 JavaScript 代码在不同平台(如 Web、移动应用)上运行,只需在不同平台上实现对应的 DOM 操作
- 简化开发:开发者可以专注于状态变化,而不必担心 DOM 操作的细节
虚拟 DOM 的实现:
- 许多现代前端框架,如 React、Vue 和 Inferno,都使用虚拟 DOM 作为它们的核心机制之一
使用虚拟 DOM 一定更快吗?
虽然虚拟 DOM 提供了许多性能优势,但它并不一定总是比直接操作真实 DOM 更快。以下是一些考虑因素:
- 小规模更新:对于小规模的 DOM 更新,直接操作真实 DOM 可能更快,因为避免了虚拟 DOM 的比较和同步开销
- 复杂度:在非常复杂的应用中,虚拟 DOM 的差异算法可能变得不那么高效
- 初始渲染:对于初始渲染,虚拟 DOM 可能比直接操作 DOM 慢,因为它需要构建整个树结构
- 环境因素:在某些低性能的设备或环境中,虚拟 DOM 的额外计算可能成为性能瓶颈
# JS 脚本延迟加载的方式有哪些?
JavaScript 脚本的延迟加载是一种优化技术,用于提高网页加载速度和改善用户体验。以下是一些常见的延迟加载 JavaScript 脚本的方法:
defer
属性: 使用 <script>
标签的 defer
属性可以将脚本延迟到文档解析完成后执行,不阻塞页面渲染
<script src="script.js" defer></script>
async
属性: async
属性允许脚本异步加载,加载完成后立即执行,可能阻塞页面渲染。适用于不依赖于其他脚本的独立模块
<script src="script.js" async></script>
动态脚本加载: 使用 JavaScript 的 document.createElement
创建 <script>
元素,并设置 src
属性后将其添加到页面中,实现延迟加载
var script = document.createElement("script");
script.src = "script.js";
document.head.appendChild(script);
2
3
事件监听器: 在特定事件(如 DOMContentLoaded
、load
或自定义事件)触发后加载脚本
window.addEventListener("DOMContentLoaded", (event) => {
var script = document.createElement("script");
script.src = "script.js";
document.head.appendChild(script);
});
2
3
4
5
滚动监听器: 当用户滚动到页面的特定部分时,再加载脚本。这适用于无限滚动或分页的网页
window.addEventListener("scroll", () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
var script = document.createElement("script");
script.src = "script.js";
document.head.appendChild(script);
}
});
2
3
4
5
6
7
MutationObserver: 使用 MutationObserver
API 监听 DOM 变化,当特定条件满足时加载脚本
var observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (condition) {
var script = document.createElement("script");
script.src = "script.js";
document.head.appendChild(script);
observer.disconnect();
}
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
2
3
4
5
6
7
8
9
10
11
12
# 什么是点击穿透,怎么解决?
点击穿透(Click Through)是一个前端开发中的问题,指的是当用户尝试点击一个覆盖在其他元素之上的元素时,实际上却触发了被覆盖元素的点击事件。这通常发生在使用了 CSS 定位的元素上,尤其是当这些元素的 z-index
属性导致它们在页面上的层叠顺序发生变化时
点击穿透的原因:
- 层叠上下文:元素通过
position
(如absolute
、relative
、fixed
或sticky
)和z-index
属性创建了新的层叠上下文 - 覆盖问题:一个元素在视觉上覆盖了另一个元素,但是没有正确地捕获点击事件
- 事件传播:点击事件可能会穿透覆盖层,被下面的元素捕获
解决点击穿透的方法:
- 确保正确的
z-index
值: 确保覆盖元素的z-index
值高于被覆盖元素的z-index
值 - 使用
pointer-events
属性: CSS 的pointer-events
属性可以用来控制元素是否响应指针事件(如鼠标点击)pointer-events: none;
可以阻止元素响应指针事件,这样点击事件就会穿透到下面的元素pointer-events: auto;
(默认值)允许元素响应指针事件
/* 阻止元素响应点击事件 */
.element {
pointer-events: none;
}
2
3
4
使用遮罩层: 在覆盖元素和被覆盖元素之间添加一个遮罩层,用于捕获点击事件,但不显示任何内容
JavaScript 事件委托: 使用事件委托来捕获点击事件,并在事件处理函数中检查事件的目标元素或使用
event.stopPropagation()
阻止事件冒泡。
parentElement.addEventListener("click", function (event) {
if (event.target === childElement) {
// 处理点击事件
}
});
2
3
4
5
- 优化 HTML 结构: 重新考虑 HTML 元素的布局和结构,避免不必要的覆盖和层叠上下文
- 使用 CSS 框架的解决方案: 如果你使用的是 CSS 框架,如 Bootstrap,它可能提供了内建的解决方案来处理点击穿透问题
- 临时禁用被覆盖元素的交互: 在需要的时候,可以通过 JavaScript 临时禁用被覆盖元素的点击事件
- 使用 HTML5
draggable
属性: 如果点击穿透问题发生在拖拽操作中,确保draggable
元素正确处理dragstart
和dragend
事件
# 什么是 JS 对象的可枚举性(enumerable)?
在 JavaScript 中,对象属性的可枚举性(Enumerable)是指属性是否可以在 for...in
循环或 Object.keys()
、Object.values()
、Object.entries()
等方法中被枚举出来。
可枚举性的理解:
默认可枚举:
- 通过对象字面量或构造函数创建的对象,默认情况下,它们的属性都是可枚举的
不可枚举属性:
- 某些内置对象的属性或通过特定方式添加的属性可能设置为不可枚举
属性特性:
- 每个属性都有一个特性(property descriptor),它定义了属性的行为,包括是否可枚举
特性修改:
- 使用
Object.defineProperty()
方法可以修改属性的特性,包括可枚举性
- 使用
示例:
let obj = {
a: 1,
b: 2,
};
// 默认情况下,属性 a 和 b 都是可枚举的
console.log(Object.getOwnPropertyDescriptor(obj, "a")); // { value: 1, writable: true, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptor(obj, "b")); // { value: 2, writable: true, enumerable: true, configurable: true }
// 修改属性 b 的可枚举性
Object.defineProperty(obj, "b", {
value: 2,
writable: true,
enumerable: false, // 这里设置为不可枚举
configurable: true,
});
// 检查属性 b 是否可枚举
console.log(obj.propertyIsEnumerable("b")); // false
// for...in 循环将枚举可枚举的属性
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
console.log(prop); // 将打印 'a',不会打印 'b'
}
}
// Object.keys() 将返回所有可枚举的自有属性的数组
console.log(Object.keys(obj)); // ['a']
// Object.values() 和 Object.entries() 也只考虑可枚举的属性
console.log(Object.values(obj)); // [1]
console.log(Object.entries(obj)); // [['a', 1]]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
可枚举性的应用:
- 保护数据:通过设置属性为不可枚举,可以减少属性被意外枚举和修改的风险
- 兼容性:在与其他系统或库交互时,了解属性的可枚举性有助于确保兼容性
# JS 如何顺序执行 10 个异步任务?
回调函数
使用回调函数可以按顺序执行异步任务,但这种方法可能会导致“回调地狱”(Callback Hell),即嵌套多层回调函数,难以维护
function asyncTask(i, callback) {
console.log(`Task ${i} started`);
setTimeout(() => {
console.log(`Task ${i} completed`);
callback();
}, 1000);
}
function startTasks() {
const tasks = [];
for (let i = 1; i <= 10; i++) {
tasks.push((callback) => {
asyncTask(i, callback);
});
}
tasks.reduce(
(chain, task) => {
return () => {
task(() => chain());
};
},
() => console.log("All tasks completed")
);
}
startTasks();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Promises
使用Promise
可以更好地管理异步任务,避免回调地狱,并使用.then()
方法按顺序执行
function asyncTask(i) {
return new Promise((resolve) => {
console.log(`Task ${i} started`);
setTimeout(() => {
console.log(`Task ${i} completed`);
resolve();
}, 1000);
});
}
function startTasks() {
asyncTask(1)
.then(() => asyncTask(2))
.then(() => asyncTask(3))
// ... 继续链式调用
.then(() => asyncTask(10))
.then(() => console.log("All tasks completed"));
}
startTasks();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async/await
async/await
是基于Promise
的语法糖,可以以同步的方式编写异步代码,非常适合顺序执行任务
async function asyncTask(i) {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(`Task ${i} completed`);
}
async function startTasks() {
for (let i = 1; i <= 10; i++) {
await asyncTask(i);
}
console.log("All tasks completed");
}
startTasks();
2
3
4
5
6
7
8
9
10
11
12
13
递归
使用递归函数来按顺序执行异步任务,每次递归调用完成后再开始下一个任务
function asyncTask(i, callback) {
setTimeout(() => {
console.log(`Task ${i} completed`);
callback(i + 1);
}, 1000);
}
function startTasks() {
function runTask(currentTask) {
if (currentTask > 10) {
console.log("All tasks completed");
return;
}
asyncTask(currentTask, runTask);
}
runTask(1);
}
startTasks();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
队列
使用队列来管理任务,每次从队列中取出一个任务执行,完成后再取出下一个
function asyncTask(i) {
return new Promise((resolve) =>
setTimeout(() => {
console.log(`Task ${i} completed`);
resolve();
}, 1000)
);
}
function startTasks() {
const taskQueue = Array.from({ length: 10 }, (_, i) => i + 1);
const executeTask = () => {
const nextTask = taskQueue.shift();
if (nextTask) {
asyncTask(nextTask).then(executeTask);
} else {
console.log("All tasks completed");
}
};
executeTask();
}
startTasks();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 介绍一下 JS 中 setTimeout 的运行机制?
JavaScript 中的 setTimeout
函数用于在指定的毫秒数后执行一次代码。它是 Web API 的一部分,通常用于实现延迟执行和异步操作。以下是 setTimeout
的运行机制的关键点:
异步执行:
setTimeout
将回调函数排队到任务队列中,并在指定的延迟后将其移至执行栈事件循环:
setTimeout
的行为受到 JavaScript 事件循环的影响。在事件循环中,一旦执行栈为空,事件循环机制会检查任务队列,并根据延迟时间将任务排队到执行栈最小延迟:
setTimeout
的最小延迟时间是 4 毫秒。即使设置的延迟时间小于 4 毫秒,实际的延迟也会至少为 4 毫秒延迟时间:
setTimeout
的第一个参数是延迟时间(以毫秒为单位)。如果延迟时间小于 4 毫秒,实际的执行时间将被调整为 4 毫秒回调函数:
setTimeout
的第二个参数是回调函数,这个函数将在延迟时间后执行。可以向回调函数传递额外的参数定时器标识符:
setTimeout
返回一个定时器标识符,这个值可以用于clearTimeout
函数取消定时器宏任务:
setTimeout
回调函数被实现为一个宏任务。在事件循环的每个迭代中,即使有多个宏任务,也只会执行一个宏任务执行顺序: 如果多次调用
setTimeout
,它们将根据创建顺序排队执行,但实际执行顺序可能受到延迟时间和事件循环的影响执行环境:
setTimeout
的回调函数将在全局执行环境中执行,即在浏览器中是window
对象,在 Node.js 中是global
对象错误处理:
setTimeout
回调中的任何错误都不会影响其他setTimeout
回调的执行,但应该通过try...catch
语句来捕获和处理错误
示例代码:
console.log("Start");
setTimeout(function () {
console.log("Timeout 1 after 1000ms");
}, 1000);
setTimeout(function () {
console.log("Timeout 2 after 0ms");
}, 0);
console.log("End");
2
3
4
5
6
7
8
9
10
11
在这个示例中,尽管 Timeout 2
的延迟时间为 0 毫秒,但由于 setTimeout
的行为和事件循环,Timeout 1
可能在 Timeout 2
之前执行
# 怎么用 JS 实现大型文件上传?要考虑哪些问题?
实现大型文件上传时,需要考虑多个方面,包括性能、用户体验、安全性和可靠性。以下是一些关键点和实现方法:
考虑的问题:
- 分片上传: 大文件应该被分割成多个小块(分片)进行上传,这有助于提高上传的稳定性和效率
- 并发上传: 利用多线程或并发上传分片可以提高上传速度
- 断点续传: 如果上传中断,应能从中断的地方继续上传,而不是重新开始
- 上传进度: 提供实时的上传进度反馈给用户
- 错误处理: 实现错误检测和重试机制,如某个分片上传失败时,只重试该分片
- 文件校验: 上传前后进行文件完整性校验,如使用 MD5 或 SHA 算法
- 安全性: 确保上传过程中的数据安全,使用 HTTPS 等加密传输
- 服务端处理能力: 服务端需要能够处理大量并发的上传请求
- 资源消耗: 大文件上传可能会消耗大量服务器资源,需要合理分配和优化
- 用户体验: 提供友好的用户界面和交互,如暂停、取消上传等
- 兼容性: 考虑不同浏览器和设备的支持情况
实现方法:
- 客户端分片: 在前端将文件分割成多个分片,可以使用
Blob.slice()
方法 - 并发控制: 使用
Promise.all()
或Promise.allSettled()
来并发上传多个分片 - 进度条: 使用 JavaScript 更新进度条,可以通过监听每个分片上传的进度事件来计算总进度
- 服务端接口: 设计 RESTful API 来处理分片上传,包括初始化上传、上传分片、完成上传等
- 服务端合并: 上传完成后,服务端需要将所有分片合并成原始文件
- 异常处理: 实现异常捕获和处理逻辑,确保上传的鲁棒性
- 安全性措施: 使用身份验证和授权机制,确保只有授权用户可以上传文件
- 服务端配置: 配置服务端以支持大文件上传,如调整文件大小限制
- 前端库/框架: 使用成熟的库或框架(如 Resumable.js、Dropzone.js)来简化实现
示例代码:
function uploadFileInChunks(file) {
const chunkSize = 2 * 1024 * 1024; // 2MB
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
function uploadNextChunk() {
if (currentChunk >= chunks) return;
const chunk = file.slice(
currentChunk * chunkSize,
(currentChunk + 1) * chunkSize
);
const formData = new FormData();
formData.append("file", chunk);
formData.append("chunkIndex", currentChunk);
formData.append("totalChunks", chunks);
fetch("/upload", {
method: "POST",
body: formData,
})
.then((response) => {
if (response.ok) {
currentChunk++;
uploadNextChunk();
} else {
// 处理错误
}
})
.catch((error) => {
// 处理网络或其他错误
});
}
uploadNextChunk();
}
// 使用
const fileInput = document.querySelector("#fileInput");
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) {
uploadFileInChunks(file);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
实现大型文件上传是一个复杂的过程,需要综合考虑前端和后端的多个方面。通过使用现代 Web API 和库,可以有效地简化开发过程并提高上传效率
# 什么是箭头函数?能使用 new 来创建箭头函数么?
箭头函数(Arrow Function)是 ECMAScript 6(ES6)引入的一种新的函数定义方式,提供了一种更简洁的语法来编写函数。箭头函数的写法比传统的函数表达式更简洁,并且它们没有自己的 this
、arguments
、super
或 new.target
箭头函数的基本语法:
// 没有参数或多个参数的情况
const func = () => {
// 函数体
};
// 单个参数的情况,可以省略括号
const func = (param) => {
// 函数体
};
// 单个表达式的情况,可以省略花括号和返回语句
const func = (param) => param * 2;
2
3
4
5
6
7
8
9
10
11
12
箭头函数的特点:
- 没有自己的
this
:箭头函数不绑定this
,它会捕获其所在上下文的this
值,作为自己的this
- 没有
arguments
对象:不能使用arguments
对象,但可以使用剩余参数(...args
) - 不能作为构造函数:不能使用
new
关键字来实例化箭头函数,因为它们没有constructor
属性 - 不能使用
yield
:箭头函数中不能使用yield
表达式,因此它们不能用作生成器
使用 new
来创建箭头函数的问题:
箭头函数不能使用 new
关键字来调用,因为它们不包含 [[Construct]]
内部方法,这是 ES6 规范所禁止的。尝试使用 new
来调用箭头函数会抛出错误
const arrowFunc = () => {};
// 下面的代码会抛出错误:箭头函数不能用作构造函数
const instance = new arrowFunc();
2
3
4
箭头函数与普通函数的区别示例:
const traditionalFunc = function () {
console.log(this);
};
const arrowFunc = () => {
console.log(this);
};
// 在对象的方法中调用
const obj = {
traditionalMethod: traditionalFunc,
arrowMethod: arrowFunc,
};
obj.traditionalMethod(); // 输出 obj,传统函数的 this 绑定到调用它的对象
obj.arrowMethod(); // 输出 undefined,箭头函数捕获其所在上下文的 this 值
// 使用 call 或 apply 调用
traditionalFunc.call(obj); // 输出 obj,改变了传统函数的 this 绑定
arrowFunc.call(obj); // 输出 global(或 undefined,如果是严格模式),箭头函数的 this 不变
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
箭头函数的简洁性和 this
值的捕获特性,使得它们在很多场景下成为首选的函数定义方式,尤其是在处理数组的回调函数或需要保持特定 this
值的函数时。然而,它们并不适合所有场景,特别是需要构造函数或需要使用 arguments
对象的情况
# 什么是前端路由?什么时候适合使用前端路由?它有哪些优点和缺点?
前端路由是指在前端应用中,通过 JavaScript 管理页面路由和导航的方式。它允许开发者在不重新加载整个页面的情况下,根据 URL 的变化来显示不同的内容。前端路由在单页应用(Single Page Application, SPA)中非常常见
何时适合使用前端路由:
- 单页应用:当开发一个单页应用时,前端路由是必不可少的,因为它允许应用在不刷新页面的情况下进行页面间的导航
- 用户交互:需要丰富的用户交互和动态内容更新的应用,例如在线商店、仪表板或任何需要用户登录并浏览不同视图的应用
- SEO 需求低:如果应用不需要优化搜索引擎排名,或者可以通过预渲染或服务器端渲染来解决 SEO 问题
前端路由的优点:
- 用户体验:提供无刷新的页面导航,使用户体验更加流畅
- 减少服务器负载:前端路由不需要从服务器加载整个页面,减少了服务器的负担
- 易于维护:路由逻辑集中管理,易于维护和扩展
- 利于开发:前端路由允许开发者专注于构建用户界面和交互逻辑,而不需要处理后端路由的复杂性
- SPA 架构:适合构建交互性强、动态内容多的单页应用
前端路由的缺点:
- SEO 问题:由于内容是动态加载的,搜索引擎爬虫可能无法正确索引内容
- 初次加载时间:对于大型应用,初始加载可能包含较多的 JavaScript,这可能影响首次加载性能
- 复杂性:对于需要深度链接或复杂路由逻辑的应用,前端路由可能会变得复杂难以管理
- 浏览器历史:需要处理浏览器的历史记录(如前进和后退按钮),这可能增加实现的复杂性
- 资源限制:在某些旧的浏览器或设备上,JavaScript 性能可能受限,影响前端路由的性能
# 为什么 JS 要被设计为单线程?
JavaScript 最初被设计为单线程的语言,主要是基于以下几个原因:
- 简单性: 单线程模型简化了语言的设计和实现。开发者不需要处理多线程编程中的并发和同步问题,如死锁、竞态条件等
- 浏览器环境: JavaScript 最初是为浏览器设计的,主要用途是操作 DOM 和响应用户事件。在这种场景下,单线程足以处理用户界面的交互,并且可以保证 DOM 操作的安全性和一致性
- 性能: 在早期的 Web 开发中,JavaScript 引擎的性能并不是主要瓶颈。通过单线程,可以减少多线程带来的上下文切换和同步开销,从而提高性能
- 事件循环: JavaScript 使用事件循环(Event Loop)机制来处理异步操作,这使得 JavaScript 能够在单线程中高效地处理大量异步事件,如网络请求、定时器等
- 资源共享: 在浏览器中,JavaScript 通常与其他语言(如 HTML、CSS)共享资源(如 DOM)。单线程模型简化了资源管理和访问控制
- 历史原因: JavaScript 的设计受到了当时其他语言和环境的影响,例如 Java 也有类似的单线程事件模型
然而,随着 Web 应用的复杂性增加,单线程模型在处理大量计算密集型任务时可能会遇到性能瓶颈。为了解决这个问题,现代 JavaScript 引擎引入了 Web Workers,允许 JavaScript 在后台线程中运行脚本,从而实现并行计算。但是,主线程(UI 线程)仍然是单线程的,以保持 DOM 操作的安全性和简单性
# JS 代码中的 use strict 是什么?有什么作用?
"use strict";
是 JavaScript 中的一个指令,用于启用严格模式(Strict Mode)。在严格模式下,JavaScript 的运行环境会更加严格,这有助于使用更安全、更可预测的代码。
严格模式的作用:
- 消除不严谨的语法: 严格模式下,某些不严谨或不安全的语法将不再被允许,例如使用未声明的变量将导致错误
- 函数和 eval 的作用域限制: 在严格模式中,函数和
eval
的代码不能修改其外部作用域的变量 - 禁止使用
with
语句:with
语句因其作用域的复杂性而被禁止,因为它可能导致代码难以阅读和维护 - 增强的异常抛出: 某些在非严格模式下不会产生错误的操作,在严格模式下会抛出异常
- 禁止八进制数字字面量: 在 ES6 中,八进制数字字面量(使用前缀
0o
或0
)在严格模式下是不允许的 - 限制
arguments
对象的使用: 严格模式下,arguments
对象不能被重新赋值,也不能用于覆盖函数的参数 - 禁止删除不可删除的属性: 尝试使用
delete
操作符删除一个不可删除的属性(如对象的自有属性)将抛出错误 - 限制赋值给只读属性: 尝试给一个只读属性赋值将抛出错误
- 提高变量声明的严格性: 在函数内部,如果未使用
var
、let
或const
声明变量,将抛出错误 - 显式异常: 某些在非严格模式下静默失败的操作,在严格模式下会抛出异常
如何使用 "use strict";
:
全局严格模式: 在脚本的最开始添加
"use strict";
,这将整个脚本置于严格模式"use strict"; console.log(a); // ReferenceError: a is not defined
1
2函数级别严格模式: 将
"use strict";
放在函数内部,这将仅该函数置于严格模式复制function foo() { "use strict"; console.log(a); // ReferenceError: a is not defined }
1
2
3
4
# 如何使用 JS 判断某个字符串长度(要求支持 Emoji 表情)?
在 JavaScript 中,通常情况下可以使用字符串的 length
属性来获取字符串的长度。然而,这个属性并不适用于包含 Emoji 表情或某些特殊字符的字符串,因为这些字符可能占用超过一个代码点
为了准确计算包含 Emoji 表情的字符串长度,可以使用 Array.from()
方法将字符串转换成一个字符数组,然后使用 length
属性来获取数组的长度。这种方法可以正确处理多代码点字符(如 Emoji)
以下是如何实现的示例代码:
function getStringLength(str) {
// 将字符串转换为字符数组并获取其长度
return Array.from(str).length;
}
// 示例
const emojiStr = "Hello 👋🌍";
console.log(`字符串长度(包括 Emoji): ${getStringLength(emojiStr)}`); // 输出 9
console.log(`字符串原始长度: ${emojiStr.length}`); // 输出 7,不准确
2
3
4
5
6
7
8
9
在这个示例中,getStringLength
函数将字符串转换为数组,并返回数组的长度,这可以正确处理 Emoji 和其他多代码点字符
另外,如果你正在处理一个需要高度准确性的国际化应用程序,你还可以考虑使用第三方库,如 unicode-length
,来更准确地获取字符串长度
使用第三方库的方法:
// 假设已经安装了 unicode-length 库
const unicodeLength = require("unicode-length");
const str = "Hello 👋🌍";
console.log(`准确的字符串长度: ${unicodeLength(str)}`); // 输出 9
2
3
4
5
请注意,使用 Array.from()
方法在大多数情况下已经足够,但如果你遇到一些特殊字符或组合,使用专门的库可能更可靠
# JS 在什么情况下会存在数字精度丢失的问题,如何解决?
JavaScript 中的数字精度丢失问题主要与 JavaScript 采用 IEEE 754 标准的双精度浮点数格式(64 位格式)表示所有数字有关。以下是一些常见的导致精度丢失的情况以及解决方法:
导致精度丢失的情况:
- 大数运算: 进行大数相加或相减时,可能会导致精度丢失
- 小数运算: 在进行小数运算时,尤其是涉及多位小数的计算,可能会丢失精度
- 浮点数表示限制: 某些小数在二进制浮点数中无法精确表示,如
0.1
- 运算顺序: 不同运算顺序可能导致不同的精度丢失
- 累计误差: 在循环或递归中累积的小误差可能会放大
- 科学记数法: 当数字非常大或非常小时,可能会以科学记数法表示,导致精度丢失
解决方法:
- 使用整数: 如果可能,尽量使用整数进行运算
- 使用第三方库: 使用如
decimal.js
或big.js
等第三方库来处理高精度的数字运算 - 避免大数运算: 尽量避免直接操作大数,可以将数字分解成更小的部分进行运算
- 四舍五入: 在显示或使用前对数字进行四舍五入处理
- 使用
toFixed()
: 对于需要显示或传输的数字,使用toFixed()
方法可以限制小数点后的位数 - 避免累积误差: 在循环或递归中,注意累积误差的影响,适时进行误差校正
- 使用
Math
对象: 对于一些特定的运算,可以使用Math
对象中的方法,如Math.round()
、Math.floor()
或Math.ceil()
- 字符串操作: 对于复杂的数字运算,可以将数字转换为字符串,然后使用字符串操作进行计算,最后再转换回数字
- 避免使用科学记数法: 当处理的数字可能非常大或非常小时,避免使用可能导致精度丢失的科学记数法
- 使用 BigInt: 对于非常大的整数,可以使用 ES2020 引入的
BigInt
类型,它允许安全地表示和操作大整数
const bigNumber = BigInt("1234567890123456789012345678901234567890n");
数字精度丢失是浮点数表示的一个固有问题,但通过上述方法,可以在大多数情况下避免或减少精度丢失带来的影响
# 说说你对 JS 模块化方案的理解,比如 CommonJS、AMD、CMD、ES Module 分别是什么?
JavaScript 模块化是一种将代码划分为独立、可重用模块的方法,有助于代码组织、维护和重用。以下是一些主流的 JavaScript 模块化方案:
CommonJS
CommonJS 是一个模块化规范,主要用于 Node.js 环境。它定义了 require
方法来加载模块,以及 module.exports
对象来导出模块的接口
- 同步加载:CommonJS 模块是同步加载的,这意味着在模块加载完成之前,代码执行会被阻塞
- 文件作用域:每个模块都有自己的作用域,不会影响到其他模块或全局作用域
- 导出和导入:使用
require()
函数来导入其他模块,使用module.exports
来导出模块的公共接口
AMD (Asynchronous Module Definition)
AMD 是一种异步模块加载规范,主要用于浏览器环境。它允许模块异步加载,有助于提高页面加载性能
- 异步加载:使用
define
函数来定义模块,可以指定依赖模块,这些依赖模块会异步加载 - 依赖执行:AMD 模块在所有依赖模块加载完成后才会执行,确保了依赖关系的正确性
- 动态导入:可以使用
require
函数动态地导入模块
CMD (Common Module Definition)
CMD 是另一种在浏览器端使用的模块定义方式,与 AMD 类似,但有一些关键区别
- 依赖就近:CMD 推崇“依赖就近”的原则,即在定义模块时才声明依赖,而不是在加载
- 运行时分析:CMD 规范的模块是运行时分析的,可以在模块定义时确定依赖关系
- exports 对象:使用
exports
对象来导出模块的公共接口,而不是module.exports
ES Module
ES Module 是 ECMAScript 标准中提出的模块化方案,是目前现代浏览器和 JavaScript 环境推荐的模块化标准。
- 静态加载:ES Module 支持静态分析,可以在编译时确定模块的依赖关系。
- 原生支持:现代浏览器和 JavaScript 引擎原生支持 ES Module,无需额外工具。
import
和export
:使用import
语句来导入模块,使用export
语句来导出模块的公共接口- 默认和命名导出:支持默认导出(
export default
)和命名导出(export { name }
)
总结
不同的模块化方案适用于不同的环境和场景。CommonJS 主要用于 Node.js,AMD 和 CMD 主要用于早期的浏览器环境,而 ES Module 作为现代标准,适用于现代浏览器和 JavaScript 环境。随着前端工具链的发展,如 Webpack、Rollup 等,ES Module 逐渐成为主流的模块化方案。这些工具可以帮助我们将 ES Module 转换为兼容 CommonJS 或 AMD 的格式,以适应不同的运行环境
# 如果使用 Math.random() 来计算中奖概率,会有什么问题吗?
使用 Math.random()
来计算中奖概率在大多数情况下是可行的,但需要注意以下几个潜在问题:
- 随机性:
Math.random()
生成一个 [0, 1) 区间的伪随机浮点数。对于简单的随机事件,如中奖,它提供了足够的随机性。但如果你的应用需要高度随机性或安全性,Math.random()
可能不够强,因为它可以被预测 - 种子问题:
Math.random()
基于一个种子值生成随机数,这个种子通常是基于时间的。如果在极短的时间内多次调用Math.random()
,可能会得到相同的结果 - 性能问题: 在高频调用的情况下,使用
Math.random()
可能会有一定的性能影响。虽然这种影响通常很小,但在极端情况下可能需要考虑 - 分布均匀性:
Math.random()
生成的随机数在理论上是均匀分布的,但在实际应用中,特别是在小数点后几位,可能会有轻微的偏差 - 初始化问题: 在某些旧的浏览器或环境中,如果
Math
对象没有被正确初始化,Math.random()
可能不会按预期工作 - 精度问题: JavaScript 的数字是双精度 64 位格式的 IEEE 754 浮点数,这可能导致在某些情况下出现精度问题,尤其是在生成大量随机数时
- 并发问题: 在多线程环境中(虽然在浏览器中的 JavaScript 通常是单线程的),需要确保随机数生成器的线程安全
- 依赖于实现:
Math.random()
的实现可能依赖于 JavaScript 引擎,不同的引擎可能有不同的算法和性能特性 - 小概率事件: 如果你需要处理非常小的概率(如千万分之一),直接使用
Math.random()
可能不够精确,因为生成的随机数是浮点数,可能无法精确表示这种小概率
总的来说,Math.random()
适用于大多数简单的随机事件需求,但如果你需要更高质量的随机性、安全性或特定分布的随机数,可能需要考虑其他方法,如使用密码学安全的随机数生成器或特定的统计库
# 怎么使用 JS 实现元素拖拽功能?
实现元素拖拽功能通常涉及到监听鼠标事件,并在这些事件中执行相应的逻辑。以下是使用原生 JavaScript 实现元素拖拽的基本步骤:
- 设置可拖拽元素:选择需要拖拽的元素,并为其添加
draggable
属性(如果元素是 HTML5 允许的可拖拽类型) - 监听
mousedown
事件:当鼠标按下时,记录初始位置和鼠标的当前位置 - 监听
mousemove
事件:在鼠标移动时,更新元素的位置 - 监听
mouseup
事件:当鼠标释放时,结束拖拽行为 - 可选:监听
dragstart
和dragend
事件,以便在拖拽开始和结束时执行特定操作
下面是一个简单的示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Draggable Element</title>
<style>
#draggable {
width: 100px;
height: 100px;
background-color: red;
color: white;
line-height: 100px;
text-align: center;
user-select: none; /* 禁止用户选择文本 */
position: absolute; /* 绝对定位以便于拖动 */
}
</style>
</head>
<body>
<div id="draggable">拖我</div>
<script>
// 获取可拖拽元素
const draggableElement = document.getElementById("draggable");
let currentX = 0;
let currentY = 0;
let initialX = 0;
let initialY = 0;
draggableElement.addEventListener("mousedown", (e) => {
// 记录鼠标按下时的位置
initialX = e.clientX - currentX;
initialY = e.clientY - currentY;
// 监听鼠标移动和释放事件
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
function onMouseMove(e) {
// 更新元素的位置
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
draggableElement.style.left = `${currentX}px`;
draggableElement.style.top = `${currentY}px`;
}
function onMouseUp() {
// 移除鼠标移动和释放事件的监听器
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
注意事项:
- 确保在
mousemove
和mouseup
事件处理函数中移除事件监听器,以防止内存泄漏或意外行为 - 使用
e.preventDefault()
可以阻止默认行为,例如在mousedown
事件中阻止文本选择 - 根据需要调整元素的
position
属性(如relative
、absolute
或fixed
)以控制拖拽行为 - 考虑在拖拽期间禁止滚动,可以使用
body
的overflow
样式属性实现
# JS 会出现内存泄漏问题么?在哪些情况下可能会出现内存泄漏?
JavaScript(特别是在使用旧版本的浏览器时)也会出现内存泄漏问题。内存泄漏是指在程序中,由于疏忽或错误导致的一种内存分配问题,内存被分配给了不再需要的变量或对象,但无法被垃圾回收器回收。以下是一些可能导致 JavaScript 内存泄漏的常见情况:
- 全局变量的不当使用:
- 意外创建的全局变量可能会持续持有引用,导致内存无法释放
- 闭包造成的内存泄漏:
- 闭包可能会捕获并长期持有外部作用域中的变量,即使这些变量不再需要
- 循环引用:
- 在循环或递归函数中,如果没有适当的终止条件或释放机制,可能会持续消耗内存
- 事件监听器未移除:
- 为元素添加的事件监听器如果没有在适当的时候使用
removeEventListener
移除,会持续占用内存
- 为元素添加的事件监听器如果没有在适当的时候使用
- 定时器的滥用:
- 设置了大量定时器(如
setInterval
、setTimeout
)而没有清除,可能会造成内存泄漏
- 设置了大量定时器(如
- 脱离 DOM 的引用:
- 即使 DOM 元素已经被移除,如果 JavaScript 代码中仍然持有对这些元素的引用,它们将无法被垃圾回收
- Web 字体的长时间加载:
- 加载的 Web 字体如果长时间未能加载完成,可能会持续占用内存
- 未释放的回调函数:
- 某些 API 如
XMLHttpRequest
、setTimeout
、setInterval
的回调函数如果没有被正确处理,可能会造成内存泄漏
- 某些 API 如
- 遗留的定时任务:
- 在组件卸载或页面关闭后,未清除的定时任务仍然会占用内存
- 第三方库的使用:
- 使用第三方库时,如果没有正确管理库中的资源和事件监听器,可能会导致内存泄漏
- 单页应用(SPA)中的组件:
- 在单页应用中,如果组件没有正确卸载或清除状态,可能会导致内存泄漏
- 缓存的使用:
- 使用
localStorage
或sessionStorage
等缓存时,如果没有适当的数据管理策略,可能会造成内存泄漏
- 使用
为了防止内存泄漏,开发者应该采取以下措施:
- 确保不再使用的全局变量被删除或设为
null
- 谨慎使用闭包,避免无意中捕获不必要的变量
- 为所有添加的事件监听器注册相应的移除逻辑
- 清除不再需要的定时器
- 组件卸载时,确保清理所有相关资源和事件监听器
- 使用浏览器的开发者工具进行内存泄漏检测和分析
# 什么是 Javascript 的事件流?有哪些事件流模型?
JavaScript 的事件流是指事件从触发到最终处理的整个过程。在 Web 应用中,当用户与页面上的元素交互时(如点击、滚动等),浏览器会产生事件,并按照特定的流程将这些事件传递给相应的处理程序
事件流模型:
- 冒泡(Bubbling):
- 事件首先在最具体的元素(事件的目标元素)上发生,然后逐级向上传播到较为不具体的节点(直到文档的根元素)
- 可以通过
event.stopPropagation()
来阻止事件继续向上冒泡
- 捕获(Capturing):
- 事件从文档的根元素开始,向下传播到目标元素
- 捕获阶段的事件可以被沿途的节点捕获,但默认情况下,浏览器的事件处理机制不会在捕获阶段触发事件监听器
- 事件委托(Event Delegation):
- 利用事件冒泡的原理,在父元素上设置监听器来处理子元素的事件,从而实现对多个子元素的统一事件处理
- 事件委托可以减少事件监听器的数量,提高性能,并且可以在运行时动态添加的元素上触发
- 直接事件模型(Direct Event Model):
- 直接在目标元素上附加事件监听器,事件触发时直接执行,不涉及冒泡或捕获
- 微任务与宏任务(Microtasks and Macrotasks):
- JavaScript 运行时中的事件循环机制将事件分为微任务(如
Promise
回调)和宏任务(如 setTimeout、setInterval、I/O 操作等) - 微任务优先于宏任务执行,宏任务在一次事件循环中可能包含多个微任务的执行
- JavaScript 运行时中的事件循环机制将事件分为微任务(如
- 异步事件模型(Asynchronous Event Model):
- 某些事件处理器(如
setTimeout
、requestAnimationFrame
)会异步执行,不会阻塞浏览器的其他操作
- 某些事件处理器(如
- 合成事件(Synthetic Events):
- 在某些前端框架(如 React)中,事件被封装成合成事件,以提供跨浏览器的一致性和额外的功能
- 事件池(Event Pooling):
- 某些框架(如 jQuery)为了性能优化,会重用事件对象,避免频繁创建和销毁事件对象
事件处理流程:
- 事件触发:用户与页面交互,浏览器生成事件对象
- 事件捕获:事件从根元素向下传播到目标元素(可选)
- 目标处理:事件到达目标元素,触发事件处理程序
- 事件冒泡:事件从目标元素向上传播到根元素
- 默认行为:如果没有阻止默认行为,浏览器执行事件的默认操作(如表单提交)
# ES6 有哪些新特性?
- 箭头函数:提供了一种更简洁的函数书写方式,并且不绑定自己的
this
值
const add = (a, b) => a + b;
- 类(Class):虽然 JavaScript 是基于原型的,但 ES6 引入了基于类的语法,使得面向对象编程更加直观
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, my name is ${this.name}`;
}
}
2
3
4
5
6
7
8
- 模块:ES6 提供了模块的原生支持,允许开发者定义模块并导出(export)和导入(import)模块
// export
export const name = "Kimi";
export default function () {}
// import
import { name } from "./module";
import defaultFunction from "./module";
2
3
4
5
6
7
- 模板字符串:允许使用反引号(``)来创建字符串,并支持字符串插值
const name = "Kimi";
console.log(`Hello, my name is ${name}.`);
2
- 解构赋值:允许从数组或对象中快速提取值并赋值给变量
const [a, b] = [1, 2];
const { name, age } = { name: "Kimi", age: 1 };
2
- 默认参数值:允许在函数参数中设置默认值
function greet(name = "Guest") {
console.log(`Hello, ${name}!`);
}
2
3
- 展开运算符:允许将数组或对象的元素展开到新数组或新对象中
const numbers = [1, 2, ...[3, 4, 5]];
const person = { ...{ name: "Kimi" }, age: 1 };
2
- Promises:提供了一种更好的异步编程方式,用于处理异步操作
function getJSON(url) {
return fetch(url).then((response) => response.json());
}
2
3
- 新的数据类型:Symbol:Symbol 是一种新的原始数据类型,用于创建唯一的不可变的代号
const mySymbol = Symbol("mySymbol");
- for...of 循环:提供了一种遍历数组、字符串和其他可迭代对象的新方式
for (let char of "Kimi") {
console.log(char);
}
2
3
Proxy 和 Reflect:Proxy 允许你定义对象的自定义行为(如属性查找、赋值、枚举等),而 Reflect 提供了一种方法来使用 Proxy 对象
Map 和 Set:Map 是一种新的集合类型,允许你存储键值对,而 Set 存储唯一的值
WeakMap 和 WeakSet:这些是 Map 和 Set 的弱引用版本,它们不阻止它们包含的对象被垃圾回收
迭代器(Iterator)和生成器(Generator):迭代器允许你遍历容器对象,而生成器是一种特殊的函数,可以暂停和恢复
Unicode 字符扩展:ES6 增加了对 Unicode 的支持,允许使用更多的字符
# 什么是 ES6 中的 Promise?它的使用场景有哪些?
在 ES6 中,Promise
是一种用于异步编程的对象。它代表了异步操作的最终完成或失败,其主要目的是提供一种更合理、更强大的异步编程模型
什么是 Promise?
一个Promise
对象代表了一个可能还不可用的值,或者一个在未来某个时间点才可用的最终值。Promise
有三种状态:
- Pending(等待态):初始状态,既不是成功,也不是失败状态
- Fulfilled(已成功):意味着操作成功完成
- Rejected(已失败):意味着操作失败
Promise 的构造函数
创建一个新的Promise
对象需要提供一个执行器函数(executor function),这个函数将在Promise
创建后立即执行。执行器函数接受两个参数,通常命名为resolve
和reject
:
resolve
:当异步操作成功时调用,返回操作的结果reject
:当异步操作失败时调用,返回操作的错误
const myPromise = new Promise((resolve, reject) => {
// 异步操作
if (/* 异步操作成功 */) {
resolve(value); // 操作成功,返回结果
} else {
reject(error); // 操作失败,返回错误
}
});
2
3
4
5
6
7
8
使用场景
Promise
的使用场景非常广泛,以下是一些常见的例子:
- Ajax 请求:在发送 HTTP 请求时,可以使用
Promise
来处理请求的响应或错误 - 定时器:使用
setTimeout
或setInterval
时,可以用Promise
来延迟执行某些操作 - 文件操作:在读取或写入文件时,可以用
Promise
来处理文件的异步读写操作 - 数据库操作:在执行数据库查询或事务时,可以用
Promise
来处理查询结果或事务的完成 - Web Workers:在 Web Workers 中进行异步计算时,可以用
Promise
来处理计算结果 - 事件监听:在某些情况下,可以使用
Promise
来封装事件监听器,以便在某个事件触发时执行异步操作
Promise 的方法
.then()
:添加在 Promise 成功时需要执行的回调函数.catch()
:添加在 Promise 失败时需要执行的回调函数.finally()
:无论 Promise 成功还是失败,都会执行的回调函数
示例
function getJSON(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((data) => resolve(data))
.catch((error) => reject(error));
});
}
getJSON("https://api.example.com/data")
.then((data) => console.log(data))
.catch((error) =>
console.error("There was a problem with your fetch operation:", error)
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ES6 中的 Reflect 对象有什么用?
在 ES6 中,Reflect
对象是一个内置的对象,提供了拦截 JavaScript 操作的方法。它将某些操作封装成函数,使得这些操作可以被显式调用。Reflect
对象的方法与全局函数相对应,但有一些区别和优势:
- 显式调用:
Reflect
方法提供了一种显式调用这些操作的方式,而不是依赖于全局函数 - 一致性:
Reflect
方法提供了一致的接口,使得代码更易于理解和维护 - 返回值:
Reflect
方法总是返回一个值,而不是像某些全局函数那样在失败时抛出异常 - 错误处理:
Reflect
方法在操作失败时会返回false
,而不是抛出异常,这使得错误处理更加灵活
常用方法
以下是一些常用的Reflect
方法及其用途:
- Reflect.get(target, propertyKey[, receiver]):
- 用于获取对象的属性值
- 与
target[propertyKey]
类似,但不会抛出异常。
- Reflect.set(target, propertyKey, V[, receiver]):
- 用于设置对象的属性值
- 与
target[propertyKey] = V
类似,但不会抛出异常
- Reflect.has(target, propertyKey):
- 用于检查对象是否具有某个属性
- 与
propertyKey in target
类似,但不会抛出异常
- Reflect.deleteProperty(target, propertyKey):
- 用于删除对象的属性
- 与
delete target[propertyKey]
类似,但不会抛出异常
- Reflect.apply(target, thisArgument, argumentsList):
- 用于调用函数,并传递参数
- 与
Function.prototype.apply
类似
- Reflect.construct(target, argumentsList[, newTarget]):
- 用于构造一个新对象,并调用构造函数
- 与
new target(...argumentsList)
类似
- Reflect.defineProperty(target, propertyKey, attributes):
- 用于定义或修改对象的属性
- 与
Object.defineProperty
类似
- Reflect.getOwnPropertyDescriptor(target, propertyKey):
- 用于获取对象属性的描述
- 与
Object.getOwnPropertyDescriptor
类似
- Reflect.isExtensible(target):
- 用于检查对象是否可扩展
- 与
Object.isExtensible
类似
- Reflect.preventExtensions(target):
- 用于阻止对象的扩展
- 与
Object.preventExtensions
类似
使用示例
const obj = { x: 1, y: 2 };
// 获取属性值
const value = Reflect.get(obj, "x"); // 1
// 设置属性值
Reflect.set(obj, "z", 3); // true
console.log(obj); // { x: 1, y: 2, z: 3 }
// 检查属性是否存在
const hasY = Reflect.has(obj, "y"); // true
// 删除属性
const deleted = Reflect.deleteProperty(obj, "x"); // true
console.log(obj); // { y: 2, z: 3 }
// 定义属性
Reflect.defineProperty(obj, "w", { value: 4, writable: false });
console.log(obj.w); // 4
obj.w = 5; // 无效,因为属性不可写
// 获取属性描述
const descriptor = Reflect.getOwnPropertyDescriptor(obj, "w");
console.log(descriptor); // { value: 4, writable: false, enumerable: false, configurable: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 什么是浏览器的同源策略?为什么要有同源策略?
浏览器的同源策略(Same-Origin Policy)是一种安全措施,用来限制一个源(origin)的文档或脚本如何与另一个源的资源进行交互。源由协议(protocol)、域名(domain)和端口(port)组成。如果两个 URL 的协议、域名和端口都相同,则它们具有相同的源
同源策略的限制
- 数据访问限制:不同源的网页无法通过 JavaScript 读取或修改对方网页的 DOM 和数据
- 网络请求限制:不同源的网页无法通过 XMLHttpRequest 等 API 发起网络请求,除非服务器支持 CORS(跨源资源共享)
为什么要有同源策略?
同源策略的存在主要是出于以下原因:
- 安全:防止恶意网站通过 JavaScript 读取或修改其他网站的敏感数据,如用户信息、会话令牌等
- 隐私:防止跨站点脚本(XSS)攻击,即攻击者在一个网站上注入恶意脚本,然后通过该脚本访问其他网站的数据
- 隔离:确保不同源的网页在浏览器中运行时相互独立,避免相互干扰
# 怎么解决跨域问题?
解决跨域问题的方法有很多,以下是一些常见的解决方案:
JSONP:
JSONP(JSON with Padding)利用
<script>
标签没有跨域限制的特性,通过动态创建<script>
标签并设置其src
属性来实现跨域请求。服务器端返回的数据会被包裹在一个回调函数中,从而被前端脚本调用示例代码:
var script = document.createElement("script"); script.type = "text/javascript"; script.src = "http://a.qq.com/index.php?callback=handleCallback"; document.head.appendChild(script); function handleCallback(res) { alert(JSON.stringify(res)); }
1
2
3
4
5
6
7
8CORS(跨源资源共享):
CORS 是一种现代的跨域解决方案,通过在服务器端设置适当的 HTTP 响应头(如
Access-Control-Allow-Origin
)来允许或限制跨域请求示例配置:
location / { add_header Access-Control-Allow-Origin *; }
1
2
3代理服务器:
通过在服务器端设置代理,将跨域请求转发到目标服务器,从而绕过浏览器的同源策略限制
示例配置:
server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; proxy_cookie_domain www.domain2.com www.domain1.com; add_header Access-Control-Allow-Origin http://www.domain1.com; add_header Access-Control-Allow-Credentials true; } }
1
2
3
4
5
6
7
8
9
10document.domain:
适用于主域相同但子域不同的跨域场景。通过在两个页面中设置
document.domain
为相同的主域,可以绕过同源策略限制示例代码:
<!-- A页面 http://a.qq.com/a.html --> <iframe id="iframe" src="http://b.qq.com/b.html"></iframe> <script> document.domain = "qq.com"; var windowB = document.getElementById("iframe").contentWindow; alert("B页面的user变量:" + windowB.user); </script>
1
2
3
4
5
6
7location.hash + iframe:
利用 URL 的 hash 值改变但不刷新页面的特性,通过 iframe 和同源的中间页面实现跨域通信
示例代码:
<!-- A页面 http://a.qq.com/a.html --> <iframe id="iframe" src="http://b.qq1.com/b.html" style="display:none;" ></iframe> <script> var iframe = document.getElementById("iframe"); setTimeout(function () { iframe.src = iframe.src + "#user=admin"; }, 1000); function onCallback(res) { alert("data from c.html ---> " + res); } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15window.name + iframe:
window.name
属性在不同页面加载后依旧存在,并且可以支持非常长的 name 值(2MB)。通过 iframe 和同源的中间页面实现跨域通信示例代码:
<!-- A页面 http://a.qq.com/a.html --> <iframe id="iframe" src="http://b.qq1.com/b.html"></iframe> <script> var state = 0; var iframe = document.getElementById("iframe"); iframe.onload = function () { if (state === 1) { alert(iframe.contentWindow.name); } else if (state === 0) { state = 1; } }; </script>
1
2
3
4
5
6
7
8
9
10
11
12
13postMessage:
postMessage
是 HTML5 中引入的 API,用于解决页面和其打开的新窗口的数据传递、多窗口之间消息传递、页面与嵌套 iframe 消息传递等问题示例代码:
<!-- A页面 http://a.qq.com/a.html --> <iframe id="iframe" src="http://b.qq1.com/b.html"></iframe> <script> var iframe = document.getElementById("iframe"); iframe.onload = function () { var data = { message: "这里是A页面发的消息" }; iframe.contentWindow.postMessage( JSON.stringify(data), "http://b.qq1.com" ); }; window.addEventListener( "message", function (e) { alert("B页面发来消息:" + JSON.parse(e.data)); }, false ); </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19WebSocket 协议:
WebSocket 协议实现了浏览器与服务器的全双工通信,同时允许跨域通信。使用 Socket.io 可以简化 WebSocket 的使用
示例代码:
<!-- 前端代码 --> <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script> <script> var socket = io("http://www.domain2.com:8080"); socket.on("connect", function () { socket.on("message", function (msg) { console.log("data from server: ---> " + msg); }); socket.on("disconnect", function () { console.log("Server socket has closed."); }); }); document.getElementsByTagName("input")[0].onblur = function () { socket.send(this.value); }; </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这些方法各有优缺点,选择合适的方案需要根据具体的应用场景和需求来决定。
# 浏览器的本地存储方式有哪些,有什么区别,分别有哪些应用场景?
浏览器提供了多种本地存储方式,用于在用户设备上保存数据。以下是一些常见的浏览器本地存储方式及其区别和应用场景:
- Cookie
- 区别:存储在浏览器中,由服务器设置,每次请求都会发送给服务器,大小限制约为 4KB
- 应用场景:会话管理、个人设置、追踪用户行为
- Web Storage(LocalStorage 和 SessionStorage)
- 区别:
LocalStorage
:没有时间限制的数据存储,直到主动删除SessionStorage
:在页面会话结束时清除,例如关闭浏览器标签或窗口
- 应用场景:
LocalStorage
:长期存储用户偏好设置、主题选择等SessionStorage
:存储页面会话过程中的数据,如表单数据
- 区别:
- IndexedDB
- 区别:低等级的 API,提供大量结构化数据的存储,比 Cookie 和 Web Storage 更大的存储容量(通常为数百 MB)
- 应用场景:存储复杂数据对象,如数据库记录、大文件
- Web SQL Database
- 区别:已被废弃,允许使用 SQL 语句操作数据
- 应用场景:曾用于需要使用 SQL 操作的复杂数据存储
- FileSystem API
- 区别:允许 Web 应用程序访问文件系统,进行文件读写操作
- 应用场景:文件上传、下载,以及在本地编辑和保存文件
- Cache API
- 区别:与 Service Workers 结合使用,用于缓存 Web 应用程序的资源,支持离线应用
- 应用场景:创建离线应用,缓存 Web 页面和资源以加速加载
- Web SQL Database(注意:已被废弃,不推荐使用)
- 区别:曾提供 SQLite 数据库的功能,允许执行 SQL 语句
- 应用场景:曾用于存储大量结构化数据,但因缺乏统一标准和安全性问题被废弃
- Cookies vs. Web Storage
- Cookie 数据会在每次请求时发送给服务器,适合小数据量的存储,如会话标识;Web Storage 则存储在浏览器,不发送给服务器,适合大量数据存储,如用户偏好设置
- LocalStorage vs. SessionStorage
LocalStorage
提供持久化存储,数据在浏览器关闭后依然存在;SessionStorage
提供临时存储,数据在页面会话结束时清除
应用场景示例:
- Cookie:用户登录状态保持,跨页面的用户会话跟踪
- LocalStorage:主题选择(如深色模式),用户偏好设置(如字体大小)
- SessionStorage:表单填写过程中的数据暂存,如用户在填写多步骤表单时的数据保存
- IndexedDB:在线游戏的本地数据存储,复杂应用的数据缓存
- Cache API:离线地图应用,离线阅读应用,需要在无网络环境下访问的内容
- FileSystem API:Web 应用程序中的文件上传和下载功能,本地文件编辑器
# 什么是回流和重绘?什么场景下会触发?怎么减少回流和重绘?
在 Web 前端开发中,回流(Reflow)和重绘(Repaint)是影响页面性能的两个重要概念
回流: 回流是指浏览器重新计算元素的几何属性(如位置、大小等),并重新排列这些元素的过程。当 DOM 的一部分结构或者样式发生变化时,浏览器需要重新计算相关元素的布局,这个过程就是回流。回流可能导致整个页面或者页面的一部分区域的重新渲染
重绘: 重绘是指浏览器在不影响元素几何属性的情况下,仅对元素进行外观的变更,如颜色、背景色、边框颜色等。重绘不会引发布局计算,因此通常比回流要便宜(性能上更优)
# 什么场景下会触发回流和重绘?
触发回流的场景:
- 页面初次加载时
- 元素的尺寸、位置或内容发生变化,如修改元素的宽度、高度、边框等
- 元素的属性如
display
、position
、left
、top
等发生变化 - 浏览器窗口大小改变或设备方向变化
- 元素的
class
或id
发生变化,导致应用的样式改变
触发重绘的场景:
- 元素的颜色、背景色、边框颜色等属性改变
- 元素的可见性发生变化,如
visibility
、opacity
、display
属性改变 - 元素的
box-shadow
、text-shadow
等属性改变
怎么减少回流和重绘?
- 减少 DOM 操作:批量修改 DOM,而不是单个修改,可以减少浏览器的计算次数
- 使用
documentFragment
:在内存中完成 DOM 操作,然后再一次性将结果添加到文档中 - 使用
requestAnimationFrame
:在浏览器的下一次重绘之前执行 DOM 操作,避免多次回流 - 避免使用内联样式:修改 CSS 类而不是直接操作元素的内联样式,因为内联样式会立即触发回流
- 使用
transform
和opacity
属性:这些属性的动画效果可以由合成器(compositor)层处理,不会引起回流和重绘 - 使用
will-change
属性:告知浏览器哪些属性可能会变化,让浏览器提前做好准备 - 避免大面积的 DOM 操作:如果需要修改大量 DOM 元素,考虑使用虚拟 DOM 技术,如 React
- 使用 CSS 变量:修改 CSS 变量的值可以避免直接修改样式,减少回流
- 使用
display: none
代替visibility: hidden
:display: none
不会触发回流,而visibility: hidden
只是隐藏元素,但元素仍占据布局空间 - 合理使用
float
和position
属性:这些属性可以减少布局的计算量
# 说说 Vue 中的 diff 算法
Diff 算法是一种比较两棵树(在 Vue 中即为真实 DOM 和虚拟 DOM)差异的算法,目的是为了找出它们之间的最小差异,然后将这些差异应用到真实 DOM 上,从而减少不必要的 DOM 操作,提高性能
Vue 中的 Diff 算法特点:
- 同层比较:Vue 的 Diff 算法只在相同层级的节点进行比较,不会跨层级操作
- 最小单位:Diff 算法的最小单位是元素而不是文本,即算法会将元素视为整体进行比较
- 列表比较:Vue 优化了对列表的 Diff 算法,使用了“key”属性来识别列表中的每个元素,以实现快速的列表更新
- 异步更新队列:Vue 维护了一个异步更新队列,当数据变化时,Vue 会将更新操作放入队列而不是立即执行,通过 nextTick 等待同一事件循环中的所有数据变化完成,然后执行一次 DOM 更新
Vue Diff 算法流程:
- 旧节点和新节点的比较:Vue 首先会比较新旧虚拟 DOM 树的根节点,如果它们不是相同类型的节点,那么会直接替换掉整个元素
- 遍历虚拟 DOM 树:Vue 递归地遍历新旧虚拟 DOM 树,比较每个节点和它的子节点
- 元素级别的比较:如果节点类型相同,Vue 会比较它们的属性、子节点等,根据比较结果决定是否更新属性或替换子节点
- 文本节点的比较:对于文本节点,Vue 会比较它们的文本内容,如果内容不同,则更新文本
- 子节点的比较:对于有子节点的元素,Vue 会根据子节点的 key 和标签类型进行比较,找出需要添加、删除或更新的子节点
- 更新真实 DOM:根据比较结果,Vue 执行最少的 DOM 操作来更新真实 DOM,以确保它与新的虚拟 DOM 树同步
# Vue 模板是如何编译的?经历了哪些过程?
Vue.js 使用一个基于 HTML 的 模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板最终都会编译成 JavaScript 代码,以便于 Vue 的运行时能够执行。以下是 Vue 模板编译的大致过程:
- 解析(Parsing):
- 模板首先被解析成抽象语法树(AST, Abstract Syntax Tree)。解析过程是将模板字符串转换成一个由节点组成的树结构,每个节点代表了模板中的一个元素、指令、表达式等
- 优化(Optimization):
- 在解析过程中,Vue 会对静态节点进行标记(例如,使用 v-node 的静态/纯静态属性)。静态节点是指在组件的整个生命周期内都不会改变的节点,这样在虚拟 DOM 重绘时可以被忽略,从而提高性能
- 代码生成(Code Generation):
- 根据 AST,编译器生成用于构建虚拟 DOM 的 JavaScript 代码。这个过程涉及到将模板中的动态内容转换为 JavaScript 表达式,并生成用于创建虚拟 DOM 节点的代码
- 创建虚拟 DOM 树(Virtual DOM Creation):
- 通过生成的 JavaScript 代码,Vue 创建了一个虚拟 DOM 树。这个虚拟 DOM 树是对真实 DOM 的一个抽象,使得 Vue 能够在内存中进行 DOM 操作,而不需要直接操作浏览器的 DOM
- 挂载(Mounting):
- 一旦虚拟 DOM 树创建完成,Vue 会将这个虚拟 DOM 树挂载(mount)到真实的 DOM 节点上。这个过程涉及到将虚拟 DOM 转换为真实 DOM,并替换掉 Vue 实例的挂载点
- 更新(Updating):
- 当 Vue 实例的数据发生变化时,虚拟 DOM 树会相应地更新。Vue 通过比较新旧虚拟 DOM 树的差异(即 diff 算法),计算出需要对真实 DOM 执行的最小更新操作
- patch(差异应用):
- Vue 使用一个名为
patch
的过程来将虚拟 DOM 的差异应用到真实 DOM 上。这个过程是高效的,因为它只更新变化的部分
- Vue 使用一个名为
- 渲染完成(Rendering Completed):
- 一旦所有必要的 DOM 更新完成,组件就完成了渲染。Vue 会发出渲染完成的回调,如果存在的话
# Vue 中 computed 和 watch 区别?分别适用于什么场景?
computed(计算属性)
特点:
computed
属性是基于它们的依赖进行缓存的。只有当依赖发生变化时,计算属性才会重新计算computed
属性是声明性的,它们描述了数据之间的一种关系或转换computed
属性经常用于模板中,作为渲染逻辑的一部分
使用场景:
- 当你需要根据组件中其他数据计算派生状态时
- 当你想要避免在模板中使用复杂的表达式时,可以将逻辑封装在计算属性中
- 当你需要缓存计算结果以提高性能时
示例:
new Vue({
data: {
firstName: "Jane",
lastName: "Doe",
},
computed: {
// 计算属性 fullName 是一个派生状态
fullName: function () {
return this.firstName + " " + this.lastName;
},
},
});
2
3
4
5
6
7
8
9
10
11
12
watch(侦听器)
特点:
watch
属性不进行缓存,每次被侦听的数据变化时,回调函数都会被调用watch
属性是命令式的,它们可以执行更复杂的逻辑,如异步操作watch
属性不仅限于模板渲染,还可以用于执行副作用操作
使用场景:
- 当你需要在数据变化时执行异步操作或较为复杂的逻辑时
- 当你需要执行一些副作用,例如路由导航、发送请求等
- 当你需要在数据变化时执行某些同步操作
示例:
new Vue({
data: {
searchQuery: "",
},
watch: {
// 侦听器 searchQuery 在数据变化时调用
searchQuery: function (newQuery) {
// 执行搜索操作
this.search(newQuery);
},
},
methods: {
search: function (query) {
// 执行搜索逻辑
},
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
区别总结:
- 缓存:
computed
是缓存的,只有当依赖项变化时才会重新计算;watch
没有缓存,每次数据变化都会触发回调 - 用途:
computed
适用于声明性地描述数据之间的关系;watch
适用于执行副作用或复杂的响应式逻辑 - 性能:由于
computed
属性的缓存机制,它们通常在性能上更优,特别是当计算结果不需要在每次数据变化时都重新计算时 - 响应性:
watch
可以用于监听更复杂的响应式场景,包括异步操作和多次数据变化的累积处理
# 什么是 Vuex?使用 Vuex 有哪些好处?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式和库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 适用于中大型单页应用程序(SPA),它提供了一种结构化的方法来管理应用级的状态
使用 Vuex 的好处:
- 集中式存储:
- 应用的所有状态(state)被集中存储在 Vuex 的 store 对象中,这使得状态更容易维护
- 状态变化可预测:
- Vuex 通过提交 mutations 来改变状态,mutations 是同步函数,这使得状态变化变得可预测
- 组件解耦:
- 组件不需要知道其他组件的状态是如何更新的,只需要通过 dispatch action 或提交 mutation 来触发状态变化
- 数据流明确:
- 所有的状态变化都是可追踪的,这有助于开发过程中的状态管理和调试
- 开发工具支持:
- Vuex 与 Vue Devtools 紧密集成,可以在开发工具中观察状态的变化,执行动作(actions),或者追踪 mutations
- 支持复杂逻辑:
- 通过 actions 可以执行异步操作,处理复杂的业务逻辑
- 模块化:
- 当应用变得复杂时,store 可以被分割成模块,每个模块拥有自己的 state、mutations、actions 和 getters
- 响应式更新:
- Vuex 与 Vue 的响应系统完美集成,当状态更新时,所有依赖于该状态的组件都会响应式更新
- 时间旅行调试:
- 通过 Vue Devtools,可以进行时间旅行调试,即在不同的状态版本之间来回穿梭,查看状态的历史和未来
- 中间件支持:
- Vuex 允许你将一些逻辑放在 actions 执行的中间件中,用于处理日志记录、持久化、测试等
- 严格模式:
- 开发过程中可以启动严格模式,这将帮助你捕捉到 Vuex store 中的运行时错误
Vuex 的主要概念:
- State:存储在 Vuex store 中的数据
- Getters:从 store 中获取数据的函数,可以对状态进行计算
- Mutations:同步函数,用于更改 store 中的状态
- Actions:可以包含任意异步操作的函数,用于提交 mutations 或执行其他 actions
- Modules:用于将 store 分割成模块,每个模块可以包含自己的 state、mutations、actions 和 getters
# Vue Router 的 $route 和 $router 对象有什么区别?
$route
对象
$route
是一个路由参数对象,它代表当前激活的路由的状态信息。这个对象是只读的,并且包含了以下属性:
path
:当前路由的路径name
:如果当前路由有命名,则是该路由的名称query
:一个包含当前路由查询参数的对象hash
:当前路由的哈希值params
:一个对象,包含路由路径中的动态片段meta
:如果路由定义了元信息(meta 字段),则这里会包含这些信息matched
:一个数组,包含当前路由的所有父路由记录
$route
对象通常用于模板中,或者在组件的计算属性或方法中,以获取当前路由的信息
示例:
// 在模板中使用
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
// 在组件中使用
export default {
computed: {
userName() {
return this.$route.params.userName;
}
}
};
2
3
4
5
6
7
8
9
10
11
$router
对象
$router
是 Vue Router 的实例本身,提供了丰富的方法来与路由交互。它是可操作的,允许你执行导航到不同的路由、全局跳转等操作。以下是一些常用的方法:
push(location, onComplete?, onAbort?)
:向 history 堆栈中添加一个新的记录,但不会替换当前记录replace(location, onComplete?, onAbort?)
:替换当前条目go(n)
:类似于 window.history.go(n)back()
:等同于go(-1)
forward()
:等同于go(1)
getCurrentLocation()
:返回当前的路由信息
$router
对象通常在组件的方法中使用,或者在程序的其他部分需要进行路由操作时使用
示例:
// 导航到一个新的 URL
this.$router.push("/new-path");
// 替换当前的 URL
this.$router.replace("/new-path");
// 执行后退操作
this.$router.go(-1);
2
3
4
5
6
7
8
区别总结:
$route
是当前路由的状态快照,用于读取当前路由信息$router
是 Vue Router 的实例,用于操作路由,执行导航等
# Vue Router 路由有哪些模式?各模式有什么区别?
Vue Router 是 Vue.js 官方的路由管理器,用于构建单页面应用程序。Vue Router 支持多种路由模式,每种模式都决定了路由的匹配规则和 URL 的显示方式。以下是 Vue Router 支持的主要路由模式及其区别:
hash
模式
- 特点:使用 URL 的 hash 部分(即
#
后面的部分)来存储路由状态 - URL 示例:
http://example.com/#/home
- 兼容性:兼容性最好,支持所有浏览器,包括较旧的浏览器
- 缺点:URL 中的 hash 部分不会被包含在 HTTP 请求中,因此对于服务器来说,hash 模式下的路由不可见。服务器需要配置以正确返回单页面应用的入口文件
history
模式
- 特点:利用 HTML5 History API 来实现无刷新的页面导航
- URL 示例:
http://example.com/home
- 兼容性:现代浏览器支持,不支持的浏览器会回退到
hash
模式 - 优点:URL 更加美观,且可以被服务器正确处理(如果服务器配置了相应的路由重写规则)
- 缺点:需要服务器配置支持,否则可能遇到 404 错误
abstract
模式
- 特点:不依赖于浏览器的 URL,完全在 JavaScript 中管理路由
- 使用场景:通常用于服务端渲染(SSR)或测试环境
- 优点:不依赖于浏览器的 URL,可以完全控制路由
- 缺点:不适用于普通的单页面应用,因为用户无法通过浏览器的地址栏输入 URL 或使用书签
hash + history
混合模式
- 特点:结合了
hash
和history
模式的特点 - 使用场景:在需要同时支持较旧浏览器和现代浏览器的情况下使用
- 实现方式:在
history
模式的基础上,通过监听hashchange
事件来模拟history
模式的行为
服务器配置示例
对于 history
模式,服务器需要配置以确保所有路由都返回单页面应用的入口文件,以便 Vue Router 可以接管路由。以下是一个简单的服务器配置示例(使用 Node.js 和 Express):
const express = require("express");
const app = express();
// 静态文件服务
app.use(express.static("./dist"));
// 所有路由重定向到入口文件
app.get("*", (req, res) => {
res.sendFile(__dirname + "/dist/index.html");
});
app.listen(8080);
2
3
4
5
6
7
8
9
10
11
12
在这个配置中,除了静态文件请求外,所有的路由请求都会被重定向到 index.html
文件,Vue Router 将接管后续的路由处理
总结
hash
模式:兼容性最好,但 URL 中包含#
history
模式:URL 更加美观,需要服务器配置支持abstract
模式:不依赖于浏览器 URL,适用于 SSR 或测试环境hash + history
混合模式:结合了两者的优点,适用于需要同时支持新旧浏览器的情况
# 说说你对 TypeScript 的理解?与 JavaScript 的区别?
TypeScript 是一种由 Microsoft 开发的开源编程语言。它是 JavaScript 的一个超集,意味着任何有效的 JavaScript 代码都是有效的 TypeScript 代码。TypeScript 在 JavaScript 的基础上添加了类型系统和对 ESNext 特性的支持,使得它更适合大型应用程序的开发
TypeScript 的主要特点:
- 静态类型系统:TypeScript 最显著的特点是它的静态类型系统。这允许开发者在编译时就发现类型错误,而不是在运行时
- 类型推断:TypeScript 拥有强大的类型推断能力,可以在不显式指定类型的情况下,根据变量的初始值推断其类型
- 接口:TypeScript 提供了接口(Interfaces),允许定义对象的结构,这有助于确保对象符合预期的形状
- 类:TypeScript 增强了 JavaScript 的类(Class)支持,提供了更丰富的特性,如访问修饰符、继承等
- 命名空间:命名空间(Namespaces)用于组织代码,避免命名冲突
- 装饰器:TypeScript 支持装饰器(Decorators),这是一种特殊类型的声明,可以被附加到类、方法、属性或参数上
- 高级类型:TypeScript 提供了多种高级类型,如联合类型、交叉类型、元组等,以表达更复杂的类型关系
- ESNext 支持:TypeScript 支持许多最新的 JavaScript 特性,如 async/await、模块、生成器等
- 编译时检查:TypeScript 代码在运行前会被编译成 JavaScript,编译过程中会进行类型检查和语法检查
- 工具支持:TypeScript 拥有强大的工具链,包括类型检查、自动补全、重构支持等
JavaScript 与 TypeScript 的区别:
- 类型系统:JavaScript 是一种动态类型语言,变量的类型在运行时确定;而 TypeScript 是静态类型的,类型在编译时检查
- 编译过程:TypeScript 代码需要被编译成 JavaScript 代码才能运行,而 JavaScript 代码可以直接运行在浏览器或 Node.js 环境中
- 语法特性:TypeScript 支持一些 JavaScript 中尚未实现的语言提案,如装饰器、更丰富的类型系统等
- 工具链:TypeScript 拥有更复杂的工具链,包括编译器、类型检查器、集成开发环境(IDE)支持等
- 适用场景:JavaScript 更适合快速开发和小型项目;TypeScript 更适合大型项目和需要类型安全的场景
- 学习曲线:JavaScript 的学习曲线相对较低,因为它不需要理解类型系统;TypeScript 的学习曲线较高,需要学习其类型系统和特性
- 社区和生态系统:JavaScript 拥有更广泛的社区和生态系统,几乎所有的现代 Web 开发工具和库都支持 JavaScript;TypeScript 虽然在快速发展,但相对来说社区和生态系统较小
# TypeScript 的内置数据类型有哪些?
TypeScript 作为 JavaScript 的一个超集,其内置数据类型既包括了 JavaScript 的基本数据类型,也包括了一些 TypeScript 特有的类型系统特性。以下是 TypeScript 的一些内置数据类型:
- 基本数据类型:
number
:代表数值,例如3
或-42
string
:代表字符串,例如"hello"
boolean
:代表布尔值,true
或false
- 空值:
null
:表示故意赋予变量的空值undefined
:表示变量已声明但未初始化
- 数组类型:
let arr: number[] = [1, 2, 3];
表示arr
是一个包含数字的数组- 使用泛型数组:
let arr: Array<number> = [1, 2, 3];
- 元组类型 (Tuple): 允许表示一个固定长度的数组,其中每个元素的类型可以不同
let x: [string, number] = ["hello", 10];
- 枚举类型 (Enum): 一种特殊的类型,由一组命名的常量组成
- `enum Color {Red, Green, Blue};}
- 任意类型 (Any):
let notSure: any = 4; notSure = "maybe a string"; notSure = false;
表示变量可以是任何类型
- 未知类型 (Unknown):
let unknownVar: unknown;
与any
类似,但unknown
类型在赋值和方法调用前需要类型守卫或类型断言
- Never 类型:
- 表示位置在某个位置之后不会执行(例如,函数中抛出了错误或调用了
process.exit()
)
- 表示位置在某个位置之后不会执行(例如,函数中抛出了错误或调用了
- Void 类型:
- 与
null
和undefined
不同,void
类型表示没有任何类型,常用于那些不返回任何值的函数 function warnUser(): void { ... }
- 与
- 类型别名 (Type Alias):
- 使用
type
关键字为类型创建一个新的名称,可以是基本类型、联合类型、交叉类型等的组合
- 使用
- 联合类型 (Union):
let value: string | number;
表示value
可以是string
或number
类型
- 交叉类型 (Intersection):
type Person = Name & Aged;
创建一个新类型,它是多个类型的组合
- 字面量类型 (Literal Types):
- 包括字符串字面量类型、数字字面量类型和布尔字面量类型
let size: 'small' | 'medium' | 'large';
- 模板字面量类型 (Template Literal Types):
- 允许对模板字符串的每个部分进行类型检查
let build:
Hello, ${string};
- 索引访问类型 (Indexable Types):
- 允许对数组或对象的索引进行类型检查
- 条件类型 (Conditional Types):
- 基于类型系统的条件语句,允许基于条件表达式创建新类型
- 映射类型 (Mapped Types):
- 允许基于现有类型创建新类型,通常与
keyof
一起使用
- 允许基于现有类型创建新类型,通常与
- 只读类型 (Readonly):
let ro: readonly [number, string] = [1, 'two'];
使数组的每个元素都是只读的
# TypeScript 中的 Declare 关键字有什么用?
在 TypeScript 中,declare
关键字用于声明那些在 TypeScript 代码中使用,但在当前项目中不实际存在或不需要实现的变量、函数、类、接口、类型别名或枚举。这通常用于以下场景:
声明全局变量: 当使用一个在全局作用域中定义的变量或函数,但在 TypeScript 项目中没有定义时,可以使用 declare
来声明
declare var globalVar: string;
声明模块: 当引用一个模块,而该模块没有类型定义文件时,可以使用 declare module
来声明模块及其导出的内容
declare module "some-module" {
export function doSomething(value: string): void;
}
2
3
声明合并: 使用 declare
可以扩展或合并已有的类、接口或类型。这在扩展第三方库的类型定义时非常有用
interface Window {
myNewProperty: string;
}
2
3
声明文件: 在全局作用域中声明内容时,可以创建一个 .d.ts
文件,该文件专门用于声明,不包含任何实现
声明第三方库的类型: 当第三方库没有提供 TypeScript 定义文件(.d.ts
),但你知道如何正确地类型化它的 API 时,可以自己声明这些类型
避免命名冲突: 使用 declare
可以避免命名冲突,因为声明的变量、函数等不会被包含在最终的 JavaScript 输出中
声明现有的 JavaScript 库: 当使用 JavaScript 库时,可以通过声明来为库的函数、类和接口提供 TypeScript 类型支持
声明抽象类型: 当需要定义一些抽象类型,只在类型系统中使用而不实际实现时,可以使用 declare
声明外部函数或变量: 当外部脚本或浏览器环境提供了某些函数或变量,但 TypeScript 环境不知道它们的类型时,可以使用 declare
来声明它们
通过使用 declare
,TypeScript 开发者可以为现有的 JavaScript 代码库添加类型安全,或者扩展第三方库的类型定义,而无需修改原始的库代码。这使得 TypeScript 更加强大和灵活,能够适应各种不同的开发环境和需求
# 什么是 TypeScript 中的命名空间和模块?两者有什么区别?
在 TypeScript 中,命名空间(Namespaces)和模块(Modules)都是用于代码组织和封装的工具,但它们的用途和行为有所不同
- 命名空间(Namespaces)
命名空间是 TypeScript 中用于将相关的值(比如变量、函数、类、接口等)组织在一起的方式。命名空间提供了一种将代码划分为不同部分的方法,有助于避免命名冲突
特点:
- 命名空间不是一个新的 JavaScript 对象或构造,它在编译为 JavaScript 后会被移除,只存在于 TypeScript 编译阶段
- 命名空间允许开发者将功能组织成逻辑单元,并且可以在不同的文件中声明同一个命名空间的不同部分
示例:
namespace MyNamespace {
export function sayHello() {
console.log("Hello");
}
function privateFunction() {
console.log("This function is private.");
}
}
MyNamespace.sayHello(); // 调用命名空间中的公开函数
// privateFunction(); // 这将导致错误,因为 privateFunction 是私有的
2
3
4
5
6
7
8
9
10
11
12
- 模块(Modules)
模块是 TypeScript 中用于代码封装和重用的一种方式,它们在编译后的 JavaScript 中以对象的形式存在。模块允许开发者定义私有的和公开的 API,并且可以跨文件共享
特点:
- 模块在编译为 JavaScript 后以对象的形式存在,每个文件就是一个模块
- 使用
import
和export
关键字来导入和导出模块的成员 - 模块支持 ES6 模块规范,允许将类、接口、函数、变量等作为模块的一部分
示例:
// myModule.ts
export function sayHello() {
console.log("Hello from module");
}
// 使用 ES6 模块导入
import { sayHello } from "./myModule";
sayHello();
2
3
4
5
6
7
8
- 区别
- 作用域:
- 命名空间是 TypeScript 编译器的概念,用于组织类型和避免命名冲突,它们在编译后的 JavaScript 中不存在
- 模块是 JavaScript 的概念,用于创建封装的代码块,它们在编译后的 JavaScript 中以对象的形式存在
- 使用方式:
- 命名空间使用
namespace
关键字声明,并通过export
关键字导出 - 模块通过
export
导出成员,并通过import
导入其他模块的成员
- 命名空间使用
- 编译结果:
- 命名空间在编译为 JavaScript 后不会产生任何代码,它们仅在类型检查阶段起作用
- 模块编译为 JavaScript 对象,并且需要通过模块加载器(如 CommonJS、ES6 模块系统)来加载
- 代码组织:
- 命名空间更多地用于内部逻辑的组织,不关心外部如何引用
- 模块关注于公开的 API 和如何被外部代码重用
- 跨文件使用:
- 命名空间可以跨多个文件声明,所有声明最终会合并成一个单一的命名空间
- 模块是独立的文件,每个文件都是一个模块,通过
import
和export
与其他模块交互
- 作用域:
# 说说你对 Node.js 的理解?优缺点?应用场景?
理解 Node.js
- 事件驱动和非阻塞 I/O:Node.js 采用事件循环和回调函数来处理 I/O 操作,这使得它在处理大量并发连接时非常高效
- 单线程:虽然 Node.js 在单个线程上运行,但它通过事件循环和回调队列来管理并发,避免了多线程编程中的复杂性
- NPM(Node Package Manager):Node.js 拥有庞大的第三方库生态系统,NPM 是世界上最大的软件注册表,包含数百万的包
- 跨平台:Node.js 可以在多个平台上运行,包括 Windows、Linux 和 macOS
- 适用于构建多种类型的应用程序:虽然 Node.js 最初是为 Web 服务器设计的,但它也被用于构建各种类型的应用程序,如命令行工具、桌面应用程序、物联网设备等
优点
- 性能:由于基于 V8 引擎,Node.js 在执行 JavaScript 代码时具有很高的性能
- 可扩展性:Node.js 的事件驱动模型使其在处理大量并发连接时非常可扩展
- 开发效率:使用 JavaScript 编写服务器端和客户端代码,提高了开发效率并减少了上下文切换
- 丰富的生态系统:拥有大量的模块和库,可以加速开发过程
- 社区支持:拥有活跃的开发者社区,提供大量的资源和支持
- 适用于实时应用:适合构建需要实时功能的应用,如在线游戏、聊天应用等
缺点
- 单线程:虽然事件驱动模型提供了并发性,但 Node.js 的单线程模型意味着在 CPU 密集型任务中可能不是最佳选择
- 内存消耗:Node.js 可能比其他技术(如 Java 或 .NET)消耗更多的内存
- 错误处理:由于 Node.js 依赖回调函数,错误处理可能变得复杂,尤其是在回调地狱(callback hell)的情况下
- 学习曲线:对于不熟悉 JavaScript 或事件驱动编程的开发者,Node.js 可能有一个陡峭的学习曲线
应用场景
- Web 应用后端:构建 RESTful API 和实时 Web 应用
- 实时应用:如在线多人游戏、协作工具和聊天应用
- 命令行工具:使用 Node.js 创建跨平台的命令行应用程序
- 桌面应用程序:通过 Electron 框架,使用 Web 技术构建桌面应用程序
- 物联网(IoT):在 IoT 设备上运行 JavaScript 代码,处理设备与服务器之间的通信
- 网络服务:构建各种网络服务,如邮件服务器、代理服务器等
# 什么是 Node.js 中的 process?它有哪些方法和应用场景?
在 Node.js 中,process
是一个全局对象,它代表了当前的进程。这个对象与浏览器中的 window
对象或 Java 中的 System
类似,提供了一些与运行时环境交互的方法和属性
process
对象的主要特点:
- 全局访问:在 Node.js 中,
process
对象可以在任何地方使用,不需要导入 - 进程信息:提供了当前进程的信息,如版本号、平台、内存使用情况等
- 环境变量:通过
process.env
访问环境变量 - 标准输入输出:通过
process.stdin
、process.stdout
和process.stderr
进行标准输入输出操作
process
对象的常用方法:
- process.exit([code]):退出进程并返回一个可选的退出码
- process.chdir(directory):改变当前工作目录
- process.cwd():返回当前工作目录
- process.env:包含环境变量的对象
- process.argv:包含命令行参数的数组
- process.execPath:返回执行当前脚本的 Node.js 进程的路径
- process.execArgv:返回启动 Node.js 进程的命令行参数数组
- process.stdin、process.stdout、process.stderr:标准输入、输出和错误输出流
- process.on('event', listener):监听进程生成的事件,如
'exit'
、'beforeExit'
、'SIGINT'
等
应用场景:
- 处理命令行参数:使用
process.argv
处理传递给 Node.js 脚本的命令行参数 - 环境变量:使用
process.env
读取和修改环境变量,这在配置应用程序时非常有用 - 优雅地关闭应用程序:使用
process.exit
来优雅地关闭应用程序,允许在退出前执行清理工作 - 改变工作目录:使用
process.chdir
在脚本中改变工作目录 - 监听进程事件:使用
process.on
监听和响应进程事件,如捕获中断信号(SIGINT
)来实现控制台的优雅退出 - 日志记录:使用
process.stdout
和process.stderr
进行日志记录,将信息输出到控制台或写入到文件 - 性能监控:使用
process.memoryUsage
监控内存使用情况,帮助优化应用程序性能 - 子进程管理:使用
process.spawn
或相关方法创建和管理子进程
示例:
// 打印所有命令行参数
console.log(process.argv);
// 监听 SIGINT 信号,例如 Ctrl+C
process.on("SIGINT", function () {
console.log("收到 SIGINT 信号,正在退出...");
process.exit();
});
// 使用环境变量
console.log("环境变量 EXAMPLE:", process.env.EXAMPLE);
// 改变当前工作目录
process.chdir("/tmp");
console.log("当前工作目录:", process.cwd());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
process
对象是 Node.js 中与当前进程交互的关键,它为开发者提供了丰富的方法来控制和监视 Node.js 应用程序的运行时环境
# 什么是 npm?你用过哪些 npm 包?是否开发过自己的 npm 包?
npm(Node Package Manager,Node 包管理器)是一个 JavaScript 编程语言的包管理器,用于 Node.js 应用程序。它是 Node.js 的默认包管理工具,用于管理项目中的依赖关系,包括安装、更新和删除包
npm 的主要特点:
- 依赖管理:npm 能够自动安装模块及其依赖,确保应用程序的依赖项得到满足
- 包仓库:npm 拥有一个庞大的公共包仓库(npm registry),包含数百万的包
- 版本控制:遵循语义化版本控制规范,方便管理不同版本的包
- 本地缓存:npm 会将下载的包缓存到本地,加速后续的安装过程
- 脚本运行:通过
package.json
中的"scripts"
字段,可以运行自定义的脚本命令 - 私有包支持:支持发布和使用私有包
- 工作流工具集成:与许多持续集成/持续部署(CI/CD)工具和编辑器集成
我使用过的 npm 包:
- express:一个用于构建 Web 应用和 API 的流行 Node.js 框架
- react 和 vue:用于构建用户界面的前端 JavaScript 库
- lodash 或 underscore:提供实用工具函数的库
- axios:一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js
- mongoose:一个 MongoDB 对象模型工具
- jsonwebtoken:用于生成和验证 JSON Web Tokens(JWT)
- webpack:一个现代 JavaScript 应用程序的静态模块打包器
- babel:一个 JavaScript 编译器,用于将 ES6 代码转换为向后兼容的 JavaScript 版本
- jest:一个愉快且高效的 JavaScript 测试框架
是否开发过自己的 npm 包?
- 创建包:在项目中创建
package.json
文件,定义包的名称、版本、描述、入口点等 - 编写代码:实现包的功能代码
- 添加文档:编写 README 和 JSDoc 注释,说明如何使用包
- 测试:编写测试用例,确保代码质量
- 发布:使用
npm login
登录 npm registry,然后使用npm publish
发布包 - 维护:根据用户反馈修复问题,更新版本
# 什么是 Node.js 的事件循环机制?它是怎么实现的?
Node.js 的事件循环机制是其非阻塞 I/O 模型的核心,它允许 Node.js 应用在单线程中处理大量并发操作。事件循环机制主要包括以下几个步骤:
- 定时器(Timers):检查是否有已经达到预定时间的定时器,如果有,则执行它们
- I/O 事件(I/O Events):处理所有 I/O 操作的回调函数,比如网络请求、文件读写等
- 空闲、准备和轮询阶段(Idle, Prepare, Poll):
- 空闲阶段:执行一些必要的操作,比如设置轮询所需的时间
- 准备阶段:收集所有事件,包括 I/O 事件和定时器
- 轮询阶段:等待轮询结束,处理 I/O 事件
- 检查阶段(Check):执行一些特定的操作,比如关闭服务器
- 关闭回调阶段(Close Callbacks):处理文件描述符等资源的关闭操作
事件循环的实现细节依赖于 Node.js 的底层架构。Node.js 使用了 libuv 库来实现事件循环和异步 I/O 操作。libuv 是一个跨平台的异步 I/O 库,它提供了事件循环、文件系统操作、DNS 查询、TCP 和 UDP 网络操作等功能
libuv 通过以下方式实现事件循环:
- 事件队列:所有事件和回调函数都被存储在一个队列中
- 异步 I/O:使用操作系统提供的异步 I/O 机制,比如 epoll(Linux)、kqueue(BSD)和 IOCP(Windows)
- 线程池:libuv 有一个线程池,用于处理那些不能在主线程中完成的任务,比如文件系统操作
- 定时器:libuv 使用操作系统的定时器机制来实现定时器事件
# Node.js 有哪些全局对象?它们分别有什么作用?
- global:代表 Node.js 的全局对象,类似于浏览器中的
window
对象。它包含了一些全局可用的属性和方法 - process:代表当前 Node.js 进程的控制对象。它提供了一些与进程控制相关的方法,如退出(
process.exit()
)、信号处理(process.on('SIGINT', ...)
)等 - Buffer:用于处理二进制数据的全局对象。在 Node.js 中,Buffer 是一个特殊的类,用于表示固定长度的字节序列
- console:提供控制台输出功能的对象,包括
console.log()
、console.error()
等方法 - require:Node.js 的模块加载函数,用于加载模块
- module:代表当前模块的对象,包含了当前模块的元数据和方法,如
module.exports
用于导出模块的接口 - __dirname:返回当前执行脚本所在的目录
- __filename:返回当前执行脚本的文件名
- setTimeout 和 clearTimeout:用于设置和清除定时器
- setInterval 和 clearInterval:用于设置和清除重复执行的定时器
- setImmediate 和 clearImmediate:用于在事件循环的当前阶段结束后立即执行一个函数
- process.nextTick:用于将回调函数安排在下一个事件循环的迭代中执行
- exports:通常用于导出模块的接口,是
module.exports
的引用 - Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError 和 URIError:这些是错误对象的构造函数,用于抛出不同类型的错误。
- clearImmediate:清除由
setImmediate
设置的立即执行的回调 - clearTimeout:清除由
setTimeout
设置的定时器 - Intl:国际化 API,用于语言敏感的字符串比较、数字格式化等
# 介绍下从 HTTP/1.0、HTTP/1.1 到 HTTP/2 再到 HTTP/3 的演化过程,各个阶段相对前一阶段做了哪些优化?
HTTP(超文本传输协议)是互联网上应用最广泛的协议之一,用于客户端和服务器之间的通信。以下是从 HTTP/1.0 到 HTTP/3 的演化过程,以及各个阶段的优化:
HTTP/1.0
- 发布时间:1996 年
- 特性:
- 简单性:定义了请求和响应的基本格式
- 无状态性:每个请求都是独立的,服务器不会记住任何客户端信息
- 连接:每个请求/响应都需要建立新的 TCP 连接,完成后关闭连接
HTTP/1.1
- 发布时间:1999 年
- 优化:
- 持久连接:通过
Connection: keep-alive
头部,允许在一个 TCP 连接上发送多个 HTTP 请求和响应,减少了连接建立和关闭的开销 - 管道化:允许客户端在第一个请求的响应到来之前发送多个请求,但服务器仍然需要按请求顺序响应
- 缓存机制:引入了更多的缓存控制机制,如
ETag
、Last-Modified
等 - 分块传输编码:允许数据以块的形式传输,可以动态生成内容
- 更多的 HTTP 方法和状态码:增加了如
PUT
、DELETE
等方法,以及更多的状态码
- 持久连接:通过
HTTP/2
- 发布时间:2015 年
- 优化:
- 二进制协议:采用二进制格式,解析更快,更高效
- 多路复用:允许在单个 TCP 连接上并行交错发送多个请求和响应,彻底解决了队头阻塞问题
- 头部压缩:使用 HPACK 算法压缩请求和响应的头部,减少冗余头部信息
- 服务器推送:服务器可以主动向客户端推送资源,而不需要客户端明确请求
- 流量控制:通过流控制机制,避免发送方过载接收方
HTTP/3
- 发布时间:2019 年(草案,2022 年成为正式标准)
- 优化:
- 基于 QUIC 协议:HTTP/3 不再基于 TCP,而是使用 QUIC 协议,这是一个基于 UDP 的传输层协议,提供了更好的性能和安全性
- 0-RTT 连接建立:允许在某些情况下,客户端可以在第一次握手时就发送数据,减少了连接建立的延迟
- 改进的流量控制和拥塞控制:QUIC 协议提供了更精细的流量控制和拥塞控制机制
- 更好的多路复用:由于基于 QUIC,HTTP/3 的多路复用机制更加健壮,不受 TCP 的一些问题影响,如 TCP 的队头阻塞
- 安全性:QUIC 提供了内置的加密,类似于 TLS,但更高效
# DNS HTTP 缓存有哪些实现方式?什么是协商缓存和强制缓存?
DNS 缓存:DNS 缓存通常由递归查询的 DNS 服务器实现,当客户端向 DNS 服务器发起查询请求时,服务器会将查询结果存储在缓存中,以加快后续相同查询的响应速度。DNS 缓存的实现涉及到本地 DNS 服务器的缓存策略,以及可能的权威 DNS 服务器的缓存设置。DNS 缓存记录中包含 TTL(Time to Live)值,指示记录的有效期
HTTP 缓存:HTTP 缓存主要实现在客户端(如浏览器)和中间代理服务器上。它通过 HTTP 响应头中的字段来控制,如 Cache-Control
、Expires
、ETag
和 Last-Modified
。HTTP 缓存分为两种类型:
- 强制缓存(HTTP 强缓存):通过
Cache-Control
头中的max-age
或Expires
头来指定资源的新鲜度。在资源的新鲜期内,浏览器可以直接从本地缓存读取数据,无需向服务器发送请求 - 协商缓存:当资源可能已经更新时,浏览器会使用
If-None-Match
(与ETag
配合)或If-Modified-Since
(与Last-Modified
配合)向服务器查询资源是否已更新。如果服务器确定资源未更新,则返回304 Not Modified
状态码,告知浏览器可以继续使用缓存中的资源
强制缓存和协商缓存的主要区别在于它们检查资源是否更新的时机和方式。强制缓存在本地就可以确定资源是否可使用,而协商缓存需要与服务器进行通信来验证资源的新鲜度。强制缓存适用于不经常变动的资源,以减少不必要的网络请求;协商缓存适用于频繁更新的资源,以确保用户总是获取最新内容
# 简述 TCP/IP 网络模型,分为几层?每层的职责和作用是什么?
TCP/IP 网络模型是一种网络通信的抽象框架,它将网络通信的复杂过程分解为若干层次,每层负责不同的功能。这个模型通常被描述为一个四层或五层结构,具体取决于是否将“会话层”和“表示层”分开。以下是 TCP/IP 模型的各层及其职责和作用:
链路层(Link Layer)或网络接口层(Network Interface Layer):
- 职责:负责在物理网络媒介上进行二进制数据帧的传输,包括介质访问控制(MAC 地址)、错误检测和物理寻址
- 作用:确保数据可以在相邻的网络设备之间正确传输
网络层(Internet Layer):
- 职责:负责将数据包从源主机路由到目的主机,处理数据包在整个互联网中的传输
- 作用:提供主机到主机的通信,使用 IP 协议实现不同网络之间的互联
传输层(Transport Layer):
- 职责:负责在两个主机的应用程序之间提供可靠的通信服务,确保数据的完整性和顺序
- 作用:通过 TCP(传输控制协议)提供面向连接的、可靠的字节流传输服务;通过 UDP(用户数据报协议)提供无连接的、尽最大努力交付的数据报服务
应用层(Application Layer):
- 职责:为应用软件提供网络服务,处理特定的应用程序细节,如 HTTP、FTP、SMTP 等
- 作用:允许应用程序通过网络发送和接收数据,实现网络资源的访问和操作
在某些文献中,TCP/IP 模型还包括以下两层:
会话层(Session Layer):
- 职责:负责在网络中的两个节点之间建立、管理和终止会话
- 作用:确保会话的持续性和同步
表示层(Presentation Layer):
- 职责:负责数据的表示、编码和转换,确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取
- 作用:处理数据格式和加密解密等任务
# 什么是 webpack?它有什么作用?
Webpack 是一个开源的前端资源模块打包器(module bundler),主要设计用于现代 JavaScript 应用程序的开发。它能够将项目中的所有依赖项(包括 JavaScript、图片、CSS 等)打包成一个或多个 bundle,这些 bundle 可以被浏览器加载和执行
Webpack 的作用:
- 模块打包:Webpack 接受项目中的一个或多个入口文件,然后根据模块之间的依赖关系,递归地将所有模块打包成一个或多个 bundle
- 代码分割:Webpack 支持将代码分割成多个小块(chunks),这些小块可以按需加载,从而提高应用的加载性能
- 模块化开发:Webpack 支持 CommonJS、AMD、ES6 模块等不同的模块化规范,使得开发者可以方便地进行模块化开发
- 热更新:Webpack 支持热模块替换(HMR - Hot Module Replacement),在开发过程中,当模块文件被替换或更新时,无需重新加载整个页面,就可以实现局部更新
- 多种类型资源处理:Webpack 可以处理 JavaScript 之外的多种类型的资源,如图片、CSS、JSON 等,通过 loader 转换这些资源,使其能够被打包到最终的 bundle 中
- 优化:Webpack 提供了多种优化手段,包括但不限于压缩代码、提取公共模块、缩小图片尺寸等,以提高最终打包文件的性能
- 插件系统:Webpack 拥有丰富的插件生态系统,可以通过插件来扩展其功能,比如自动添加 CSS 前缀、压缩图片、生成环境变量等
- 开发和生产环境区分:Webpack 允许开发者为开发环境和生产环境配置不同的构建选项,比如在生产环境中可以关闭 source map、开启压缩等
- 灵活的配置:Webpack 提供了高度可配置的选项,可以根据项目需求定制打包流程
# 如何提高 webpack 的打包速度?
- 优化 Loader 配置:通过设置
include
、exclude
和test
属性来精确匹配需要处理的文件,减少不必要的文件处理。例如,使用babel-loader
时,可以指定cacheDirectory
选项来开启缓存,只对项目中的特定目录下的文件应用该 loader - 合理使用 Resolve 配置:通过配置
resolve.extensions
减少文件后缀的搜索次数,优化resolve.modules
减少模块搜索路径,使用resolve.alias
为常用路径设置别名以减少查找过程 - 使用 DLLPlugin 插件:将不常改变的代码抽离成 DLL 库,在之后的编译过程中被引用,避免重复打包
- 使用 Cache-loader:对性能开销较大的 loader 使用 cache-loader,将结果缓存到磁盘中,提升二次构建速度
- Terser 启动多线程:使用多进程并行运行来提高构建速度,尤其是在压缩 JavaScript 代码时
- 合理使用 SourceMap:适当配置 sourceMap,过于详细的 sourceMap 信息会减慢打包速度
- 使用 HardSourceWebpackPlugin:这个插件可以对 webpack 的构建过程进行缓存,显著提升二次构建速度,尤其适用于大型项目
- 利用 HappyPack 开启多线程:通过 HappyPack 插件,可以将任务分解到多个进程中并行处理,提高构建速度
- 更新到最新版本的 webpack:新版本通常会包含性能改进和优化
- 减少无用代码和模块:使用 Tree Shaking 去除无用代码,减少打包体积
- 模块懒加载:对于不会立即使用的模块,使用懒加载的方式导入,减少初始加载时间
- 使用 Webpack Bundle Analyzer:作为模块打包分析工具,帮助开发者了解打包后的文件构成,进一步优化
# 什么是 webpack 的热更新?它的实现原理是什么?
Webpack 的热更新(Hot Module Replacement,HMR) 是一种在应用开发过程中,当源代码发生变化时,无需手动刷新页面,就可以将变更的模块替换到运行中应用程序的技术。它主要用于提高开发效率,允许开发者在不中断程序运行的情况下实时看到更改效果
实现原理:
- 模块热替换 API:Webpack 通过实现 HMR 运行时(通常是一个小型的 JavaScript 脚本来管理模块的更新),利用了浏览器的
__webpack_require__
函数的钩子(hooks)来拦截模块的加载 - 模块标识:每个模块在编译时都会被分配一个唯一的标识符(module identifier)。当源代码发生变化时,Webpack 会根据模块的依赖关系图确定需要更新哪些模块
- 更新通知:Webpack 编译器在检测到源文件更改时,会触发一个更新过程。编译器将更新后的模块通过 HMR 运行时通知给应用程序
- 替换模块:HMR 运行时接收到更新通知后,会检查当前页面是否已经加载了需要更新的模块。如果是,它会使用新的模块版本替换旧版本,这个过程不需要刷新页面
- 状态保持:在模块被替换的过程中,HMR 运行时会尝试保持应用程序的状态不变,确保用户的操作状态和应用状态得以保留
- 样式更新:对于 CSS 文件的更新,Webpack 会通过添加新的
<style>
标签来替换旧的样式,同时确保旧的样式被移除 - 热更新中间件:在使用 webpack-dev-server 时,它提供了一个中间件来支持 HMR。当检测到文件更改时,服务器会发送一个包含更新信息的事件,客户端接收到事件后执行相应的更新逻辑
- 兼容性处理:HMR 运行时还会处理模块更新时的兼容性问题,比如当模块的 API 发生变化时,HMR 会尝试提供相应的降级或兼容策略
- 开发体验:HMR 极大提升了开发体验,尤其是在单页面应用(SPA)开发中,开发者可以快速迭代和测试功能,而无需担心每次更改代码后都需要重新加载页面
# 什么是前后端分离?它有什么优点和缺点?
前后端分离是一种软件开发架构模式,其中前端(客户端)和后端(服务器端)的开发工作是分开进行的,它们通过 API(通常是 RESTful API 或 GraphQL)进行通信。这种分离模式与传统的单体应用(Monolithic Application)模式相对,后者的前端和后端代码通常紧密耦合在一起
优点:
- 工作分离:前后端分离允许前端和后端的开发工作可以独立进行,互不干扰,提高开发效率
- 技术栈灵活性:团队可以选择最适合项目需求的前端和后端技术栈,不受限制
- 可维护性:分离的架构使得代码更加模块化,便于维护和扩展
- 可测试性:前后端分离可以独立测试前端和后端,提高测试效率和质量
- 可扩展性:后端服务可以独立于前端进行扩展,更容易应对不同的负载需求
- 跨平台性:后端 API 可以被多个前端应用(如 Web、移动应用等)重用
- 持续集成/持续部署(CI/CD):前后端分离有助于实施 CI/CD,提高部署的自动化程度
- 用户体验:前端可以专注于提供更好的用户体验,如通过单页面应用(SPA)实现更流畅的页面交互
缺点:
- 复杂性:前后端分离增加了项目的复杂性,需要更多的协调和通信
- 调试难度:分离的架构可能使得问题定位和调试更加困难
- 性能考虑:需要考虑 API 设计和数据加载策略,以避免性能瓶颈
- 安全性:API 安全性成为一个重要考虑因素,需要确保数据传输和访问的安全
- 状态管理:前端需要处理更多的状态管理问题,尤其是在 SPA 中
- SEO 挑战:对于依赖于服务器端渲染的搜索引擎优化(SEO),前后端分离可能需要额外的工作,如使用服务端渲染(SSR)或预渲染技术
- 团队协作:需要良好的团队协作和沟通机制,以确保前后端的接口设计和实现一致
- 资源投入:初期可能需要更多的时间和资源来设置和维护前后端分离的架构
# 你用过哪些包管理工具?它们各有什么特点?
npm(Node Package Manager):
- 特点:专为 Node.js 设计,拥有庞大的包生态系统(npm registry)
- 使用场景:JavaScript 项目中管理依赖
Yarn:
- 特点:由 Facebook 推出,提供了更快的安装速度、更可靠的安装过程以及更好的缓存机制
- 使用场景:与 npm 兼容,可用作替代品,提高前端项目依赖管理的效率
Maven:
- 特点:Java 项目的项目管理和构建自动化工具,支持依赖管理
- 使用场景:Java 项目构建和依赖管理
# 什么是 CSS 工程化?你用过哪些相关的工具?
CSS 工程化是指将 CSS 开发视为一种工程实践,通过工具、方法和流程来提高 CSS 代码的可维护性、可扩展性、性能和协同效率。CSS 工程化的目标是解决传统 CSS 开发中遇到的问题,如样式冲突、全局污染、难以维护和扩展等
- CSS 预处理器:
- Sass:提供变量、混合(mixins)、函数等编程特性
- LESS:与 Sass 类似,但集成了 JavaScript 的语法特性
- 构建工具:
- Webpack:模块打包器,通过 loader 处理 CSS,并提供代码分割、压缩等功能
- Gulp:流式自动化构建工具,可以用来编译 CSS、优化图片等
- CSS 框架:
- Bootstrap:提供响应式布局和预设的样式组件
- Tailwind CSS:实用工具类优先的 CSS 框架,提供大量可组合的工具类
- CSS 架构:
- BEM(Block Element Modifier):一种 CSS 命名和架构方法,用于解决样式冲突和提高可维护性
- 版本控制:
- Git:用于跟踪和管理 CSS 代码的变更历史
- 测试工具:
- Stylelint:CSS 代码质量和风格检查工具
- BackstopJS:视觉回归测试工具
- 文档工具:
- Storybook:用于开发和文档化 UI 组件的工具
- 性能优化工具:
- PurgeCSS:移除未使用的 CSS
- CSS Minifier:压缩 CSS 文件,减少文件大小
- 代码规范工具:
- Husky:Git 钩子工具,可以在提交前运行测试等脚本
- 模块化 CSS:
- CSS Modules:将 CSS 局部化到组件,避免全局污染
# 说说常规的前端性能优化手段
- 减少 HTTP 请求:
- 合并文件,如将多个 CSS 或 JavaScript 文件合并成一个
- 使用图片精灵(CSS Sprites)来减少图片请求
- 利用浏览器缓存:
- 通过设置合适的 HTTP 缓存头,使得用户再次访问时可以复用资源
- 压缩资源文件:
- 使用工具压缩 JavaScript、CSS 和 HTML 文件,减小文件体积
- 使用内容分发网络(CDN):
- 将资源部署到 CDN 上,使用户可以从最近的服务器获取资源,减少延迟
- 代码分割:
- 使用 Webpack 等打包工具进行代码分割,按需加载资源
- 优化图片:
- 使用合适的图片格式(如 WebP)和压缩工具减小图片大小
- 使用响应式图片技术,根据设备加载合适大小的图片
- 减少重绘和回流:
- 优化 CSS,减少页面的重绘(repaint)和回流(reflow)
- 使用预加载和预取:
- 使用
<link rel="preload">
预加载关键资源 - 使用
<link rel="prefetch">
预取可能需要的资源
- 使用
- 服务端渲染(SSR)或静态站点生成(SSG):
- 对于首屏渲染,服务端渲染可以更快地提供 HTML 内容
- 使用懒加载:
- 对于非首屏的图片和组件,可以使用懒加载技术延迟加载
- 优化第三方脚本:
- 延迟加载或异步加载第三方脚本,避免阻塞页面渲染
- 使用 Web 字体优化技术:
- 使用
font-display
属性控制字体的加载行为
- 使用
- 优化 CSS 选择器性能:
- 避免使用复杂的 CSS 选择器,减少匹配时间
- 减少 JavaScript 执行时间:
- 优化 JavaScript 代码,减少复杂计算和 DOM 操作
- 使用服务工作者(Service Workers):
- 通过 Service Workers 实现离线缓存和网络请求拦截,提升性能
- 优化数据库查询:
- 如果涉及到服务器端渲染,优化数据库查询可以减少服务器响应时间
- 使用性能监测工具:
- 使用 Lighthouse、WebPageTest 等工具监测和分析性能瓶颈
- 优化移动设备性能:
- 考虑到移动设备的硬件限制,优化图片大小和脚本执行效率
- 移除无用代码:
- 使用 Tree Shaking 等技术移除未使用的代码
- 使用 HTTP/2:
- 利用 HTTP/2 的特性,如头部压缩、多路复用,提高传输效率
# 前端性能优化指标有哪些?怎么进行性能检测?
前端性能优化指标主要包括以下几个关键性能指标:
- 首次内容绘制(First Contentful Paint, FCP):衡量浏览器首次呈现任何文本、图像或其他非空白内容所需的时间,是用户感知页面加载速度的重要指标
- 首次有意义绘制(First Meaningful Paint, FMP):表示用户首次看到主要内容的时间点,反映了页面主体内容的加载进度
- 最大内容绘制(Largest Contentful Paint, LCP):记录页面最大可见内容元素加载完成的时间,是衡量页面视觉加载完整度的关键指标
- 首次输入延迟(First Input Delay, FID):测量用户首次与页面交互到浏览器实际响应这段时间,反映页面交互响应的即时性
- 累积布局偏移(Cumulative Layout Shift, CLS):量化页面在加载过程中发生的视觉不稳定程度,直接影响用户体验
性能检测可以通过多种工具进行:
- Lighthouse:谷歌开源的自动化审计工具,可生成详细的性能报告并提供改进建议,适用于 Chrome 浏览器
- PageSpeed Insights:谷歌开发的工具,用于测试页面在移动和桌面设备上的性能,并提供改进建议
- WebPageTest:一个在线性能评测网站,提供详细的 web 页面性能分析,支持多地点、多设备模拟
- SpeedCurve:前端性能综合监控网站,提供性能测试监控报告
- SiteSpeed.io:开源的 Web 性能测试工具,衡量 Web 网站的综合性能,帮助分析网页的加载速度和渲染性能
- Chrome DevTools:内置性能分析器、网络面板、内存分析等强大功能,是性能优化的必备工具
- Pingdom:在线工具,测试页面加载速度,分析并找出性能瓶颈
# DNS 预解析是什么?怎么实现?
DNS 预解析(DNS Pre-resolution 或 DNS Prefetching)是一种网络优化技术,用于改善网页的加载时间。当浏览器遇到一个未知的域名时,它会向 DNS 服务器发送请求以解析该域名的 IP 地址。这个过程称为 DNS 解析,是网络请求过程中的一个步骤
DNS 预解析的目的是减少网页加载过程中的延迟,通过提前获取域名对应的 IP 地址,当实际需要与服务器通信时,就可以避免额外的 DNS 解析时间
如何实现 DNS 预解析:
HTML 标签: 在 HTML 文档中使用 <link rel="dns-prefetch" href="//example.com">
标签可以告诉浏览器提前解析指定的域名。这是一种显式的预解析请求
浏览器行为: 一些现代浏览器可能会自动进行 DNS 预解析,特别是对于那些在页面中多次出现的域名。浏览器的这种行为可能不需要开发者进行任何特别的指示
HTTP 头部: 使用 Link
头部可以实现类似的效果,例如:
Link: <https://example.com>; rel="dns-prefetch"
这可以作为 HTTP 响应的一部分发送给浏览器
服务端配置: 服务器可以配置为在响应中包含上述的 Link
头部,以提示浏览器进行 DNS 预解析
客户端脚本: 使用 JavaScript 可以在客户端动态执行 DNS 预解析。例如:
function prefetchDNS(hostname) {
const obj = new URL(hostname);
return obj.hostname;
}
prefetchDNS("https://example.com");
2
3
4
5
这段代码尝试创建一个指向指定主机名的 URL 对象,这可以触发浏览器进行 DNS 解析
操作系统和网络设置: 在某些操作系统或网络配置中,可能存在 DNS 预解析的设置,允许系统自动解析常见的域名
# 怎么进行站点内的图片性能优化?
- 正确选择图片格式:
- 使用适合的图片格式,例如 WebP、JPEG 2000、AVIF,这些格式提供了更好的压缩率
- 图片压缩:
- 使用工具如 TinyPNG、ImageOptim 或在线服务压缩图片,减少文件大小而不显著损失质量
- 使用图片精灵:
- 将多个小图标合并成一个图片精灵,减少 HTTP 请求的数量
- 懒加载图片:
- 使用
loading="lazy"
属性或 JavaScript 实现懒加载,延迟非首屏图片的加载
- 使用
- 响应式图片:
- 使用
<picture>
元素或srcset
属性提供不同分辨率的图片,根据设备显示不同大小的图片
- 使用
- 使用 CSS 背景图片:
- 对于小图标,可以使用 CSS 背景图片代替图片文件,利用 CSS 的压缩和缓存优势
- 利用现代图像编码:
- 利用现代图像编码技术,如 WebP,它提供了比传统 JPEG 和 PNG 更好的压缩
- 设置合适的图片尺寸:
- 确保图片的尺寸与在页面上的显示尺寸一致,避免使用过大的图片
- 使用 CDN 分发图片:
- 通过 CDN 缓存和分发图片,减少服务器的负担,加快图片的加载速度
- 利用缓存:
- 通过设置合适的 HTTP 缓存头,使得图片可以在用户浏览器中被缓存
- 避免使用过多的图片:
- 评估页面上图片的必要性,去除不必要的图片,减少页面的负载
- 使用图片 CDN:
- 使用专门的图片 CDN,它们提供了图片压缩、优化和快速分发服务
- 使用服务工作者(Service Workers):
- 通过 Service Workers 缓存图片,实现离线访问和快速加载
- 图片的异步加载:
- 避免在文档加载的关键路径上同步加载图片,可以异步加载以提高首屏渲染速度
- 使用图片预加载:
- 对于重要的图片,可以使用
preload
标签预加载,确保页面渲染时图片已经可用
- 对于重要的图片,可以使用
- 优化图片的 alt 属性:
- 确保 alt 属性提供图片的描述,这有助于搜索引擎优化和屏幕阅读器
- 使用图片 CDN 的自动优化功能:
- 一些图片 CDN 提供自动优化功能,如自动调整图片大小、格式转换等
# SPA(单页应用)首屏加载速度慢怎么解决?
- 代码分割(Code Splitting):
- 使用 Webpack 等打包工具进行代码分割,只加载首屏需要的代码和资源
- 服务端渲染(Server-Side Rendering, SSR):
- 通过 SSR 首次请求时直接返回完整的 HTML,加快首屏渲染速度
- 静态站点生成(Static Site Generation, SSG):
- 在构建时生成静态 HTML 文件,首屏加载时直接从这些静态文件服务
- 使用 Skeleton Screens:
- 使用骨架屏占位,提高用户感知的加载速度
- 优化资源文件:
- 压缩和合并 CSS、JavaScript 文件,减少文件大小
- 利用浏览器缓存:
- 通过设置合适的 HTTP 缓存头,使得资源可以被缓存和复用
- 预加载关键资源:
- 使用
<link rel="preload">
预加载首屏需要的关键资源
- 使用
- 延迟加载非首屏资源:
- 使用懒加载技术延迟加载非首屏的图片和组件
- 优化第三方脚本:
- 延迟加载或异步加载第三方脚本,避免阻塞页面渲染
- 使用 CDN:
- 通过 CDN 加速资源文件的加载
- 减少重绘和回流:
- 优化 CSS 和 JavaScript,减少页面的重绘和回流
- 优化字体加载:
- 使用
font-display
属性控制字体的加载行为,避免字体加载导致的渲染阻塞
- 使用
- 使用 Service Workers:
- 通过 Service Workers 缓存资源,实现快速加载
- 优化图片加载:
- 使用图片压缩、懒加载和响应式图片技术
- 减少初始化数据的加载:
- 按需加载初始化数据,避免一次性加载过多数据
- 使用 HTTP/2 或 HTTP/3:
- 利用 HTTP/2 的多路复用减少连接建立时间,HTTP/3 提供更快的连接设置
- 性能监测与分析:
- 使用 Lighthouse、WebPageTest 等工具定期检查性能瓶颈
- 设置性能预算:
- 限制打包后文件的大小,确保不超过设定的性能预算
- 优化 Web 字体的使用:
- 避免使用过多的 Web 字体,或使用子集以减少字体文件的大小
- 使用交互式元素的渐进式加载:
- 对于复杂的交互式元素,可以先加载一个简单的静态版本,然后逐步加载完整的交互功能
# git stash 命令有什么作用?什么时候适合用它?
git stash
的作用:
- 保存当前工作目录的更改:将未提交的更改(包括新文件和修改过的文件)保存起来,让工作目录回到最后一次提交的状态
- 临时切换分支:在需要切换到其他分支进行工作时,可以使用
git stash
保存当前更改,切换分支后再git stash apply
或git stash pop
来恢复更改 - 清理工作目录:在需要进行
git pull
或其他需要干净工作目录的操作时,git stash
可以临时存储更改,避免冲突 - 管理多个更改集:
git stash
可以存储多个不同的更改集,每个更改集都有自己的堆栈
适合使用 git stash
的情况:
- 需要切换分支:当你需要切换到另一个分支,但当前分支的更改尚未完成或不想提交时
- 需要重置当前分支:在进行实验性更改后,需要回到分支的干净状态,但又不想丢失这些更改
- 解决合并冲突:在合并时遇到冲突,需要临时撤回更改以便重新合并
- 保持工作目录整洁:在进行日常开发时,需要保持工作目录的整洁,避免因未提交的更改导致的问题
- 临时应用某个更改:当你想临时应用某个更改,看看效果,但不想立即提交时
- 处理紧急修复:在处理紧急修复时,可以使用
git stash
保存当前工作,修复完成后再恢复之前的工作
使用 git stash
时的注意事项:
- 使用
git stash list
查看当前存储的 stash 列表 - 使用
git stash apply
恢复 stash 列表中的更改,但不从堆栈中删除该 stash - 使用
git stash pop
恢复 stash 并从堆栈中删除该 stash - 使用
git stash drop
删除指定的 stash - 使用
git stash clear
清空所有 stash
# git pull 和 git fetch 命令分别有什么用?二者有什么区别?
git pull
和 git fetch
都是 Git 分布式版本控制系统中用于与远程仓库进行交互的命令,但它们的作用和行为有所不同
git pull 命令的作用:
git pull
命令用于从远程仓库获取最新提交并尝试将它们合并到当前分支- 它实际上是
git fetch
加上git merge
的快捷方式,即首先获取远程分支的最新状态,然后将这些更改合并到当前分支
git fetch 命令的作用:
git fetch
命令用于从远程仓库获取所有分支的更新,但不会自动合并到当前分支- 它只会将远程分支的最新状态下载到本地,让你可以查看或手动合并这些更改
二者之间的区别:
- 合并操作:
git pull
会自动执行合并操作,将远程分支的更改合并到当前分支git fetch
不会自动合并,只下载远程分支的最新状态
- 自动性:
git pull
是一个自动合并的过程,可能会在没有准备的情况下改变你的工作目录git fetch
需要手动合并或查看更改,给你更多的控制权
- 冲突处理:
git pull
在合并过程中可能会产生冲突,需要你解决这些冲突git fetch
不会合并,因此不会产生合并冲突,但你可以检查git diff
来查看远程分支与本地分支的差异
- 安全性:
git pull
可能会因为自动合并而覆盖本地的更改,如果不正确处理,可能会丢失工作git fetch
更安全,因为它不会改变本地的工作目录状态
- 使用场景:
git pull
适用于当你想要快速获取远程更改并合并到本地时git fetch
适用于当你想要查看远程更改或在合并前进行一些准备工作时
- 变基操作:
git pull
可以使用参数--rebase
来执行变基而不是合并,使得历史更加线性git fetch
只负责获取,不涉及变基操作,但获取后可以手动执行git rebase
# 什么是低代码?你用过哪些低代码工具?
**低代码(Low-Code)**是一种开发方法论,它允许开发者通过图形界面和模型驱动的抽象来构建应用程序,而不需要手写大量的代码。低代码平台通常提供拖放界面、预构建的模板、逻辑构建块和流程自动化工具,使得开发过程更加快速和高效
低代码开发的主要特点包括:
- 快速开发:通过减少代码编写量,加快应用开发速度
- 易于使用:图形界面使得非专业开发者也能参与应用构建
- 集成性:低代码平台通常能够轻松集成各种数据源和服务
- 可扩展性:虽然通过低代码平台快速构建应用,但通常也支持扩展和自定义代码
- 跨平台:低代码应用往往能够跨多个平台和设备运行
低代码工具:
- OutSystems:一个领先的低代码开发平台,提供广泛的功能来构建、部署和管理企业级应用
- Mendix:提供快速应用程序开发的工具,支持模型驱动和事件驱动的逻辑
- Appian:专注于业务流程管理和自动化的低代码平台
- Microsoft Power Apps:微软提供的低代码解决方案,可以快速构建自定义应用并与 Microsoft 服务集成
- Salesforce Lightning Web Components:用于构建自定义、可重用的 Web 组件的低代码工具
- Google App Maker:现已停用,之前是 Google 提供的用于构建企业应用的低代码平台
- Zoho Creator:一个在线低代码开发平台,用于构建自定义业务应用
- WaveMaker:提供快速构建跨平台企业级 Web 和移动应用的工具
- Betty Blocks:专注于构建企业级应用的低代码开发平台
- FileMaker:虽然不是传统意义上的低代码平台,但提供了可视化的数据库和应用构建工具
# 什么是前端跨平台?你用过哪些跨平台框架?
前端跨平台是指开发一次,能够在多个平台(如不同的操作系统、浏览器或设备)上运行的前端应用程序的方法或技术。这种能力通常通过抽象层来实现,该抽象层隐藏了底层平台的特定实现细节
前端跨平台的目标是:
- 代码复用:减少为每个平台编写和维护特定代码的需要
- 用户体验一致性:在所有平台上提供相似的用户体验和界面
- 开发效率:加快开发速度,降低成本
# 跨平台框架:
- React Native:
- 允许使用 JavaScript 和 React 来开发原生移动应用
- Flutter:
- 由 Google 开发,使用 Dart 语言,提供丰富的组件来构建跨平台的移动和桌面应用
- Xamarin:
- 允许使用 C# 和 .NET 来构建跨平台的移动应用
- Apache Cordova / PhoneGap:
- 使用 HTML、CSS 和 JavaScript 来构建封装在 WebView 中的移动应用
- Electron:
- 允许使用 Web 技术(HTML、CSS 和 JavaScript)来构建跨平台的桌面应用
- Ionic:
- 结合 Angular 或其他前端框架,使用 Web 技术构建跨平台的移动和桌面应用
- NativeScript:
- 使用 JavaScript 或 TypeScript 来构建原生移动应用
- Framework7:
- 适用于构建 iOS 和 Android 应用的全 JavaScript 解决方案
- Quasar:
- 一个使用 Vue.js 的框架,可以构建响应式的网站、移动应用和桌面应用
- Svelte Native:
- 基于 Svelte 的框架,用于构建本地移动应用
- Uni-app:
- 使用 Vue.js 开发所有前端应用的框架,支持编译到 iOS、Android、Web(包括 PC 和移动端浏览器)平台
- Taro:
- 一个使用 React 语法的框架,用于构建跨平台应用