手撕
原地修改链表

javascript
// 定义链表节点类
class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
/**
* 重排链表函数
* @param {ListNode} head 链表头节点
* @return {void} 不返回任何值,直接修改原链表
*/
const reorderList = function(head) {
// 处理空链表或只有一个节点的情况,无需调整
if (!head || !head.next) return;
// 步骤1:使用快慢指针找到链表的中间节点
let slow = head;
let fast = head;
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
}
// 步骤2:反转链表的后半部分
let prev = null;
let curr = slow.next;
slow.next = null; // 将链表分为前后两部分
while (curr) {
let nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
// 此时 prev 是反转后后半部分链表的头节点
// 步骤3:合并前半部分链表和反转后的后半部分链表
let first = head;
let second = prev;
while (second) {
let temp1 = first.next; // 保存 first 的下一个节点
let temp2 = second.next; // 保存 second 的下一个节点
first.next = second; // first 指向 second
second.next = temp1; // second 指向 first 原来的下一个节点
first = temp1; // first 移动至原下一个节点
second = temp2; // second 移动至原下一个节点
}
let first=head;
let second=prev;
while(second){
let temp1=first.next;
let temp2=second.next;
first.next=second;
second.next=temp1;
first=temp1;
second=temp2;
}
};
// 辅助函数:根据数组创建链表
function createList(arr) {
if (arr.length === 0) return null;
let head = new ListNode(arr[0]);
let current = head;
for (let i = 1; i < arr.length; i++) {
current.next = new ListNode(arr[i]);
current = current.next;
}
return head;
}
// 辅助函数:将链表转换为数组(用于输出)
function listToArray(head) {
let arr = [];
let current = head;
while (current) {
arr.push(current.val);
current = current.next;
}
return arr;
}
// 测试示例
// 示例1: 链表 0->1->2->3
let head1 = createList([0, 1, 2, 3]);
reorderList(head1);
console.log(listToArray(head1)); // 输出: [0, 3, 1, 2]
// 示例2: 链表 0->1->2->3->4
let head2 = createList([0, 1, 2, 3, 4]);
reorderList(head2);
console.log(listToArray(head2)); // 输出: [0, 4, 1, 3, 2]
// 示例3: 链表 0->1->2->3->4->5
let head3 = createList([0, 1, 2, 3, 4, 5]);
reorderList(head3);
console.log(listToArray(head3)); // 输出: [0, 5, 1, 4, 2, 3]
串行执行Promise

- 1.async/await
javascript
async function runPromiseInSeries(promiseArray){
const results=[];
for(const promiseFun of promiseArray){
try{
const result=await promiseFun();
results.push(result);
}catch(err){
console.log("promise执行失败",err);
throw err;
}
}
return results;
}
const promiseFunctions=[
()=>new Promise(resolve=>setTimeout(console.log('Promise 1');resolve('Result1'),1000)),
()=>new Promise(resolve=>setTimeout(console.log('Promise 2');resolve('Result2'),1000))
];
runPromisesInSeries(promiseFunctions)
.then(finalResults => console.log('全部完成:', finalResults))
.catch(error => console.error('链中发生错误:', error));
-
- Array.reduce()
javascript
function runPromiseInSeriesWithReduce(promiseArray){
return promiseArray.reduce((promiseChain,currentPromiseFunc)=>{
return promiseChain.then(chianResult=>{
return [...chainResults,currentResult];
})
},Promise.resolve([]))
}
// 示例用法
const promiseFunctions = [
() => new Promise(resolve => setTimeout(() => { console.log('Promise 1'); resolve('Result 1'); }, 1000)),
() => new Promise(resolve => setTimeout(() => { console.log('Promise 2'); resolve('Result 2'); }, 500)),
() => new Promise(resolve => setTimeout(() => { console.log('Promise 3'); resolve('Result 3'); }, 800))
];
runPromisesInSeriesWithReduce(promiseFunctions)
.then(finalResults => console.log('全部完成:', finalResults))
.catch(error => console.error('链中发生错误:', error));
-
- 递归
javascript
function runPromiseInSeriesRecusively(promiseArray,index=0,results=[]){
if(index>=promiseArray.length){
return Promise.resolve(results);
}
return promiseArray[index]()
.then(currentResult=>{
results.push(currentResult);
return runPromiseInSeriesRecursively(promiseArray,index=1,results);
})
}
千位格式化
javascript
/*
1)将数字转换为字符串,以便使用字符串方法进行处理。
2)使用正则表达式匹配字符串中的位置,在每三个数字前插入一个逗号。
3)返回格式化后的字符串。
*/
function formatNumberWithCommasCustom(number) {
// 将数字转换为字符串,并【去掉可能的小数点!】
let str = Math.floor(number).toString();
// 初始化结果字符串和一个计数器
let result = '';
let count = 0;
// 从字符串的【最后一个】字符开始遍历
for (let i = str.length - 1; i >= 0; i--) {
// 将当前字符添加到结果字符串的前面
result = str[i] + result;
// 每添加一个字符,计数器加1
count++;
// 如果计数器达到3(意味着已经添加了3个字符),则插入一个逗号,并重置计数器
if (count === 3 && i !== 0) {
result = ',' + result;
count = 0;
}
}
// 如果原始数字有小数部分,则将其添加到结果字符串的后面
if (number % 1 !== 0) {
result += '.' + (number - Math.floor(number)).toFixed(2).slice(2); // 保留两位小数
}
return result;
}
找出不在指定区间内的数字

