前端复习整理





























前端面试复习整理
html css
常见标签
hr标签是水平线。属性直接设置。
b strong标签都是加粗,但是推荐strong标签,对于爬虫更友好,更语义化。
i和em标签都是倾斜,推荐em
s和del都是删除线,推荐del
u 文本下划线
sub下标
sup上标
特殊标签
将标签转化成字符显示在网页中。

<hr color=“red” size=“5” width=“50%” />
‘&emsp’空格,占据一个中文宽度
有序列表
ol li的type只有1,a,A,i,I。这几种类型,输入其他的默认为数字类型
start表示从哪开始,取值只能是数字
自定义列
-
图片
- 文字
html5新增标签
header
nav 定义导航链接区域,如主导航、侧边栏菜单等。
main每个页面只能有一个
footer
audio
video
canvas
svg
time
元素选择器
直接子元素 > 选择div下面的直接的p标签,而不是全部的
相邻兄弟元素 + div+p 相邻的p标签
css3新特性
flex grid布局
{
display:grid;
grid-template-columns:repeat(4,1fr);平均分为4份
grid-template-columns:repeat(auto-fill,minmax(260px,1fr));设置响应式,最小260px
gap:30px;间隔
}
.item{
grid-row:1/3;从第一行往下合并两个。就是1+2
grid-column:1/3;从第一列往右合并两个
}
媒体查询
渐变
background: linear-gradient(to right, red, yellow); /* 线性渐变 */
background: radial-gradient(circle, blue, green); /* 径向渐变 */
圆角 border-radius: 上左 上右 下右 下左; /* 按顺时针方向指定每个角 */
阴影 box-shadow
/* 基本阴影(右下偏移,5px模糊) */
水平阴影位置(必需),正值向右,负值向左。
垂直阴影位置(必需),正值向下,负值向上。
模糊半径(可选),值越大阴影越模糊。
.shadow-basic {
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3);
}
文字阴影
text-shadow
动画 transform
animation:name duration timing-function delay iteratiom-count direction;
| 值 | 描述 |
|---|---|
| name | 动画名称 |
| duration | 动画持续时间 |
| timing-function | 动画效果的速率 |
| delay | 动画开始的延迟时间 |
| iteratiom-count | 动画循环的次数,infinite为无限循环 |
| direction | 设置动画播放方向 |
| animation-play-state | 动画播放状态,running播放,paused停止 |
| timing-function | 描述 |
|---|---|
| ease | 默认,逐渐变慢 |
| linear | 匀速 |
| ease-in | 加速 |
| ease-out | 减速 |
| ease-in-out | 先加速后减速 |
| direction | 描述 |
|---|---|
| normal | 默认值,向前播放 |
| alternate | 偶数次向前,奇数次反方向 |
@keyframes mDH {
0% {
color: red;
}
50% {
color: blue;
}
100% {
color: red;
}
}
媒体查询


