作业帮前端面试(准备)
手撕
原地修改链表
// 定义链表节点类
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 指向 secondsecond.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
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()
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));
-
- 递归
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);})
}
千位格式化
/*
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;
}
找出不在指定区间内的数字
/*** 找出所有不在任何已使用区间内的数字* @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 -> 未被使用
排序算法
快速排序
的思路
function quickSort(arr,low,high){if(low>=high)return;const pivot=arr[high];let left=low;//i=lowfor(let i=low;i<high;i++){//pivotif(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);
}
归并排序
的稳定性很重要
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
接收。
<!-- 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>
<!-- ChildComponent.vue -->
<template><div>{{ message }} - {{ count }}</div>
</template><script setup>
// defineProps 是一个编译宏,无需导入
const props = defineProps({message: {type: String,required: true},count: Number
});
</script>
子传父 (自定义事件)
子组件通过 defineEmits
定义事件,然后通过 emit
触发。父组件监听该事件。
<!-- ChildComponent.vue -->
<template><button @click="sendMessage">Click Me</button>
</template><script setup>
const emit = defineEmits(['messageSent']);const sendMessage = () => {emit('messageSent', 'Data from child!');
};
</script>
<!-- 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 传递给一个子组件,并监听另一个子组件的事件来更新状态。
<!-- 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):
// eventBus.js import mitt from 'mitt'; const emitter = mitt(); export default emitter;
-
兄弟组件 A (发送事件):
<!-- 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 (接收事件):
<!-- 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。
<!-- 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>
<!-- 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:
// 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: { // 异步操作,提交 mutationincrementAsync({ commit }) {setTimeout(() => {commit('increment');}, 1000);}},getters: { // 计算属性doubleCount(state) {return state.count * 2;}} });
-
在组件中使用:
<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:
// 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} });
-
在组件中使用:
<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>
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 是现代布局的首选方式,非常简单可靠。
<div class="container"><span>Span 1</span><span>Span 2</span><span>Span 3</span>
</div>
.container {display: flex;align-items: center; /* 垂直居中 */justify-content: center; /* 水平居中 (如果需要) */height: 200px; /* 必须给容器一个高度 */border: 1px solid #ccc;
}
align-items: center
会使 flex 容器内的所有项目(包括 span)在交叉轴上(默认是垂直方向)居中。
方法 2:Grid 布局
Grid 布局同样能轻松实现居中。
.container {display: grid;place-items: center; /* 同时实现水平和垂直居中 */height: 200px;border: 1px solid #ccc;
}
place-items
是 align-items
(垂直) 和 justify-items
(水平) 的简写。
方法 3:行高 (Line-height) - 适用于单行文本
如果容器高度固定且内容只有一行文本,可以设置 line-height
等于容器高度。
.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. 预检流程
其预检机制是为了保障安全,具体流程可参考下图:
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 进行缩放。
/* 下边框 */
.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 中的 viewport 标签 -->
<meta name="viewport" id="viewportMeta" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
// 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、图片等)。
<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 代码实现
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) {// 当前元素与之前连续的元素相同,长度加1currentLength++;} 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)。