判断对象是否存在指定键名
Object.keys(data).find(x => x === 'name')
Object.keys(data) :作用:获取对象 newQuery 所有自身可枚举属性的键名,返回一个字符串数组
示例:若 data = { id: 1, copy: true, name: 'test' },则返回 ['id', 'copy', 'name']
.find(x => x === 'name')
作用:遍历数组(此处是键名数组),找到第一个满足条件(x === 'name')的元素并返回;若遍历完无匹配元素,返回 undefined。
示例:遍历 ['id', 'copy', 'name'] 时,第一个匹配 'copy' 的元素是 'copy',故返回 'copy';若数组中无 'copy',则返回 undefined。
使用场景:精准判断对象键名存在性
判断对象是否自身拥有某键名(排除原型链上的属性)
const data = { name: '1' }; if (Object.keys(data).find(x => x === 'name')) { console.log('存在 name 键'); // 执行 }
传统写法:data.hasOwnProperty('name')(返回布尔值 true/false)
返回键名 'copy' 或 undefined,可直接用于条件判断(效果等同于 hasOwnProperty)
使用场景:动态匹配键名(支持变量 / 模糊匹配)
键名不确定(由变量传入),或需要模糊匹配(如包含某字符串、符合正则) 示例 1:动态判断变量键名
```
const key = 'copy'; // 可从接口/用户输入获取
const hasKey = Object.keys(data).find(x => x === key);
if (hasKey) {
console.log(`存在键 ${hasKey}`);
}
```
示例 2:模糊匹配(判断是否存在以 copy 开头的键)
```
const data = { copy1: 'a', copy2: 'b', other: 'c' };
const matchKey = Object.keys(data).find(x => x.startsWith('copy'));
console.log(matchKey); // 'copy1'(返回第一个匹配项)
```
在键名数组中查找并后续操作
找到键名后,需直接使用该键名(如获取对应属性值、修改属性)
const data = { id: 1, copy: '原始数据' };
const targetKey = Object.keys(data).find(x => x === 'copy');
if (targetKey) {
console.log(data[targetKey]); // 输出 '原始数据'(通过找到的键名获取值)
data[targetKey] = '修改后数据'; // 通过键名修改值
}
处理可能存在的原型链污染(安全判断)
避免对象原型链上的属性干扰判断(如 __proto__、constructor 等)
const data = {};
newQuery.__proto__.copy = '原型链上的属性';
console.log('copy' in data); // true(不期望的结果)
console.log(Object.keys(data).find(x => x === 'copy')); // undefined(正确,排除原型链)
性能略低于 hasOwnProperty,但简单直观、支持动态查找、可用于复杂条件匹配
替代:Object.prototype.hasOwnProperty.call(obj, key)、Reflect.has(obj, key)
Object.prototype.hasOwnProperty.call(obj, key)、Reflect.has(obj, key)区别
hasOwnProperty 只检查对象自身的属性,不包括原型链;Reflect.has 检查对象自身及其原型链上的所有属性
Object.prototype.hasOwnProperty.call(obj, key)
作用:仅判断指定的 key 是否为对象 obj 自身的、可枚举或不可枚举的属性(不包括原型链上继承的属性)。
Reflect.has(obj, key)
作用:判断指定的 key 是否为对象 obj 自身的属性,或者是其原型链上任何一个对象的属性。
// 1. 创建一个原型对象
const animalPrototype = {
type: 'animal' // 这是一个原型上的属性
};
// 2. 使用 Object.create 创建一个新对象,并将其原型指向 animalPrototype
const cat = Object.create(animalPrototype);
cat.name = 'Tom'; // 这是 cat 对象自身的属性
console.log('--- 检查自身属性 "name" ---');
console.log(Object.prototype.hasOwnProperty.call(cat, 'name')); // true
console.log(Reflect.has(cat, 'name')); // true
console.log('-------------------------\n');
console.log('--- 检查原型链上的属性 "type" ---');
console.log(Object.prototype.hasOwnProperty.call(cat, 'type')); // false -> 'type' 不是 cat 自身的属性
console.log(Reflect.has(cat, 'type')); // true -> 'type' 是 cat 原型链上的属性
console.log('-------------------------\n');
console.log('--- 检查一个不存在的属性 "age" ---');
console.log(Object.prototype.hasOwnProperty.call(cat, 'age')); // false
console.log(Reflect.has(cat, 'age')); // false
--- 检查自身属性 "name" ---
true
true
-------------------------
--- 检查原型链上的属性 "type" ---
false
true
-------------------------
--- 检查一个不存在的属性 "age" ---
false
false
使用场景推荐
什么时候用 Object.prototype.hasOwnProperty.call?
需要严格判断一个属性是否是对象自身定义的,而不是从原型链上继承来的。例如在遍历对象属性时(for...in 循环)。
什么时候用 Reflect.has?
需要检查一个属性是否在对象或其原型链上存在时,它的功能与 in 运算符完全相同,但语法是函数式的,更适合用于函数式编程或需要将操作作为参数传递的场景。
当你在使用其他 Reflect 方法(如 Reflect.get, Reflect.set)时,为了保持代码风格的一致性,可以统一使用 Reflect.has。
想替代 'key' in obj,就用 Reflect.has(obj, 'key')
JavaScript 的 in 运算符用于检查对象是否拥有某个属性
JavaScript 的 in 运算符用于检查对象是否拥有某个属性,语法:
if (propertyName in object) {
// ...
}
1 . propertyName 必须是字符串或 Symbol
2 . 如果是变量,直接写变量名(不需要加引号)
const person = {
name: '张三',
age: 20
};
// 1. 检查存在的属性
if ('name' in person) {
console.log('person 有 name 属性');
}
// 2. 检查不存在的属性
if ('gender' in person) {
console.log('person 有 gender 属性');
} else {
console.log('person 没有 gender 属性');
}
// 3. 使用变量作为属性名
const prop = 'age';
if (prop in person) {
console.log(`person 有 ${prop} 属性`);
}
in 检查的是索引,不是值:
const arr = ['a', 'b', 'c'];
if (2 in arr) {
console.log('arr 有索引 2'); // 输出:arr 有索引 2
}
if ('b' in arr) {
console.log('arr 包含 "b"'); // 不输出
}
in 检查的是属性名,不是值:
const person = { name: '张三' };
if ('张三' in person) {
console.log('person 包含 "张三"'); // 不输出
}
盒模型
当一个DOM节点在浏览器中渲染后会占用一个方形区域,这个方形区域就是盒子。
盒子的组成分别是:
1.宽度:这个属性决定了此盒子的宽度。
2.高度:这个属性决定了此盒子的高度。
3.内边距:盒子里内容与边框之间的距离。
4.外边距:盒子与盒子之间的距离。
因为浏览器不同分为两种盒模型:
1.标准盒模型:由W3C组织制定的,除了低版本IE外,其他浏览器都遵循标准。width和height的值是内容区域的大小,而盒子实际大小需要加上边框和内边距的值。
2.怪异盒模型:又叫IE盒模型,width和height的值就是盒子实际大小,边框和内边距都是在宽和高的值内,内容区域就是减去边框和外边框的值。
标准盒子模型下进行布局时需要计算下内边距和边框,并不方便计算及布局。所以怪异模式下,盒子大小确定,设置好内边距和边框,内容区域浏览器进行计算,方便很多。
box-sizing属性解决了这个问题,css中通过设置box-sizing指定使用哪种盒子模型,进行混合使用。
外边距折叠: margin是盒子与盒子之间的距离,如果盒子都设置了外边距可能会出现外边距折叠,一般是指垂直方向相邻的外边距会发生重叠现象,对于上下相邻的块级元素和父子元素之间的外边距。
如A盒子下边距10px,B盒子上边距20px,中间相隔的距离是20px,而不是相加一起30px。因为盒子间距实际值如果两个值都是正值取两个盒子之间较大的那个值,如果两个值一正一负,取两个值的和,如果两个都是负值,取绝对值较大的。
父元素的上边距和第一个子元素的上边距会折叠或父元素的下边距和最后一个子元素的下边距会折叠,取其中较大的值作为最终的外边距。
文档流
DOM节点排版布局过程中,元素自动从左往右,从上往下的流式排列。一行排满自动换下一行,如果使用绝对定位,固定位或者浮动,这个元素就会脱离文档流。所以HTML是一个三维的空间,有平面的 x,y 位置,也有 z 轴上的层叠关系。
层叠关系就是:标准文档流内容在第一层,使用浮动的元素,在标准文档流之后渲染,处于标准文档流之上。而绝对定位的元素最后渲染,处于最上面一层,但是可以通过z-index 来操作层叠的位置。
页面渲染
1.输入URL获取到HTML文件,并进行解析。
2.解析HTML过程中,如果发现含有外部资源链接,js,css,图片时,会启动其他线程进行下载。但是遇见js文件时候,会停止HTML解析,等js文件下载结束并执行完,再进行解析。是因为js可能会出现修改DOM已经完成解析的结果,所以等js结束才会继续解析。
3.HTML解析的同时,解析器把解析完的HTML转化成DOM对象,再进一步构建DOM树。
4.css下载完之后,css解析器开始解析css文件,解析成css对象,构成CSSOM树。
5.DOM树和CSSOM树都构建完成以后,浏览器会根据这两棵树构建出一颗渲染树。
6.渲染树构建完成之后,进行布局计算。
7.布局计算完成后,页面渲染元素,内容呈现在屏幕上。
重排:
DOM数中进行增加,删除,修改元素的大小,位置,布局方式等都需要重新计算,重新经历DOM改动,CSSOM树的构建,渲染树的构建,重新绘制整个流程,也叫回流
重绘:
改变元素颜色等外观属性时候,不改变位置大小,影响其他元素布局,这个就无需重新构建渲染树,就只对样式进行重新绘制。
vue3:获取当前路由的完整 URL 路径
router.currentRoute.value.fullPath 的作用是 获取当前路由的完整 URL 路径(包含查询参数和哈希值)
-
router:这通常是指在 Vue.js 项目中通过
createRouter创建的路由实例。它管理着整个应用的路由配置和导航状态。 -
currentRoute:这是
router实例上的一个属性,它返回一个 响应式的(reactive)路由对象。 这个对象包含了当前页面的路由信息,例如路径 (path)、参数 (params)、查询参数 (query) 等。 在 Vue 3 中,currentRoute是一个Ref对象,所以你需要通过.value来访问它的值。 -
value:因为
currentRoute是一个Ref对象,它的值被包裹在.value属性中。 所以router.currentRoute.value才是获取到的当前路由的实际对象。 -
fullPath:这是路由对象上的一个属性。
它返回当前路由的完整路径字符串,包括:
- 路径 (`path`) - 查询参数(以 `?` 开头) - 哈希值(以 `#` 开头)
假设你当前在浏览器中的 URL 是:http://localhost:8080/user/profile?id=123&name=张三#info
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
// 获取当前路由对象
const currentRoute = router.currentRoute;
console.log(currentRoute.value.fullPath);
// 输出: "/user/profile?id=123&name=张三#info"
// 你也可以分别获取其他属性
console.log(currentRoute.value.path);
// 输出: "/user/profile" (不包含查询参数和哈希)
console.log(currentRoute.value.query);
// 输出: { id: '123', name: '张三' } (一个解析后的对象)
console.log(currentRoute.value.hash);
// 输出: "#info"
</script>
常见使用场景
-
在组件中获取当前 URL 信息:
当你需要根据当前 URL 的查询参数来加载不同的数据时。 例如,列表页根据
?page=2来加载第二页的数据。 -
导航守卫(Navigation Guards) :
在路由守卫中,你可能需要根据从哪个页面来(
from.fullPath)或者要到哪个页面去(to.fullPath)来做一些判断逻辑。 例如,判断用户是否有权限访问某个需要特定查询参数的页面。 -
记录或分享当前页面地址:
如果你想实现一个 “分享当前页面” 的功能,
fullPath就非常有用,因为它包含了所有必要的信息,可以让其他人打开后看到和你一模一样的页面状态。 -
处理面包屑导航(Breadcrumb) :
虽然面包屑导航主要依赖
$route.matched数组,但在某些复杂情况下,也可能需要用到fullPath来生成正确的链接。
vue3+@ant-design/icons-vue 图标使用
Input 组件的 suffix 插槽允许你在输入框的右侧(清除按钮之前)插入任何自定义内容,非常适合放置搜索图标。
引入图标:首先,请确保你已经从 @ant-design/icons-vue 中引入了 SearchOutlined 图标。你可以在脚本的顶部找到其他图标的引入语句,并添加它。
import { SearchOutlined, CloseCircleOutlined } from '@ant-design/icons-vue';
- 在
Input组件内部添加了<template #suffix>。#suffix是v-slot:suffix的简写形式。 - 在
template中,放置了<SearchOutlined>组件。 - 通过
style属性,设置了图标的颜色为rgba(0, 0, 0, 0.45),这与 Ant Design Vue 的默认图标颜色保持一致,并添加了cursor: pointer以提示用户这是一个可点击的图标。 - 图标绑定了一个
@click事件handleSearchIconClick,在这个事件处理函数中执行搜索相关的逻辑。
prefix 和 suffix 插槽是 Ant Design Vue 为 Input 组件提供的标准功能,用于扩展输入框,这比使用 CSS 伪元素等 hack 方法更稳定、更可维护。
<Input ref="inputRef" v-model:value="data" :placeholder="name || '请输入数据'"
@focus="focus" @blur="blur" @input="input" >
<!-- 添加搜索图标 -->
<template #suffix>
<SearchOutlined @click="handleSearchIconClick" />
</template>
</Input>
<style lang="scss" scoped>
/* 搜索图标 hover 变蓝色 */
:deep(.ant-input-suffix .anticon-search) {
transition: color 0.3s ease; /* 加个过渡,颜色变化更平滑 */
}
:deep(.ant-input-suffix .anticon-search:hover) {
color: #1890ff !important; /* Ant Design 主题蓝色,!important 确保覆盖原有颜色 */
}
</style>
deep() 是 Vue 3 中替代 ::v-deep 和 >>> 的深度选择器,用了 <style scoped>,必须用它才能穿透到子组件(Input 内部的图标)的样式
// 在 <script setup> 中
const handleSearchIconClick = () => {
// 触发搜索逻辑
};
finally
finally 是 Promise 链式调用中的一个方法,无论 Promise 最终是成功(resolved)还是失败(rejected),它内部的代码都会执行。
- 哪怕
then里的逻辑正常执行(接口请求成功),finally会跑; - 哪怕
catch捕获到错误(接口请求失败 / 解析数据报错),finally也会跑; - 它的核心价值是:处理「无论成功 / 失败都必须执行的收尾逻辑」。
finally 是 ES2018 纳入标准的 Promise 方法,专为「异步操作收尾逻辑」设计,核心特性可总结为以下 5 点,结合业务场景拆解
| 特性 | 详细说明 | 业务层面的价值 |
|---|---|---|
| 执行必然性 | 无论 Promise 是 resolved(成功)还是 rejected(失败),finally 必执行 | 避免 “成功时处理收尾、失败时遗漏” 的低级 bug(比如加载状态没关闭、资源没释放) |
| 无结果参数 | 回调函数无入参(无法获取 then 的 res 或 catch 的 err) | 强制区分 “结果处理逻辑(then/catch)” 和 “收尾逻辑(finally)”,代码职责更清晰 |
| 状态透传性 | 不改变原 Promise 的状态,返回新 Promise 与原状态一致 | 不影响后续链式调用(比如 finally 后仍能接 then/catch 处理结果) |
| 异常不屏蔽 | 若 finally 内部抛出错误,会覆盖原 Promise 的状态(变为 rejected) | 需避免在 finally 中写易报错的逻辑,否则会 “吞掉” 原错误 |
| 同步执行特性 | 回调函数是同步执行的(除非内部包含异步操作) | 收尾逻辑(如关闭 loading)能立即生效,无需等待异步 |
场景 1:关闭加载状态(最高频)
业务背景:前端发起请求时,页面会显示 loading 动画 / 骨架屏,无论请求成功(渲染数据)还是失败(提示错误),都必须关闭 loading,否则页面会一直处于 “加载中” 状态,用户体验极差。
const DataConent = () => {
Loading.value = true; // 开启加载
GetData({})
.then((res) => {
const data = res
})
.catch((err) => {
ElMessage.error('数据加载失败,请重试');
})
.finally(() => {
Loading.value = false; // 无论成败,必关闭加载
});
};
释放异步操作的资源
业务背景:发起异步请求时,可能会创建一些临时资源(如定时器、WebSocket 连接、文件读取流),无论请求成败,都需要释放这些资源,避免内存泄漏。
文件上传释放资源
// 文件上传函数
const uploadFile = (file) => {
// 创建上传进度定时器(临时资源)
const progressTimer = setInterval(() => {
console.log('上传进度检测中...');
}, 1000);
// 发起上传请求
uploadFileApi(file)
.then((res) => {
ElMessage.success(`文件 ${file.name} 上传成功`);
})
.catch((err) => {
console.error('上传失败:', err);
ElMessage.error(`文件 ${file.name} 上传失败`);
})
.finally(() => {
clearInterval(progressTimer); // 无论成败,清除定时器(释放资源)
console.log('上传资源已释放');
});
};
场景 3:重置表单 / 按钮状态
const submitForm = () => {
// 禁用提交按钮,防止重复提交
submitBtnDisabled.value = true;
// 表单校验 + 提交
formRef.value.validate()
.then(() => {
// 校验通过,发起提交请求
return submitFormApi(formData.value);
})
.then((res) => {
ElMessage.success('表单提交成功');
formRef.value.resetFields(); // 重置表单字段
})
.catch((err) => {
console.error('提交失败:', err);
ElMessage.error('表单提交失败,请检查内容');
})
.finally(() => {
submitBtnDisabled.value = false; // 无论成败,恢复按钮可用
});
};
场景 4:统一的日志 / 埋点上报
业务背景:需要记录接口请求的 “结束时间”,无论成败都要上报埋点(比如统计接口耗时、请求完成率),这类通用日志逻辑适合放在 finally 中。 埋点上报
const fetchUserList = () => {
const startTime = Date.now(); // 记录请求开始时间
getUserListApi()
.then((res) => {
userList.value = res.data;
})
.catch((err) => {
console.error('获取用户列表失败:', err);
})
.finally(() => {
// 无论成败,上报请求耗时
const duration = Date.now() - startTime;
// 埋点接口(通用逻辑)
reportBuriedPoint({
api: 'getUserList',
duration,
endTime: new Date().getTime()
});
});
};
场景 5:弹窗 / 遮罩层关闭
业务背景:点击 “确认” 按钮触发异步操作(如提交订单)时,弹出加载遮罩层,无论操作成功(跳转到支付页)还是失败(提示订单创建失败),都需要关闭遮罩层。
const createOrder = () => {
// 打开加载遮罩层
maskLoading.value = true;
createOrderApi(orderData.value)
.then((res) => {
// 成功:跳转到支付页
router.push(`/pay?orderId=${res.orderId}`);
})
.catch((err) => {
console.error('创建订单失败:', err);
ElMessage.error('订单创建失败,请重试');
})
.finally(() => {
// 无论成败,关闭遮罩层
maskLoading.value = false;
});
};
使用 finally 的注意事项
-
避免在 finally 中处理 “结果相关逻辑” :finally 没有 res/err 参数,若强行通过外部变量获取结果,会导致代码耦合(比如在 finally 中根据请求结果修改页面数据),这类逻辑应放在 then/catch 中。
-
finally 内部报错会覆盖原错误:
GetData({})
.catch((err) => {
console.error('原错误:', err); // 若 finally 报错,这里的错误会被覆盖
})
.finally(() => {
// 错误示例:访问不存在的变量
console.log(undefinedVar); // 抛出新错误,原 Promise 状态变为 rejected
});
3.finally 不适合异步收尾逻辑 若 finally 中包含异步操作(如接口请求),需注意:finally 本身是同步执行的,异步操作不会阻塞后续代码,如需等待异步收尾完成,需返回 Promise:
.finally(async () => {
await dataPoint(); // 等待埋点上报完成
Loading.value = false;
});
finally 的核心价值是分离 “结果处理逻辑” 和 “通用收尾逻辑” ,让代码更简洁、易维护。在业务中,只要遇到 “无论异步操作成败都必须执行的逻辑”(关闭 loading、释放资源、重置状态、统一埋点),都应该优先使用 finally,而非重复写在 then/catch 中。