九宫格鼠标移入其中一个就会自动变大
sass
<div class='contain'>
<div class='item'></div>
<div class='item'></div>
<div class='item'></div>
<div class='item'></div>
<div class='item'></div>
<div class='item'></div>
<div class='item'></div>
<div class='item'></div>
<div class='item'></div>
</div>
.contain{
width:400px;
height:400px;
display:grid;
grid-template-columns:1fr 1fr 1fr;
grid-template-rows:1fr 1fr 1fr;
gap:10px;间隔
}
循环1到9 增加背景颜色
@for $i form 1 through 9 {
.item:nth-child(#{$i}){
background:hsl($i * 40%,100%(饱和度),80%亮度)
}
}
给父节点增加判断,当第一个子节点移入的时候,父元素发生改变
.contain:has(.item:nth-child(1):hover){
grid-template-columns:2fr 1fr 1fr;
grid-template-rows:2fr 1fr 1fr;
}
元素不可见的几种方式及区别
总结对比表
| 方式 | 渲染存在 | 占位空间 | 事件响应 | 过渡支持 | 典型场景 |
|---|---|---|---|---|---|
display: none | ❌ | ❌ | ❌ | ❌ | 彻底移除元素 |
visibility: hidden | ✅ | ✅ | ❌ | ✅(需配合) | 保留布局的临时隐藏 |
opacity: 0 | ✅ | ✅ | ✅(默认) | ✅ | 渐变隐藏 / 保留交互 |
position: absolute | ✅ | ❌ | ✅ | ❌ | 屏幕外隐藏(无障碍) |
clip-path | ✅ | ✅ | ✅(区域内) | ✅ | 自定义形状裁剪 |
z-index | ✅ | ✅ | ✅ | ❌ | 层级覆盖(非标准方式) |
aria-hidden | ✅ | ✅ | ✅ | ❌ | 无障碍隐藏(需配合 CSS) |
怎么让一个 div 水平垂直居中
一、Flexbox(现代方案)
特点:简单高效,兼容性好(IE11+),适合大多数场景。
1. 父元素设置 Flexbox
<div class="parent">
<div class="child">居中内容</div>
</div>
<style>
.parent {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
height: 300px; /* 需要明确高度 */
border: 1px solid #ccc;
}
.child {
padding: 20px;
background: #eee;
}
</style>
2. 父元素设置 Inline-Flex
.parent {
display: inline-flex;
justify-content: center;
align-items: center;
height: 300px;
width: 100%;
}
二、Grid(现代方案)
特点:二维布局,更强大的居中能力,兼容性稍差(IE 不支持)。
<div class="parent">
<div class="child">居中内容</div>
</div>
<style>
.parent {
display: grid;
place-items: center; /* 同时实现水平和垂直居中 */
height: 300px;
border: 1px solid #ccc;
}
.child {
padding: 20px;
background: #eee;
}
</style>
三、绝对定位 + 负边距(已知宽高)
特点:兼容性好(IE6+),但需要明确知道元素的宽高。
<div class="parent">
<div class="child">居中内容</div>
</div>
<style>
.parent {
position: relative;
height: 300px;
border: 1px solid #ccc;
}
.child {
position: absolute;
top: 50%; /* 顶部偏移 50% */
left: 50%; /* 左侧偏移 50% */
width: 200px; /* 明确宽度 */
height: 100px; /* 明确高度 */
margin-top: -50px; /* 向上偏移自身高度的一半 */
margin-left: -100px; /* 向左偏移自身宽度的一半 */
background: #eee;
}
</style>
四、绝对定位 + transform(未知宽高)
特点:无需知道元素宽高,兼容性良好(IE9+)。
<div class="parent">
<div class="child">居中内容</div>
</div>
<style>
.parent {
position: relative;
height: 300px;
border: 1px solid #ccc;
}
.child {
position: absolute;
top: 50%; /* 顶部偏移 50% */
left: 50%; /* 左侧偏移 50% */
transform: translate(-50%, -50%); /* 向相反方向偏移自身宽高的 50% */
background: #eee;
}
</style>
五、绝对定位 + 四方向 0 + margin: auto
特点:无需知道元素宽高,兼容性良好(IE8+)。
<div class="parent">
<div class="child">居中内容</div>
</div>
<style>
.parent {
position: relative;
height: 300px;
border: 1px solid #ccc;
}
.child {
position: absolute;
top: 0; /* 上、下、左、右均为 0 */
bottom: 0;
left: 0;
right: 0;
margin: auto; /* 自动计算边距 */
width: 200px; /* 需指定宽度 */
height: 100px; /* 需指定高度 */
background: #eee;
}
</style>
常见伪类选择器及使用场景
一、基础状态伪类
作用:匹配元素的状态(如交互状态、选中状态等)。
使用场景:
-
:hover(鼠标悬停)- 按钮悬停时改变颜色或样式(如电商网站商品卡片悬停显示更多信息)。
- 导航菜单悬停时展开子菜单。
.button:hover { background-color: #4CAF50; } -
:active(激活状态,鼠标按下未松开)- 按钮点击时的视觉反馈(如按钮按下时轻微下沉效果)。
.button:active { transform: scale(0.98); } -
:focus(获取焦点,如表单输入框)- 突出显示获得焦点的输入框(常用于提升 accessibility)。
input:focus { border-color: #2196F3; box-shadow: 0 0 5px rgba(33, 150, 243, 0.3); } -
:checked(单选框 / 复选框选中状态)- 自定义表单控件的选中样式(如替换默认 checkbox 的外观)。
input[type="checkbox"]:checked + label { color: #4CAF50; }
二、结构性伪类
作用:匹配文档结构中的元素(如父元素中的第 n 个子元素)。
使用场景:
-
:nth-child(n)(匹配父元素的第 n 个子元素)- 奇偶行交替背景色(如表格隔行变色)。
tr:nth-child(even) { background-color: #f2f2f2; }- 每 3 列设置不同宽度(响应式布局)。
.column:nth-child(3n) { width: 30%; } -
:first-child/:last-child(首个 / 最后一个子元素)- 列表首尾元素特殊样式(如删除首尾元素的边框)。
li:first-child { border-top: none; } li:last-child { border-bottom: none; } -
:empty(匹配无子元素或文本的空元素)- 隐藏空容器(如无内容的提示框)。
.message:empty { display: none; }
三、目标伪类
作用:匹配当前 URL 指向的锚点元素。
使用场景:
-
:target(哈希值匹配的元素)
- 锚点跳转时的高亮效果(如目录跳转时突出显示目标章节)。
#section:target { border: 2px solid #2196F3; }
四、否定伪类 :not()
作用:排除匹配的选择器,匹配不符合条件的元素。
语法::not(selector)(括号内为要排除的选择器)。
使用场景:
-
排除特定元素
- 选择所有非
<button>元素的按钮类(如只样式化<a>标签的按钮)。
.btn:not(button) { background-color: #673AB7; } /* 匹配 <a class="btn"> 等非 button 元素 */ - 选择所有非
-
批量排除多个条件
- 选择列表中不是第 1 个和最后 1 个的元素(中间元素特殊样式)。
li:not(:first-child):not(:last-child) { margin: 0 10px; } -
表单校验场景
- 选中未勾选的复选框的标签(提示未选中状态)。
input:not(:checked) + label { color: #ff4444; }
伪类选择器的注意事项
- 优先级:伪类属于 CSS 选择器的一部分,优先级按选择器类型计算(如类选择器优先级为 10,伪类同理)。
- 顺序问题:状态伪类(
:hover/:active/:focus)需按:link→:visited→:hover→:active的顺序定义(LVHA 规则),否则可能不生效。 - 兼容性:现代浏览器基本支持所有伪类,但旧版本 IE(如 IE8 及以下)对部分结构性伪类支持有限,需注意兼容性处理。
CSS 盒模型(Box Model)
盒模型分为两种类型:标准盒模型(W3C 盒模型)**和**IE 盒模型(怪异盒模型),两者的主要区别在于尺寸计算方式不同。
通过box-sizing属性可以手动切换盒模型类型:
1. box-sizing: content-box;(默认值,标准盒模型)
- 尺寸计算遵循标准盒模型规则(
width/height仅表示内容区)。不包括padding,border,margin
2. box-sizing: border-box;(怪异盒模型)
- 尺寸计算遵循 IE 盒模型规则(
width/height包含内容区、内边距、边框)。不包括margin
什么是CSS预处理器?它们有哪些优点?
CSS 预处理器是一种扩展 CSS 的工具,允许开发者使用更高级的语法来编写样式表。这些预处理器通过提供变量、嵌套、混入(mixins)、函数等功能,使得 CSS 的书写更加简洁和模块化。预处理器通常会将这些高级语法编译成标准的 CSS 代码。
预处理器的工作流程
编写代码:使用预处理器提供的扩展语法编写样式。
编译:运行预处理器,将编写的代码转换为标准的 CSS 文件。
使用:在 HTML 文件中引入编译后的 CSS 文件。
- CSS 预处理器的优势
3.1 变量
预处理器允许使用变量来存储常用的值,如颜色、字体、间距等。这种方式使得样式的修改和维护变得更加方便。
$primary-color: #3498db;
.button {
background-color: $primary-color;
}
3.2 嵌套
预处理器支持嵌套规则,使得样式的结构更加清晰和直观。
.nav {
ul {
list-style: none;
}
li {
display: inline-block;
}
}
3.3 混入(Mixins)
混入允许定义可重用的样式块,可以接受参数,从而实现更灵活的样式复用。
@mixin border-radius($radius) {
border-radius: $radius;
}
.box {
@include border-radius(10px);
}
3.4 函数
预处理器提供函数,可以对值进行计算和处理,增强了样式的动态性。
$base-font-size: 16px;
.text {
font-size: calc(1.5 * $base-font-size);
}
3.5 模块化
预处理器支持将样式分割成多个文件,通过导入(import)功能实现样式的模块化,提升了项目的组织性。
// main.scss
@import ‘variables’;
@import ‘mixins’;
@import ‘components’;
原文链接:https://blog.csdn.net/qq_38822390/article/details/147119845
CSS动画和过渡(Transitions)有什么区别?请给出一个使用CSS动画的例子,如何让一个div360°不停旋转
核心区别
| 特性 | 过渡(Transitions) | 动画(Animations) |
|---|---|---|
| 触发方式 | 状态变化(如:hover、:focus、JS 动态修改) | 自动执行或通过 JS 触发 |
| 关键帧控制 | 仅起始和结束状态(2 个关键帧) | 支持多个关键帧(0%~100% 任意百分比) |
| 循环与方向 | 不支持循环(需配合 JS),单向过渡 | 支持无限循环(infinite)、反向播放(alternate) |
| 时间函数 | 预定义曲线(如ease)或贝塞尔曲线 | 同过渡,但可针对每个关键帧设置不同曲线 |
| 应用场景 | 简单状态变化(如悬停效果、显示 / 隐藏) | 复杂动画(旋转、弹跳、淡入淡出序列) |
.rotating-element {
width: 100px;
height: 100px;
background-color: #3498db;
margin: 50px auto;
/* 应用旋转动画 */
animation: rotate 3s linear infinite;
}
rotate:动画名称(对应 @keyframes 的名称)。
3s:动画周期为 3 秒。
linear:匀速旋转(无加速减速)。
infinite:无限循环。
/* 定义关键帧动画 */
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 可选:悬停时暂停动画 */
.rotating-element:hover {
animation-play-state: paused;
}
如何实现响应式布局
媒体查询
/* 超小屏幕(手机) */
@media (max-width: 575px) { ... }
/* 小屏幕(平板) */
@media (min-width: 576px) and (max-width: 767px) { ... }
flex
grid
流式布局通常指用百分比代替固定像素定义元素宽度,使内容随容器宽度伸缩。
有没有用过svg,如何改变svg字体颜色
一、直接在 SVG 文件中修改
如果 SVG 是内联在 HTML 中的,可以直接修改 <text> 元素的 fill 属性:
<svg width="200" height="100" viewBox="0 0 200 100">
<text x="50" y="50" fill="#FF0000">红色文本</text> <!-- 直接设置 fill 属性 -->
</svg>
二、通过 CSS 类或 ID 选择器修改
为 SVG 文本添加类名或 ID,然后通过 CSS 样式表修改:
<svg width="200" height="100" viewBox="0 0 200 100">
<text id="myText" class="text-style" x="50" y="50">可变色文本</text>
</svg>
<style>
/* 方法1:通过类选择器 */
.text-style {
fill: blue; /* 设置文本颜色 */
}
/* 方法2:通过 ID 选择器(优先级更高) */
#myText {
fill: green; /* 覆盖类的颜色 */
}
</style>
三、使用 CSS 变量(Custom Properties)动态控制
通过 CSS 变量可以在运行时动态修改 SVG 颜色:
<svg width="200" height="100" viewBox="0 0 200 100">
<style>
text {
fill: var(--text-color, black); /* 使用 CSS 变量,默认黑色 */
}
</style>
<text x="50" y="50">动态颜色文本</text>
</svg>
<script>
// 通过 JavaScript 修改 CSS 变量
document.querySelector('svg').style.setProperty('--text-color', 'purple');
</script>
四、外部 CSS 文件引用
如果 SVG 作为独立文件存在,可以通过 <style> 标签或外部 CSS 引用:
<!-- my-svg.svg -->
<svg width="200" height="100" viewBox="0 0 200 100">
<style>
@import url("styles.css"); /* 引用外部 CSS */
</style>
<text class="styled-text" x="50" y="50">外部样式文本</text>
</svg>
/* styles.css */
.styled-text {
fill: orange;
}
五、通过 JavaScript 动态修改
使用 JavaScript 直接操作 SVG 元素的属性:
<svg id="mySvg" width="200" height="100" viewBox="0 0 200 100">
<text id="dynamicText" x="50" y="50">点击改变颜色</text>
</svg>
<script>
const textElement = document.getElementById('dynamicText');
// 点击时改变颜色
textElement.addEventListener('click', () => {
textElement.setAttribute('fill', '#2ECC71'); // 设置为绿色
});
</script>
六、注意事项
- 优先级规则:内联样式(
fill属性) > ID 选择器 > 类选择器 > 外部 CSS。 - 继承性:如果
<text>元素没有明确设置fill,它会继承父元素的fill值。 - CSS 作用域:外部 CSS 对 SVG 的作用取决于 SVG 的引入方式(内联 vs.
<img>标签)。使用<img>标签引入的 SVG 无法被外部 CSS 样式覆盖。 - 与
<tspan>结合:如果 SVG 文本使用了<tspan>子元素,每个<tspan>可以单独设置颜色。
<text x="50" y="50" fill="blue">
蓝色文本
<tspan fill="red">红色部分</tspan>
</text>
js
如何在JavaScript中创建新的DOM元素,并将其添加到现有的DOM中?
1. 创建新元素:document.createElement(tagName)
const newDiv = document.createElement('div'); // 创建<div>元素
2. 设置元素属性与内容
-
设置属性:
element.setAttribute(name, value)newDiv.setAttribute('id', 'myNewDiv'); // 设置ID newDiv.setAttribute('class', 'container'); // 设置类名 -
直接操作属性:
newDiv.id = 'myNewDiv'; newDiv.className = 'container'; newDiv.style.color = 'red'; // 直接设置样式 -
设置内容:
newDiv.textContent = '这是文本内容'; // 纯文本 newDiv.innerHTML = '<p>这是HTML内容</p>'; // HTML内容
3. 添加到现有 DOM 中
-
追加到父元素末尾:
parentElement.appendChild(newElement)document.body.appendChild(newDiv); // 添加到<body>末尾 -
插入到指定位置:
parentElement.insertBefore(newElement, referenceElement)const parent = document.getElementById('parent'); const refChild = document.getElementById('reference'); parent.insertBefore(newDiv, refChild); // 插入到reference元素之前
数据类型检测的方式有哪些?(instanceof的实现原理)?null 和 undefined所属的数据类型(基本)? 区别?
1. typeof 操作符
-
适用场景:检测基本数据类型(除
null外)。 -
返回值:
number、string、boolean、object、function、undefined、symbol、bigint。 -
局限性
typeof null返回object(历史遗留问题)。
-
无法区分
Array、Date、RegExp等对象类型(均返回object)。
2. instanceof 操作符
-
作用:检测对象是否属于某个构造函数的实例(通过原型链判断)。
-
示例
const arr = [];
arr instanceof Array; // true
arr instanceof Object; // true(所有对象继承自 Object)
- 实现原理
判断右边构造函数的
prototype
属性是否存在于左边对象的原型链上。
等价于以下代码:
```javascript
function myInstanceof(obj, Constructor) {
const prototype = Constructor.prototype;
let proto = obj.__proto__; // 或 Object.getPrototypeOf(obj)
while (proto !== null) {
if (proto === prototype) return true;
proto = proto.__proto__;
}
return false;
}
3. constructor 属性
-
作用:返回创建对象的构造函数。
-
示例
[].constructor === Array; // true
(new Date()).constructor === Date; // true
-
局限性
- 可以被修改(如
obj.constructor = null)。
- 可以被修改(如
-
null和undefined没有constructor属性。
4. Object.prototype.toString.call()
-
作用:返回对象的精确类型(内部
[[Class]]属性)。 -
示例
Object.prototype.toString.call([]); // “[object Array]”
Object.prototype.toString.call(null); // “[object Null]”
Object.prototype.toString.call(new Date()); // “[object Date]”
- **优点**:能区分所有内置对象类型,包括 `null` 和 `undefined`。
#### **5. Array.isArray()**
- **作用**:专门检测是否为数组(等价于 `obj instanceof Array`)。
- 示例
```javascript
Array.isArray([]); // true
Array.isArray({}); // false
### **null 和 undefined 的区别**
#### **1. 数据类型归属**
- **null**:属于基本数据类型,值为 `null`,表示**有意为之的空值**。
- **undefined**:属于基本数据类型,值为 `undefined`,表示**未定义或缺失值**。
#### **2. 产生场景**
- **null**:
- 手动赋值(如 `const obj = null`)。
- DOM 查询未找到元素时返回 `null`(如 `document.getElementById('not-exist')`)。
- **undefined**:
- 变量声明但未赋值(如 `let x; // x === undefined`)。
- 函数无返回值时默认返回 `undefined`。
- 访问对象不存在的属性(如 `obj.unknown`)。
- 函数参数未传递时(如 `function f(a) { a === undefined }`)。
#### **3. 严格比较**
```javascript
null === undefined; // false
null == undefined; // true(宽松相等)
4. 用途差异
- null:主动释放对象引用(如
obj = null帮助垃圾回收)。 - undefined:表示自然缺失的状态(如函数参数未传值)。
总结
| 方法 | 适用场景 | 局限性 |
|---|---|---|
typeof | 基本类型检测(除 null) | 无法区分 Array、Date 等对象类型 |
instanceof | 检测对象是否为某个构造函数的实例 | 受原型链影响,无法检测基本类型 |
constructor | 返回对象的构造函数 | 可被修改,null/undefined 无效 |
Object.prototype.toString.call() | 精确检测所有内置对象类型 | 无法检测自定义类 |
Array.isArray() | 专门检测数组 | 仅适用于数组检测 |
null vs. undefined:
null是主动设置的空值,undefined是自然产生的未定义值。typeof null为object,typeof undefined为undefined。- 两者宽松相等(
==),但严格不相等(===)。
如何判断一个变量是不是数组?数据循环常用方法?数组去重?数组扁平化?
Array.isArray()
-
推荐方式
:直接判断是否为数组,兼容性良好(IE9+)。
Array.isArray([]); // true
Array.isArray({}); // false
2. Object.prototype.toString.call()
- 精确判断
[object Array]
,不受原型链影响。
Object.prototype.toString.call([]) === '[object Array]'; // true
3. instanceof 操作符
-
基于原型链
:判断对象是否由
[] instanceof Array; // true
局限性
:在 iframe 或跨窗口环境中可能失效(不同窗口的构造函数不同)。
#### **4. constructor 属性**
- 检查构造函数
:数组的指向。
```javascript
[].constructor === Array; // true
局限性
:可被修改(如
arr.constructor = null
)。
二、数据循环常用方法
1. for 循环
-
适用场景
:数组、类数组对象(如arguments、DOM NodeList)。
const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
2. for…of 循环
-
适用场景
:可迭代对象(数组、字符串、Set、Map 等)。
const arr = [1, 2, 3];
for (const item of arr) {
console.log(item);
}
优势
:支持break,continue,直接获取值而非索引。
#### **3. forEach 方法**
- 数组专用
:遍历数组,对每个元素执行回调函数。
```javascript
const arr = [1, 2, 3];
arr.forEach((item, index, array) => {
console.log(item);
});
局限性
:无法中断循环(无break)。
#### **4. map 方法**
- 数组专用
:返回新数组,新元素为回调函数的返回值。
```javascript
const arr = [1, 2, 3];
const doubled = arr.map(x => x * 2); // [2, 4, 6]
5. filter 方法
-
数组专用
:过滤元素,返回满足条件的新数组。
const arr = [1, 2, 3];
const even = arr.filter(x => x % 2 === 0); // [2]
6. reduce 方法
-
数组专用
:累加器,将数组元素合并为单个值。
const arr = [1, 2, 3];
const sum = arr.reduce((acc, curr) => acc + curr, 0); // 6
### 三、数组去重方法
#### **1. Set 数据结构(ES6)**
- 原理
:Set 不允许重复值。
```javascript
const arr = [1, 2, 2, 3];
const unique = [...new Set(arr)]; // [1, 2, 3]
优势
:简洁高效,适用于基本类型。
#### **2. filter + indexOf**
- 原理
:保留首次出现的元素。
```javascript
const arr = [1, 2, 2, 3];
const unique = arr.filter((item, index) => arr.indexOf(item) === index); // [1, 2, 3]
局限性
:对对象引用无效(如[{a:1}, {a:1}]无法去重)。
3. 循环 + 对象键名唯一性
-
原理
:利用对象键名不重复的特性。
const arr = [1, 2, 2, 3];
const obj = {};
const unique = arr.filter(item => {
return obj.hasOwnProperty(item) ? false : (obj[item] = true);
}); // [1, 2, 3]
优势
:可自定义判断逻辑(如忽略对象属性差异)。
### 四、数组扁平化(多维数组转一维)
#### **1. flat () 方法(ES2019)**
- 参数
:指定扁平化深度(默认 1),
```js
Infinity flat() 会移除数组中的空项(如 undefined、空槽)
表示完全扁平化。
const arr = [1, [2, [3]]];
arr.flat(2); // [1, 2, 3]
arr.flat(Infinity); // [1, 2, 3]
2. 递归 + reduce
-
原理
:遍历数组,遇到子数组则递归处理。
const arr = [1, [2, [3]]];
const flatten = arr => arr.reduce(
(acc, curr) => acc.concat(Array.isArray(curr) ? flatten(curr) : curr),
[]
); // [1, 2, 3]
#### **3. 扩展运算符 + 循环**
- 原理
:通过扩展运算符展开一层数组,循环至完全扁平化。
```javascript
const arr = [1, [2, [3]]];
let result = arr;
while (result.some(Array.isArray)) {
result = [].concat(...result);
} // [1, 2, 3]
五、综合示例
判断数组并去重
function processArray(input) {
if (!Array.isArray(input)) {
throw new Error('输入必须是数组');
}
return [...new Set(input)];
}
processArray([1, 2, 2, 3]); // [1, 2, 3]
扁平化并去重
const nested = [1, [2, [3, 3]]];
const flattened = nested.flat(Infinity); // [1, 2, 3, 3]
const unique = [...new Set(flattened)]; // [1, 2, 3]
六、性能与兼容性
| 方法 | 性能(百万级数据) | 兼容性 |
|---|---|---|
Set | 最快 | IE 不支持 |
filter+indexOf | 中等 | 全浏览器支持 |
flat() | 中等 | IE/Edge 不支持 |
| 递归 | 较慢(深层嵌套时) | 全浏览器支持 |
推荐组合:
- 判断数组:优先
Array.isArray(),兼容需求高时用toString.call()。 - 去重:基本类型用
Set,复杂对象用filter+indexOf。 - 扁平化:现代浏览器用
flat(Infinity),否则用递归。
map和foreach有什么区别?
map() 和 forEach() 是 JavaScript 数组的两个常用迭代方法,它们的主要区别体现在 返回值、用途 和 链式调用能力 上:
一、返回值与用途
| 特性 | map() | forEach() |
|---|---|---|
| 返回值 | 新数组:包含每个元素处理后的结果 | undefined:仅执行副作用 |
| 核心用途 | 转换数据:生成与原数组等长的新数组 | 执行操作:遍历数组执行副作用(如修改 DOM、打印日志) |
| 链式调用 | 支持(返回数组) | 不支持(返回 undefined) |
怎么创建一个对象,如何新增对象属性?如何判断一个对象是空对象?(.stringify、Object.keys())
创建对象的方法
1. 对象字面量(最常用)
const obj = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'coding']
};
2. 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person('Bob', 25);
// person: { name: 'Bob', age: 25 }
3. Object.create()
- 创建一个继承指定原型的新对象。
const prototype = { greet() { return `Hello, ${this.name}`; } };
const obj = Object.create(prototype);
obj.name = 'Charlie';
obj.greet(); // "Hello, Charlie"
4. 类语法(ES6)
class Animal {
constructor(name) {
this.name = name;
}
}
const cat = new Animal('Kitty');
// cat: { name: 'Kitty' }
5. Object.assign()
- 合并多个对象,返回新对象。
const obj = Object.assign(
{ name: 'Alice' },
{ age: 30 }
);
// obj: { name: 'Alice', age: 30 }
二、新增对象属性的方法
1. 点表示法
const obj = {};
obj.name = 'Alice';
obj['age'] = 30; // 方括号语法(动态属性名)
2. 方括号表示法
- 适用于属性名包含特殊字符或变量的场景。
const key = 'hobbies';
obj[key] = ['reading', 'coding'];
3. Object.defineProperty()
- 精确控制属性配置(如是否可枚举、可写)。
Object.defineProperty(obj, 'email', {
value: 'alice@example.com',
writable: false, // 不可修改
enumerable: true // 可枚举(出现在for...in中)
});
4. 解构赋值(ES6)
- 合并现有对象并添加新属性。
const obj = { name: 'Alice' };
const newObj = { ...obj, age: 30 };
// newObj: { name: 'Alice', age: 30 }
三、判断空对象的方法
1. Object.keys ()(推荐)
- 判断对象是否没有自有可枚举属性。
const obj = {};
Object.keys(obj).length === 0; // true
2. for…in 循环
- 检查是否存在可枚举属性(包括原型链上的)。
function isEmpty(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) return false; // 存在自有属性
}
return true;
}
3. JSON.stringify()
- 适用于简单对象(不含函数、Symbol 等无法序列化的值)。
JSON.stringify(obj) === '{}'; // true
4. Reflect.ownKeys()
- 判断对象是否没有任何自有属性(包括不可枚举和 Symbol 类型)。
Reflect.ownKeys(obj).length === 0; // true
for…in 和 for…of 的区别?(in会遍历整个对象原型链,of遍历当前对象)
for...in 和 for...of 是 JavaScript 中两种不同的循环语法,主要区别体现在 遍历对象类型、遍历内容 和 设计目的 上:
一、语法与基本差异
| 特性 | for...in | for...of |
|---|---|---|
| 遍历对象 | 可枚举属性(包括原型链) | 可迭代对象(如数组、字符串、Set、Map) |
| 遍历内容 | 键名(字符串或 Symbol) | 值 |
| 设计目的 | 遍历对象属性 | 遍历数据结构的值 |
如何实现浏览器内多个标签页之间的通信?localStorage和cookie有什么区别;有没有用过WebSockets
一、多标签页通信的实现方式
1. localStorage + Storage 事件
-
原理:当一个标签页修改
localStorage时,其他标签页会触发storage事件。// 发送数据 localStorage.setItem('message', 'Hello from Tab 1'); // 接收数据(在其他标签页监听) window.addEventListener('storage', (event) => { if (event.key === 'message') { console.log('收到消息:', event.newValue); } });注意:修改localStorage的标签页不会触发自身的storage事件。
2. Broadcast Channel API
-
原理:创建命名频道,同一域名下的所有标签页可监听该频道。
// 发送数据 const channel = new BroadcastChannel('my-channel'); channel.postMessage('Hello from Tab 1'); // 接收数据 const channel = new BroadcastChannel('my-channel'); channel.onmessage = (event) => { console.log('收到消息:', event.data); };优势
:简洁高效,支持双向通信。
3. SharedWorker
-
原理:多个标签页共享同一个后台线程,通过
postMessage通信。// 主页面代码 const worker = new SharedWorker('shared-worker.js'); worker.port.postMessage('Hello from main page'); worker.port.onmessage = (event) => { console.log('收到Worker消息:', event.data); }; // shared-worker.js self.onconnect = (event) => { const port = event.ports[0]; port.onmessage = (event) => { // 广播给所有连接的标签页 port.postMessage('Worker received: ' + event.data); }; };局限性
:需单独维护 Worker 文件,兼容性略差。
4. IndexedDB
- 原理:多个标签页可访问同一数据库,通过监听变化实现通信。
- 适用场景:需持久化存储且数据量较大的场景。
二、localStorage vs. cookie
| 特性 | localStorage | cookie |
|---|---|---|
| 存储大小 | 约 5-10MB(因浏览器而异) | 约 4KB |
| 有效期 | 永久存储(需手动删除) | 可设置过期时间(expires/max-age) |
| 数据发送 | 仅在浏览器端可用,不随请求发送 | 自动随 HTTP 请求发送到服务器 |
| 作用域 | 同一域名下所有标签页共享 | 基于域名和路径(path 属性) |
| 安全性 | 不加密,易受 XSS 攻击 | 可设置 HttpOnly、Secure 等属性 |
| API 复杂度 | 简单(setItem/getItem) | 需手动解析和序列化 |
三、WebSocket 使用经验
1. WebSocket 简介
- 特点:双向实时通信,基于 TCP 协议,无轮询开销。
- 适用场景:实时聊天、实时数据推送(如股票行情)、游戏等。
2. 基本用法
// 客户端代码
const socket = new WebSocket('ws://example.com/socket');
// 连接建立时
socket.onopen = () => {
socket.send('Hello, server!');
};
// 收到消息时
socket.onmessage = (event) => {
console.log('收到服务器消息:', event.data);
};
// 连接关闭时
socket.onclose = () => {
console.log('连接已关闭');
};
// 错误处理
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
Ajax默认异步还是同步?ajax异步编程的实现方式?ajax的实现原理?如何实现多个请求并发(promise.all或者async await)
一、Ajax 默认异步还是同步?
默认是异步的。从 XMLHttpRequest(XHR)的设计来看:
- 异步模式(默认):请求发送后,JS 代码会继续执行,不会阻塞后续逻辑。
- 同步模式(已被弃用):通过
xhr.open(method, url, false)设置,但会导致页面卡顿,现代浏览器已限制其在主线程的使用。
示例(同步模式,不推荐):
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', false); // 第三个参数为false表示同步
xhr.send();
console.log(xhr.responseText); // 会阻塞直到请求完成
二、Ajax 异步编程的实现方式
1. 回调函数(传统方式)
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true); // 异步
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText); // 请求成功的回调
}
};
xhr.send();
2. Promise(ES6+)
function fetchData() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
}
// 使用 Promise
fetchData().then(data => console.log(data));
3. fetch API(现代方式)
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
4. async/await(ES2017+)
async function getData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
三、Ajax 的实现原理
核心步骤:
- 创建 XMLHttpRequest 实例:
const xhr = new XMLHttpRequest() - 配置请求:
xhr.open(method, url, async) - 监听状态变化:
xhr.onreadystatechange或xhr.onload - 发送请求:
xhr.send() - 处理响应:根据
xhr.status和xhr.readyState判断结果
流程图:
创建 XHR 实例 → 配置请求(方法、URL、异步) → 监听事件 → 发送请求 →
服务器处理请求 → 响应数据 → XHR 状态变化 → 触发回调函数
关键属性:
readyState:请求状态(0-4,4 表示完成)status:HTTP 状态码(如 200、404)responseText/responseJSON:响应内容
四、多个请求并发的实现
1. Promise.all(并行执行)
const fetchUser = fetch('/api/user').then(res => res.json());
const fetchPosts = fetch('/api/posts').then(res => res.json());
Promise.all([fetchUser, fetchPosts])
.then([user, posts]) => {
console.log(user, posts); // 两个请求都完成后执行
})
.catch(error => console.error(error));
2. async/await + Promise.all
async function fetchAll() {
try {
const [user, posts] = await Promise.all([
fetch('/api/user').then(res => res.json()),
fetch('/api/posts').then(res => res.json())
]);
console.log(user, posts);
} catch (error) {
console.error(error);
}
}
3. 控制并发数量(高级场景)
当需要限制并发请求数量时(如避免服务器过载),可使用 Promise.allSettled 或自定义队列:有失败的也会全部执行并返回每个接口的状态,是否成功
async function fetchWithConcurrency(urls, concurrency = 3) {
const results = [];
let index = 0;
async function worker() {
while (index < urls.length) {
const currentIndex = index++;
try {
const response = await fetch(urls[currentIndex]);
results[currentIndex] = await response.json();
} catch (error) {
results[currentIndex] = error;
}
}
}
// 启动指定数量的并发工作线程
await Promise.all(Array(concurrency).fill().map(worker));
return results;
}
五、并发 vs. 串行
1. 并发(Parallel)
- 优点:速度快,适合相互独立的请求。
- 缺点:可能占用过多资源,服务器压力大。
2. 串行(Sequential)
- 优点:控制请求顺序,适合依赖关系的请求。
- 缺点:总耗时较长。
示例(串行):
async function fetchSequentially() {
const user = await fetch('/api/user').then(res => res.json());
const posts = await fetch(`/api/posts?userId=${user.id}`).then(res => res.json());
return { user, posts };
}
六、总结
| 场景 | 推荐方案 | 示例代码 |
|---|---|---|
| 简单异步请求 | fetch API | fetch('/api').then(...) |
| 复杂异步流程控制 | async/await | const data = await fetch(...) |
| 多个独立请求并发 | Promise.all | Promise.all([fetch1, fetch2]) |
| 限制并发数量 | 自定义并发队列 | 见上文 fetchWithConcurrency 示例 |
| 需要顺序执行 | 串行 async/await | await req1(); await req2() |
性能优化建议:
- 对于无依赖的请求,优先使用
Promise.all并发处理。 - 对大量并发请求,考虑使用节流或队列控制(如
p-limit库)。 - 使用
AbortController取消不再需要的请求(如页面切换时)。
POST 和 GET 方式提交数据有什么区别?
在 HTTP 协议中,POST 和 GET 是两种最常用的请求方法,它们的主要区别体现在 数据传输方式、安全性、幂等性 和 应用场景 上:
一、数据传输方式
| 特性 | GET | POST |
|---|---|---|
| 数据位置 | 参数附加在 URL 的查询字符串中 | 参数放在请求体(body)中 |
| 编码格式 | 仅支持 ASCII,需编码特殊字符 | 支持多种编码(如 application/x-www-form-urlencoded、multipart/form-data、JSON) |
| 可见性 | 参数暴露在 URL 中 | 参数不可见(在请求体中) |
| 数据长度限制 | 受浏览器和服务器限制(通常 < 2KB) | 无明确限制(取决于服务器配置) |
二、安全性
| 方面 | GET | POST |
|---|---|---|
| 敏感数据 | 不适合:参数会被缓存、记录在历史记录和服务器日志中 | 更安全:参数不在 URL 中 |
| CSRF 风险 | 更高:URL 易被伪造(如通过图片 src) | 较低:需通过表单或 AJAX 发送 |
| 加密 | 参数会被明文传输(除非使用 HTTPS) | 同 GET,但参数不在 URL 中 |
三、幂等性
-
幂等性
:多次执行相同操作产生的效果与一次执行相同。
GET:幂等(如查询数据,多次请求结果相同)。POST:非幂等(如提交表单创建资源,多次提交可能创建多个资源)。
四、应用场景
| 场景 | GET | POST |
|---|---|---|
| 数据获取 | ✅ 最适合(如搜索、分页) | ❌ 不推荐(语义不符) |
| 数据提交 | ❌ 仅适合简单、非敏感数据 | ✅ 最适合(如表单提交、创建资源) |
| 缓存 | ✅ 可被浏览器缓存 | ❌ 通常不缓存 |
| 书签 / 分享 | ✅ URL 可直接分享 | ❌ 无法通过 URL 分享请求数据 |
文件上传和下载?表单提交的数据格式?
AJAX 上传(XMLHttpRequest)
- 使用 FormData 对象:自动处理文件二进制数据。
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = () => console.log('上传完成');
xhr.send(formData);
1. 直接链接下载
<a href="/path/to/file.pdf" download>下载文件</a>
download属性:强制下载而非打开(仅同源文件有效)。
2. 动态生成链接(Blob 对象)
- 场景:从 API 获取二进制数据后下载。
fetch('/api/download')
.then(response => response.blob())
.then(res => {
const blob = new Blob([res], {
type: "application/xlsx", // text/plain
}); // 构造一个blob对象来处理数据,并设置文件类型
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'filename.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
| 操作 | 关键技术点 |
|---|---|
| 文件上传 | - 使用 multipart/form-data - FormData 对象 - 服务器处理二进制数据 |
| 文件下载 | - Content-Disposition 头 - Blob 对象与 URL.createObjectURL () - 同源策略限制 |
| 表单数据格式 | - 普通表单:application/x-www-form-urlencoded - 文件上传:multipart/form-data - JSON API:application/json |
防抖和节流的实现及区别
防抖和节流是两种优化高频触发事件的技术,主要区别在于执行时机:
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 在事件停止触发后延迟执行 | 固定周期执行一次,无论触发多频繁 |
| 应用场景 | 搜索框联想、窗口大小调整、按钮防双击 | 滚动加载、高频点击(如点赞)、拖拽反馈 |
| 核心逻辑 | 重置定时器 | 控制执行间隔 |
一、防抖(Debounce)的实现
1. 基础实现(延迟执行)
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 每次触发时重置定时器
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用示例
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
console.log('搜索:', e.target.value);
}, 300));
2. 立即执行版本
function debounce(fn, delay, immediate = false) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
fn.apply(this, args); // 立即执行
}
timer = setTimeout(() => {
if (!immediate) fn.apply(this, args);
timer = null;
}, delay);
};
}
二、节流(Throttle)的实现
1. 时间戳实现(立即执行)
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用示例
window.addEventListener('scroll', throttle(() => {
console.log('滚动事件触发');
}, 200));
2. 定时器实现(延迟执行)
function throttle(fn, interval) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
}
};
}
3. 结合版(兼顾立即执行和尾调用)
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
// 剩余时间
const remaining = interval - (now - lastTime);
// 如果没有剩余时间或已超过上次执行时间
if (remaining <= 0 || remaining > interval) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer) {
// 最后一次调用
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
}
三、核心区别对比
| 对比项 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 触发频率 | 高频触发时只执行最后一次 | 高频触发时固定间隔执行 |
| 执行次数 | 1 次(停止触发后) | N 次(根据时间间隔) |
| 内存消耗 | 较低(仅维护一个定时器) | 中等(维护时间戳或定时器) |
| 响应特性 | 延迟响应(需等待停止触发) | 均匀响应(固定周期执行) |
四、应用场景选择
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 搜索联想 | 防抖 | 只需要处理最后一次输入,减少服务器压力。 |
| 窗口大小调整(resize) | 防抖 | 只需要在调整结束后计算布局,避免频繁重排。 |
| 滚动加载更多 | 节流 | 固定间隔触发,避免频繁请求数据。 |
| 按钮防双击 | 防抖 | 只执行最后一次点击,防止重复提交。 |
| 拖拽反馈 | 节流 | 固定频率更新位置,保证视觉流畅性。 |
五、总结
- 防抖:适合需要合并高频事件为一次执行的场景(如搜索、窗口调整)。
- 节流:适合需要控制高频事件执行频率的场景(如滚动加载、拖拽)。
记忆口诀:
- 防抖:等人不说话了再发言(停止触发后执行)。
- 节流:按固定节奏发言(固定间隔执行)。
ES6常用的新特性(解构赋值、箭头函数?),扩展运算符的作用及使用场景
| 特性 | 核心作用 | 典型场景 |
|---|---|---|
| 解构赋值 | 从数组 / 对象中提取值并赋值给变量 | 参数解析、配置合并、变量交换 |
| 箭头函数 | 简化函数定义,绑定外层 this | 回调函数、需要保留上下文的场景 |
| 扩展运算符 | 展开数组 / 对象元素,用于合并、复制操作 | 数组拼接、函数参数传递、对象浅拷贝 |
记忆口诀:
- 解构:拆包取值,按需分配。
- 箭头:简洁回调,稳绑
this。 - 扩展:打散重组,灵活复制。
js 延迟加载的方式有哪些?setTimeout和setInterval的区别?
| 技术 | 核心作用 | 典型场景 |
|---|---|---|
| 动态 script | 异步加载 JS 文件,不阻塞页面 | 按需加载第三方脚本(如广告、地图) |
| defer 属性 | 异步加载,DOM 解析完成后按顺序执行 | 需要操作 DOM 且按顺序执行的脚本 |
| async 属性 | 异步加载,下载完成立即执行 | 独立脚本(不依赖 DOM 且无顺序要求) |
| setTimeout | 延迟执行单次任务 | 延迟加载、动画启动、超时处理 |
| setInterval | 周期性执行任务 | 轮询数据、实时更新、动画循环 |
最佳实践:
- 优先使用
defer或动态创建 script 标签加载非关键 JS。 - 使用
setTimeout递归调用替代setInterval,避免回调堆叠。 - 所有定时器都应在不再需要时使用
clearTimeout/clearInterval清除。
JS实现继承的几种方式? (原型链继承【原型和原型链】、构造函数继承)
JavaScript 的继承机制基于原型(Prototype),主要有以下几种实现方式:
一、原型链继承
核心原理:
通过原型对象实现继承,子类的原型指向父类的实例。
实现步骤:
- 创建父类构造函数。
- 定义父类原型方法。
- 创建子类构造函数。
- 将子类的原型设置为父类的实例。
示例代码:
// 父类
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
// 子类
function Dog(name) {
this.name = name;
}
// 关键步骤:Dog 的原型指向 Animal 的实例
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修复 constructor
// 使用
const dog = new Dog("Buddy");
dog.speak(); // "Buddy makes a sound."
特点:
-
子类实例继承父类的属性和方法。
-
所有子类实例共享父类原型上的属性。
-
缺点
- 父类的引用类型属性会被所有子类实例共享(如数组、对象)。
-
创建子类实例时,无法向父类构造函数传递参数。
二、构造函数继承
核心原理:
在子类构造函数中调用父类构造函数,使用 call() 或 apply() 绑定 this。
示例代码:
// 父类
function Animal(name) {
this.name = name;
this.colors = ["black", "white"];
}
// 子类
function Dog(name, breed) {
// 关键步骤:调用父类构造函数并绑定 this
Animal.call(this, name);
this.breed = breed;
}
// 使用
const dog1 = new Dog("Buddy", "Labrador");
dog1.colors.push("brown");
console.log(dog1.colors); // ["black", "white", "brown"]
const dog2 = new Dog("Max", "Poodle");
console.log(dog2.colors); // ["black", "white"](不受 dog1 影响)
特点:
-
子类实例拥有自己的父类属性副本(不共享引用类型)。
-
可以向父类构造函数传递参数。
-
缺点
- 只能继承父类构造函数中的属性和方法,无法继承父类原型上的方法。
-
方法都在构造函数中定义,导致每次创建实例都会重新创建方法(无法复用)。
三、组合继承(原型链 + 构造函数)
核心原理:
结合原型链和构造函数继承的优点,使用原型链继承原型上的属性和方法,使用构造函数继承实例属性。
示例代码:
// 父类
function Animal(name) {
this.name = name;
this.colors = ["black", "white"];
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
// 子类
function Dog(name, breed) {
// 第一步:构造函数继承(继承实例属性)
Animal.call(this, name);
this.breed = breed;
}
// 第二步:原型链继承(继承原型方法)
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
// 子类新增方法
Dog.prototype.bark = function() {
console.log("Woof!");
};
// 使用
const dog = new Dog("Buddy", "Labrador");
dog.speak(); // "Buddy makes a sound."
dog.bark(); // "Woof!"
特点:
-
融合了原型链和构造函数继承的优点。
-
子类实例可以继承父类原型和构造函数中的属性和方法。
-
缺点
- 父类构造函数被调用两次(一次创建子类原型,一次在子类构造函数中)。
-
子类原型上存在两份相同的父类实例属性(冗余)。
四、寄生组合继承(优化版组合继承)
核心原理:
通过 Object.create() 创建父类原型的副本,避免调用父类构造函数。
示例代码:
// 父类
function Animal(name) {
this.name = name;
this.colors = ["black", "white"];
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
// 子类
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!");
};
// 使用
const dog = new Dog("Buddy", "Labrador");
dog.speak(); // "Buddy makes a sound."
dog.bark(); // "Woof!"
特点:
- 解决了组合继承的效率问题(只调用一次父类构造函数)。
- 子类原型上不会存在冗余的父类实例属性。
- ES6 之前最理想的继承方式。
五、ES6 类继承(基于寄生组合)
核心语法:
使用 class 和 extends 关键字,底层依然基于原型链。
示例代码:
// 父类
class Animal {
constructor(name) {
this.name = name;
this.colors = ["black", "white"];
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
// 子类
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log("Woof!");
}
}
// 使用
const dog = new Dog("Buddy", "Labrador");
dog.speak(); // "Buddy makes a sound."
dog.bark(); // "Woof!"
特点:
- 语法更简洁,语义更清晰。
- 支持
super关键字访问父类方法。 - 底层依然基于寄生组合继承,解决了传统继承的问题。
六、各继承方式对比
| 方式 | 继承原型方法 | 继承实例属性 | 父类调用次数 | 子类原型冗余 | 推荐场景 |
|---|---|---|---|---|---|
| 原型链继承 | ✅ // 关键步骤:Dog 的原型指向 Animal 的实例 Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; // 修复 constructor | ✅ | 1 | ✅ | 简单场景,无引用类型属性 |
| 构造函数继承 | ❌ // 子类 function Dog(name, breed) { // 关键步骤:调用父类构造函数并绑定 this Animal.call(this, name); this.breed = breed; } // 使用 const dog1 = new Dog(“Buddy”, “Labrador”); | ✅ | 1 | ❌ | 需要参数传递的场景 |
| 组合继承 | ✅// 子类 function Dog(name, breed) { // 第一步:构造函数继承(继承实例属性) Animal.call(this, name); this.breed = breed; } // 第二步:原型链继承(继承原型方法) Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; | ✅ | 2 | ✅ | 兼容性要求高的场景 |
| 寄生组合继承 | ✅// 子类 function Dog(name, breed) { // 构造函数继承 Animal.call(this, name); this.breed = breed; } // 关键步骤:创建父类原型的副本,而非实例 Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; | ✅ | 1 | ❌ | ES6 之前的最佳实践 |
| ES6 类继承 | ✅// 子类 class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类构造函数 this.breed = breed; } bark() { console.log(“Woof!”); } } // 使用 const dog = new Dog(“Buddy”, “Labrador”); | ✅ | 1 | ❌ | 现代 JavaScript |
七、原型与原型链的核心概念
1. 原型(Prototype)
- 每个对象都有一个内部属性
[[Prototype]](可通过__proto__访问)。 - 对象的
[[Prototype]]指向其原型对象。 - 当访问对象的属性或方法时,JavaScript 会先在对象本身查找,若找不到则沿原型链向上查找。
2. 原型链(Prototype Chain)
- 由多个对象的
[[Prototype]]连接形成的链条。 - 链的终点是
Object.prototype,其[[Prototype]]为null。
3. 关键关系
const obj = {};
obj.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 函数的原型链
function Foo() {}
Foo.prototype.__proto__ === Object.prototype; // true
// 数组的原型链
const arr = [];
arr.__proto__ === Array.prototype; // true
Array.prototype.__proto__ === Object.prototype; // true
总结
- 原型链继承:简单但共享引用类型属性。
- 构造函数继承:解决共享问题但无法继承原型方法。
- 组合继承:综合两者但效率低。
- 寄生组合继承:ES6 之前的最佳方案。
- ES6 类继承:现代语法,推荐使用。
记忆口诀:
- 原型链:共享属性,简单但有坑。
- 构造函数:独立属性,无法复用方法。
- 组合:取长补短,调用两次。
- 寄生组合:最优传统方案。
- ES6 类:语法糖,简洁高效。
数据结构里,比如队列(先进先出)、栈(先进后出)是什么?
在数据结构中,队列(Queue)**和**栈(Stack)**是两种基于**线性存储结构的抽象数据类型,它们的核心区别在于元素的插入和删除顺序。以下是详细介绍:
栈:原始数据类型(Undefined、Null、Boolean、Number、String)
堆:引用数据类型(对象、数组和函数)
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
一、队列(Queue):先进先出(FIFO,First-In-First-Out)
定义
队列就像现实中的 “排队”,先进入的元素先被取出,后进入的元素后被取出,保证顺序性。
核心操作
- 入队(Enqueue):向队列尾部添加元素。
- 出队(Dequeue):从队列头部移除并返回元素。
- 查看队头元素(Peek):获取队列头部元素但不删除。
实现方式
- 数组实现:用数组模拟队列,通过索引标记头尾位置(需处理 “假溢出” 问题)。
- 链表实现:用链表结构(如双向链表),头节点指向队头,尾节点指向队尾,操作效率更高。
应用场景
- 任务调度:如打印机任务队列、线程池中的任务排队。
- 广度优先搜索(BFS):遍历图或树时按层访问节点。
- 消息队列:分布式系统中缓存消息,按顺序处理(如 Kafka、RabbitMQ)。
示例(JS 数组模拟队列)
class Queue {
constructor() {
this.items = [];
}
enqueue(element) { // 入队(尾部添加)
this.items.push(element);
}
dequeue() { // 出队(头部删除)
return this.items.shift(); // shift() 时间复杂度 O(n),效率较低,实际可用链表优化
}
peek() { // 查看队头
return this.items[0];
}
isEmpty() {
return this.items.length === 0;
}
}
// 使用示例
const q = new Queue();
q.enqueue(1);
q.enqueue(2);
console.log(q.dequeue()); // 输出 1(先入先出)
二、栈(Stack):先进后出(LIFO,Last-In-First-Out)
定义
栈类似 “叠盘子”,最后放入的元素最先被取出,最早放入的元素最后被取出,强调操作的逆序性。
核心操作
- 入栈(Push):向栈顶添加元素。
- 出栈(Pop):从栈顶移除并返回元素。
- 查看栈顶元素(Peek):获取栈顶元素但不删除。
实现方式
- 数组实现:用数组模拟栈,利用
push()和pop()方法(时间复杂度均为 O (1))。 - 链表实现:用链表(如单链表),头节点作为栈顶,插入和删除操作在头部进行。
应用场景
- 函数调用栈:JavaScript 引擎用栈管理函数调用顺序(如递归时的调用栈溢出问题)。
- 表达式求值:计算算术表达式时处理括号匹配(如中缀表达式转后缀表达式)。
- 撤销操作:文本编辑器的 “撤销” 功能(记录操作历史,逆序回退)。
- 深度优先搜索(DFS):遍历图或树时用栈模拟递归过程。
示例(JS 数组模拟栈)
class Stack {
constructor() {
this.items = [];
}
push(element) { // 入栈(顶部添加)
this.items.push(element);
}
pop() { // 出栈(顶部删除)
return this.items.pop();
}
peek() { // 查看栈顶
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
}
// 使用示例
const stack = new Stack();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 输出 2(后入先出)
三、队列 vs 栈:核心区别对比
| 特性 | 队列(FIFO) | 栈(LIFO) |
|---|---|---|
| 插入位置 | 尾部(Enqueue) | 顶部(Push) |
| 删除位置 | 头部(Dequeue) | 顶部(Pop) |
| 典型操作顺序 | 先入先出(如排队买票) | 后入先出(如叠盘子取最上层) |
| 数据结构用途 | 处理有序任务、广度优先遍历 | 处理逆序任务、深度优先遍历、函数调用 |
总结
- 队列适合需要保持顺序性的场景(如任务排队)。
- 栈适合需要逆序处理的场景(如函数调用、撤销操作)。
两者都是基础数据结构,理解其原理对算法设计(如遍历、表达式计算)和编程逻辑(如递归优化)至关重要。
数据的加密和解密方式
数据加密是信息安全的基石,主要分为对称加密、非对称加密和哈希算法三大类
常用的富文本编辑器
WangEditor
Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做? token的使用及存储
- 登录认证:用户登录后,服务器返回 Token、角色和权限列表。
- 存储权限:将权限信息存储到 localStorage 或 Vuex。
- 路由控制:通过路由守卫验证页面访问权限。
- 组件控制:通过自定义指令或计算属性控制按钮显示。
- 请求验证:每次请求携带 Token,服务器验证权限。
使用vue渲染大量数据时应该怎么优化?说下你的思路!
vue-virtual-scroller(推荐):支持列表、网格、表格等多种布局,滚动平滑。
懒加载,监测鼠标滚轮,动态加载数据
分页
v-for的key
show替代if
使用 v-once
如果某些内容不需要更新,可以使用 v-once 指令确保它们只渲染一次。
实现方式:
<div v-once>{{ staticContent }}</div>
优点:
- 减少不必要的更新,提升性能。
有没有做过小程序和app,用过哪些插件,如何处理分辨率
使用rpx,百分比,flex,grid,根据根元素字体大小设置rem
使用postcss-px-to-viewport 直接使用ui组件
Vue组件之间都有通信方式?
1.props。$emit
2.provide + inject
3.Event Bus
// event-bus.js
import Vue from 'vue';
export const eventBus = new Vue();
// 组件A(发送)
import { eventBus } from './event-bus';
eventBus.$emit('message', 'hello from A');
// 组件B(接收)
import { eventBus } from './event-bus';
mounted() {
eventBus.$on('message', (msg) => console.log(msg));
}
beforeDestroy() {
// 该组件销毁时记得解绑,避免占用$bus的资源
this.$bus.off('message')
}
4.vuex
5.直接ref
v-if和v-for哪个优先级更高?
Vue 官方文档强烈建议避免在同一元素上同时使用 v-if 和 v-for,因为:
v-for优先级高于v-if:无论条件如何,v-for会先遍历并渲染元素,再根据v-if的结果决定是否显示。- 性能问题:即使元素最终被
v-if移除,v-for仍会执行完整的遍历过程,造成不必要的计算。
先在外层if
简述 Vue 的生命周期以及每个阶段做的事?
一、初始化阶段(创建前到创建后)
1. beforeCreate(创建前)
- 阶段特点:实例刚被创建,数据观测(data)和方法(methods)尚未初始化。
- 作用:可用于插件初始化、添加全局事件等与数据无关的操作。
- 注意:此时无法访问
this.data或this.methods。
2. created(创建后)
-
阶段特点:实例已完成数据观测、方法和计算属性的初始化,但尚未开始渲染 DOM。
-
作用
:
- 发送 AJAX 请求获取数据(最常用场景)。
- 初始化第三方库(如 echarts)。
- 复杂数据的预处理。
-
注意:可访问
this.data和this.methods,但无 DOM 节点。
二、挂载阶段(挂载前到挂载后)
3. beforeMount(挂载前)
-
阶段特点:模板已编译完成,但尚未将虚拟 DOM 渲染为真实 DOM(页面未更新)。
-
作用
:
- 对渲染前的模板进行最后修改(如动态替换模板字符串)。
- 此时
$el已存在(根元素),但$refs未生效。
4. mounted(挂载后)
-
阶段特点:真实 DOM 已渲染完成并插入文档,页面可见。
-
作用
:
- 操作真实 DOM(如获取元素尺寸、初始化插件)。
- 绑定自定义事件(如 window 滚动事件)。
- 启动定时器或订阅消息。
-
注意:
$refs可正常使用,适合需要 DOM 依赖的操作。
三、更新阶段(更新前到更新后)
5. beforeUpdate(更新前)
-
阶段特点:数据发生变化后,虚拟 DOM 重新渲染之前。
-
作用
:
- 捕获更新前的 DOM 状态(如保存旧值)。
- 暂停某些需要避免重复执行的操作(如动画)。
-
注意:此时页面尚未更新,数据已更新但 DOM 未同步。
6. updated(更新后)
-
阶段特点:虚拟 DOM 重新渲染并更新完毕,页面已反映数据变化。
-
作用
:
- 操作更新后的 DOM(如重新计算滚动位置)。
- 恢复或调整依赖更新后 DOM 的逻辑。
-
注意:避免在此处修改数据(可能触发无限循环更新)。
四、卸载阶段(卸载前到卸载后)
7. beforeUnmount(卸载前)
-
阶段特点:实例即将被销毁,但尚未清理自身及子组件。
-
作用
:
- 移除自定义事件监听器(如 window 事件)。
- 暂停定时器或取消订阅。
- 清理第三方库实例(如地图组件)。
8. unmounted(卸载后)
-
阶段特点:实例已被销毁,所有子组件、数据监听和 DOM 已移除。
-
作用
:
- 彻底清理无效引用(如全局变量、定时器)。
- 释放内存,避免内存泄漏。
-
注意:此时调用
this会指向已卸载的实例,应避免操作。
五、特殊场景钩子
1. errorCaptured(错误捕获)
- 作用:捕获子组件渲染、生命周期或自定义事件处理函数中的错误。
- 场景:用于统一错误处理、日志上报或提示用户。
2. activated/deactivated(keep-alive 缓存)
-
作用
:
activated:被keep-alive缓存的组件激活时调用。deactivated:被keep-alive缓存的组件停用时调用。
-
场景:优化列表页性能,避免重复渲染(如列表页切换标签时保持状态)。
v-model双向绑定使用和原理?
v-model 实际上是 :value 绑定 + @input 事件 的简写:
<!-- 完整写法 -->
<input
:value="message"
@input="message = $event.target.value"
>
<!-- 等价于 v-model -->
<input v-model="message">
说说nextTick的使用和原理?
nextTick 是什么?
nextTick 是 Vue 提供的一个异步 API,用于在 DOM 更新完成后 执行回调函数。
核心作用:确保获取到最新的 DOM 状态(例如获取更新后的元素高度、样式等)。
二、为什么需要 nextTick?
1. Vue 的异步更新机制
-
Vue 会将数据更新操作 缓存到队列中,通过
异步批量更新减少 DOM 操作次数(类似防抖)。this.message = 'new value'; // 数据更新,此时 DOM 未立即更新 console.log(document.getElementById('text').textContent); // 仍为旧值- 直接在数据更新后获取 DOM,得到的是 更新前的值,因为 DOM 更新是异步的。
2. nextTick 的触发时机
- 当数据变化后,Vue 会开启一个异步队列,将 DOM 更新操作放入队列中。
nextTick的回调会在 DOM 更新完成后 执行,此时能获取到最新的 DOM 状态。
三、nextTick 的使用方式
1. 基础用法
// 在组件中使用(this 指向 Vue 实例)
this.message = '更新数据';
this.$nextTick(() => {
// DOM 已更新,此处可操作最新 DOM
console.log(document.getElementById('text').textContent); // 输出 '更新数据'
});
2. 全局 API(非组件环境)
import { nextTick } from 'vue'; // Vue 3
// 或 const { nextTick } = require('vue'); // Vue 2
// 非组件环境(如 Vue 插件、自定义函数)
function updateAndLog() {
data.message = 'new value';
nextTick(() => {
console.log('DOM 更新后执行');
});
}
3. 返回 Promise(Vue 2.1.0+ / Vue 3)
this.message = '更新数据';
await this.$nextTick(); // 等待 DOM 更新完成
console.log('DOM 已更新');
四、nextTick 的核心原理
1. 核心逻辑
- 目标:确保回调函数在 DOM 更新周期结束后 执行。
- 实现方式:根据环境优先使用 微任务(Promise.then、MutationObserver),其次使用 宏任务(setTimeout)。
2. 执行流程
-
数据更新:触发
setter,标记组件需要更新。 -
异步队列:Vue 将 DOM 更新操作存入异步队列,避免频繁渲染。
-
选择异步方法
-
优先使用
微任务
(执行速度更快,不阻塞 UI):
- Promise.then(现代浏览器支持)。
-
-
MutationObserver(监听 DOM 变化,用于兼容无 Promise 的环境)。
-
若都不支持,退化为 宏任务(
setTimeout)。
- 执行回调:在异步任务队列中执行
nextTick的回调函数,此时 DOM 已更新完毕。
3. 源码简化逻辑(Vue 3)
const queue = []; // 存储待执行的回调
let isFlushing = false; // 标记是否正在刷新队列
function flushNextTick() {
if (isFlushing) return;
isFlushing = true;
// 优先使用微任务(Promise)
Promise.resolve().then(() => {
queue.forEach(fn => fn()); // 执行所有回调
}).finally(() => {
isFlushing = false;
queue.length = 0; // 清空队列
});
}
export function nextTick(fn) {
queue.push(fn);
if (!isFlushing) {
flushNextTick(); // 触发异步更新
}
}
watch和computed的区别以及选择?
写法
vue2
computed: {
// 只读计算属性(getter 形式)
fullName() {
return `${this.firstName} ${this.lastName}`;
},
// 可读写计算属性(getter/setter 形式)
formattedName: {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
const [firstName, lastName] = value.split(' ');
this.firstName = firstName;
this.lastName = lastName || '';
}
}
}
watch: {
// 当 question 变化时执行
question(newVal, oldVal) {
if (newVal) {
this.fetchAnswer();
}
},
userInfo: {
handler(newVal, oldVal) {
console.log('userInfo 变化:', newVal);
},
deep: true // 深度监听
},
},
vue3
// 创建只读计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
const [first, last] = value.split(' ');
firstName.value = first;
lastName.value = last || '';
}
});
// 监听 ref
watch(count, (newVal, oldVal) => {
console.log(`count 变化: ${oldVal} → ${newVal}`);
});
watch(
[firstName, lastName], // 监听多个源
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名变化: ${oldFirst} ${oldLast} → ${newFirst} ${newLast}`);
}
);
watch(
user, // 直接监听整个对象
(newUser, oldUser) => {
console.log('用户对象变化:', newUser);
},
{ deep: true } // 开启深度监听
);
watchEffect(() => {
// 自动监听所有被访问的响应式数据
console.log('count 变化:', count.value);
});
vue2普通监听是函数,设置逻辑就变对象。vue3全是函数
核心区别
| 特性 | computed | watch |
|---|---|---|
| 依赖关系 | 基于其他响应式数据计算生成结果 | 监听特定数据变化并执行回调 |
| 缓存机制 | 有缓存,仅依赖项变化时重新计算 | 无缓存,每次变化都触发回调 |
| 触发时机 | 依赖项变化时自动更新 | 数据变化后执行回调(异步或同步) |
| 适用场景 | 复杂计算逻辑、多数据源整合 | 监听数据变化并执行副作用(如异步请求) |
| 代码结构 | 声明式(类似属性访问) | 命令式(需要定义回调函数) |
说一下 Vue 子组件和父组件创建和挂载顺序
父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
子 mounted
父 mounted
怎么缓存当前的组件?缓存后怎么更新?
keep-alive 通过 include/exclude 指定组件名
activated钩子中获取数据进行更新
你有写过自定义指令吗?自定义指令的应用场景有哪些?
自定义指令是一种强大的工具,用于封装 DOM 操作逻辑,使代码可复用
说下 a t t r s 和 attrs和 attrs和listeners的使用场景
属性透传,爷组件通过父组件传递给孙组件
$attrs不是响应式的
父组件A给子组件B传值,B组件又继续传递给C,最终执行的是C。
但是B组件并不需要使用A组件传过来的值,B组件只是起了传递作用,
此时可以使用$attrs和$listeners的方法来传递;中间组件B只需要v-bind="$attrs" v-on="$listeners"就可以。
v-once的使用场景有哪些?
v-once 是 Vue 提供的一个特殊指令,用于只渲染一次元素或组件,后续数据变化时不再重新渲染。
静态页面绑定一个固定的数据
内容不需要响应式更新,渲染内容复杂或计算开销大
Vue中什么是递归组件?举个例子说明下?
递归组件是指在组件模板中直接或间接调用自身的组件。这种技术常用于处理具有树形结构的数据(如菜单、文件目录、评论嵌套等)
一定要有明确的停止条件判断。
树结构,文件夹,菜单嵌套,表格嵌套
嵌套评论
异步组件是什么?使用场景有哪些?
异步组件是 Vue 中一种特殊的组件加载方式,允许你在需要时才加载组件代码,而非在应用初始化时全部加载。这对于优化大型应用的加载性能尤为重要。
什么时候用在import(‘./AsyncComponent.vue’);引用
路由级代码分割
*二、**Vue3*
你知道哪些vue3新特性
选项式,需要引入使用
生命周期变化
-
setup:对应Vue2.0的 beforeCreate 和 created,是Composition AIP的入口API -
beforeUnmount, unmounted:对应2.0的 beforeDestroyed 和 destroyedProxy 替代 Object.defineProperty
- 支持深层响应式,无需
Vue.set。
- 支持深层响应式,无需
-
支持监听数组索引变化和属性添加 / 删除。
Vue3.0的设计目标是什么?做了哪些优化?
Vue 3.0 的开发始于 2018 年,历时两年多,其核心设计目标围绕性能提升、代码可维护性、类型安全和扩展性展开
核心设计目标
1. 提升性能与效率
-
目标:在保持易用性的同时,大幅提升渲染效率和内存利用率。
-
关键优化
-
Proxy 响应式系统:替代 Vue 2 的 Object.defineProperty,支持深层响应式监听,解决了 Vue 2 中动态添加 / 删除属性的限制。
-
编译时优化
- 静态提升:将静态节点(如纯文本)提升到渲染函数外部,减少重复创建。
-
-
Patch Flag:标记动态节点类型,仅更新变化的部分,减少虚拟 DOM 比对开销。
- Hoist Static:提取静态 VNode,避免重复生成。
2. 增强代码可维护性
-
目标:解决大型应用中组件逻辑复杂、代码难以组织的问题。
-
关键优化
- Composition API:按功能而非生命周期组织代码,支持逻辑复用和代码分割。
-
类型安全:原生支持 TypeScript,减少类型定义的复杂度。
3. 优化扩展性
-
目标:支持更灵活的自定义和扩展。
-
关键优化
- 自定义渲染器 API:支持创建自定义渲染器(如渲染到 Canvas、WebGL)。
-
组合式插件:使用组合函数创建插件,替代 Vue 2 的混入(mixins)。
4. 改善开发者体验
-
目标:简化 API 设计,提供更友好的错误提示。
-
关键优化
- API 一致性:统一选项式 API 和 Composition API 的行为。
-
更好的调试支持:增强 DevTools,提供更详细的组件状态和性能分析。
二、具体优化措施
1. 响应式系统重构
-
Proxy 替代 Object.defineProperty
// Vue 3 响应式原理简化
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
track(target, key); // 依赖收集
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key); // 触发更新
return true;
}
});
};
- 优势
- 支持监听数组索引变化和属性添加 / 删除。
- 无需递归遍历对象,性能显著提升。
#### **2. 编译时优化**
- 静态提升示例
```vue
<!-- 原始模板 -->
<div>
<p>静态文本</p> <!-- 静态节点 -->
<p>{{ dynamicText }}</p> <!-- 动态节点 -->
</div>
<!-- Vue 3 编译后 -->
const _hoisted_1 = /*#__PURE__*/createVNode("p", null, "静态文本", -1 /* HOISTED */)
return function render(_ctx, _cache) {
return (openBlock(), createBlock("div", null, [
_hoisted_1, // 直接复用静态节点
createVNode("p", null, toDisplayString(_ctx.dynamicText), 1 /* TEXT */)
]))
}
-
Patch Flag
// 标记 class 和 text 为动态
createVNode(“div”, {
class: _ctx.className
}, toDisplayString(_ctx.text), 3 /* CLASS | TEXT */)
#### **3. Composition API**
- 逻辑组合与复用
```javascript
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
const handleMouseMove = (event) => {
x.value = event.pageX;
y.value = event.pageY;
};
onMounted(() => {
window.addEventListener('mousemove', handleMouseMove);
});
onUnmounted(() => {
window.removeEventListener('mousemove', handleMouseMove);
});
return { x, y };
}
4. 类型安全增强
-
自动类型推导:
import { ref } from ‘vue’;
const count = ref(0); // 类型自动推导为 Ref
count.value = ‘text’; // TypeScript 报错:不能将 string 赋值给 number
- **组件类型定义**:
```javascript
import { defineComponent } from 'vue';
const MyComponent = defineComponent({
props: {
message: {
type: String,
required: true
}
},
setup(props) {
// props.message 类型自动推导为 string
return () => <div>{props.message}</div>;
}
});
5. 架构优化
-
模块化设计:
- 将核心功能拆分为独立包(如
@vue/reactivity、@vue/runtime-core),支持按需引入。
- 将核心功能拆分为独立包(如
-
自定义渲染器:
import { createRenderer } from ‘vue’;
const { render, createApp } = createRenderer({
// 自定义渲染 API
createElement(type) {
// 创建元素逻辑
},
setElementText(el, text) {
// 设置文本逻辑
},
// 其他方法…
});
### **三、性能对比**
| **指标** | **Vue 2** | **Vue 3** | **提升幅度** |
| ---------------- | -------------- | -------------- | ------------ |
| 初始渲染时间 | 100ms | 60ms | ~40% |
| 更新渲染时间 | 50ms | 25ms | ~50% |
| 内存占用 | 10MB | 6MB | ~40% |
| 创建组件实例速度 | 10,000 个 / 秒 | 25,000 个 / 秒 | ~150% |
### **四、总结**
Vue 3.0 通过一系列精心设计的优化,实现了**性能提升**、**代码可维护性增强**和**扩展性提高**的目标。核心改进包括:
1. **响应式系统升级**:使用 Proxy 替代 Object.defineProperty,提升性能和功能。
2. **编译时优化**:静态提升、Patch Flag 等技术减少运行时开销。
3. **Composition API**:解决大型组件逻辑组织问题,支持更好的代码复用。
4. **TypeScript 支持**:原生类型定义,自动类型推导,提升开发体验。
5. **架构模块化**:支持按需引入,自定义渲染器,增强扩展性。
### ref和reactive异同

| **特性** | **ref** | **reactive** |
| ------------------ | ---------------------------------------------- | -------------------------------------- |
| **定义方式** | `const count = ref(0)` | `const state = reactive({ count: 0 })` |
| **底层实现** | 使用 `Proxy` 和 `Object.defineProperty` 实现 | 仅使用 `Proxy` 实现 |
| **适用数据类型** | 适用于所有数据类型(基本类型、对象、数组等) | 仅适用于对象和数组(包括 Map、Set 等) |
| **访问方式** | 通过 `.value` 访问(模板中除外) | 直接访问属性 |
| **响应式特性** | 深层响应式,但替换整个对象需保持 `.value` 引用 | 深层响应式,可直接替换整个对象 |
| **类型推导** | 自动推导为 `Ref<T>` 类型 | 保持原始对象类型(更好的类型支持) |
| **解构丢失响应式** | 解构后会丢失响应式 | 解构后需使用 `toRefs` 保持响应式 |
```javascript
// ref
const user = ref({ name: 'John' });
user.value = { name: 'Jane' }; // 有效,保持引用
// reactive
const userState = reactive({ name: 'John' });
userState = { name: 'Jane' }; // 无效,丢失响应式连接
// 正确做法:修改对象内部属性
userState.name = 'Jane';
// 正确:使用 toRefs 保持响应式
import { toRefs } from 'vue';
const { count, name } = toRefs(state);
count.value++; // 触发更新
1. 优先使用 ref 的场景
- 存储基本类型数据(如数字、字符串、布尔值)。
- 需要在函数间传递响应式数据。
- 在循环中动态创建响应式变量。
- 与第三方库集成时需要保持一致的 API。
2. 优先使用 reactive 的场景
- 处理复杂对象或数组。
- 需要深层响应式而无需担心
.value语法。 - 定义组件的状态对象(如表单数据、列表数据)。
五、性能考量
- ref:在创建时需要额外的对象包装,对于大型数据集可能略慢。
- reactive:直接操作原始对象,性能略优,但仅限于对象类型。
watch和watchEffect异同
一、相同点
1. 响应式监听
- 两者都能监听响应式数据的变化,并在数据变化时执行回调函数。
2. 副作用清理
-
都支持副作用清理(如定时器、网络请求取消),通过返回清理函数实现。
watchEffect((onCleanup) => {
const timer = setInterval(() => { /* … */ }, 1000);
onCleanup(() => clearInterval(timer)); // 组件卸载或依赖变化时清理
});
#### **3. 组件生命周期关联**
- 默认都在组件挂载后执行,并且会在组件卸载时自动停止监听。
### **二、不同点**
| **特性** | **watch** | **watchEffect** |
| ---------------- | ------------------------------------------------------------ | -------------------------------------------------------- |
| **显式指定依赖** | 需要显式指定要监听的数据源(如 `ref`、`reactive` 属性或计算属性) | 自动收集回调函数中使用的所有响应式依赖 |
| **惰性执行** | 默认惰性执行(仅在依赖变化时触发,初始不执行) | 立即执行,并在依赖变化时再次执行 |
| **获取新旧值** | 回调函数可获取变化前后的值(`newValue`, `oldValue`) | 无法直接获取新旧值,只能访问最新值 |
| **适用场景** | 监听特定数据变化,执行副作用(如网络请求、DOM 操作) | 需要根据响应式状态自动应用副作用(如同步 DOM、缓存数据) |
| **深层监听** | 需显式设置 `deep: true` 以监听对象内部属性的变化 | 自动深层监听所有依赖 |
| **立即执行** | 通过 `immediate: true` 选项实现初始执行 | 默认立即执行,无需额外配置 |
### **三、详细对比**
#### **1. 基本语法差异**
```javascript
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
// watch
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`);
});
// watchEffect
watchEffect(() => {
console.log(`count 当前值:${count.value}`);
});
2. 依赖收集方式
const count = ref(0);
const double = computed(() => count.value * 2);
// watch 需要显式指定依赖
watch(double, (value) => {
console.log(`double 值变化为:${value}`);
});
// watchEffect 自动收集所有依赖
watchEffect(() => {
console.log(`count: ${count.value}, double: ${double.value}`);
});
3. 深层监听
const user = reactive({
name: 'John',
address: {
city: 'New York'
}
});
// watch 需要显式设置 deep
watch(
() => user.address,
(newAddr, oldAddr) => {
console.log('地址变化', newAddr, oldAddr);
},
{ deep: true }
);
// watchEffect 自动深层监听
watchEffect(() => {
console.log('用户地址', user.address.city);
});
4. 立即执行
const count = ref(0);
// watch 默认惰性执行
watch(count, (newValue) => {
console.log('count 变化', newValue); // 初始不执行
});
// watchEffect 默认立即执行
watchEffect(() => {
console.log('count 当前值', count.value); // 初始执行
});
// watch 实现立即执行
watch(count, (newValue) => {
console.log('立即执行', newValue);
}, { immediate: true });
四、适用场景
1. 优先使用 watch 的场景
- 需要监听特定数据变化并执行副作用。
- 需要获取变化前后的值进行比较。
- 需要惰性执行(避免初始执行开销)。
- 监听深层对象或数组的变化。
2. 优先使用 watchEffect 的场景
- 需要根据多个响应式依赖自动执行副作用。
- 需要副作用与依赖建立自动关联(如表单验证、DOM 同步)。
- 需要立即执行副作用并自动跟踪依赖。
五、性能考量
- watch:由于显式指定依赖,Vue 可以更精确地优化监听逻辑。
- watchEffect:自动收集依赖可能导致监听范围扩大,需注意避免不必要的依赖。
六、避坑指南
1. 避免循环依赖
// 错误:watchEffect 内部修改依赖,导致无限循环
watchEffect(() => {
count.value++; // 修改 count 会触发下一次执行
});
// 正确:使用 watch 并添加条件判断
watch(count, (newValue) => {
if (newValue < 10) {
count.value++;
}
});
2. 深层监听的性能问题
// 对大型对象使用 deep: true 可能影响性能
watch(
() => user,
(newUser) => { /* ... */ },
{ deep: true } // 谨慎使用
);
// 优化:仅监听需要的属性
watch(
() => user.name,
(newName) => { /* ... */ }
);
3. 响应式丢失陷阱
const state = reactive({ list: [] });
// 错误:监听非响应式引用
watch(state.list, (newList) => { /* ... */ }); // 不会触发
// 正确:监听返回数组的函数
watch(
() => state.list,
(newList) => { /* ... */ }
);
七、总结
| 场景 | 推荐选择 | 示例 |
|---|---|---|
| 监听特定数据变化 | watch | watch(count, (newVal) => { /* ... */ }) |
| 获取变化前后的值 | watch | watch(count, (newVal, oldVal) => { /* ... */ }) |
| 初始不执行,仅响应变化 | watch | watch(count, (newVal) => { /* ... */ }) |
| 自动跟踪所有依赖 | watchEffect | watchEffect(() => { console.log(count.value); }) |
| 立即执行副作用 | watchEffect | watchEffect(() => { /* 初始执行并跟踪依赖 */ }) |
| 深层监听对象 | watch + deep | watch(obj, (newObj) => { /* ... */ }, { deep: true }) |
Vue3.0 性能提升主要是通过哪几方面体现的?
Vue 3.0 的性能提升是多维度优化的结果,主要体现在响应式系统重构、虚拟 DOM 优化、编译时优化、Tree-shaking 支持和打包体积优化等方面。以下是具体说明:
一、响应式系统升级(核心优化)
Vue 3 放弃了 Vue 2 基于 Object.defineProperty 的响应式机制,改用 ES6 Proxy 实现响应式,带来以下提升:
1. 更高效的依赖收集
- Proxy 直接代理整个对象,可监听对象属性的新增 / 删除、数组索引修改等操作(Vue 2 对这类操作无法检测)。
- 依赖收集更精准:通过
TrackOpTypes和TriggerOpTypes枚举精准区分依赖类型(GET/SET/ADD/DELETE),避免不必要的更新。
2. 避免性能损耗
- Vue 2 中
Object.defineProperty对数组的push/splice等方法需要重写(如Array.prototype方法劫持),而 Proxy 原生支持数组操作,性能更优。 - 减少内存开销:响应式对象无需额外存储
__ob__标记,内存占用降低约 40%。
3. 更好的 TypeScript 支持
- Proxy 原生支持
undefined和null作为目标对象,结合Ref和Reactive类型,类型推导更精准。
二、虚拟 DOM 优化
Vue 3 对虚拟 DOM(VNode)和 diff 算法进行了重构,提升渲染效率。
1. 更轻量化的 VNode 结构
- 用
Proxy替代 Vue 2 的VNode类,减少对象属性(如tag、data、children等直接作为 Proxy 的属性),内存占用减少约 50%。 - 引入 Fragment 支持(
v-fragment),无需额外包裹<div>即可渲染多个根节点,减少 DOM 节点创建开销。
2. 优化的 Diff 算法
- 最长递增子序列(LIS)算法:在对比新旧子节点时,通过计算最小移动距离(仅移动真实 DOM,而非重新创建),减少约 40% 的 DOM 操作。
- 静态标记(Static Markers):编译阶段自动标记模板中的静态节点(如
<div>Hello</div>),渲染时跳过静态节点的 diff 过程,提升动态更新性能。
三、编译时优化(Compiler 优化)
Vue 3 将部分运行时逻辑提前到编译阶段处理,减少运行时开销。
1. 模板预编译与静态提升
- 静态提升(Hoisting):编译时将静态节点(如
<h1>标题</h1>)提升为常量,只在首次渲染时创建,后续渲染直接复用,减少动态创建节点的性能消耗。 - 事件监听器缓存:编译时自动缓存静态节点的事件处理函数(如
@click),避免每次渲染都重新生成函数。
2. 按需编译(On-Demand Compilation)
- 仅编译模板中实际使用的指令(如
v-if、v-for),未使用的指令(如v-show)不会被打包,减小运行时代码体积。
3. 动态指令优化
- 对动态指令(如
v-bind:[key])进行优化,通过PatchFlags标记动态类型(如 PROPS、EVENTS 等),diff 时仅对比特定类型的动态属性,跳过无关检查。
四、Tree-shaking 支持(打包优化)
Vue 3 采用 ES Module 原生模块化,支持 Tree-shaking(摇树优化),可移除未使用的代码:
- 例如,未使用的全局 API(如
Vue.nextTick)在打包时会被自动剔除,相比 Vue 2 体积减少约 40%(完整版 Vue 2 约 228KB,Vue 3 约 133KB)。 - 组合式 API(如
ref、reactive)可按需引入,进一步减小打包体积。
五、其他性能优化
1. 更好的 SSR 支持
- 服务器端渲染(SSR)时,静态内容生成速度提升约 2-3 倍,得益于编译时的静态标记和更高效的 VNode 结构。
2. 组件初始化优化
- 组件创建过程中,
setup函数在响应式系统初始化前执行,避免不必要的依赖收集,提升组件初始化速度。
3. 自定义渲染器(Custom Renderer)
- 允许开发者基于 Vue 核心实现自定义渲染目标(如小程序、WebGL 等),避免冗余逻辑,提升特定场景下的性能。
性能对比数据
| 场景 | Vue 2.x | Vue 3.x | 提升幅度 |
|---|---|---|---|
| 初始化渲染 | 100% | ~133% | +33% |
| 数据更新(小组件) | 100% | ~55% | -45% |
| 数据更新(小组件) | 依赖数量多时有卡顿 | 依赖收集更精准,卡顿减少 | - |
| 内存占用 | 较高(含 __ob__ 标记) | 较低(Proxy 原生实现) | ~40% 减少 |
| 打包体积(生产环境) | 228KB(完整版) | 133KB(完整版) | ~42% 减少 |
总结
Vue 3 的性能提升是响应式系统重构、编译时优化和虚拟 DOM 升级共同作用的结果,尤其在大型应用和高频更新场景下优势显著。开发者通过合理使用组合式 API、静态标记和 Tree-shaking,可进一步放大这些优化效果,打造更高效的 Vue 应用。
Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?
Vue 3.0 用 Proxy API 替代 Object.defineProperty 主要是为了解决 Vue 2 响应式系统的核心限制,并提升性能、简化实现。以下是具体原因及对比分析:
一、Object.defineProperty 的局限性
1. 无法检测属性的新增或删除
-
Vue 2 必须通过Vue.set/Vue.delete
手动处理属性的添加 / 删除:
const obj = reactive({ name: 'John' }); obj.age = 30; // Vue 2 无法检测,需用 Vue.set(obj, 'age', 30)
#### **2. 深层响应式需要递归遍历**
- Vue 2 在初始化时需递归遍历对象所有属性,将其转换为 getter/setter:
```javascript
const obj = reactive({
nested: { count: 0 } // 需递归处理所有嵌套属性
});
3. 对数组的支持有限
-
Vue 2 无法监听数组索引的变化(如arr[0] = 1),需重写数组方法(如push/splice)
const arr = reactive([1, 2, 3]);
arr[0] = 100; // Vue 2 无法检测,需用 arr.splice(0, 1, 100)
#### **4. 初始性能开销大**
- 大型对象初始化时需递归转换所有属性,导致初始化时间较长。
### **二、Proxy API 的优势**
#### **1. 直接监听对象和数组**
- 完整拦截能力
:Proxy 可监听对象的所有操作(包括属性新增 / 删除、数组索引修改等):
```javascript
const obj = reactive({ name: 'John' });
obj.age = 30; // 自动响应式,无需额外操作
const arr = reactive([1, 2, 3]);
arr[0] = 100; // 自动响应式
2. 无需递归,按需响应式
-
懒代理(Lazy Proxy)
:只有访问嵌套属性时才会将其转换为响应式对象,提升初始化性能:
const obj = reactive({
nested: { count: 0 } // 初始只代理顶层对象
});
console.log(obj.nested.count); // 访问时才将 nested 转换为响应式
#### **3. 更好的性能**
- **更少的内存开销**:无需为每个属性创建 getter/setter,内存占用减少约 40%。
- **更高效的依赖跟踪**:Proxy 直接拦截属性访问,依赖收集更精准。
#### **4. 原生支持 ES6 新特性**
- 可拦截 `Map`、`Set`、`Promise` 等 ES6 数据结构,而 `Object.defineProperty` 仅支持普通对象。
### **三、Proxy 实现响应式的核心原理**
#### **1. 基本代理结构**
```javascript
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key);
// 返回属性值(如果是对象,递归创建 Proxy)
const value = Reflect.get(target, key, receiver);
return isObject(value) ? reactive(value) : value;
},
set(target, key, value, receiver) {
// 保存旧值用于比较
const oldValue = Reflect.get(target, key, receiver);
// 设置新值
const result = Reflect.set(target, key, value, receiver);
// 触发更新(仅当值变化时)
if (value !== oldValue) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
// 删除属性时触发更新
const hadKey = key in target;
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key);
}
return result;
}
});
}
2. 依赖收集与触发
- 依赖收集(Track):在
get拦截器中记录哪个副作用依赖了当前属性。 - 触发更新(Trigger):在
set/deleteProperty拦截器中通知所有依赖更新。
四、Proxy 的局限性与解决方案
1. 兼容性问题
-
IE 不支持
:Proxy 是 ES6 特性,IE 全版本不支持。
- 解决方案:Vue 3 放弃对 IE 的支持,若需兼容可考虑 Vue 2 或使用 polyfill(如
proxy-polyfill,但功能有限)。
- 解决方案:Vue 3 放弃对 IE 的支持,若需兼容可考虑 Vue 2 或使用 polyfill(如
2. 无法代理原始值
-
Proxy 只能代理对象,无法直接代理基本类型(如number,string)。
-
解决方案
Vue 3 引入ref,通过包装对象({ value: T })实现基本类型的响应式:
-
const count = ref(0); // 本质是 { value: 0 } 的 Proxy
```
五、性能对比
| 指标 | Object.defineProperty | Proxy | 提升幅度 |
|---|---|---|---|
| 对象属性添加 / 删除 | 需手动处理(如 Vue.set) | 自动响应式 | +∞ |
| 深层对象初始化 | 全量递归转换 | 按需懒代理 | ~50% |
| 大型对象内存占用 | 较高(每个属性有 getter/setter) | 较低(直接代理对象) | ~40% |
| 依赖收集精准度 | 有限(如数组索引变化无法跟踪) | 全面(所有操作可拦截) | +∞ |
六、总结
| 场景 | Object.defineProperty | Proxy |
|---|---|---|
| 监听属性新增 / 删除 | ❌ 需手动处理 | ✅ 自动支持 |
| 监听数组索引变化 | ❌ 需重写数组方法 | ✅ 直接支持 |
| 深层响应式性能 | ❌ 初始化时全量递归 | ✅ 按需懒代理 |
| 内存占用 | ❌ 每个属性有额外开销 | ✅ 直接代理对象 |
| ES6 数据结构支持(Map/Set) | ❌ 不支持 | ✅ 直接支持 |
| 兼容性 | ✅ 支持 IE | ❌ 不支持 IE |
Vue3 所采用的 Composition API 与 Vue2 使用的 Options API 有什么不同?
Vue 3 的 Composition API 与 Vue 2 的 Options API 在设计理念、代码组织和功能实现上有显著差异。以下从多个维度对比分析两者的核心区别:
一、代码组织方式:按逻辑分组 vs 按选项分组
Options API(Vue 2)
-
按选项分类组织代码:数据、方法、生命周期钩子等按
data、methods、mounted等选项分组。 -
逻辑复用困难:相似逻辑需通过
mixins复用,但存在命名冲突、数据来源不清晰等问题。 -
示例
export default {
data() { return { count: 0 } },
methods: { increment() { this.count++ } },
mounted() { console.log(‘组件挂载’) }
}
#### **Composition API(Vue 3)**
- **按逻辑功能分组**:将实现同一功能的代码(如数据、方法、生命周期)整合到自定义函数中(如 `useCounter`)。
- **逻辑复用灵活**:通过自定义 `hook` 函数复用逻辑,避免 `mixins` 的缺陷,且可直接通过变量名追踪数据来源。
- 示例
```javascript
import { ref, onMounted } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => count.value++;
onMounted(() => console.log('组件挂载'));
return { count, increment };
}
}
二、响应式数据:隐性 vs 显性
Options API
-
隐性响应式:通过
data声明的属性自动转为响应式,但需通过this访问,类型推断不友好。 -
局限性
- 复杂逻辑中
this指向易混乱(如箭头函数中丢失上下文)。
- 复杂逻辑中
-
类型声明需依赖
this,IDE 智能提示受限(需搭配vue-class-component等库)。
Composition API
-
显性响应式:通过
ref/reactive显式创建响应式数据,类型推断更直接。 -
优势
- 变量直接声明在作用域内,无需依赖
this,避免上下文问题。
- 变量直接声明在作用域内,无需依赖
-
支持 TypeScript 原生类型推导,代码更健壮。
-
示例
// 基本类型响应式(ref)
const count = ref(0); // 类型显式声明
count.value++; // 显式操作值
// 对象响应式(reactive)
const state = reactive<{ name: string }>({ name: ‘Alice’ });
state.name = ‘Bob’; // 直接修改
### **三、生命周期钩子:选项式 vs 函数式**
#### **Options API**
- **通过选项声明钩子**:如 `mounted`、`updated` 等,代码分散在不同选项中。
- 示例
```javascript
export default {
mounted() { console.log('挂载'); },
updated() { console.log('更新'); }
}
Composition API
-
通过函数注册钩子:在
setup中调用onMounted、onUpdated等函数,逻辑集中。 -
示例
import { onMounted, onUpdated } from ‘vue’;
setup() {
onMounted(() => console.log(‘挂载’));
onUpdated(() => console.log(‘更新’));
}
### **四、逻辑复用:Mixins vs Custom Hooks**
#### **Options API(Mixins)**
- 通过混入复用逻辑
:多个组件可引入同一个mixin,但存在以下问题:
- **命名冲突**:不同 `mixin` 或组件可能定义同名属性 / 方法。
- **数据来源不清晰**:组件属性可能来自多个 `mixin`,调试困难。
- 示例
```javascript
// mixin.js
export const counterMixin = {
data() { return { count: 0 } },
methods: { increment() { this.count++ } }
};
// 组件中使用
export default {
mixins: [counterMixin]
};
Composition API(Custom Hooks)
-
通过自定义函数(Hook)复用逻辑:返回响应式数据和方法,完全避免命名冲突,数据来源清晰。
-
示例
// useCounter.js
export function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}
// 组件中使用
setup() {
const { count, increment } = useCounter();
return { count, increment };
}
### **五、类型支持:弱类型 vs 强类型**
#### **Options API**
- **类型支持较弱**:依赖 `this` 访问属性,需借助装饰器或复杂类型声明(如 `vue-property-decorator`)。
- 示例(TypeScript)
```typescript
import { Vue, Component } from 'vue-property-decorator';
@Component
export default class Counter extends Vue {
count = 0; // 需通过装饰器声明响应式
increment() { this.count++; }
}
Composition API
-
原生支持 TypeScript:变量类型由
ref/reactive直接推断,无需额外配置。 -
示例(TypeScript)
setup(): { count: Ref; increment: () => void } {
const count = ref(0); // 类型为 Ref
const increment = () => count.value++;
return { count, increment };
}
### **六、性能与打包体积**
#### **Options API**
- **固定打包内容**:无论是否使用,Vue 2 的选项式 API 相关代码会全部打包。
#### **Composition API**
- **Tree-shaking 友好**:按需引入 `ref`、`reactive` 等函数,未使用的 API 会被摇树优化移除,减小打包体积。
### **七、适用场景对比**
| **场景** | Options API | Composition API |
| ----------------------- | -------------------- | -------------------------- |
| **小型组件 / 快速开发** | ✅ 代码结构直观 | ✅ 也适用,但可能稍显繁琐 |
| **复杂逻辑组件** | ❌ 逻辑分散,难以维护 | ✅ 逻辑按功能分组,易于扩展 |
| **逻辑复用(跨组件)** | ❌ Mixins 存在缺陷 | ✅ Custom Hooks 灵活无冲突 |
| **TypeScript 项目** | ❌ 需要额外配置 | ✅ 原生支持,类型推导精准 |
| **大型团队协作项目** | ❌ 数据来源难追踪 | ✅ 变量作用域明确,调试容易 |
### 用Vue3.0 写过组件吗?如果想实现一个 Modal你会怎么设计?
***\*三、\*******\*综合应用\****
### vue中如何扩展一个组件
#### **组件继承(推荐)**
通过 `extends` 选项继承现有组件,新增或覆盖原有功能。
```javascript
// 基础组件
const BaseButton = {
props: {
size: String,
type: {
type: String,
default: 'primary'
}
},
template: `
<button
:class="[type, size]"
@click="$emit('click')"
>
<slot />
</button>
`
};
// 扩展组件
const SubmitButton = {
extends: BaseButton,
props: {
loading: Boolean // 新增属性
},
template: `
<button
:class="[type, size]"
:disabled="loading"
@click="$emit('click')"
>
<span v-if="loading">加载中...</span>
<slot v-else />
</button>
`
};
子组件可以直接改变父组件的数据么,说明原因
一、为什么禁止直接修改?
1. 单向数据流原则
Vue 遵循单向数据流模式,数据流动是单向的:
- 父组件 → 子组件:通过
props传递数据。 - 子组件 → 父组件:通过
$emit触发事件通知父组件自行修改。
直接修改会破坏数据流的可预测性,导致:
- 数据流向混乱,难以调试。
- 状态变化不可追溯,违背 Vue 的响应式设计初衷。
2. 维护组件独立性
子组件直接修改父组件数据会导致:
- 组件间强耦合,父组件状态的修改逻辑分散在多个子组件中。
- 父组件无法控制数据的修改时机和方式,失去对状态的主导权。
从0到1自己构架一个vue项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织
vue create name
选配置
npm下载依赖
引入路由,vuex等
ui,axios等
src/
├─ api/ # API 接口
├─ assets/ # 静态资源
├─ components/ # 公共组件
├─ directives/ # 自定义指令
├─ filters/ # 自定义过滤器
├─ router/ # 路由配置
├─ store/ # Vuex 状态管理
├─ utils/ # 工具函数
├─ views/ # 视图组件
├─ App.vue # 根组件
├─ main.js # 入口文件
你知道哪些vue代码优化策略
一、组件设计优化
1. 组件拆分与单一职责
-
原则:每个组件只负责单一功能,避免组件过大。
-
技巧
- 将复杂组件拆分为更小的子组件(如
Form→Input、Select、Button)。
- 将复杂组件拆分为更小的子组件(如
-
使用
mixins或Composition API提取公共逻辑(如表单验证、数据请求)。
2. 合理使用组件通信方式
-
父子组件:优先使用
props和$emit,避免滥用$parent或$children。 -
跨层级组件
- 少量数据:使用
event bus或provide/inject。
- 少量数据:使用
-
大量数据:使用状态管理库(Vuex/Pinia)。
3. 减少组件层级嵌套
-
问题:过深的组件嵌套会导致性能下降和调试困难。
-
优化
- 使用
Vue.mixin()或全局组件减少重复代码。
- 使用
-
避免无意义的包装组件(如单纯传递 props 的中间层)。
二、响应式优化
1. 避免过度响应式
-
问题:大型对象或数组转为响应式会消耗大量内存。
-
优化
- 使用
Object.freeze()冻结不需要响应式的数据。
- 使用
-
对大型列表使用
shallowReactive或shallowRef(仅顶层响应式)。
2. 合理使用计算属性与监听器
-
计算属性
:用于缓存依赖响应式数据的纯函数计算。
// 正确:缓存结果
computed: {
fullName() {
return ${this.firstName} ${this.lastName};
}
}
- **监听器**:处理异步操作或非响应式依赖(如定时器)。
#### **3. 避免频繁触发响应式更新**
- **问题**:在循环中修改响应式数据会导致频繁渲染。
- 优化
- 批量更新数据(如使用 `Vue.nextTick()`)。
- 使用 `v-show` 替代频繁切换的 `v-if`。
### **三、性能优化**
#### **1. 虚拟列表(Large Lists)**
- **问题**:渲染大量数据时会导致卡顿。
- 解决方案
:使用虚拟列表库(如vue-virtual-scroller,vue-virtual-list)。
```javascript
<RecycleScroller
class="scroller"
:items="items"
:item-size="32"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>
2. 懒加载与异步组件
-
路由懒加载
{
path: ‘/about’,
component: () => import(‘./views/About.vue’)
}
- 组件懒加载
```javascript
const HeavyComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'));
3. 减少 DOM 操作
-
问题:直接操作 DOM 会破坏 Vue 的响应式系统。
-
优化
- 通过修改数据驱动视图更新。
-
使用
v-html时确保内容安全,避免频繁更新。
四、工程化优化
1. 代码分割与 Tree-shaking
- 配置:使用 Webpack 或 Vite 进行代码分割。
- 效果:减少首屏加载时间,仅加载必要代码。
2. 静态资源优化
- 图片压缩:使用
image-webpack-loader压缩图片。 - CDN 加载:将第三方库通过 CDN 引入,减小打包体积。
3. 构建优化
-
生产环境配置
// vue.config.js
module.exports = {
productionSourceMap: false,
chainWebpack: config => {
config.optimization.splitChunks({
chunks: ‘all’
});
}
};
### SPA、SSR的区别是什么
SPA(单页应用)和 SSR(服务器端渲染)是前端应用的两种主要渲染模式,它们在渲染流程、性能、SEO 等方面存在显著差异。以下是核心区别的对比分析:
### **一、渲染流程对比**
#### **SPA(单页应用)**
- 流程
1. 浏览器加载 HTML 文件(通常为空或仅含基本结构)。
2. 加载 JavaScript 代码(如 Vue/React 应用)。
3. JavaScript 在客户端初始化应用状态,动态渲染 DOM。
- 特点
- **首次加载慢**:需等待 JS 加载并执行后才能渲染内容。
- **后续交互快**:只更新变化的 DOM 部分,无需重新加载页面。
- 示例(Vue SPA)
```html
<!-- index.html -->
<div id="app"></div>
<script src="app.js"></script> <!-- 加载整个应用 -->
SSR(服务器端渲染)
-
流程
- 服务器收到请求后,在服务器端执行 Vue/React 代码。
- 生成完整的 HTML 页面(包含预渲染内容)。
- 服务器将 HTML 发送到浏览器,浏览器直接渲染。
- 客户端 JavaScript 加载后,“激活”(hydrate)静态 HTML,使其变为可交互的应用。
-
特点
- 首次加载快:用户立即看到完整 HTML 内容。
-
服务器压力大:每次请求都需服务器重新渲染。
-
示例(Vue SSR)
// 服务器端代码
app.get(‘*’, (req, res) => {
const context = { url: req.url };
renderer.renderToString(context, (err, html) => {
res.send( <html> <body> <div id="app">${html}</div> <script src="app.js"></script> </body> </html> );
});
});
### **二、核心差异对比表**
| **维度** | **SPA(单页应用)** | **SSR(服务器端渲染)** |
| ---------------- | ------------------------------------------ | ------------------------------------------- |
| **首次加载时间** | 慢(需等待 JS 加载并执行) | 快(直接渲染预生成的 HTML) |
| **SEO 友好度** | 差(搜索引擎爬虫难以解析 JS 动态内容) | 好(服务器返回完整 HTML,包含内容) |
| **服务器压力** | 低(仅提供静态文件) | 高(每次请求都需服务器渲染) |
| **交互体验** | 流畅(仅更新变化的 DOM 部分) | 初始渲染快,但可能有 "闪烁"(hydrate 过程) |
| **开发复杂度** | 低(常规前端开发) | 高(需处理服务器端和客户端的差异) |
| **缓存策略** | 简单(缓存整个应用) | 复杂(需根据内容动态缓存) |
| **适用场景** | 内部管理系统、社交平台(SEO 不重要的场景) | 电商网站、新闻网站(需要 SEO 和首屏速度) |
### **三、关键技术对比**
#### **1. 实现方式**
- SPA
- 使用 Vue Router、React Router 等实现前端路由。
- 所有状态管理和渲染在客户端完成。
- SSR
- 使用 Vue Server Renderer、Next.js(React)等框架。
- 需要 Node.js 服务器支持。
#### **2. 数据获取**
- SPA
- 在组件生命周期钩子(如 `mounted`)中获取数据。
```javascript
export default {
async mounted() {
this.data = await fetchData();
}
};
```
- SSR
- 在服务器端获取数据并注入 HTML(如 Vue 的 `asyncData`)。
```javascript
export default {
asyncData({ store, route }) {
return store.dispatch('fetchPost', route.params.id);
}
};
```
#### **3. 状态管理**
- SPA
- 状态仅存在于客户端,刷新页面会丢失。
- SSR
- 需要在服务器和客户端之间传递状态(如使用 `window.__INITIAL_STATE__`)。
```javascript
// 服务器端
const context = { state: store.state };
renderer.renderToString(context, (err, html) => {
html = html.replace('<!-- APP_STATE -->', `
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(context.state)}
</script>
`);
});
```
### **四、优缺点与适用场景**
#### **SPA 的优势与适用场景**
- 优势
- 开发简单,无需服务器配置。
- 适合内部系统或对 SEO 要求不高的应用。
- 良好的交互体验(类似原生应用)。
- 适用场景
- 后台管理系统。
- 社交平台(如 Twitter、Facebook)。
- 游戏或富交互应用。
#### **SSR 的优势与适用场景**
- 优势
- 更好的 SEO 表现。
- 更快的首屏加载速度(对移动设备和弱网环境友好)。
- 统一的代码结构(前后端共享逻辑)。
- 适用场景
- 电商网站(如淘宝、京东)。
- 新闻网站(如新浪、腾讯新闻)。
- 需要高 SEO 排名的内容型网站。
### **五、进阶方案:SSG 与 CSR**
#### **1. SSG(静态站点生成)**
- **原理**:构建时生成静态 HTML 文件,运行时无需服务器渲染。
- **工具**:VuePress、Nuxt.js 的静态生成模式、Next.js 的 SSG。
- 优势
- 性能极致(静态文件)。
- 极低的服务器成本。
- **适用场景**:博客、文档网站、企业官网。
#### **2. CSR(客户端渲染,SPA 的别名)**
- **特点**:所有渲染在客户端完成,首屏依赖 JS 加载。
- **对比**:与 SSR 相对,适用于对 SEO 和首屏速度要求不高的场景。
### **六、如何选择?**
1. **优先考虑 SEO 和首屏速度**:选择 SSR 或 SSG。
2. **开发复杂度和服务器成本**:
- 若团队熟悉前端且无服务器资源,选择 SPA(CSR)。
- 若需要 SEO 且有服务器支持,选择 SSR。
3. **内容更新频率**:
- 静态内容(如博客):选择 SSG。
- 动态内容(如电商商品详情):选择 SSR。
4. **渐进式升级**:
- 从 SPA 起步,后续根据需求引入 SSR(如使用 Nuxt.js、Next.js 的混合模式)。
### **总结**
SPA 和 SSR 各有优劣,选择时需根据项目需求、团队技术栈和性能要求综合考虑。现代框架(如 Vue、React)提供了灵活的渲染模式支持,可根据场景混合使用,实现最佳用户体验和开发效率。
###
### 1、 什么是 DOM 和 BOM?
### **DOM(文档对象模型,Document Object Model)**
#### **定义**
DOM 是 **HTML/XML 文档的编程接口**,它将文档转换为一个由多层节点(Node)构成的树形结构(**DOM 树**),允许通过 JavaScript 等脚本语言动态访问、修改文档的内容、结构和样式。
#### **核心作用**
1. **文档结构化解析**
将 HTML/XML 文档解析为树形结构,每个节点代表文档中的一个元素(如 `<div>`、`<p>`)、属性、文本内容等。
```html
<!-- 示例 HTML 对应的 DOM 树结构 -->
<html>
<head>
<title>页面标题</title>
</head>
<body>
<div id="container">Hello, DOM!</div>
</body>
</html>
- 根节点:
html - 子节点:
head和body - 叶子节点:文本节点(如 “页面标题”“Hello, DOM!”)
-
动态操作文档
通过 JavaScript 可以:- 增删改节点:创建新元素、删除旧元素、修改元素内容(如
document.createElement、element.appendChild)。 - 修改样式:直接操作元素的
style属性(如div.style.color = 'red')。 - 事件交互:为元素绑定事件监听(如
click、mouseover)。
- 增删改节点:创建新元素、删除旧元素、修改元素内容(如
-
跨语言交互
DOM 不仅限于 JavaScript,还可通过其他语言(如 Python、Java)操作,但前端开发中最常用的是 JavaScript。
关键对象与 API
-
document对象:代表整个 HTML 文档,是 DOM 树的入口(如document.getElementById、document.querySelector)。 -
节点类型
- 元素节点(
Element):HTML 标签(如<div>)。
- 元素节点(
-
文本节点(
Text):标签中的文本内容。- 属性节点(
Attr):元素的属性(如id="container")。
- 属性节点(
-
常用操作 API
const div = document.createElement(‘div’); // 创建元素节点
div.textContent = ‘新内容’; // 修改文本节点
document.body.appendChild(div); // 将节点添加到 body 中
### **BOM(浏览器对象模型,Browser Object Model)**
#### **定义**
BOM 是 **浏览器提供的一套对象模型**,用于操作浏览器窗口、地址栏、历史记录、弹出框等**浏览器自身功能**,与网页内容无关。
#### **核心对象与功能**
1. **`window` 对象**
- **浏览器窗口的顶级对象**:在浏览器中,`window` 是全局作用域,所有全局变量和函数默认都是它的属性(如 `window.alert`、`window.setTimeout`)。
- 常用属性和方法
- **窗口尺寸**:`innerWidth`/`innerHeight`(视口尺寸)、`outerWidth`/`outerHeight`(窗口整体尺寸)。
- **滚动控制**:`scrollTo(x, y)`、`scrollBy(x, y)`。
- **定时器**:`setTimeout`(单次延迟执行)、`setInterval`(循环执行)。
- **弹出框**:`alert`(警告框)、`confirm`(确认框,返回 `true/false`)、`prompt`(输入框,返回用户输入值)。
2. **`location` 对象**
- 操作浏览器地址栏
```javascript
console.log(location.href); // 当前完整 URL(如 "https://example.com/path?query=1")
location.href = 'https://new-url.com'; // 跳转到新 URL
location.reload(); // 刷新页面
```
- **常用属性**:`protocol`(协议,如 `http:`)、`host`(主机名,如 `example.com`)、`pathname`(路径)、`search`(查询参数)。
3. **`history` 对象**
- 操作浏览器历史记录
```javascript
history.back(); // 回退到上一页(等价于浏览器的“后退”按钮)
history.forward(); // 前进到下一页
history.pushState(state, title, url); // 在历史记录中添加新条目(HTML5 新增)
```
4. **`navigator` 对象**
- 获取浏览器信息
```javascript
console.log(navigator.userAgent); // 用户代理字符串(包含浏览器类型、版本等)
console.log(navigator.platform); // 操作系统(如 'Windows'、'MacIntel')
-
screen对象-
获取屏幕信息
-
console.log(screen.width); // 屏幕宽度(像素)
console.log(screen.availWidth); // 可用宽度(排除任务栏等)
### **DOM vs BOM:核心区别**
| **维度** | **DOM(文档对象模型)** | **BOM(浏览器对象模型)** |
| -------------- | -------------------------------------- | ----------------------------------------- |
| **作用范围** | 操作 **网页内容**(HTML/XML 文档结构) | 操作 **浏览器自身功能**(窗口、地址栏等) |
| **标准化程度** | W3C 标准(有统一规范) | 由浏览器厂商定义(不同浏览器可能有差异) |
| **核心对象** | `document`(文档入口) | `window`(浏览器窗口入口) |
| **典型操作** | 修改 `div` 的内容、样式,绑定点击事件 | 跳转页面、获取屏幕尺寸、弹出警告框 |
### **实际开发中的应用场景**
1. **DOM 的典型场景**
- 动态渲染列表数据(如从后端获取数据后生成 `<li>` 元素)。
- 实现页面交互效果(如点击按钮切换样式、动态加载内容)。
- 操作表单元素(如获取输入框的值 `input.value`)。
2. **BOM 的典型场景**
- 页面跳转与路由管理(通过 `location.href` 或 `history.pushState`)。
- 窗口大小监听(`window.addEventListener('resize', ...)`)。
- 定时任务(如轮播图自动切换 `setInterval`)。
- 检测用户浏览器环境(通过 `navigator.userAgent` 判断设备类型)。
### **总结**
- **DOM** 是网页内容的 “操作接口”,聚焦于文档结构的动态修改。
- **BOM** 是浏览器功能的 “控制接口”,聚焦于浏览器窗口的行为和属性。
- 在前端开发中,两者通常配合使用:
- 用 **DOM** 操作页面内容,实现业务逻辑;
- 用 **BOM** 处理浏览器环境相关的需求(如路由、窗口交互)。
- 注意:`window` 对象同时包含 DOM 和 BOM 的功能(如 `window.document` 指向 DOM 的 `document` 对象),这是两者的主要关联点。
### 16、 原型和原型链?
在 JavaScript 中,**原型(Prototype)** 和 **原型链(Prototype Chain)** 是实现继承和对象属性共享的核心机制。理解它们对于掌握 JavaScript 的面向对象编程至关重要。
### **一、原型(Prototype)的基本概念**
#### **1. 每个对象都有一个内部属性 `[[Prototype]]`**
- **`[[Prototype]]`** 是一个隐藏属性(在浏览器中通常以 `__proto__` 形式访问),指向该对象的原型对象。
- **原型对象** 也是一个普通对象,它自身也有 `[[Prototype]]`,以此类推,直到最顶层的原型对象 `Object.prototype`(其 `[[Prototype]]` 为 `null`)。
#### **2. 原型的作用:实现属性和方法的共享**
- 当访问一个对象的属性时,JavaScript 首先在该对象本身查找,若找不到,则沿着 `[[Prototype]]` 链向上查找,直到找到该属性或到达链的末尾(`null`)。
#### **3. 创建对象时的原型关联**
- 通过构造函数创建对象
```javascript
function Person(name) {
this.name = name;
}
// 给构造函数的 prototype 属性添加方法
Person.prototype.sayHello = function() {
return `Hello, ${this.name}`;
};
const person1 = new Person("Alice");
const person2 = new Person("Bob");
// person1 和 person2 的 [[Prototype]] 都指向 Person.prototype
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
console.log(person1.sayHello()); // "Hello, Alice"(通过原型链访问)
-
通过
Object.create()创建对象
const animal = {
eat() {
console.log(“Eating…”);
}
};
const dog = Object.create(animal); // dog 的 [[Prototype]] 是 animal
dog.bark = function() {
console.log(“Woof!”);
};
dog.eat(); // “Eating…”(通过原型链访问)
### **二、原型链(Prototype Chain)的工作原理**
#### **1. 原型链的本质**
- **原型链是由多个对象通过 `[[Prototype]]` 连接而成的链条**。
- 当访问一个对象的属性或方法时,JavaScript 会:
1. 先检查该对象本身是否有此属性。
2. 若没有,则沿着 `[[Prototype]]` 链向上查找,直到找到或到达 `Object.prototype`(若仍未找到则返回 `undefined`)。
#### **2. 示例:原型链的查找过程**
```javascript
// 定义构造函数
function Animal() {
this.type = "animal";
}
Animal.prototype.getInfo = function() {
return `Type: ${this.type}`;
};
function Dog(name) {
this.name = name;
}
// 设置 Dog 的原型为 Animal 的实例
Dog.prototype = new Animal();
Dog.prototype.bark = function() {
return `${this.name} barks!`;
};
const myDog = new Dog("Buddy");
// 访问属性和方法的查找路径:
console.log(myDog.name); // 1. 在 myDog 本身查找 → "Buddy"
console.log(myDog.type); // 1. myDog 没有 → 2. 查找 Dog.prototype(即 Animal 的实例) → "animal"
console.log(myDog.bark()); // 1. myDog 没有 → 2. Dog.prototype 有 → "Buddy barks!"
console.log(myDog.getInfo()); // 1. myDog 没有 → 2. Dog.prototype 没有 → 3. Animal.prototype 有 → "Type: animal"
三、JavaScript 中的原型继承
1. 基于原型链的继承
-
实现方式:通过修改构造函数的
prototype属性,使其指向另一个对象的实例。 -
示例
// 父类
function Vehicle() {
this.wheels = 4;
}
Vehicle.prototype.drive = function() {
return “Driving…”;
};
// 子类
function Car() {
Vehicle.call(this); // 继承实例属性
}
Car.prototype = Object.create(Vehicle.prototype); // 继承原型方法
Car.prototype.constructor = Car; // 修复 constructor
const myCar = new Car();
console.log(myCar.wheels); // 4(继承自 Vehicle)
console.log(myCar.drive()); // “Driving…”(继承自 Vehicle.prototype)
#### **2. 现代继承方式(ES6 Class)**
- `class` 和 `extends` 语法糖
:本质上还是基于原型链,但语法更简洁。
```javascript
class Vehicle {
constructor() {
this.wheels = 4;
}
drive() {
return "Driving...";
}
}
class Car extends Vehicle {
constructor() {
super(); // 调用父类构造函数
}
}
const myCar = new Car();
console.log(myCar.wheels); // 4
console.log(myCar.drive()); // "Driving..."
四、原型链的顶层:Object.prototype
-
所有对象的最终原型都是
Object.prototype。包含许多内置方法,如:
toString():返回对象的字符串表示。hasOwnProperty():检查对象本身是否有某个属性(不包括原型链)。valueOf():返回对象的原始值。
示例:验证原型链终点
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null(原型链终点)
五、原型链的性能与注意事项
1. 性能考虑
- 属性查找效率:访问原型链上的属性会比直接访问对象本身的属性慢,尤其是当链很长时。
- 避免过度嵌套原型链:过长的原型链会导致性能下降,建议控制链的长度。
2. 常见陷阱
-
原型属性共享问题
function MyClass() {}
MyClass.prototype.items = []; // 引用类型的原型属性
const obj1 = new MyClass();
const obj2 = new MyClass();
obj1.items.push(1);
console.log(obj2.items); // [1](共享同一个数组)
- **解决方案**:将引用类型的属性放在构造函数中初始化,而非原型上。
### **六、总结:原型与原型链的核心**
1. **每个对象都有一个原型对象(`[[Prototype]]`)**,形成一条链直到 `Object.prototype`。
2. **属性和方法的查找**:先在对象本身查找,再沿原型链向上。
3. **继承实现**:通过修改原型链(如 `Object.create()` 或 ES6 的 `class/extends`)。
4. **注意事项**:避免过长的原型链,警惕原型属性的共享问题。
### 17、 0.1 + 0.2 === 0.3 嘛?为什么?怎么可以等于?
### **一、为什么不等于?**
#### **1. 二进制浮点数的存储限制**
- 计算机使用 **二进制浮点数**(遵循 [IEEE 754 标准](https://en.wikipedia.org/wiki/IEEE_754))存储数字,整数可以精确表示,但**小数(尤其是无限循环小数)无法精确转换为二进制**。
- **示例:0.1 的二进制表示**
`0.1` 转换为二进制是一个无限循环小数:
`0.1₁₀ = 0.00011001100110011...₂`(循环节为 `0011`)。
计算机只能用有限位存储(如 64 位双精度浮点数),因此会**舍入近似**,导致精度丢失。
- **0.1 + 0.2 的计算过程**:
两者的二进制近似值相加后,结果的二进制表示也无法精确等于 `0.3` 的二进制近似值。实际计算结果为:
`0.30000000000000004`(因舍入误差产生微小偏差)。
#### **2. 验证示例**
```javascript
console.log(0.1 + 0.2); // 输出 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // 输出 false
二、如何让它们 “相等”?
由于浮点数精度问题无法避免,需通过数值比较策略绕过直接相等判断。以下是常用方法:
1. 设定误差范围(最小精度值)
使用一个极小值(如 EPSILON)作为允许的误差范围,判断计算结果与目标值的差值是否在范围内。
const EPSILON = 1e-10; // 可根据需求调整精度
function isEqual(a, b) {
return Math.abs(a - b) < EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // 输出 true
2. 转换为整数计算(适用于小数位数确定的场景)
将小数乘以 10^n 转换为整数,计算后再转换回小数(需注意数值范围避免溢出)。
function add(a, b, decimal = 1) {
const factor = 10 ** decimal; // 小数位数,如 0.1 和 0.2 是 1 位小数
return (Math.round(a * factor) + Math.round(b * factor)) / factor;
}
const result = add(0.1, 0.2, 1); // 转换为 1 和 2 相加,结果为 3/10 = 0.3
console.log(result === 0.3); // 输出 true
3. 使用高精度计算库
若需处理复杂的高精度运算(如金融场景),可引入第三方库避免手动处理误差。
-
推荐库
- decimal.js:用于任意精度的十进制运算。
-
big.js:轻量级高精度数值库。
const Decimal = require('decimal.js');
const result = new Decimal('0.1').add('0.2');
console.log(result.equals(new Decimal('0.3'))); // 输出 true
三、总结
-
根本原因:二进制浮点数无法精确表示某些十进制小数,导致计算时产生舍入误差。
-
避免直接比较:永远不要用
===直接比较两个可能存在精度问题的浮点数。 -
解决方案
- 用误差范围判断近似相等;
-
转换为整数运算;
- 使用高精度库处理复杂场景。
20、 xml和 json的区别?
XML(可扩展标记语言)和 JSON(JavaScript 对象表示法)是两种常用的数据交换格式,它们在语法、用途、性能和生态等方面存在显著差异。以下是核心对比:
一、语法结构对比
XML
-
标签结构:使用闭合标签(如
<tag>内容</tag>),支持嵌套。 -
自描述性:标签名和属性可自定义,结构灵活。
-
示例
JSON
-
键值对结构:使用
{}表示对象,[]表示数组,键必须用双引号。 -
轻量级:无冗余标签,格式紧凑。
{ "bookstore": { "book": { "category": "fiction", "title": { "lang": "en", "#text": "Harry Potter" }, "author": "J.K. Rowling", "price": 29.99 } } }
二、核心差异对比表
| 维度 | XML | JSON |
|---|---|---|
| 语法复杂度 | 高(需闭合标签、属性、命名空间) | 低(简洁的键值对) |
| 数据类型 | 仅字符串(需自行解析类型) | 支持 string、number、boolean、null、object、array |
| 可读性 | 结构清晰,但标签冗余 | 简洁,适合人类阅读 |
| 解析难度 | 需 XML 解析器(如 DOM、SAX) | 原生支持 JSON.parse() 和 JSON.stringify() |
| 扩展性 | 强(支持命名空间、DTD、XSD 验证) | 弱(无内置验证机制,依赖外部规范) |
| 体积与性能 | 文本冗余,体积大,解析慢 | 体积小,解析快(适合网络传输) |
| 应用场景 | WebService、配置文件、RSS、API 文档 | RESTful API、前后端数据交换、配置文件 |
三、关键特性对比
1. 数据类型支持
- XML:所有数据均为字符串,需手动解析类型(如将
"29.99"转为数字)。 - JSON:原生支持多种类型,解析后直接可用。
2. 验证机制
-
XML
:支持 DTD(文档类型定义)和 XSD(XML 模式定义)进行严格验证。
<!ELEMENT book (title, author, price)>
<!ATTLIST book category CDATA #REQUIRED>
]>
- **JSON**:无内置验证,需依赖外部工具(如 JSON Schema、TypeScript 类型定义)。
#### **3. 命名空间与嵌套**
- **XML**:支持命名空间(如 `<ns:tag>`),适合复杂结构化数据。
- **JSON**:通过嵌套对象实现类似功能,但无显式命名空间语法。
#### **4. 注释支持**
- **XML**:支持注释(`<!-- 注释 -->`)。
- **JSON**:标准中不支持注释(部分实现通过非标准扩展支持)。
### **四、优缺点与适用场景**
#### **XML 的优势与适用场景**
- 优势
- 严格的验证机制(DTD/XSD)确保数据结构正确性。
- 支持命名空间,适合复杂系统集成。
- 良好的文档性,适合需要详细注释的配置文件。
- 适用场景
- 企业级系统间的数据交换(如 SOAP、WebService)。
- 配置文件(如 AndroidManifest.xml、Maven 的 pom.xml)。
- 需要严格格式验证的场景(如金融、医疗领域)。
#### **JSON 的优势与适用场景**
- 优势
- 轻量级,解析速度快(尤其在前端)。
- 与 JavaScript 无缝集成,无需额外转换。
- 生态丰富(几乎所有语言都有 JSON 解析库)。
- 适用场景
- 现代 Web API(如 RESTful、GraphQL)。
- 前端状态管理(如 Vuex、Redux 的数据存储)。
- 配置文件(如 package.json、.eslintrc.json)。
### **五、格式转换与工具链**
#### **1. XML → JSON**
- **工具**:`xml2js`(Node.js)、XSLT(复杂转换)。
- 示例(JavaScript)
```javascript
const xml = '<book><title>Harry Potter</title></book>';
const parser = new DOMParser();
const doc = parser.parseFromString(xml, 'text/xml');
const json = {
book: {
title: doc.querySelector('title').textContent
}
};
2. JSON → XML
-
工具:
json2xml(Node.js)、自定义转换函数。 -
示例(JavaScript)
const json = { book: { title: ‘Harry Potter’ } };
const xml = <book><title>${json.book.title}</title></book>;
### **六、如何选择?**
1. **优先考虑兼容性与生态**:
- 若需与旧系统集成(如 Java 后端),选 XML。
- 若为现代 Web 应用(前后端分离),选 JSON。
2. **权衡体积与性能**:
- 对带宽敏感的场景(如移动端),JSON 更优。
3. **验证需求**:
- 若需严格格式验证,XML 的 DTD/XSD 更方便;JSON 需额外工具。
4. **数据复杂度**:
- 简单数据(如 API 返回)用 JSON;复杂结构化数据(如文档树)用 XML。
### 21、 Es6 里面数据结构Set Map
在 ES6(ES2015)中,JavaScript 新增了两种重要的数据结构:**Set** 和 **Map**,它们分别解决了集合操作和键值对存储的痛点。以下是它们的核心特性和使用场景:
### **一、Set(集合)**
#### **特性**
- **无序且唯一**:成员的值都是唯一的,没有重复。
- **任意类型**:可以存储任意类型的值(包括对象、函数等引用类型)。
- **迭代器支持**:可通过 `for...of` 或 `forEach` 遍历。
#### **基本操作**
```javascript
// 创建 Set
const set = new Set([1, 2, 2, 'hello', { id: 1 }]);
console.log(set.size); // 4(自动去重)
// 添加元素
set.add(3);
set.add(3); // 重复添加无效
// 删除元素
set.delete(2);
// 检查存在
console.log(set.has('hello')); // true
// 遍历
set.forEach(item => console.log(item));
for (const item of set) console.log(item);
// 清空
set.clear();
应用场景
-
数组去重
const arr = [1, 2, 2, 3];
const uniqueArr = […new Set(arr)]; // [1, 2, 3]
2. 交集、并集、差集
```javascript
const a = new Set([1, 2, 3]);
const b = new Set([3, 4, 5]);
// 交集
const intersection = new Set([...a].filter(x => b.has(x))); // Set {3}
// 并集
const union = new Set([...a, ...b]); // Set {1, 2, 3, 4, 5}
// 差集(a 有但 b 没有)
const difference = new Set([...a].filter(x => !b.has(x))); // Set {1, 2}
二、Map(映射)
特性
- 键值对存储:类似对象(Object),但键可以是任意类型(包括对象、函数等)。
- 有序性:按插入顺序迭代,不同于对象的无序性。
- 高效操作:
size属性直接获取键值对数量,而对象需手动计算。
基本操作
// 创建 Map
const map = new Map();
const keyObj = { id: 1 };
// 设置键值对
map.set('name', 'John');
map.set(keyObj, '关联对象的值');
map.set(123, '数字键');
// 获取值
console.log(map.get('name')); // 'John'
console.log(map.get(keyObj)); // '关联对象的值'
// 检查存在
console.log(map.has(123)); // true
// 删除键值对
map.delete('name');
// 遍历
map.forEach((value, key) => console.log(key, value));
for (const [key, value] of map) console.log(key, value);
// 获取大小
console.log(map.size); // 2
// 清空
map.clear();
与 Object 的对比
| 特性 | Object | Map |
|---|---|---|
| 键的类型 | 只能是字符串或 Symbol | 可以是任意类型 |
| 有序性 | 无序(ES2015+ 按插入顺序,但非标准) | 有序(按插入顺序) |
| 大小计算 | 需手动计算(如 Object.keys(obj).length) | 直接通过 size 属性获取 |
| 默认属性 | 有原型链,存在默认属性(如 toString) | 无默认属性 |
| 性能 | 适合少量键值对,频繁增删效率低 | 适合大量键值对,增删效率高 |
三、WeakSet 与 WeakMap
WeakSet
-
特性
- 只能存储对象引用,不能存储原始值。
-
对象是弱引用:若对象被其他地方销毁,WeakSet 中的引用会自动消失(不阻止垃圾回收)。
- 不可枚举,无
size、clear、forEach等方法。
- 不可枚举,无
-
应用场景:存储 DOM 元素引用,避免内存泄漏。
const weakSet = new WeakSet();
const obj = {};
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
// 若 obj 被设为 null,WeakSet 中的引用自动消失
obj = null;
WeakMap
-
特性
- 键必须是对象,值可以是任意类型。
-
键是弱引用:若键对象被销毁,对应键值对自动消失。
- 不可枚举,无
size、clear、forEach等方法。
- 不可枚举,无
-
应用场景
- 为对象添加私有属性(外部无法访问键)。
-
缓存 DOM 元素的状态(元素销毁时自动清除缓存)。
const weakMap = new WeakMap();
const element = document.getElementById('myButton');
// 为 DOM 元素关联数据
weakMap.set(element, { clickCount: 0 });
element.addEventListener('click', () => {
const data = weakMap.get(element);
data.clickCount++;
});
四、应用场景总结
| 数据结构 | 典型场景 |
|---|---|
| Set | - 数组去重 - 集合运算(交集、并集、差集) - 存储不重复的值(如用户 ID) |
| Map | - 键值对需任意类型的场景 - 需要有序遍历的场景 - 频繁增删键值对的场景 |
| WeakSet | - 临时存储 DOM 元素(避免内存泄漏) - 简单的对象存在性检查 |
| WeakMap | - 为对象添加私有状态 - 缓存与对象生命周期绑定的数据 |
五、性能对比
- 插入 / 删除操作:
Map和Set的性能优于普通对象(尤其是大量数据时)。 - 查找操作:
Map和Set的平均时间复杂度为 O (1),对象为 O (n)(需遍历所有键)。
总结
- Set 适合存储唯一值的集合,支持高效的成员检查和集合运算。
- Map 适合键值对的灵活存储,尤其是键为非字符串类型的场景。
- WeakSet/WeakMap 适合需要弱引用的场景(如缓存、避免内存泄漏)。
22、 web开发中会话跟踪的方法有哪些
在 Web 开发中,会话跟踪(Session Tracking)是指识别和维护用户跨多个请求的状态的机制。由于 HTTP 协议是无状态的,因此需要额外的技术来实现会话状态的持久化。以下是常见的会话跟踪方法及其优缺点:
一、基于 Cookie 的会话跟踪
1. 普通 Cookie
-
原理:服务器在响应头中设置
Set-Cookie,浏览器将 Cookie 存储并在后续请求中自动携带。 -
示例
// 服务器响应
Set-Cookie: session_id=123456; Path=/; Domain=.example.com; Expires=Wed, 21 Oct 2025 07:28:00 GMT; HttpOnly
// 客户端后续请求
Cookie: session_id=123456
- 特点
- **优点**:实现简单,浏览器自动处理。
- 缺点
- 安全性低(可被篡改,需配合 `HttpOnly`、`Secure` 等属性)。
- 存储容量有限(约 4KB)。
- 会增加请求头大小,影响性能。
#### **2. 加密 Cookie**
- **原理**:将会话数据加密后存储在 Cookie 中,服务器解密验证。
```javascript
const encryptedCookie = encrypt(sessionData, secretKey);
res.setHeader('Set-Cookie', `session=${encryptedCookie}; HttpOnly`);
-
特点
-
优点:无需服务器存储会话数据,减轻服务器压力。
-
缺点
- 安全性依赖加密算法,若密钥泄露风险高。
-
-
数据量大时会导致 Cookie 臃肿。
二、基于 URL 参数的会话跟踪
1. 显式 URL 重写
-
原理:在 URL 中附加会话 ID(如
http://example.com/page?session_id=123456)。 -
特点
-
优点:无需 Cookie 支持,兼容性强。
-
缺点
- URL 冗长,影响用户体验。
-
-
安全性低(会话 ID 可能被记录或共享)。
- 深层链接易失效(如分享带会话 ID 的 URL)。
2. 隐式 URL 重写
-
原理:服务器自动修改页面中的所有 URL,插入会话 ID(如 JSP 的
response.encodeURL())。 -
特点
-
优点:对开发者透明,无需手动处理每个 URL。
-
缺点
- 需框架支持,实现复杂。
-
-
动态生成的 URL(如 AJAX 请求)可能遗漏。
三、基于 Session 的会话跟踪
1. 服务器端 Session
-
原理
- 服务器生成唯一 Session ID(如 UUID),并存储 Session 数据(如内存、数据库、Redis)。
- 通过 Cookie 或 URL 将 Session ID 传递给客户端。
- 客户端请求时携带 Session ID,服务器根据 ID 查找对应数据。
HttpSession session = request.getSession(); session.setAttribute("username", "john"); // 服务器存储数据 // 客户端通过 Cookie 携带 JSESSIONID -
特点
-
优点
- 安全性高(敏感数据存储在服务器)。
-
-
数据量大(仅需传递 Session ID)。
-
缺点
- 服务器压力大(需管理大量 Session)。
-
分布式系统中需解决 Session 共享问题(如 Session 粘性、Redis 集群)。
2. 分布式 Session
-
原理:将 Session 数据存储在分布式缓存(如 Redis、Memcached)或数据库中,所有服务器共享访问。
-
特点
-
优点:支持负载均衡,服务器间无状态。
-
缺点
- 依赖外部服务,增加系统复杂度。
-
-
网络延迟可能影响性能。
四、基于 Token 的会话跟踪
1. JWT(JSON Web Token)
-
原理
- 服务器生成包含用户信息的 JWT,签名后返回给客户端。
- 客户端存储 JWT(如 LocalStorage、Cookie),并在请求头中携带(如
Authorization: Bearer token)。 - 服务器验证签名,解析 Token 中的数据。
// 生成 JWT const token = jwt.sign({ userId: 123 }, 'secretKey', { expiresIn: '1h' }); // 客户端请求 fetch('/api/data', { headers: { 'Authorization': `Bearer ${token}` } }); -
特点
-
优点
- 无状态,适合微服务和跨域场景。
-
-
可自定义 Token 有效期,支持刷新机制。
-
缺点
- Token 体积大(包含用户信息)。
-
安全性依赖签名密钥,泄露后风险高。
- 无法主动失效 Token(需结合黑名单机制)。
2. OAuth 2.0/OpenID Connect
-
原理:第三方认证服务(如 Google、GitHub)颁发访问令牌,客户端通过令牌访问资源服务器。
-
特点
-
优点
- 支持第三方登录,简化用户体验。
-
-
细粒度的权限控制(Scope)。
-
缺点
- 依赖外部服务,需处理重定向和令牌交换。
-
实现复杂度高(如授权码模式、刷新令牌)。
五、其他会话跟踪方法
1. HTML5 Web Storage(LocalStorage/SessionStorage)
-
原理:客户端存储数据,不随请求发送,需手动管理。
// 存储 localStorage.setItem('session_id', '123456'); // 读取 const sessionId = localStorage.getItem('session_id'); -
特点
-
优点
- 存储容量大(约 5-10MB)。
-
-
不影响请求性能。
-
缺点
- 仅客户端可用,服务器无法直接读取。
-
安全性低(可被 JavaScript 访问)。
2. HTTP 基本认证 / 摘要认证
-
原理:客户端在请求头中携带用户名和密码(基本认证)或哈希值(摘要认证)。
-
特点
-
优点:实现简单。
-
缺点
- 每次请求都需携带凭证,安全性低。
-
-
无会话状态管理,需额外实现。
六、方法对比与选择建议
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 普通 Cookie | 简单易用,浏览器自动处理 | 安全性低,容量有限 | 小型网站,非敏感数据 |
| 服务器 Session | 安全性高,数据量大 | 服务器压力大,分布式系统复杂 | 企业级应用,敏感数据存储 |
| JWT | 无状态,适合分布式和跨域 | Token 体积大,无法主动失效 | API 认证,微服务架构 |
| OAuth 2.0 | 第三方登录,权限精细控制 | 实现复杂,依赖外部服务 | 社交登录,开放平台 |
| URL 重写 | 无需 Cookie 支持 | URL 冗长,安全性低 | 兼容性要求高的场景 |
| Web Storage | 客户端大容量存储 | 服务器无法直接访问,安全性低 | 纯前端状态管理,缓存非敏感数据 |
七、安全增强措施
无论采用哪种方法,都建议结合以下安全措施:
-
HTTPS:防止中间人攻击和数据窃听。
-
Cookie 安全属性
HttpOnly:防止 JavaScript 访问 Cookie。
Secure:仅在 HTTPS 下传输。SameSite:防止 CSRF 攻击(如SameSite=Strict)。
-
Token 刷新机制:定期更换 JWT,减少令牌泄露风险。
-
会话超时:设置合理的会话有效期,降低长时间未活动的会话被攻击风险。
-
CSRF 防护:使用验证码、双 Cookie 验证等防止跨站请求伪造。
总结
选择会话跟踪方法时需综合考虑:
- 安全性要求:敏感数据优先使用服务器 Session 或 JWT。
- 性能与扩展性:分布式系统推荐 JWT 或 Redis 共享 Session。
- 兼容性:需支持无 Cookie 环境时使用 URL 重写。
- 用户体验:第三方登录场景使用 OAuth 2.0。
23、 网页验证码是干嘛的?是为了解决什么安全问题?
网页验证码(CAPTCHA,全自动区分计算机和人类的图灵测试)是一种广泛应用于互联网的安全机制,核心目标是区分用户是人类还是自动化程序,从而解决一系列由恶意机器人引发的安全问题。以下是其主要作用、解决的安全问题及技术原理:
一、网页验证码的核心作用
- 防止自动化攻击
- 阻止机器人程序(如爬虫、脚本)批量执行注册、登录、发帖、刷票、薅羊毛等操作,避免服务器资源被滥用。
- 案例:恶意用户通过脚本批量注册虚假账号用于诈骗或广告,验证码可拦截此类行为。
- 保护用户数据安全
- 防止暴力破解密码(如通过脚本不断尝试用户名和密码组合),增加攻击成本。
- 原理:机器人难以自动识别和输入验证码,而人类可轻松完成,从而保护账户安全。
- 对抗垃圾信息
- 减少论坛、评论区的垃圾广告帖,或邮箱中的垃圾邮件(通过阻止机器人批量发送)。
- 合规与风控需求
- 符合数据保护法规(如 GDPR)对自动化行为的限制,帮助网站运营者降低法律风险。
二、解决的具体安全问题
1. 批量注册与账号滥用
- 问题:黑产利用机器人批量注册账号,用于刷单、虚假流量、传播恶意内容等。
- 验证码作用:要求用户手动输入或操作(如点击特定图片),阻止机器人自动化注册。
2. 暴力破解攻击
- 问题:攻击者通过脚本循环尝试密码组合(如 “admin/123456”),企图登录他人账户。
- 验证码作用:在多次失败登录后强制要求输入验证码,增加攻击难度(部分场景需配合 “账户锁定” 机制)。
3. 爬虫与数据窃取
- 问题:机器人程序未经授权爬取网站数据(如商品信息、用户隐私),影响业务逻辑或导致数据泄露。
- 验证码作用:对高频访问或疑似爬虫的请求触发验证码,阻断无授权的数据抓取。
4. 刷票与作弊行为
- 问题:在投票、抢购、抽奖等场景中,机器人通过脚本批量操作刷取结果(如秒杀器抢购限量商品)。
- 验证码作用:确保每次操作由真实用户发起,维护活动公平性。
5. 垃圾信息传播
- 问题:机器人自动在论坛、社交媒体发布广告、钓鱼链接或恶意代码。
- 验证码作用:强制用户完成人机验证后才能发布内容,减少垃圾信息数量。
三、常见验证码类型及技术原理
| 类型 | 技术原理 | 安全性 | 用户体验 |
|---|---|---|---|
| 文本验证码 | 随机生成扭曲、干扰线的字符图片,用户需手动识别并输入。 | 中 | 低(易被 OCR 破解) |
| 图形验证码 | 要求用户点击符合条件的图片(如 “选择所有含汽车的图片”),基于图像识别技术。 | 高 | 中 |
| 行为验证码 | 检测用户操作轨迹(如滑动拼图、点击顺序),分析人类行为特征(如鼠标移动曲线)。 | 高 | 高 |
| 语音验证码 | 播放语音片段,用户需听取并输入内容,适用于视力障碍用户或对抗图像识别攻击。 | 中 | 低 |
| 数学运算验证码 | 显示简单算术题(如 “2+3=?”),利用机器人难以理解语义的特性。 | 低 | 高 |
| 无感验证码 | 后台通过分析用户设备指纹、行为模式(如停留时间、点击频率)自动判断是否为人类,无需用户主动操作。 | 极高 | 极高 |
四、验证码的局限性与发展趋势
- 局限性
- 用户体验问题:复杂验证码可能导致正常用户操作受阻(如老年人难以识别扭曲字符)。
- 绕过手段:OCR 技术、打码平台(利用真人手动识别)或机器学习模型可破解部分验证码。
- 发展趋势
- 无感验证:通过设备指纹、行为数据分析实现 “静默验证”,减少用户感知(如 Google 的 reCAPTCHA v3)。
- 多因素结合:结合短信验证码、生物识别(如指纹)进一步提升安全性。
- 隐私保护:采用联邦学习等技术,避免在验证过程中收集用户敏感数据。
总结
网页验证码是互联网安全的第一道防线,核心价值在于平衡自动化效率与安全风险—— 既允许真实用户顺畅操作,又能有效拦截机器人的恶意行为。随着技术发展,验证码正从 “用户主动输入” 向 “智能无感验证” 演进,在提升安全性的同时优化用户体验。
24、 常见 Web 安全及防护原理?
Web 安全问题贯穿于网站开发、部署和运行的全生命周期,常见攻击类型和防护原理可从攻击层面和防御层面分别解析。以下是核心内容及技术实现:
一、常见 Web 安全攻击类型及原理
1. 跨站脚本攻击(XSS,Cross-Site Scripting)
-
攻击原理
攻击者在网页中注入恶意脚本(如 JavaScript),当用户访问页面时,脚本自动执行,窃取用户 Cookie、会话令牌或进行钓鱼操作。
-
分类
- 反射型 XSS:恶意代码通过 URL 参数直接注入响应页面(如搜索框攻击)。
-
-
存储型 XSS:恶意代码存储在服务器数据库中(如论坛发帖),长期威胁用户。
- DOM 型 XSS:通过修改前端 DOM 结构注入攻击代码。
-
典型场景:用户输入未过滤,直接输出到页面(如评论区、URL 参数)。
2. SQL 注入攻击(SQL Injection)
- 攻击原理:
攻击者通过构造恶意 SQL 语句(如' OR 1=1--),绕过身份验证或窃取数据库数据。 - 典型场景:
未对用户输入(如登录表单、搜索框)进行 SQL 语句过滤,直接拼接至数据库查询语句。 - 危害:
泄露敏感数据(用户信息、订单记录)、篡改数据、甚至获取服务器权限(通过数据库扩展功能)。
3. 跨站请求伪造(CSRF,Cross-Site Request Forgery)
- 攻击原理:
利用用户已登录的身份,诱骗其访问包含恶意请求的链接(如自动发送转账请求),伪装成用户执行操作。 - 核心条件:
浏览器自动携带用户会话 Cookie,而服务器未验证请求来源。
4. 文件上传漏洞(File Upload Vulnerability)
- 攻击原理:
允许用户上传恶意文件(如.php木马、病毒文件),攻击者通过访问上传路径执行代码,控制服务器。 - 典型场景:
未校验文件类型、内容或存储路径,直接将文件保存至可执行目录。
5. 命令注入攻击(Command Injection)
- 攻击原理:
通过拼接用户输入到操作系统命令中(如使用system()、exec()函数),执行任意命令(如rm -rf /)。 - 技术场景:
后端使用用户输入动态构造系统命令(如文件操作、网络请求)且未过滤特殊字符(如;,&,|)。
6. 认证与会话管理漏洞
-
攻击方式
- 会话劫持:窃取用户会话 Cookie(如通过 XSS、中间人攻击),伪装用户身份。
-
弱密码 / 暴力破解:通过字典攻击猜测用户密码,或利用默认密码登录。
-
核心原因:
未使用 HTTPS 加密传输、会话令牌易被预测(如简单数字序列)、未限制登录失败次数。
7. 信息泄露与敏感数据暴露
- 攻击原理:
服务器错误配置或代码逻辑缺陷导致敏感信息泄露(如数据库错误提示、备份文件、配置文件)。 - 典型案例:
访问robots.txt发现未授权目录、通过 URL 猜测获取phpinfo.php敏感信息。
8. DDOS 攻击(分布式拒绝服务攻击)
-
攻击原理:
利用大量傀儡机(肉鸡)向目标服务器发送海量请求,耗尽带宽或资源,导致正常用户无法访问。 -
分类
- 流量型攻击(如 DNS 放大攻击):通过反射机制制造超大规模流量。
-
资源耗尽型攻击(如 CC 攻击):针对 Web 应用层发起高频请求(如 HTTP POST)。
二、Web 安全防护原理与技术实现
1. 输入输出过滤与转义(针对 XSS/SQL 注入等)
-
防护原理:
对用户输入进行严格校验,对输出内容进行编码转义,防止恶意代码执行。 -
技术实现
-
输入校验
- 使用正则表达式限制输入格式(如手机号、邮箱)。
-
-
白名单策略:仅允许特定字符(如字母、数字),禁止特殊符号(
<,>,',",;)。 -
输出转义
- 在 HTML 中对特殊字符转义(如将
<转为<,>转为>)。
- 在 HTML 中对特殊字符转义(如将
-
在 JavaScript 中使用
JSON.stringify()避免脚本注入。- 在 SQL 查询中使用 预编译语句(Prepared Statements) 或 ORM 框架(如 MyBatis 的
#{}),避免拼接 SQL。
- 在 SQL 查询中使用 预编译语句(Prepared Statements) 或 ORM 框架(如 MyBatis 的
2. CSRF 防护
-
防护原理:
验证请求来源是否为可信站点,防止跨站伪造请求。 -
技术实现
-
同源检查:通过
Referer或Origin头字段验证请求来源是否与站点域名一致。 -
CSRF Token
- 服务器生成随机 Token 并存储(如 Session 或 Cookie)。
-
- 在表单或请求中携带 Token,服务器验证 Token 与存储值是否匹配。
- SameSite Cookie:设置 Cookie 的
SameSite=Strict/Lax,限制跨站请求携带 Cookie。
3. 文件上传安全控制
-
防护原理:
严格限制上传文件类型、内容及存储路径,避免执行恶意代码。 -
技术实现
-
文件类型校验
- 白名单校验文件扩展名(如仅允许
.jpg,.pdf),结合文件头检测(MIME 类型)。
- 白名单校验文件扩展名(如仅允许
-
-
禁止上传可执行文件(如
.php,.jsp,.asp)。 -
内容扫描:使用病毒扫描引擎(如 ClamAV)检测文件是否包含恶意代码。
- 存储隔离:将上传文件存储在无执行权限的目录(如 Nginx 配置为仅静态文件访问),或重命名文件(如随机哈希值命名)。
4. 命令注入防护
-
防护原理:
避免直接使用用户输入构造系统命令,必要时使用安全函数或沙箱机制。 -
技术实现
- 禁止使用危险函数(如 PHP 的
system(),exec(),Python 的os.system())。
- 禁止使用危险函数(如 PHP 的
-
必须执行命令时,使用 参数化方式(如 Python 的
subprocess模块传递列表参数),避免拼接字符串。- 对白名单内的命令参数进行严格过滤(如仅允许数字、字母)。
5. 认证与会话安全
-
防护原理:
强化身份验证流程,确保会话令牌安全可靠。 -
技术实现
-
密码安全
- 强制密码复杂度(如 8 位以上、包含大小写字母和数字)。
-
-
使用哈希加盐(Hash + Salt)存储密码(如
bcrypt,Argon2),避免明文存储。 -
会话管理
-
会话令牌使用强随机数(如 UUIDv4),定期刷新令牌(如设置
refresh token)。 -
设置 Cookie 的HttpOnly和Secure
标志:
HttpOnly防止 XSS 窃取 Cookie;
-
Secure确保 Cookie 仅通过 HTTPS 传输。
-
-
多因素认证(MFA):结合短信验证码、TOTP 动态令牌、生物识别等二次验证。
6. 安全配置与错误处理
-
防护原理:
关闭不必要的服务,隐藏敏感信息,避免错误提示泄露系统细节。 -
技术实现
-
服务器配置
- 禁用 HTTP 方法(如
PUT,DELETE),仅保留必要方法(GET,POST)。
- 禁用 HTTP 方法(如
-
-
配置
X-Content-Type-Options: nosniff防止浏览器误解析文件类型。 -
错误页面定制:
统一返回通用错误页面(如500 Internal Server Error),不显示具体错误堆栈或数据库信息。 -
敏感文件防护:
通过.htaccess或 Nginx 配置禁止访问敏感目录(如config.php,backups/)。
7. DDOS 攻击防护
-
防护原理:
通过流量清洗、资源限制和负载均衡,区分正常流量与攻击流量。 -
技术实现
- 流量清洗:使用专业防护服务(如 Cloudflare, AWS WAF)过滤异常流量(如超大包、高频请求)。
-
请求限速:对同一 IP 的请求频率进行限制(如每分钟最多 100 次请求)。
- CDN 加速:通过内容分发网络分散流量,隐藏真实服务器 IP。
- TCP 指纹识别:利用机器学习模型分析请求特征(如 TCP 握手延迟),识别机器人流量。
8. 安全框架与中间件
-
防护原理:
借助成熟的安全工具链,自动化处理常见安全问题。 -
技术实现
-
Web 框架内置防护
- Django 自动转义模板变量,防止 XSS;
-
-
Rails 自动过滤 SQL 注入风险参数。
-
安全中间件
- 使用 OWASP 的
ESAPI(Enterprise Security API)进行输入验证和编码;
- 使用 OWASP 的
-
在 Nginx 中配置
mod_security规则,拦截恶意请求。
三、安全开发流程(SDL)与最佳实践
- 威胁建模(Threat Modeling)
在开发初期识别潜在风险(如资产价值、攻击面),优先处理高风险漏洞(如 SQL 注入、XSS)。 - 代码审计与安全测试
- 静态代码分析:使用工具(如 SonarQube, Checkmarx)扫描代码中的安全缺陷。
- 动态安全测试:通过渗透测试(如 OWASP ZAP, Burp Suite)模拟攻击,发现运行时漏洞。
- 自动化测试:将安全测试集成到 CI/CD 流程中,确保每次代码变更均通过安全校验。
- 安全补丁与漏洞管理
定期更新框架、库和服务器组件(如 PHP、Node.js 版本),及时修复已知漏洞(参考 CVE 数据库)。 - 用户安全教育
通过提示引导用户设置强密码、警惕钓鱼链接,减少社会工程学攻击风险。
四、总结:Web 安全的核心原则
- 最小权限原则:程序仅拥有完成功能所需的最低权限(如数据库账户仅允许特定表的读写)。
- 纵深防御(Defense in Depth):结合网络层(防火墙)、系统层(补丁管理)、应用层(代码安全)多层防护。
- 数据最小化:仅收集必要用户数据,加密存储敏感信息(如信用卡号使用 AES-256 加密)。
通过 “预防 - 检测 - 响应” 的完整闭环,结合技术手段与安全流程,可有效降低 Web 应用的安全风险。
dom事件
1、html事件
<button @onClick=test()>
按钮
</button>
缺点:js和html不分开
function test(){
}
2.dom0级事件
<button id=btn>
按钮
</button>
缺点:定义多个点击事件,后面的会覆盖前面的事件
var btn=document.getElementById('btn')
btn.onclick=function test(){
}
2.dom1级事件
<button id=btn>
按钮
</button>
不会覆盖
var btn=document.getElementById('btn')
btn.addEventListener('click',function test(){
})

防抖节流
作用域
全局作用域
script标签和.js文件的最外层就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。全局作用域中声明的变量,任何其他作用域都可以被访问。
局部作用域
函数作用域,只能在函数内部访问
块级作用域,if,for循环中。但是如果for循环的时候是用var定义的i,那么在for循环外面也能访问到i。
作用域链
本质是底层的变量查找机制。在函数执行时,会优先查找当前函数作用域中查找变量。如果当前作用域中查不到则会依次逐级查找父级作用域,直到全局作用域。
垃圾回收机制 GC
js中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
内存的生命周期
1.内存分配:当我们声明变量,函数,对象的时候,系统会自动为他们分配内存。
2.内存使用:即读写内存,也就是使用变量,函数等。
3.内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存。
全局变量一般不会回收(关闭页面回收),局部变量一般不使用了就会自动回收。
内存泄漏:程序中分配的内存由于某种原因程序未释放或无法释放
垃圾回收机制算法说明
堆栈空间分配区别:
1.栈:有操作系统自动分配释放函数的参数值,局部变量等,基本数据类型放在栈中。
2,堆:一般由程序员分配释放。若程序员不释放,由垃圾回收机制回收。复杂数据类型存在堆中。
ie的引用计数法,但是现在浏览器基本不使用了。

标记清除法

闭包 函数加词法环境
内层函数加上外层函数内部的词法环境
内层函数访问外层函数的变量
很容易造成内存泄漏
内存泄漏
有垃圾没有被回收,就是将来不会用到的东西。在将来你想用也用不了的东西。比如将一个数组设置为null,那么之前数组的值你就不会在访问到了。垃圾回收器回收了一部分没有用到的,但是还有一部分你也不用了,但是没有回收,这就叫内存泄漏。
闭包和内存泄漏
1.有本该被回收的函数没有被回收,从而导致其关联的词法环境也没法被回收。最终导致内存泄漏。
2.一个词法环境里面有两个函数,其中一个函数没有用到词法环境里面的东西。但是这个函数后面还需要用到,反而是用到词法环境的函数后面用不到。但是他们公用一个词法环境,就导致词法环境一直占用内存,但是实际访问不到它。
一、常见内存泄漏场景
**1. 全局变量未释放
// 错误示例:意外创建全局变量
function leak() {
leakedVar = '这是一个全局变量'; // 未使用var/let/const声明
}
// 正确做法:使用局部变量
function fixed() {
const localVar = '这是局部变量'; // 函数执行后可被回收
}
**2. 定时器 / 回调未清除
// 错误示例:定时器未清除
const intervalId = setInterval(() => {
console.log('定时执行');
}, 1000);
// 组件销毁时未清除定时器
// 正确做法:在合适时机清除
clearInterval(intervalId);
**3. DOM 引用未释放
// 错误示例:保存DOM引用但未清理
const elements = [];
const element = document.getElementById('leaked-element');
elements.push(element);
// 即使DOM元素被移除,仍存在JS引用导致无法回收
document.body.removeChild(element);
4. 闭包引用外部变量
// 错误示例:闭包持有外部变量引用
function createLeak() {
const largeData = new Array(1000).fill('data');
return function() {
console.log(largeData.length); // 闭包引用largeData
};
}
const leakyFunc = createLeak(); // largeData无法被回收
5. 事件监听器未移除
// 错误示例:添加事件监听器但未移除
element.addEventListener('click', handler);
// 元素被移除前未清理监听器
document.body.removeChild(element); // 监听器仍存在
6. Web Worker 未终止
// 错误示例:创建Worker但未终止
const worker = new Worker('worker.js');
// 使用后未终止
worker.terminate(); // 正确做法
二、检测内存泄漏的工具
1. Chrome DevTools - Memory 面板
- Heap Snapshot(堆快照):
对比不同时间点的内存快照,找出未释放的对象。 - Allocation Timeline(分配时间线):
记录对象分配的时间点,追踪内存增长来源。 - Memory Profiles(内存分析):
监测内存使用趋势,识别持续增长的异常点。
2. Lighthouse
- 分析性能并检测潜在的内存泄漏问题。
3. JavaScript 内存分析库
memory-profiler:分析 JavaScript 对象分配情况。why-did-you-update:追踪 React 组件不必要的更新导致的内存问题。
三、预防和解决内存泄漏的方法
1. 避免全局变量
- 使用严格模式(
'use strict')避免意外创建全局变量。 - 使用模块系统(ES6 Modules)封装作用域。
2. 清理定时器和异步操作
// 组件销毁时清理定时器
componentWillUnmount() {
clearInterval(this.intervalId);
}
// 使用AbortController取消fetch请求
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(response => /* ... */)
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求被取消');
}
});
// 需要取消时
controller.abort();
3. 移除 DOM 引用和事件监听器
// 组件销毁时移除事件监听器
componentWillUnmount() {
element.removeEventListener('click', this.handler);
}
// 清理DOM引用数组
function cleanup() {
elements.length = 0; // 清空数组
}
4. 优化闭包
-
避免在闭包中引用大型对象或 DOM 元素。
-
使用箭头函数时注意隐式闭包:
// 错误:闭包捕获this setTimeout(() => { this.doSomething(); // 持有this引用 }, 1000); // 正确:明确绑定或使用局部变量 const self = this; setTimeout(function() { self.doSomething(); }, 1000);
5. 使用 WeakMap/WeakSet
-
弱引用集合不会阻止对象被垃圾回收:
const weakMap = new WeakMap(); const element = document.getElementById('example'); weakMap.set(element, '关联数据'); // 当element被移除时,关联数据可被回收
变量提升

函数动态参数 arguments
function getSum(){
log(arguments)是个伪数组,只存在于函数中
}
getSum(1,2,3)
剩余参数
function getSum(...arr){
这是一个数组
}
getSum(1,2,3)
js那些操作会改变原数组
push,pop,shift,unshift,splice,sort,reverse
创建对象,数组的几种方式
1.对象字面量创建对象
let obj={
name:111
}
2.new Object
let obj=new Object({})
3.构造函数 存在浪费内存,因为每个对象方法都占用内存。可以通过prototype,把方法挂载到原型上解决
喜洋洋,创建一个羊的对象,属性都是一样的
function Yang(name,age){
this.name=name
this.age=age
}
let xiyangyang=new Yang('喜洋洋',6)
大写字母开头
内置构造函数

Object.keys(obj)获取所有属性名,返回一个数组
Object.values(obj)获取所有属性值,返回一个数组
for in
for of
Object.assign(新的obj,oldObj)
Object.assign(obj,{信息:xxx}) 常用于给对象增加属性
伪数组变为真数组
扩展运算符,循环遍历,Array.from()****Array.prototype.slice.call()
资源提示符
常见的有四种
script
如果这两个提示符都没有,那遇到js就会立刻去下载执行,会导致页面卡顿。
async浏览器先去解析页面,遇到
defer浏览器先去解析页面,遇到
link
都是去尽早的下载资源,可以是js,css,图片等,但是他只是下载,不会阻碍主线程。
prefetch 优先级低
preload 优先级高,一般是是下载本页面马上用到的
判断数组的方法 isArray
其他的方法都有缺陷
Object.ptototype.toString.call(arr)他返回的是'[object,array]',但是有一种情况
let arr={[Symbol.toString]:'array'}
他可以修改返回结果
instanceof 之后适用于单窗口,不能有嵌套的ifram
下载文件
1.同源。直接a标签,在服务器设置后响应头。
2.同源。a标签,增加download=‘文件名’
封装一个通用组件。右键菜单
在生产环境要把log去掉,会造成内存泄漏,可以是用terser进行配置
withResolvers语法糖
let res ,rej;
const promise =new Promise({resolve,reject}=>{
res=resolve
rej=reject
})
现在可以这样
const {promise,resolve,reject}=Promise.withResolvers()
前段进阶
事件循环
进程模型
何为进程
每个程序运行需要他自己的专属内存空间,可以把这块空间简单的理解为进程。
每个应用至少有一个进程,进程之间相互独立,即使要通信,也要双方同意。
何为线程
有了进程后就可以运行程序的代码,运行程序的东西叫做线程。一个进程至少一个线程,在进程开启后会自动创建一个线程来运行代码,该线程称为主线程。如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。


为什么浏览器渲染进程不开启多个线程
单线程是异步产生的原因,事件循环是异步的实现方式。



promise.resolve.then() then里面的函数是微任务,优先级最高。定时器优先级低
浏览器如何渲染页面


2.样式计算
3.布局

4.分层
5.绘制
6.分块




面试题


vue的响应式
通过observe监听数据,get读取数据,set修改数据。
vue2源码
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
let obj={}
Object.defineProperty(obj,'a',{
这些属性又叫做属性描述符
value:1,//设置值
writable:true/false;//设置这个对象的值能否被赋值运算符修改,默认为false
enumerable:true/false;//为true该属性才会出现在对象的枚举属性中,也就是for in才会出现,默认为false
configurable:true/false;//为true该属性的描述符才能被改变,默认为false
})
响应式实现原理
let obj={}
let val
Object.defineProperty(obj,'a',{
get(){
//get函数和value不能同时使用
return val
},
set(newVal){
val=newVal
}
})


梳理
原生js修改数据,并不会同步html页面。然而vue可以做到,通过数据劫持,虚拟dom实现响应式。
1.数据变化的时候,需要知道哪些地方用到了这个数据,然后将变化后的数据通知用到的地方进行重新渲染。
这就需要做到,
收集那些地方用到了这个数据。监听这个数据变化。通知用到这个数据的地方进行重新渲染。
1.数据劫持 vue初始化的时候会遍历data里面的属性,把这些属性转化为getter,setter。当访问这个值的时候会触发getter,这时也就会记录下哪些地方使用了该属性进行依赖收集。修改属性时,触发setter,Vue 会通知所有依赖这个属性的地方进行更新 。
数据劫持案例,这样会造成全局变量污染。使用闭包解决。
const data = {
a:‘xxxx'
}
let val = 1
Object.defineProperty(data, a, {
get() { // 下文中该方法统称为getter
return val
},
set(newVal) { // 下文中该方法统称为setter
if (val === newVal) return
val = newVal
}
})
// value使用了参数默认值
function defineReactive(data, key, value = data[key]) {
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, a, 1)
但是目前仍然是只访问了一个属性,要访问全部的属性呢?创建一个类,类里面传入这个对象,然后类里面的方法去遍历这个传入的对象。
class Observer{
constructor(value){
this.value=value
this.walk()
}
walk(){
Object.keys(this.value).forEach(key=>defineReactive(this.value,key))
}
}
let obj={a:1,b:2}
new Observer(obj)
还是存在一个问题,如果对象里面有对象呢?那就递归调用。
基本数据类型(如 number、string、boolean 等)无法直接通过 Object.defineProperty 或 Proxy 实现响应式,因为它们没有属性可供劫持。
- 直接修改基本类型(如
this.num = 1)不会触发响应式,因为 Vue 无法监听基本类型的赋值操作。 - 需通过对象属性或
Vue.observable间接处理。
function observe(data){
if(typeof data !=='object') return
new Observer(data)
}
class Observe{
constructor(value){
this.value=value
this.walk()
}
walk(){
Object.keys(this.value).forEach(key=>defineReactive(this.value,key))
}
}
function defineReactive(data,key,value=data[key]){
observe(value)
Object.defineProperty(data,key,value){
get:function reactiveGetter(){
return value
},
set:function reactiveSetter(newValue){
if(value===newValue) return
value=newValue
observe(newValue)
}
}
}
const obj = {
a: 1,
b: {
c: 2
}
}
observe(obj)
执行observe(obj)
├── new Observer(obj),并执行this.walk()遍历obj的属性,执行defineReactive()
├── defineReactive(obj, a)
├── 执行observe(obj.a) 发现obj.a不是对象,直接返回
├── 执行defineReactive(obj, a) 的剩余代码
├── defineReactive(obj, b)
├── 执行observe(obj.b) 发现obj.b是对象
├── 执行 new Observer(obj.b),遍历obj.b的属性,执行defineReactive()
├── 执行defineReactive(obj.b, c)
├── 执行observe(obj.b.c) 发现obj.b.c不是对象,直接返回
├── 执行defineReactive(obj.b, c)的剩余代码
├── 执行defineReactive(obj, b)的剩余代码
代码执行结束

三个函数相互调用从而形成了递归,与普通的递归有所不同。到此刻完成了数据劫持。
2.收集依赖,派发更新
一个数据可能会对应多个watcher,也就是一个数据会在多个地方用到。这就形成了依赖,为了方便收集依赖和派发更新,定义了一个Dep类
class Dep{
constructor(){
this.subs=[]
}
depend(){
this.addSub(Dep.target)
}
notify(){
const subs=[...this.subs]
subs.forEach(s=>s.update())
}
addSub(sub){
this.sub.push(sub)
}
}
最终代码
// 调用该方法来检测数据
function observe(data) {
if (typeof data !== 'object') return
new Observer(data)
}
// constructor:接收一个对象 value,将其存储在实例的 value 属性中,并调用 walk 方法。
// walk:遍历对象的所有属性,对每个属性调用 defineReactive 函数进行数据拦截。
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
// 数据拦截
// 创建一个 Dep 实例 dep,用于管理依赖。
// 递归调用 observe 函数处理属性值,确保嵌套对象也能实现响应式。
// 使用 Object.defineProperty 对对象的属性进行劫持,重写 getter 和 setter:
// getter:调用 dep.depend() 收集依赖,然后返回属性值。
// setter:如果新值与旧值不同,更新属性值,递归处理新值,调用 dep.notify() 通知依赖更新。
function defineReactive(data, key, value = data[key]) {
const dep = new Dep()
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
dep.depend()
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.notify()
}
})
}
// 依赖
// Dep 类用于管理依赖,subs 数组存储所有依赖。
// depend:如果 Dep.target 存在,将其添加到 subs 数组中。
// notify:遍历 subs 数组,调用每个依赖的 update 方法。
// addSub:将依赖添加到 subs 数组中。
class Dep {
// Dep 类用于管理依赖项。
// 构造函数 constructor 在创建 Dep 类的实例时被调用,它初始化了一个空数组 this.subs,
// 这个数组用于存储所有依赖于该 Dep 实例的订阅者(观察者)。
constructor() {
this.subs = []
}
// depend 方法的作用是将当前的目标订阅者(Dep.target)添加到 subs 数组中。
// Dep.target 通常是一个全局变量,代表当前正在收集依赖的订阅者。
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
// notify 方法用于通知所有订阅者数据发生了变化。
// 首先,它创建了一个 subs 数组的副本,这样在通知过程中如果有订阅者被移除或添加,不会影响遍历。
// 然后,使用 forEach 方法遍历副本数组,对每个订阅者调用 update 方法,让订阅者更新自己的状态。
notify() {
const subs = [...this.subs]
subs.forEach((s) => s.update())
}
// addSub 方法用于将一个订阅者添加到 subs 数组中。
addSub(sub) {
this.subs.push(sub)
}
}
// Dep.target 用于临时存储当前正在处理的依赖,不设置为null,在push下一个的时候实际是上一个的
Dep.target = null
// TargetStack 用于管理 Dep.target 的栈。 TargetStack 是一个栈结构,用于保存不同层级的 Watcher 上下文:
//用来处理嵌套的依赖关系,确保在处理一个依赖时不会影响到其他依赖。
// computed: {
// // 计算属性 A 依赖 count
// A() { return this.count + 1 },
// // 计算属性 B 依赖 A
// B() { return this.A * 2 }
// }
// 执行流程:
// 计算 B() 时,创建 Watcher B,Dep.target = B。
// B() 读取 this.A,触发计算 A()。
// 计算 A() 时,创建 Watcher A,需临时将 Dep.target 设为 A。
// A() 读取 this.count,此时应将 count 的依赖收集到 A,而非 B。
// A() 计算完成后,需恢复 Dep.target = B,继续收集 B 的依赖。
const TargetStack = []
// 将当前 Dep.target 压入栈中,并更新 Dep.target。
function pushTarget(_target) {
TargetStack.push(Dep.target)
Dep.target = _target
}
// 从栈中弹出一个 Dep.target 并更新当前 Dep.target。
function popTarget() {
Dep.target = TargetStack.pop()
}
// watcher
// Watcher 类用于监听数据变化,当数据变化时执行回调函数。
class Watcher {
// 接收数据对象 data、表达式 expression 和回调函数 cb,调用 get 方法获取初始值。
constructor(data, expression, cb) {
this.data = data
this.expression = expression
this.cb = cb
this.value = this.get()
}
// 将当前 Watcher 实例设置为 Dep.target,调用 parsePath 函数获取表达式对应的值,然后恢复 Dep.target。
get() {
pushTarget(this)
const value = parsePath(this.data, this.expression)
popTarget()
return value
}
// 获取旧值,更新当前值,调用回调函数 cb。
update() {
const oldValue = this.value
this.value = parsePath(this.data, this.expression)
this.cb.call(this.data, this.value, oldValue)
}
}
// 工具函数 该函数用于解析表达式,根据表达式中的 . 分割路径,逐步获取对象的属性值
function parsePath(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
// 创建一个对象 obj,调用 observe 函数使其变为响应式对象。
// 创建一个 Watcher 实例 w1,监听 obj.a 的变化,当 obj.a 变化时,会打印变化信息。
let obj = {
a: 1,
b: {
m: {
n: 4
}
}
}
observe(obj)
let w1 = new Watcher(obj, 'a', (val, oldVal) => {
console.log(`obj.a 从 ${oldVal}(oldVal) 变成了 ${val}(newVal)`)
})
2、vue对js中7种会改变数组的方法进行了重写:push, pop, unshift, shift, splice, reverse, sort

vue3
setup和data,methods可以同时存在
data,methods可以通过this读取setup里面定义的函数,是因为setup的加载时机早于最早的生命周期。但是setup里面没有this,不能读取data里面定义的数据
torefs他把对象里面的每一个值都变成了用ref包裹的ref响应式对象,
let {name,age}=torefs(obj)
name.value=111
这样修改name,obj的name也会跟着同步修改
计算属性
vue2
computed: {
// 计算属性 fullName
fullName() {
return this.firstName + ' ' + this.lastName; // 依赖 firstName 和 lastName
}
}
computed: {
fullName: {
// Getter:返回计算值
get() {
return this.firstName + ' ' + this.lastName;
},
// Setter:当计算属性被赋值时触发
set(newValue) {
const [firstName, lastName] = newValue.split(' ');
this.firstName = firstName;
this.lastName = lastName;
}
}
}
vue3
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value;
});
// 创建可写计算属性
const fullName = computed({
// Getter:返回计算值
get() {
return firstName.value + ' ' + lastName.value;
},
// Setter:当计算属性被赋值时触发
set(newValue) {
const [first, last] = newValue.split(' ');
firstName.value = first;
lastName.value = last;
}
});
watch
vue3还能停止监听
const stop = watch(count, (newVal) => {
console.log('count:', newVal);
});
// 手动停止监听
stop();
vue2
watch: {
// 监听基本类型
message(newVal, oldVal) {
console.log(`message changed: ${oldVal} → ${newVal}`);
},
// 深度监听对象
user: {
handler(newVal, oldVal) {
console.log('user changed');
},
deep: true // 深度监听所有嵌套属性
},
// 监听对象属性(不使用 deep)
'user.name'(newVal, oldVal) {
console.log(`user.name changed: ${oldVal} → ${newVal}`);
},
// 立即执行(初始化时触发)
message: {
handler(newVal) {
console.log('message initialized:', newVal);
},
immediate: true
},
// 监听计算属性
computedValue(newVal) {
console.log('computed value changed:', newVal);
}
}
Vue3
const message = ref('Hello');
//reactive定义的数据,在监听时默认开启深度监听,并且关闭不了
const user = reactive({
name: 'John',
age: 30
});
// 监听单个 ref
watch(message, (newVal, oldVal) => {
console.log(`message changed: ${oldVal} → ${newVal}`);
});
// 监听 reactive 对象(需指定 getter)
watch(
() => user.age,
(newVal, oldVal) => {
console.log(`user.age changed: ${oldVal} → ${newVal}`);
}
);
// 深度监听 reactive 对象
watch(
() => user,
(newVal, oldVal) => {
console.log('user changed');
},
{ deep: true }
);
// 监听多个源
watch(
[message, () => user.age],
([newMessage, newAge], [oldMessage, oldAge]) => {
console.log(`message: ${oldMessage} → ${newMessage}, age: ${oldAge} → ${newAge}`);
}
);
// 立即执行
watch(
message,
(newVal) => {
console.log('message initialized:', newVal);
},
{ immediate: true }
);
watchEffect:自动收集依赖
// 自动追踪依赖,立即执行
watchEffect(() => {
console.log('count is:', count.value); // 依赖 count
});
父子组件
vue3需要defineExpose({})去暴露可以给父组件看的东西

*四、**原理源码*
说一说你对数据响应式理解?
数据响应式(Reactivity) 是一种让数据变化自动触发 UI 更新的机制。它是现代框架(如 Vue、React、Angular)的核心特性之一,极大提升了开发效率和用户体验。以下从概念、实现、优缺点及应用场景四个方面深入理解数据响应式:
一、核心概念
1. 数据与视图的绑定
- 传统方式:手动操作 DOM 更新视图(如
document.getElementById().innerText = newValue)。 - 响应式方式:数据变化后自动更新视图,开发者只需关注数据逻辑。
2. 双向与单向数据流
- 双向绑定(如 Vue):数据变化触发视图更新,视图操作也会更新数据。
- 单向数据流(如 React):数据流动是单向的(State → View),视图变化通过事件触发 State 更新。
二、实现原理
1. Vue 2(Object.defineProperty)
- 拦截属性读写:通过
Object.defineProperty()劫持对象属性的getter/setter。 - 依赖收集:读取数据时记录依赖的组件或计算属性。
- 发布更新:数据变化时通知所有依赖的组件重新渲染。
// 简化版实现
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// 收集依赖(记录哪个组件使用了该属性)
track();
return val;
},
set(newVal) {
val = newVal;
// 触发更新(通知所有依赖的组件)
trigger();
}
});
}
2. Vue 3(Proxy)
- 优势:直接代理整个对象,可拦截更多操作(如属性新增 / 删除),无需递归遍历。
- Ref 实现:基础类型通过
ref包装为带有.value的对象,再用 Proxy 代理。
// 简化版 ref 实现
function ref(value) {
const refObject = {
value,
__v_isRef: true
};
return new Proxy(refObject, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newValue) {
target[key] = newValue;
trigger(target, key);
return true
}
});
}
- Vue 2 限制:
- 无法检测对象属性的新增 / 删除(需用
Vue.set)。 - 数组直接索引修改不触发更新(需用
splice等方法)。
- 无法检测对象属性的新增 / 删除(需用
- Vue 3 改进:
- 基于 Proxy 解决了 Vue 2 的响应式边界问题。
ref统一处理基础类型和对象类型。
说一下你对虚拟 DOM 的理解?
虚拟 DOM(Virtual DOM) 是现代前端框架(如 React、Vue、Angular)实现高效渲染的核心技术之一。它通过在内存中构建轻量级的 JavaScript 对象树来模拟真实 DOM,将频繁的 DOM 操作转化为 JavaScript 对象的对比和更新,从而大幅提升渲染性能
1. 为什么需要虚拟 DOM?
- 传统 DOM 操作的性能问题:
直接操作真实 DOM(如innerHTML、appendChild)会触发浏览器的回流(Reflow)和重绘(Repaint),尤其是在大量 DOM 更新时,性能开销极大。 - 虚拟 DOM 的本质:
用 JavaScript 对象(虚拟节点)描述真实 DOM 的结构和属性,通过diff 算法对比新旧虚拟 DOM 的差异,仅将必要的更新同步到真实 DOM,减少无效操作。
2. 虚拟 DOM 的核心流程
数据变化 → 生成新虚拟 DOM → 与旧虚拟 DOM 对比(diff)→ 批量更新真实 DOM
二、实现原理
1. 虚拟节点的结构
一个虚拟节点(Virtual Node,简称 VNode)通常包含以下属性:
// 简化的 VNode 结构(React 中为 Fiber,Vue 中为 VNode)
const vnode = {
tag: 'div', // 元素标签名(如 'div'、'span')
key: 'list-item-1', // 唯一标识(用于 diff 优化)
props: { className: 'container' }, // 元素属性
children: [ // 子节点(可以是文本或子 VNode)
{ tag: 'p', children: 'Hello Virtual DOM' }
]
};
2. diff 算法(差异对比)
-
同级比较:虚拟 DOM 树的 diff 只在同级节点间进行,避免跨层级对比的复杂度(时间复杂度从 O (n³) 优化到 O (n))。
-
key 的作用
通过key标识唯一节点,避免因顺序变化导致的无效更新。例如:
// 错误示例:缺少 key,列表更新时可能错位 <li>Apple</li> <li>Banana</li> // 正确示例:添加 key,diff 可精准识别节点 <li key="apple">Apple</li> <li key="banana">Banana</li> -
差异化更新策略
- 新增节点:创建真实 DOM 并插入父节点。
- 删除节点:移除对应的真实 DOM。
- 更新节点:仅修改属性变化的真实 DOM(如
className、style)。
三、优缺点分析
优点
- 性能优化:
通过批量更新和最小化 DOM 操作,减少浏览器回流 / 重绘的次数。 - 抽象层优势:
屏蔽不同平台的差异(如浏览器、小程序、SSR),实现 “一次编码,多端运行”(如 React Native)。 - 状态管理友好:
数据驱动视图,状态变化可预测,便于调试(如 React DevTools 可追踪虚拟 DOM 变化)。
缺点
- 内存开销:
虚拟 DOM 需要额外的内存空间存储 JavaScript 对象树(但现代浏览器优化后影响较小)。 - 学习成本:
需要理解虚拟 DOM 的 diff 规则(如key的必要性)和框架的渲染机制。 - 并非所有场景都适用:
对于简单视图(如纯静态页面),直接操作 DOM 可能比虚拟 DOM 更高效。
四、典型应用场景
1. 列表渲染(性能优化重点)
// React 示例:使用 key 优化列表 diff
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
2. 复杂交互组件
- 动态表单、图表组件等需要频繁更新的场景,虚拟 DOM 可减少 DOM 操作的碎片化。
3. 跨平台开发
- React Native、Weex 等框架通过虚拟 DOM 将代码转换为移动端原生组件,实现多端统一。
五、与真实 DOM 的对比
| 维度 | 虚拟 DOM | 真实 DOM |
|---|---|---|
| 操作方式 | JavaScript 对象操作(轻量、高效) | 浏览器 API 操作(触发回流 / 重绘) |
| 更新粒度 | 批量对比后更新差异部分 | 每次操作可能触发多次渲染 |
| 内存占用 | 较高(需存储对象树) | 更高(需维护渲染树和布局信息) |
| 跨平台支持 | 良好(可适配浏览器、移动端、SSR 等) | 仅限浏览器环境 |
六、框架中的实践
1. React 的 Fiber 架构
- 增量渲染:将虚拟 DOM 的 diff 过程拆分为多个小任务,允许浏览器中途中断并处理其他事件(如动画、输入),避免主线程阻塞。
- 双缓存机制:通过
current Fiber(旧树)和workInProgress Fiber(新树)完成 diff,最终一次性提交更新。
2. Vue 的优化策略
- 静态提升:标记模板中的静态节点(如
<h1>标题</h1>),避免重复 diff。 - 事件缓存:缓存内联事件处理函数,减少虚拟 DOM 节点的差异。
七、总结
虚拟 DOM 是现代前端框架实现高效渲染的 “桥梁”,它通过空间换时间的策略(用内存存储虚拟树换取 DOM 操作的性能优化),让数据驱动视图的理念成为可能。尽管存在一定的学习成本和内存开销,但其在复杂应用中的性能优势和跨平台能力使其成为主流框架的标配。理解虚拟 DOM 的原理,有助于开发者写出更高效的组件,合理利用 key、避免不必要的状态更新,并深入掌握框架的渲染机制。
你了解diff算法吗?
diff 算法(差异对比算法)是虚拟 DOM(Virtual DOM,VDOM)技术的核心。它通过对比新旧虚拟 DOM 树的差异,高效地更新真实 DOM,避免不必要的渲染操作,从而提升性能。以下是对 diff 算法的详细解析:
一、diff 算法的核心目标
- 减少 DOM 操作:直接操作 DOM 成本较高,diff 算法通过对比虚拟 DOM 的差异,仅更新变化的部分。
- 高效对比:采用启发式策略(heuristic)降低时间复杂度,避免全量对比的性能损耗。
二、diff 算法的基本流程
1. 同级节点对比(分层对比)
- 只对比同一层级的节点,不跨层级对比(如父节点和子节点不会直接对比)。
- 例:若旧树的层级为
div -> ul -> li,新树为div -> ol -> li,则先对比ul和ol,再对比其子节点li。
2. 节点类型判断
- 不同类型节点:若新旧节点类型不同(如
div→p),直接销毁旧节点,创建新节点(无需对比子节点)。 - 相同类型节点:保留旧节点的 DOM 元素,仅对比和更新属性及子节点。
3. 子节点对比(重点逻辑)
当两个同级同类型节点的子节点为列表时(如 v-for 渲染的列表),diff 算法通过以下步骤高效对比:
三、子节点对比的核心逻辑(以 Vue 2 为例)
Vue 2 的 diff 算法采用 双端对比策略,通过四个指针(旧列表的 startIdx、endIdx,新列表的 startIdx、endIdx)逐步缩小对比范围,处理以下五种情况:
1. 旧头与新头对比
- 若旧列表头部节点(
oldStartVnode)与新列表头部节点(newStartVnode)相同(类型 + key 相同),则递归对比这两个节点,并将旧列表和新列表的头部指针后移(oldStartIdx++,newStartIdx++)。
2. 旧尾与新尾对比
- 若旧列表尾部节点(
oldEndVnode)与新列表尾部节点(newEndVnode)相同,递归对比这两个节点,并将旧列表和新列表的尾部指针前移(oldEndIdx--,newEndIdx--)。
3. 旧头与新尾对比
- 若旧列表头部节点与新列表尾部节点相同,将旧头部节点移动到旧列表尾部,并对比节点,然后
oldStartIdx++,newEndIdx--。
4. 旧尾与新头对比
- 若旧列表尾部节点与新列表头部节点相同,将旧尾部节点移动到旧列表头部,并对比节点,然后
oldEndIdx--,newStartIdx++。
5. 兜底策略:通过 key 查找匹配节点
- 若以上四种情况均不匹配,则通过旧列表节点的key构建索引表(keyToOldIdx),在新列表中查找是否有相同key的节点:
- 若找到匹配节点(类型需相同),将其移动到旧列表当前头部位置,并递归对比子节点。
- 若未找到(新列表中存在旧列表没有的节点),则创建新节点。
- 处理完所有新节点后,旧列表中剩余的节点全部删除。
四、diff 算法的优化策略
1. 基于 key 的唯一性
- 通过
key标识唯一节点,避免 “就地复用” 导致的状态混乱(如表单输入值错误复用)。 - 反例:使用索引(
index)作为 key,可能导致对比失效(见 Vue 循环的 key 的作用)。
2. 跳过非动态子节点
- 对于静态节点(
v-if条件为 false 或无动态数据),会被标记为static,diff 时直接跳过对比,提升性能。
3. 批量处理相同节点
- 当连续多个节点未变化时,一次性跳过对比,减少循环次数。
五、Vue 2 与 Vue 3 diff 算法的区别
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 子节点对比 | 双端对比 + key 索引表 | 快速路径(Fast Path) + 最长递增子序列(LIS) |
| 新增节点处理 | 尾部追加 | 允许中间插入,通过 LIS 计算最小移动量 |
| 性能 | 适合列表顺序变化少的场景 | 更适合列表频繁排序 / 插入的场景 |
Vue 3 优化点:
- 新增 快速路径:若新旧子节点顺序完全相同,直接复用旧节点,无需对比。
- 使用 最长递增子序列(LIS)算法:计算最少需要移动的节点数量,优化列表排序场景的性能(减少 DOM 移动操作)。
六、diff 算法的典型应用场景
1. 列表渲染(v-for)
- 通过
key确保节点唯一性,配合 diff 算法高效更新列表(如增删、排序)。
2. 条件渲染(v-if/v-else)
- 不同条件下的节点类型不同,diff 算法直接销毁旧节点,创建新节点。
3. 动态组件(:is 指令)
- 组件切换时,通过
key强制重新渲染(避免组件状态复用)。
七、diff 算法的性能 trade-off
- 时间复杂度:理论上为 O (n)(n 为节点数量),通过启发式策略接近最优解,但无法保证绝对最优(如极端排序场景可能退化为 O (n²))。
- 空间复杂度:需额外存储索引表或中间变量(如 Vue 3 的 LIS 需存储节点索引数组)。
总结
diff 算法是虚拟 DOM 技术的灵魂,其核心思想是通过分层对比、类型判断和基于 key 的唯一性标识,在保证正确性的前提下尽可能减少 DOM 操作。合理使用 key、避免不必要的动态节点,是优化 diff 性能的关键。Vue 3 通过 LIS 等算法进一步提升了列表排序场景的效率,体现了框架在性能优化上的持续演进。
key的作用
在 Vue 中,循环渲染列表时为每个元素添加 key 属性是一个重要的最佳实践。key 的主要作用是帮助 Vue 高效地识别每个节点,从而在更新 DOM 时最小化 DOM 操作。以下是其核心原理和应用场景:
一、key 的核心作用
1. 高效识别节点(虚拟 DOM 对比)
- Vue 使用 虚拟 DOM 差异算法(diff) 比较新旧 VNode 树时,通过
key唯一标识每个节点。 - 当列表顺序发生变化时(如插入、删除元素),
key能让 Vue 快速定位哪些节点被修改,避免重新创建不必要的 DOM 元素。
2. 避免状态复用(保持组件状态)
- 没有
key时,Vue 默认采用 “就地复用” 策略,可能导致组件状态错乱(如表单输入值被错误复用)。 - 明确的
key可确保每个组件实例被唯一跟踪,保持其独立状态。
二、最佳实践与示例
1. 使用唯一标识作为 key
<!-- 推荐:使用数据的唯一 ID 作为 key -->
<template>
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
list: [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]
};
}
};
</script>
2. 避免使用索引(index)作为 key
<!-- 不推荐:当列表顺序变化时会导致性能问题和状态混乱 -->
<li v-for="(item, index) in list" :key="index">
{{ item.name }}
</li>
3. 动态列表操作(增删改)
<!-- 正确使用 key 可确保动态操作的高效性 -->
<template>
<div>
<button @click="addItem">添加</button>
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}
<button @click="removeItem(item.id)">删除</button>
</li>
</ul>
</div>
</template>
三、为什么不推荐使用索引作为 key?
当列表顺序发生变化时(如插入、删除元素),使用索引作为 key 会导致:
- 性能下降:Vue 误以为所有节点都发生了变化,重新创建和销毁 DOM 元素。
- 状态混乱:组件实例与数据的绑定关系错乱(如表单输入值被错误复用)。
// 初始列表(索引作为 key)
[
{ id: 1, name: 'Apple' }, // key=0
{ id: 2, name: 'Banana' } // key=1
]
// 删除第一个元素后
[
{ id: 2, name: 'Banana' } // key=0(原 key=1 的元素被复用,状态错乱)
]
四、何时可以使用索引作为 key?
仅在以下场景可以考虑使用索引:
- 列表内容静态不变(如展示固定数据,无增删改操作)。
- 列表元素完全相同且无状态(如纯展示的图片列表)。
五、其他注意事项
-
key 必须唯一:同一层级的 key 不能重复,否则会导致渲染错误。
-
避免使用随机值:每次渲染生成新的随机 key 会导致所有节点被重新创建。
-
组件 key 的特殊场景
<!-- 通过 key 强制组件重新渲染 --> <MyComponent :key="componentKey" /> <!-- 当 componentKey 变化时,组件会被销毁并重新创建 --> methods: { refreshComponent() { this.componentKey = Date.now(); } }
六、性能对比
| 场景 | 使用唯一 key | 使用索引作为 key |
|---|---|---|
| 列表顺序变化(如排序) | 仅更新变化的节点 | 重新创建大部分节点 |
| 插入 / 删除元素 | 精准定位并操作对应节点 | 导致后续所有节点重新渲染 |
| 表单输入状态保持 | 正确保留每个输入框的值 | 输入值可能被错误复用 |
七、总结
- key 的本质:帮助 Vue 识别节点的唯一标识,在虚拟 DOM 对比时优化性能。
- 最佳实践:始终使用数据的唯一标识(如 ID)作为 key,避免使用索引。
- 性能考量:合理的 key 设计可减少 DOM 操作,提升渲染效率,尤其在大数据量列表中效果显著。
OM 更高效。
四、典型应用场景
1. 列表渲染(性能优化重点)
// React 示例:使用 key 优化列表 diff
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
2. 复杂交互组件
- 动态表单、图表组件等需要频繁更新的场景,虚拟 DOM 可减少 DOM 操作的碎片化。
3. 跨平台开发
- React Native、Weex 等框架通过虚拟 DOM 将代码转换为移动端原生组件,实现多端统一。
五、与真实 DOM 的对比
| 维度 | 虚拟 DOM | 真实 DOM |
|---|---|---|
| 操作方式 | JavaScript 对象操作(轻量、高效) | 浏览器 API 操作(触发回流 / 重绘) |
| 更新粒度 | 批量对比后更新差异部分 | 每次操作可能触发多次渲染 |
| 内存占用 | 较高(需存储对象树) | 更高(需维护渲染树和布局信息) |
| 跨平台支持 | 良好(可适配浏览器、移动端、SSR 等) | 仅限浏览器环境 |
六、框架中的实践
1. React 的 Fiber 架构
- 增量渲染:将虚拟 DOM 的 diff 过程拆分为多个小任务,允许浏览器中途中断并处理其他事件(如动画、输入),避免主线程阻塞。
- 双缓存机制:通过
current Fiber(旧树)和workInProgress Fiber(新树)完成 diff,最终一次性提交更新。
2. Vue 的优化策略
- 静态提升:标记模板中的静态节点(如
<h1>标题</h1>),避免重复 diff。 - 事件缓存:缓存内联事件处理函数,减少虚拟 DOM 节点的差异。
七、总结
虚拟 DOM 是现代前端框架实现高效渲染的 “桥梁”,它通过空间换时间的策略(用内存存储虚拟树换取 DOM 操作的性能优化),让数据驱动视图的理念成为可能。尽管存在一定的学习成本和内存开销,但其在复杂应用中的性能优势和跨平台能力使其成为主流框架的标配。理解虚拟 DOM 的原理,有助于开发者写出更高效的组件,合理利用 key、避免不必要的状态更新,并深入掌握框架的渲染机制。
你了解diff算法吗?
diff 算法(差异对比算法)是虚拟 DOM(Virtual DOM,VDOM)技术的核心。它通过对比新旧虚拟 DOM 树的差异,高效地更新真实 DOM,避免不必要的渲染操作,从而提升性能。以下是对 diff 算法的详细解析:
一、diff 算法的核心目标
- 减少 DOM 操作:直接操作 DOM 成本较高,diff 算法通过对比虚拟 DOM 的差异,仅更新变化的部分。
- 高效对比:采用启发式策略(heuristic)降低时间复杂度,避免全量对比的性能损耗。
二、diff 算法的基本流程
1. 同级节点对比(分层对比)
- 只对比同一层级的节点,不跨层级对比(如父节点和子节点不会直接对比)。
- 例:若旧树的层级为
div -> ul -> li,新树为div -> ol -> li,则先对比ul和ol,再对比其子节点li。
2. 节点类型判断
- 不同类型节点:若新旧节点类型不同(如
div→p),直接销毁旧节点,创建新节点(无需对比子节点)。 - 相同类型节点:保留旧节点的 DOM 元素,仅对比和更新属性及子节点。
3. 子节点对比(重点逻辑)
当两个同级同类型节点的子节点为列表时(如 v-for 渲染的列表),diff 算法通过以下步骤高效对比:
三、子节点对比的核心逻辑(以 Vue 2 为例)
Vue 2 的 diff 算法采用 双端对比策略,通过四个指针(旧列表的 startIdx、endIdx,新列表的 startIdx、endIdx)逐步缩小对比范围,处理以下五种情况:
1. 旧头与新头对比
- 若旧列表头部节点(
oldStartVnode)与新列表头部节点(newStartVnode)相同(类型 + key 相同),则递归对比这两个节点,并将旧列表和新列表的头部指针后移(oldStartIdx++,newStartIdx++)。
2. 旧尾与新尾对比
- 若旧列表尾部节点(
oldEndVnode)与新列表尾部节点(newEndVnode)相同,递归对比这两个节点,并将旧列表和新列表的尾部指针前移(oldEndIdx--,newEndIdx--)。
3. 旧头与新尾对比
- 若旧列表头部节点与新列表尾部节点相同,将旧头部节点移动到旧列表尾部,并对比节点,然后
oldStartIdx++,newEndIdx--。
4. 旧尾与新头对比
- 若旧列表尾部节点与新列表头部节点相同,将旧尾部节点移动到旧列表头部,并对比节点,然后
oldEndIdx--,newStartIdx++。
5. 兜底策略:通过 key 查找匹配节点
- 若以上四种情况均不匹配,则通过旧列表节点的key构建索引表(keyToOldIdx),在新列表中查找是否有相同key的节点:
- 若找到匹配节点(类型需相同),将其移动到旧列表当前头部位置,并递归对比子节点。
- 若未找到(新列表中存在旧列表没有的节点),则创建新节点。
- 处理完所有新节点后,旧列表中剩余的节点全部删除。
四、diff 算法的优化策略
1. 基于 key 的唯一性
- 通过
key标识唯一节点,避免 “就地复用” 导致的状态混乱(如表单输入值错误复用)。 - 反例:使用索引(
index)作为 key,可能导致对比失效(见 Vue 循环的 key 的作用)。
2. 跳过非动态子节点
- 对于静态节点(
v-if条件为 false 或无动态数据),会被标记为static,diff 时直接跳过对比,提升性能。
3. 批量处理相同节点
- 当连续多个节点未变化时,一次性跳过对比,减少循环次数。
五、Vue 2 与 Vue 3 diff 算法的区别
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 子节点对比 | 双端对比 + key 索引表 | 快速路径(Fast Path) + 最长递增子序列(LIS) |
| 新增节点处理 | 尾部追加 | 允许中间插入,通过 LIS 计算最小移动量 |
| 性能 | 适合列表顺序变化少的场景 | 更适合列表频繁排序 / 插入的场景 |
Vue 3 优化点:
- 新增 快速路径:若新旧子节点顺序完全相同,直接复用旧节点,无需对比。
- 使用 最长递增子序列(LIS)算法:计算最少需要移动的节点数量,优化列表排序场景的性能(减少 DOM 移动操作)。
六、diff 算法的典型应用场景
1. 列表渲染(v-for)
- 通过
key确保节点唯一性,配合 diff 算法高效更新列表(如增删、排序)。
2. 条件渲染(v-if/v-else)
- 不同条件下的节点类型不同,diff 算法直接销毁旧节点,创建新节点。
3. 动态组件(:is 指令)
- 组件切换时,通过
key强制重新渲染(避免组件状态复用)。
七、diff 算法的性能 trade-off
- 时间复杂度:理论上为 O (n)(n 为节点数量),通过启发式策略接近最优解,但无法保证绝对最优(如极端排序场景可能退化为 O (n²))。
- 空间复杂度:需额外存储索引表或中间变量(如 Vue 3 的 LIS 需存储节点索引数组)。
总结
diff 算法是虚拟 DOM 技术的灵魂,其核心思想是通过分层对比、类型判断和基于 key 的唯一性标识,在保证正确性的前提下尽可能减少 DOM 操作。合理使用 key、避免不必要的动态节点,是优化 diff 性能的关键。Vue 3 通过 LIS 等算法进一步提升了列表排序场景的效率,体现了框架在性能优化上的持续演进。
key的作用
在 Vue 中,循环渲染列表时为每个元素添加 key 属性是一个重要的最佳实践。key 的主要作用是帮助 Vue 高效地识别每个节点,从而在更新 DOM 时最小化 DOM 操作。以下是其核心原理和应用场景:
一、key 的核心作用
1. 高效识别节点(虚拟 DOM 对比)
- Vue 使用 虚拟 DOM 差异算法(diff) 比较新旧 VNode 树时,通过
key唯一标识每个节点。 - 当列表顺序发生变化时(如插入、删除元素),
key能让 Vue 快速定位哪些节点被修改,避免重新创建不必要的 DOM 元素。
2. 避免状态复用(保持组件状态)
- 没有
key时,Vue 默认采用 “就地复用” 策略,可能导致组件状态错乱(如表单输入值被错误复用)。 - 明确的
key可确保每个组件实例被唯一跟踪,保持其独立状态。
二、最佳实践与示例
1. 使用唯一标识作为 key
<!-- 推荐:使用数据的唯一 ID 作为 key -->
<template>
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
list: [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]
};
}
};
</script>
2. 避免使用索引(index)作为 key
<!-- 不推荐:当列表顺序变化时会导致性能问题和状态混乱 -->
<li v-for="(item, index) in list" :key="index">
{{ item.name }}
</li>
3. 动态列表操作(增删改)
<!-- 正确使用 key 可确保动态操作的高效性 -->
<template>
<div>
<button @click="addItem">添加</button>
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}
<button @click="removeItem(item.id)">删除</button>
</li>
</ul>
</div>
</template>
三、为什么不推荐使用索引作为 key?
当列表顺序发生变化时(如插入、删除元素),使用索引作为 key 会导致:
- 性能下降:Vue 误以为所有节点都发生了变化,重新创建和销毁 DOM 元素。
- 状态混乱:组件实例与数据的绑定关系错乱(如表单输入值被错误复用)。
// 初始列表(索引作为 key)
[
{ id: 1, name: 'Apple' }, // key=0
{ id: 2, name: 'Banana' } // key=1
]
// 删除第一个元素后
[
{ id: 2, name: 'Banana' } // key=0(原 key=1 的元素被复用,状态错乱)
]
四、何时可以使用索引作为 key?
仅在以下场景可以考虑使用索引:
- 列表内容静态不变(如展示固定数据,无增删改操作)。
- 列表元素完全相同且无状态(如纯展示的图片列表)。
五、其他注意事项
-
key 必须唯一:同一层级的 key 不能重复,否则会导致渲染错误。
-
避免使用随机值:每次渲染生成新的随机 key 会导致所有节点被重新创建。
-
组件 key 的特殊场景
<!-- 通过 key 强制组件重新渲染 --> <MyComponent :key="componentKey" /> <!-- 当 componentKey 变化时,组件会被销毁并重新创建 --> methods: { refreshComponent() { this.componentKey = Date.now(); } }
六、性能对比
| 场景 | 使用唯一 key | 使用索引作为 key |
|---|---|---|
| 列表顺序变化(如排序) | 仅更新变化的节点 | 重新创建大部分节点 |
| 插入 / 删除元素 | 精准定位并操作对应节点 | 导致后续所有节点重新渲染 |
| 表单输入状态保持 | 正确保留每个输入框的值 | 输入值可能被错误复用 |
七、总结
- key 的本质:帮助 Vue 识别节点的唯一标识,在虚拟 DOM 对比时优化性能。
- 最佳实践:始终使用数据的唯一标识(如 ID)作为 key,避免使用索引。
- 性能考量:合理的 key 设计可减少 DOM 操作,提升渲染效率,尤其在大数据量列表中效果显著。
理解 key 的作用和正确用法是编写高性能 Vue 应用的基础,尤其在处理动态列表时,务必确保每个节点有稳定、唯一的 key。
更多推荐

所有评论(0)