javascript
/**
* 找出所有不在任何已使用区间内的数字
* @param {number[][]} usedRanges - 二维数组,每个内层数组表示一个区间 [start, end](包含两端)
* @param {number[]} checkNums - 需要检查的数字数组
* @returns {number[]} - 所有不在任何 usedRanges 区间内的数字组成的数组
*/
function findUnusedNumbers(usedRanges, checkNums) {
// 如果 usedRanges 或 checkNums 为空,直接返回 checkNums 的副本
if (usedRanges.length === 0) {
return [...checkNums];
}
if (checkNums.length === 0) {
return [];
}
const result = []; // 存储最终结果的数组
// 遍历需要检查的每一个数字
for (const num of checkNums) {
let isUsed = false; // 标记当前数字是否落在任何一个区间内
// 遍历所有已使用的区间
for (const range of usedRanges) {
const [start, end] = range; // 解构赋值,获取当前区间的起始和结束值
// 判断当前数字 num 是否在当前区间 [start, end] 内(包含边界)
if (num >= start && num <= end) {
isUsed = true; // 如果在区间内,标记为已使用
break; // 已经找到一个区间包含它,无需检查剩余区间,跳出内层循环
}
}
// 如果遍历完所有区间,isUsed 仍为 false,说明该数字不在任何区间内
if (!isUsed) {
result.push(num); // 将其加入结果数组
}
}
return result;
}
// --- 示例测试 ---
const used = [[1, 20], [23, 40]];
const checknum = [-20, 80];
console.log(findUnusedNumbers(used, checknum)); // 输出: [-20, 80]
// 解释:-20 小于1,80大于40,都不在[1,20]和[23,40]区间内。
// 再测试一个更复杂的例子
const used2 = [[5, 10], [15, 25], [30, 35]];
const checknum2 = [3, 8, 12, 20, 28, 40];
console.log(findUnusedNumbers(used2, checknum2)); // 输出: [3, 12, 28, 40]
// 解释:
// 3: 小于5 -> 未被使用
// 8: 在 [5,10] 内 -> 被使用,跳过
// 12: 在5-10和15-25之间 -> 未被使用
// 20: 在 [15,25] 内 -> 被使用,跳过
// 28: 在15-25和30-35之间 -> 未被使用
// 40: 大于35 -> 未被使用
排序算法

快速排序
的思路
javascript
function quickSort(arr,low,high){
if(low>=high)return;
const pivot=arr[high];
let left=low;
//i=low
for(let i=low;i<high;i++){
//pivot
if(arr[i]<=pivot){
[arr[i],arr[left]]=[arr[left],arr[i]];
left++;
}
}
[arr[left],arr[high]]=[arr[high],arr[left]];
quickSort(arr,low,left-1);
quickSort(arr,left+1,high);
}
归并排序
的稳定性很重要
javascript
function mergeSort(arr){
if(arr.length<=1)return arr;
const mid=Math.floor(arr.length/2);
const left=arr.slice(0,mid);
const right=arr.slice(mid);
const merge=(left,right)=>{
//const result=[];
//while(left.length>0&&right.length>0){
// if(left[0]<right[0]){
// result.push(left.shift());
// }else{
// result.push(right.shift());
// }
//}
//return result.concat(left,right);
let result=[];
let leftIndex=0;
let rightIndex=0;
while(leftIndex<left.length&&rightIndex<rightIndex){
if(left[leftIndex]<right[rightIndex]){
result.push(left[leftIndex]);
leftIndex++;
}else{
result.push(right[rightIndex];
rightIndex++;
}
}
return result.concat(left.slice(leftIndex),right.slice(rightIndex));
}
//返回
return merge(left,right);
}

🟢 Vue2 与 Vue3 的核心区别
1. 响应式系统重构
Vue3 使用 Proxy
替代了 Vue2 中的 Object.defineProperty
来实现响应式数据。
- Vue2 (Object.defineProperty) : 只能拦截对象已有属性的读取和写入,对新增属性、数组索引修改及
Map
,Set
等数据结构支持不足,需借助Vue.set
或Vue.delete
。 - Vue3 (Proxy) : 代理整个对象,可监听各种操作(包括属性增删、数组索引变化、
Map
/Set
操作等),提供了更全面的响应式能力。
2. 组合式 API (Composition API) vs 选项式 API (Options API)
Vue3 引入了 Composition API ,提供了比 Vue2 的 Options API 更灵活的组织逻辑的方式。
- Options API (Vue2) : 将代码按选项组织,如
data
,methods
,computed
,watch
, 生命周期钩子。不利于逻辑复用,大型组件易变得臃肿且逻辑关注点分散。 - Composition API (Vue3) : 允许在
setup
(或<script setup>
) 中按功能逻辑组织代码,相关响应式数据、计算属性、方法和生命周期钩子可以放在一起,极大改善了代码的组织性和可复用性。
3. 性能优化
Vue3 在性能方面有多项改进:
- Tree-shaking 支持更优: Vue3 的全局 API 和组件 API 都设计为可 tree-shaking 的,未使用的功能不会打包到最终产物中。
- 虚拟 DOM 重写: 优化了 diff 算法,引入了静态提升、事件缓存等编译时优化,减少了运行时开销。
- 更小的体积: 尽管增加了许多新特性,但 Vue3 的整体体积比 Vue2 更小。
4. 片段 (Fragments)
- Vue2: 组件模板必须有一个根元素。
- Vue3: 组件模板支持多个根元素(Fragment)。
5. 生命周期钩子变化
Vue3 的生命周期钩子名称有变化,并移除了 beforeCreate
和 created
(因为在 setup
中,它们的行为已被涵盖)。
Vue2 Options API | Vue3 Composition API (inside setup ) |
---|---|
beforeCreate |
Not needed (use setup instead) |
created |
Not needed (use setup instead) |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeDestroy |
onBeforeUnmount |
destroyed |
onUnmounted |
errorCaptured |
onErrorCaptured |
6. TypeScript 支持
Vue3 对 TypeScript 的支持更加友好,提供了更好的类型推断。
7. 组件注册:为何 Vue3 中组件无需显式注册?
在使用 <script setup>
的单文件组件中,导入的组件模板中可直接使用 ,无需通过 components
选项显式注册。这是因为 <script setup>
是一种编译时语法糖,编译器会自动识别导入的组件并使其在模板中可用,极大地简化了代码。
🟢 Vue3 组件通信
1. 父子组件通信
父传子 (Props)
父组件通过属性绑定传递数据,子组件通过 defineProps
接收。
vue
<!-- ParentComponent.vue -->
<template>
<ChildComponent :message="parentMessage" :count="count" />
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from Parent!');
const count = ref(0);
</script>
vue
<!-- ChildComponent.vue -->
<template>
<div>{{ message }} - {{ count }}</div>
</template>
<script setup>
// defineProps 是一个编译宏,无需导入
const props = defineProps({
message: {
type: String,
required: true
},
count: Number
});
</script>
子传父 (自定义事件)
子组件通过 defineEmits
定义事件,然后通过 emit
触发。父组件监听该事件。
vue
<!-- ChildComponent.vue -->
<template>
<button @click="sendMessage">Click Me</button>
</template>
<script setup>
const emit = defineEmits(['messageSent']);
const sendMessage = () => {
emit('messageSent', 'Data from child!');
};
</script>
vue
<!-- ParentComponent.vue -->
<template>
<ChildComponent @message-sent="handleMessage" />
<p>Received: {{ childMessage }}</p>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const childMessage = ref('');
const handleMessage = (msg) => {
childMessage.value = msg;
};
</script>
2. 兄弟组件通信
兄弟组件通信通常需要通过一个共同的父组件("状态提升")或使用全局事件总线/状态管理库。
通过共同的父组件中转
父组件管理状态,通过 props 传递给一个子组件,并监听另一个子组件的事件来更新状态。
vue
<!-- ParentComponent.vue -->
<template>
<ChildA :value="sharedValue" @update="updateValue" />
<ChildB :value="sharedValue" />
</template>
<script setup>
import { ref } from 'vue';
import ChildA from './ChildA.vue';
import ChildB from './ChildB.vue';
const sharedValue = ref('');
const updateValue = (newValue) => {
sharedValue.value = newValue;
};
</script>
使用 Mitt 等事件总线库
Mitt 是一个小巧的发布订阅库,可用于组件间事件通信。
-
创建事件总线 (eventBus.js):
javascript// eventBus.js import mitt from 'mitt'; const emitter = mitt(); export default emitter;
-
兄弟组件 A (发送事件):
vue<!-- ComponentA.vue --> <template> <button @click="sendData">Send to B</button> </template> <script setup> import emitter from '../eventBus.js'; const sendData = () => { emitter.emit('data-from-A', { message: 'Hello from A!' }); }; </script>
-
兄弟组件 B (接收事件):
vue<!-- ComponentB.vue --> <template> <div>Received: {{ receivedData }}</div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue'; import emitter from '../eventBus.js'; const receivedData = ref(''); const handleData = (data) => { receivedData.value = data.message; }; onMounted(() => { emitter.on('data-from-A', handleData); }); onUnmounted(() => { emitter.off('data-from-A', handleData); // 记得移除监听以防内存泄漏 }); </script>
3. 跨层级组件通信 (provide/inject)
provide
和 inject
可以实现深层嵌套组件之间的数据传递,无需通过层层 props。
vue
<!-- AncestorComponent.vue -->
<template>
<ChildComponent />
</template>
<script setup>
import { provide, ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const theme = ref('dark');
provide('app-theme', theme); // 提供键值 'app-theme'
</script>
vue
<!-- DeepDescendantComponent.vue (任何后代组件) -->
<template>
<div :class="theme">Themed content</div>
</template>
<script setup>
import { inject } from 'vue';
// 注入祖先提供的 'app-theme',并提供默认值 'light'
const theme = inject('app-theme', 'light');
</script>
🟢 Vuex 与 Pinia 的区别与用法
Pinia 是 Vue 官方推荐的新一代状态管理库,可看作是 Vuex 的进化版。
核心区别
特性 | Vuex | Pinia |
---|---|---|
理念 | 更强调规范性,适合大型严格项目 | 更灵活轻量,API 设计更简洁直观 |
API 风格 | Options API | Composition API |
核心概念 | state , getters , mutations (同步), actions (异步) |
state , getters , actions (可同步也可异步) 移除了 mutations |
TypeScript 支持 | 支持,但需要一些配置 | 原生支持优秀,类型推断好 |
模块化 | 通过 modules 划分,需设置 namespaced: true |
每个 store 天然是模块化的,通过不同文件定义多个 store |
使用方式 | 在组件中通过 this.$store 或 mapState/mapGetters/mapActions 辅助函数 |
在组件中直接导入并使用定义的 store 函数 |
具体用法
Vuex
-
创建 Store:
javascript// store/index.js import { createStore } from 'vuex'; export default createStore({ state: { count: 0 }, mutations: { // 同步修改状态 increment(state) { state.count++; }, setCount(state, value) { state.count = value; } }, actions: { // 异步操作,提交 mutation incrementAsync({ commit }) { setTimeout(() => { commit('increment'); }, 1000); } }, getters: { // 计算属性 doubleCount(state) { return state.count * 2; } } });
-
在组件中使用:
vue<template> <div>{{ count }}</div> <div>{{ doubleCount }}</div> <button @click="increment">Increment</button> <button @click="incrementAsync">Increment Async</button> </template> <script> import { mapState, mapGetters, mapActions } from 'vuex'; export default { computed: { ...mapState(['count']), ...mapGetters(['doubleCount']) }, methods: { ...mapActions(['incrementAsync']), increment() { this.$store.commit('increment'); // 直接提交 mutation } } }; </script>
Pinia
-
创建 Store:
javascript// stores/counter.js import { defineStore } from 'pinia'; // 'counter' 是 store 的唯一 ID export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { // 可同步也可异步 increment() { this.count++; }, async incrementAsync() { setTimeout(() => { this.increment(); }, 1000); } }, getters: { doubleCount: (state) => state.count * 2 } });
-
在组件中使用:
vue<template> <div>{{ counterStore.count }}</div> <div>{{ counterStore.doubleCount }}</div> <button @click="counterStore.increment()">Increment</button> <button @click="counterStore.incrementAsync()">Increment Async</button> <!-- 或者使用解构保持响应性 --> <div>{{ count }}</div> <button @click="increment">Increment</button> </template> <script setup> import { useCounterStore } from '@/stores/counter'; import { storeToRefs } from 'pinia'; // 用于解构保持响应性 const counterStore = useCounterStore(); // 直接修改 state (Pinia 也允许) // counterStore.count++; // 如果需要解构,使用 storeToRefs 保持响应性 const { count } = storeToRefs(counterStore); const { increment } = counterStore; // 解构 action </script>
总结建议 :对于新项目,尤其是 Vue3 项目,优先推荐使用 Pinia。它更简单,类型支持更好,去除了 Vuex 中一些繁琐的概念(如 mutations)。
🟢 CSS 选择器权重
CSS 选择器的权重决定了当多条规则应用于同一元素时,哪条规则会生效。
权重由四个分量组成,通常表示为 (a, b, c, d)
或 0,0,0,0
:
- a (千位) :
内联样式
(style attribute) - 权重1,0,0,0
- b (百位) :
ID 选择器
- 权重0,1,0,0
- c (十位) :
类选择器
(class)、属性选择器
([type="text"])、伪类
(:hover) - 权重0,0,1,0
- d (个位) :
元素选择器
(div)、伪元素
(::before) - 权重0,0,0,1
通配符*
、组合器>+~
、:where()
权重为0,0,0,0
,不影响 specificity。!important
是最高优先级,但强烈建议谨慎使用。
比较规则 : 从 a 到 d 依次比较,权重高的样式生效。注意:权重不进位,1000个类选择器(c=1000)的权重也低于1个ID选择器(b=1, c=0)。
权重示例表
选择器示例 | 权重分量 | 具体权重值 (a,b,c,d) |
---|---|---|
style="..." (内联样式) |
a=1 | 1,0,0,0 |
#header |
b=1 | 0,1,0,0 |
#header #nav (2个ID) |
b=2 | 0,2,0,0 |
.menu .item (2个类) |
c=2 | 0,0,2,0 |
ul li a (3个元素) |
d=3 | 0,0,0,3 |
button.primary (1元素1类) |
c=1, d=1 | 0,0,1,1 |
#submit-btn.active (1ID1类) |
b=1, c=1 | 0,1,1,0 |
* (通配符) |
0 | 0,0,0,0 |
应用案例
假设HTML为:<button id="submit-btn" class="btn primary" style="color: red;">Submit</button>
css
button { color: black; } /* 权重: 0,0,0,1 -> 0,0,0,1 */
.btn { color: blue; } /* 权重: 0,0,1,0 -> 0,0,1,0 */
#submit-btn { color: green; } /* 权重: 0,1,0,0 -> 0,1,0,0 */
.primary { color: yellow; } /* 权重: 0,0,1,0 -> 但后声明的相同权重规则可能被覆盖 */
最终生效的是 style="color: red;"
(权重 1,0,0,0) 或 #submit-btn { color: green; }
(权重 0,1,0,0),内联样式权重更高。如果没有内联样式,则 #submit-btn
的绿色生效。
🟢 三个 Span 标签垂直居中
让行内元素如 <span>
垂直居中,需要根据其父容器的布局方式选择方法。
方法 1:Flexbox 布局 (推荐)
Flexbox 是现代布局的首选方式,非常简单可靠。
html
<div class="container">
<span>Span 1</span>
<span>Span 2</span>
<span>Span 3</span>
</div>
css
.container {
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 (如果需要) */
height: 200px; /* 必须给容器一个高度 */
border: 1px solid #ccc;
}
align-items: center
会使 flex 容器内的所有项目(包括 span)在交叉轴上(默认是垂直方向)居中。
方法 2:Grid 布局
Grid 布局同样能轻松实现居中。
css
.container {
display: grid;
place-items: center; /* 同时实现水平和垂直居中 */
height: 200px;
border: 1px solid #ccc;
}
place-items
是 align-items
(垂直) 和 justify-items
(水平) 的简写。
方法 3:行高 (Line-height) - 适用于单行文本
如果容器高度固定且内容只有一行 文本,可以设置 line-height
等于容器高度。
css
.container {
height: 200px;
border: 1px solid #ccc;
}
.container span {
line-height: 200px; /* 关键:与容器高度相同 */
}
注意: 此方法要求 span 内容不换行,且容器内无其他影响行高的元素。
重复请求控制

低代码

前端开发中经常会遇到一些特定的技术和问题,下面我将为你梳理这些知识点,希望能帮助你更好地理解和应对。
🟢 HTTP OPTIONS 请求
1. 触发时机
OPTIONS 请求主要用于"预检",即在发送某些可能涉及安全风险的请求之前,先询问服务器是否允许。浏览器会自动处理这个过程,以下情况会触发:
- 跨域请求且非简单请求 :这是最常见的触发场景。浏览器会先发送 OPTIONS 请求进行"预检"。
- 简单请求 需同时满足:方法是 GET、HEAD 或 POST;Content-Type 是
text/plain
、multipart/form-data
或application/x-www-form-urlencoded
之一;没有使用自定义头部。 - 不符合上述条件的即为非简单请求 ,例如:
- 使用了 PUT、DELETE 等方法。
- 设置了 自定义头部 (如
Authorization
、X-Custom-Header
)。 - Content-Type 为
application/json
。
- 简单请求 需同时满足:方法是 GET、HEAD 或 POST;Content-Type 是
- 显式查询服务器能力 :开发者可以主动发送 OPTIONS 请求,查询服务器对某个资源支持哪些 HTTP 方法(响应头中的
Allow: GET, POST, OPTIONS
)。
2. 预检流程
其预检机制是为了保障安全,具体流程可参考下图:
允许 拒绝 浏览器发起非简单跨域请求 自动发送OPTIONS预检请求 服务器检查请求头
Access-Control-Request-Method
Access-Control-Request-Headers 服务器返回CORS响应头 浏览器检查响应头
是否符合实际请求要求 发送实际请求 阻塞实际请求
并在控制台报错
3. 优化建议
频繁的预检请求可能影响性能,可通过设置 Access-Control-Max-Age
头部来指定预检响应的缓存时间(单位秒),在此时间内,同一请求无需再次预检。
🟢 1px 边框问题
1. 问题根源
这个问题源于 CSS 像素 与设备物理像素 之间的差异。在 DPR(设备像素比) 大于 1 的高清屏(如 Retina 屏)上,1个 CSS 像素会由多个物理像素来渲染。例如 DPR=2 时,1px
在屏幕上实际占据了 2x2 的物理像素,导致视觉上变粗。
2. 解决方案对比
解决方案 | 核心原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
伪元素 + Transform | 用伪元素生成边框,通过 scale 缩放至所需粗细 |
兼容性好,控制灵活,支持圆角 | 需额外元素或伪元素,代码稍多 | 各类边框,尤其是带圆角的边框 |
动态 Viewport | 通过 JS 动态缩放视口,使 CSS 像素与物理像素 1:1 对应 | 一劳永逸,无需为每个边框单独处理 | 会影响页面所有布局,需使用 REM 等单位适配 | 全新项目 |
0.5px (媒体查询) | 针对高 DPR 设备直接设置 border: 0.5px |
代码简单 | 安卓兼容性差,仅 iOS 8+ 支持 | 仅需适配 iOS 的场景 |
Border-Image | 使用图片模拟细边框 | 可实现复杂边框样式 | 修改颜色不便,不支持圆角 | 特殊样式的边框 |
SVG | 使用 SVG 矢量图绘制边框 | 显示精准,不受 DPR 影响 | 需要熟悉 SVG | 高保真 UI,复杂边框 |
3. 常用方案代码示例
伪元素 + Transform (推荐)
这是最常用且兼容性较好的方案,利用伪元素和 CSS Transform 进行缩放。
css
/* 下边框 */
.scale-border {
position: relative;
border: none;
}
.scale-border::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px; /* 创建原始边框 */
background-color: #000;
transform: scaleY(0.5); /* Y轴缩放至0.5倍 */
transform-origin: 0 0; /* 设置缩放原点 */
}
/* 适配不同DPR */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
.scale-border::after {
transform: scaleY(0.5);
}
}
@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 3dppx) {
.scale-border::after {
transform: scaleY(0.333);
}
}
动态 Viewport (适用于新项目)
此方案通过 JavaScript 动态调整 viewport 的缩放比例,强制让 CSS 像素与物理像素等值。
html
<!-- HTML 中的 viewport 标签 -->
<meta name="viewport" id="viewportMeta" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
javascript
// JavaScript 动态调整
const dpr = window.devicePixelRatio || 1;
const scale = 1 / dpr; // 计算缩放比例
const metaEl = document.querySelector('meta[name="viewport"]');
metaEl.setAttribute('content', `width=device-width, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no`);
// 通常还需配合 REM 布局,根据缩放后的视口宽度动态设置根字体大小
document.documentElement.style.fontSize = `${100 * (window.innerWidth / 750)}px`; // 以750px设计稿为例
🟢 资源预加载 (Preload)
1. 基本原理
Preload 是一种 资源提示 ,通过 <link rel="preload">
告诉浏览器提前获取并缓存某个重要资源(如字体、关键 CSS/JS、图片等)。
html
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.webp" as="image">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
as
属性:指定资源类型,帮助浏览器设置正确的优先级和策略。crossorigin
:加载字体等 CORS 资源时必须设置,即使同源。
2. 为何不阻塞渲染
Preload 的关键特性在于其异步非阻塞的加载方式:
- 优先级分配 :浏览器会根据
as
类型为预加载资源分配高优先级,但不会阻塞 HTML 解析和页面渲染。 - 分离加载与执行 :Preload 只加载并缓存 资源,并不立即执行(如JS代码、CSS应用)。执行时机仍由资源在文档中的位置或代码逻辑决定。
- 优化用户体验 :通过提前加载关键资源,浏览器能更快地获取它们,从而减少后续渲染过程中的等待时间,提升页面加载性能,而不阻塞当前页面的渲染。
🟢 React Suspense 原理
1. 核心机制
Suspense 的核心是让组件能够"等待"某些异步操作(如代码加载、数据获取)完成,在等待期间显示一个降级 UI(如 loading 状态)。
其底层原理依赖于 "抛出 Promise" 的机制:
- 挂起(Suspend) :当一个异步操作(如
React.lazy()
动态导入组件或自定义异步数据获取)正在进行时,相关的 React 组件会抛出一个 Promise 对象,而不是正常渲染。这不是真正的 JS 错误,而是 React 的一种特殊通信机制。 - 捕获与处理(Catch) :上层的
<Suspense>
边界 会捕获这个被抛出的 Promise。 - 显示降级 UI :
<Suspense>
会立即渲染其fallback
属性指定的内容(如一个旋转的加载器)。 - 解决与恢复(Resolve) :当抛出的 Promise 被解决(resolve)后,React 会自动重新尝试渲染之前被挂起的组件树。此时异步操作已完成,组件便能成功渲染并显示最终内容。
2. 与并发模式(Concurrent Mode)的结合
在 React 的并发模式下,Suspense 的能力得到增强:
- 可中断渲染与优先级调度:高优先级的更新可以中断正在进行的、较低优先级的异步渲染(如一个已部分渲染的懒组件),确保用户交互能得到及时响应。
- 流畅的过渡体验 :使用
startTransition
或useTransition
钩子,可以告诉 React 某个切换(如路由选项卡)是"过渡性"的,从而在准备新内容时保持旧 UI 的交互性,并优雅地显示加载状态,避免隐藏当前内容直到新内容加载完成带来的突兀感。
基础
知识领域 | 核心概念/问题 | 关键原理/机制 | 关联技术/解决方案 |
---|---|---|---|
网络协议 | TCP协议 | 面向连接、可靠传输、流量控制、拥塞控制 | 三次握手、四次挥手、滑动窗口、慢启动 |
HTTPS协议 | HTTP + SSL/TLS,加密传输、身份认证 | 非对称加密交换会话密钥、对称加密通信内容、数字证书 | |
七层网络模型 (OSI模型) | 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层 | TCP/IP协议族(对应网络层、传输层、应用层等) | |
浏览器渲染与缓存 | 页面从点击到显示的过程 | DNS解析 → TCP连接 → HTTP请求 → 服务器处理 → 浏览器渲染 | 优化DNS查询、减少HTTP请求、利用缓存 |
页面解析过程 | 解析HTML构建DOM树 → 解析CSS构建CSSOM树 → 合并成渲染树 → 布局 (Layout) → 绘制 (Paint) | 避免同步JS、使用defer /async 、优化CSS选择器 |
|
浏览器缓存 | 强缓存 (Cache-Control, Expires)、协商缓存 (Last-Modified/If-Modified-Since, ETag/If-None-Match) | 合理设置缓存策略提升性能 | |
CDN (内容分发网络) | 将资源缓存到离用户更近的边缘节点,减少网络延迟和源站压力 | 加速静态资源(图片、CSS、JS)加载 | |
前端安全 | 跨域 (Cross-Origin) | 浏览器同源策略 (协议、域名、端口任一不同即为跨域) 的限制 | CORS (设置Access-Control-Allow-Origin 等响应头)、JSONP (利用<script> 标签跨域)、反向代理 (服务器端转发请求) |
CSRF (跨站请求伪造) | 攻击者诱导用户在已登录的Web应用中执行非本意的操作 | 验证请求来源 (Referer检查)、使用Token验证 (Anti-CSRF Token)、设置SameSite Cookie属性 | |
其他前端常见攻击 (如XSS) | XSS: 攻击者向页面注入恶意脚本 | XSS: 对用户输入进行转义、使用CSP (内容安全策略) | |
前端框架与工具 | Vue双向数据绑定原理 (Vue 2) | 通过数据劫持 (Object.defineProperty )+ 发布-订阅模式 实现。Object.defineProperty 定义所有属性的 getter /setter ,在 getter 中收集依赖,在 setter 中通知更新。 |
Vue 3改用Proxy 实现,性能更优且能监听动态新增属性。 |
Vue源码 & 打包过程 | Vue源码包含编译器、响应式系统、虚拟DOM、组件系统等。打包过程通常使用Webpack或Vite,将众多模块(.vue, .js, .css)打包成少量优化后的静态资源文件(如JS Bundle)。 | Tree-shaking、代码分割、压缩混淆等优化手段。 | |
Webpack原理 | 核心概念:入口(Entry) 、输出(Output) 、加载器(Loaders) (处理非JS文件)、插件(Plugins) (执行更广的任务)、模式(Mode)(开发/生产)。 | 模块化、依赖分析、代码转换和打包。 | |
项目与部署 | 实习项目部署 | 常见方式:CI/CD流水线(如Jenkins, GitLab CI)、手动部署(SCP/FTP上传文件)。流程:构建 → 打包(生成静态文件)→ 上传至服务器(如Nginx目录)→ 配置服务器(如Nginx反向代理)。 | 自动化部署提升效率,利用Docker容器化部署增强环境一致性。 |
JS编程与算法 | 深拷贝 (Deep Clone) | 完整复制对象/数组及其嵌套引用,新老对象完全独立。 | JSON.parse(JSON.stringify(obj)) (有局限)、递归实现(处理对象、数组、循环引用)、使用第三方库(如Lodash的_.cloneDeep )。 |
遍历树的时间复杂度 | 通常为 O(n) ,其中 n 为树中节点的总数。因为每个节点都会访问一次。 |
深度优先搜索 (DFS)、广度优先搜索 (BFS)。 | |
快速排序的时间复杂度 | 平均情况:O(n log n) ;最坏情况(已排序):O(n²)。 | 分治思想,选取基准元素分区。 | |
判断数据类型 | typeof (基本类型,null 为"object" )、instanceof (检测构造函数的prototype 是否在对象原型链上)、Object.prototype.toString.call(obj) (返回[object Type] )。 |
Array.isArray() (判断是否为数组)。 |
|
编程题:最长连续相同元素 | 遍历数组,计数当前连续相同元素,更新最大计数和对应元素。 | 时间复杂度 O(n) ,空间复杂度 O(1)。 | |
其他 | 毕业设计 & 爬虫原理 & 模型改进点 | 需根据你的实际项目情况补充。爬虫原理:模拟HTTP请求获取网页内容 → 解析提取数据(正则、CSS选择器、XPath)→ 存储数据(数据库、文件)。模型改进点常指机器学习模型调参、优化算法、特征工程等。 |
🔧 编程题:寻找最长连续相同元素
题目 :给定一个数组,找出连续出现次数最多的元素及其长度。
例如:输入 [1, 2, 2, 3, 3, 3, 2]
,应返回 { element: 3, length: 3 }
。
🧠 思路
- 初始化 :我们需要变量来记录
当前连续元素
、当前连续长度
、最大连续元素
和最大连续长度
。 - 遍历数组:逐个检查数组中的元素。
- 判断连续性 :
- 如果当前元素 与上一个元素 相同,
当前连续长度
加1。 - 如果不相同,则说明一段连续结束了。此时比较
当前连续长度
和最大连续长度
,如果更长,就更新最大连续元素
和最大连续长度
。然后重置当前连续元素
和当前连续长度
为新的元素。
- 如果当前元素 与上一个元素 相同,
- 处理最后一段:遍历结束后,最后一段连续序列可能还未比较,需要再判断一次。
📜 JavaScript 代码实现
javascript
function findLongestConsecutiveSequence(arr) {
if (arr.length === 0) {
return { element: undefined, length: 0 };
}
let currentElement = arr[0];
let currentLength = 1;
let maxElement = arr[0];
let maxLength = 1;
for (let i = 1; i < arr.length; i++) {
if (arr[i] === currentElement) {
// 当前元素与之前连续的元素相同,长度加1
currentLength++;
} else {
// 当前元素发生变化,比较并更新最大记录
if (currentLength > maxLength) {
maxElement = currentElement;
maxLength = currentLength;
}
// 重置当前记录为新的元素
currentElement = arr[i];
currentLength = 1;
}
}
// 循环结束后,再次检查最后一段序列
if (currentLength > maxLength) {
maxElement = currentElement;
maxLength = currentLength;
}
return { element: maxElement, length: maxLength };
}
// 测试示例
console.log(findLongestConsecutiveSequence([1, 2, 2, 3, 3, 3, 2])); // { element: 3, length: 3 }
console.log(findLongestConsecutiveSequence(['a', 'b', 'b', 'b', 'a'])); // { element: 'b', length: 3 }
console.log(findLongestConsecutiveSequence([5])); // { element: 5, length: 1 }
console.log(findLongestConsecutiveSequence([])); // { element: undefined, length: 0 }
function findLongestConsutiveSequence(arr){
let currentElement=arr[0];
let currentLength=1;
let maxElement=arr[0];
let maxLen=1;
for(let i=1;i<arr.length;i++){
if(arr[i]===currentElement){
currentLength++;
}else{
if(currentLen>maxLen){
maxElement=currentElement;
maxLength=currentLength;
}
//重置当前记录为新的元素
currentElement=arr[i];
currentLength=1;
}
}
if(currentLength>maxLength){
maxElement=currentElement;
maxLength=currentLength;
}
return {element:maxElement,length:maxLength};
}
⏱ 时间复杂度分析
- 时间复杂度:O(n)
算法使用了一个简单的for
循环,从头到尾遍历了输入数组一次。循环内的所有操作(比较、赋值)都是常数时间O(1)
。因此,总的时间复杂度是线性的 O(n) ,其中n
是数组的长度。 - 空间复杂度:O(1)
算法只使用了几个固定的变量(currentElement
,currentLength
,maxElement
,maxLength
)来存储中间状态和结果。这些变量所占用的空间不随输入数组的大小n
而变化。因此,空间复杂度是常数级的 O(1)。