一、基础核心知识
1. HTML/CSS
-
HTML5的新特性有哪些?
- 新的语义元素(如
<article>,<section>,<header>,<footer>) - 原生支持视频和音频(
<video>,<audio>标签) - 本地存储(localStorage 和 sessionStorage)
- Canvas API,用于绘图
- 新的语义元素(如
-
如何使用CSS实现响应式布局?
- 使用媒体查询(
@media)针对不同屏幕尺寸应用不同样式。 - 使用Flexbox和CSS Grid进行灵活布局。
- 使用百分比和相对单位(如
em和rem)设置宽度和高度。
- 使用媒体查询(
-
解释CSS盒模型及其组成部分。
- 盒模型由四个部分组成:内容(content)、内边距(padding)、边框(border)、外边距(margin)。
- 可以使用
box-sizing属性控制盒模型的计算方式。
-
盒模型(标准盒模型 vs 怪异盒模型)?
- 标准盒模型:
width和height只包含内容区域。 - 怪异盒模型:
width和height包含内容、内边距和边框。 - 切换方式:
box-sizing: content-box(标准)或border-box(怪异)。
- 标准盒模型:
-
CSS3 新特性:掌握 Flexbox 和 Grid 布局,了解动画(
@keyframes)、过渡(transition)、媒体查询(@media)等。 -
Flex 布局和 Grid 布局的应用场景?
- Flex 布局:适合一维布局,即在一个方向上(行或列)排列元素。常用于导航栏、卡片布局等。
- Grid 布局:适合二维布局,即在行和列两个方向上排列元素。常用于复杂的网格布局,如仪表盘、复杂的表单布局等。提供了更好的控制。
-
BFC(块级格式化上下文)是什么?如何触发?
-
BFC:是页面上的一个独立的渲染区域,内部的元素不会影响到外部的元素。BFC可以包裹浮动元素,防止父容器高度塌陷。BFC内的元素外边距不会与外部元素的外边距重叠。常见的触发方式包括:
float不为noneposition为absolute或fixeddisplay为inline-block、table-cell、flex等overflow不为visiblecontain:值为layout、content或paint。
-
应用场景
- 清除浮动:防止父容器高度塌陷。
- 避免外边距重叠:阻止相邻元素的外边距合并。
- 自适应两栏布局:利用BFC特性实现布局。
-
响应式设计的实现方案(媒体查询、REM/VW 单位)?
- 媒体查询:
@media (max-width: 768px) { ... }。 - REM/VW:
rem基于根元素字体大小,vw基于视口宽度。
- 媒体查询:
-
CSS 选择器优先级计算规则?
- 优先级:
!important> 内联样式 > ID 选择器 > 类/伪类/属性选择器 > 元素/伪元素选择器 > 通配符。
- 优先级:
2. JavaScript
-
事件循环(Event Loop)机制,宏任务与微任务的区别?
- 事件循环:JavaScript 单线程通过事件循环处理异步任务。
- 宏任务:
setTimeout、setInterval、I/O等。 - 微任务:
Promise.then、MutationObserver等。微任务在当前执行栈清空后、下一个宏任务前执行。
-
闭包的作用与使用场景?如何避免内存泄漏?
-
闭包:函数内部可以访问外部作用域(函数)的变量 ,常用于数据私有化。
-
使用场景:模块化、私有变量、回调函数。
-
避免内存泄漏:及时释放不再使用的闭包引用。
function createCounter() { let count = 0; return function() { count++; return count; }; } const counter = createCounter();
-
-
ES6+ 新特性(箭头函数、Promise、async/await、Proxy 等)?
- 箭头函数:
() => {},没有自己的this。 - Promise:异步编程,
then、catch、finally。 - async/await:基于
Promise的语法糖,使异步代码更易读。 - Proxy:拦截对象操作,如
get、set。
const fetchData = async () => { const response = await fetch('url'); const data = await response.json(); return data; }; - 箭头函数:
-
原型链与继承的实现方式?
- 原型链:每个对象都有一个
__proto__指向其原型对象,原型又可以指向更上层的原型。。 - 继承:通过
prototype和__proto__实现。 - 构造函数继承:通过构造函数创建实例。
function Animal(name) { this.name = name; } Animal.prototype.speak = function() { console.log(this.name + ' makes a noise.'); }; function Dog(name) { Animal.call(this, name); // 继承属性 } Dog.prototype = Object.create(Animal.prototype); // 继承方法 Dog.prototype.constructor = Dog; - 原型链:每个对象都有一个
-
防抖(Debounce)与节流(Throttle)的实现原理?
-
防抖:多次触发只执行最后一次。
function debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } -
节流:多次触发按固定频率执行。
function throttle(fn, wait) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= wait) { lastTime = now; fn.apply(this, args); } }; }
-
-
什么是事件委托?
- 事件委托是将事件处理程序绑定到父元素,而不是子元素。这样可以减少内存消耗和提高性能,尤其在动态添加子元素时。
-
如何处理异步编程?请举例说明Promise和async/await的用法。
- Promise是处理异步操作的对象,表示一个可能还未完成但将来会完成的操作。例如:
const fetchData = () => { return new Promise((resolve, reject) => { // 异步操作 }); };-
async/await使得异步代码看起来更像同步代码:
const fetchData = async () => { const data = await fetch('url'); const json = await data.json(); return json; };
3. 浏览器与网络
-
浏览器渲染流程(关键渲染路径)?
- 浏览器渲染过程包括解析HTML生成DOM树、解析CSS生成CSSOM树、构建渲染树、布局(calculate layout)、绘制(paint)和合成(composite)。
- 步骤:HTML 解析 -> CSSOM 构建 -> Render Tree -> Layout -> Paint -> Composite。
-
HTTP 缓存机制(强缓存、协商缓存)?
- 强缓存:
Cache-Control、Expires缓存有效时直接使用。 - 协商缓存:
Last-Modified、ETag请求时验证缓存是否有。
- 强缓存:
-
跨域问题的解决方案(CORS、JSONP、代理)?
- CORS:服务器设置
Access-Control-Allow-Origin。 - JSONP:通过
<script>标签跨域请求。 - 代理:服务器代理请求。
- CORS:服务器设置
-
HTTPS 的工作原理及与 HTTP 的区别?
- HTTP是超文本传输协议,数据以明文形式传输。HTTPS是HTTP的安全版本,使用SSL/TLS加密数据,提供安全性和完整性。
- HTTPS:HTTP + SSL/TLS 加密 ;提供数据加密、身份验证和数据完整性。
- 区别:HTTPS 更安全,默认端口 443;HTTP 使用端口 80。
-
从输入 URL 到页面加载完成的整个过程?
- 步骤:DNS 解析 -> TCP 连接 -> HTTP 请求 -> 服务器响应 -> 浏览器渲染。
-
DNS 解析:将域名解析为 IP 地址。
-
建立 TCP 连接:与服务器建立连接。
-
发送 HTTP 请求:请求页面资源。
-
服务器响应:服务器返回 HTML、CSS、JS 等资源。
-
浏览器渲染:解析并渲染页面,执行 JS,处理 CSS。
-
- 步骤:DNS 解析 -> TCP 连接 -> HTTP 请求 -> 服务器响应 -> 浏览器渲染。
-
什么是跨域请求?如何解决跨域问题?
- 跨域请求是指在不同源之间进行的HTTP请求。可以通过CORS(跨源资源共享)、JSONP或代理服务器解决。
- 如何解决跨域问题?
-
CORS(跨源资源共享)
-
服务器可以通过设置 HTTP 头部来允许跨域请求。常用的头部包括:
Access-Control-Allow-Origin: 指定允许的源。Access-Control-Allow-Methods: 指定允许的 HTTP 方法(如 GET、POST)。Access-Control-Allow-Headers: 指定允许的请求头。
-
例如:
Access-Control-Allow-Origin: https://example.com
-
-
JSONP(JSON with Padding)
- JSONP 是一种通过
<script>标签进行跨域请求的技术。服务器返回一个 JavaScript 函数调用,而不是 JSON 数据。这种方式只支持 GET 请求。
- JSONP 是一种通过
-
代理服务器
- 使用代理服务器将请求转发到目标服务器。前端代码请求代理服务器,代理服务器再请求实际资源,从而避免跨域问题。
-
使用 iframe 和 postMessage
- 在不同源之间使用
<iframe>进行通信,利用window.postMessage方法安全地传递数据。
- 在不同源之间使用
-
WebSocket
-
WebSocket 协议不受同源策略限制,可以进行跨域通信。
-
-
二、框架相关
1. React
-
虚拟 DOM 的作用及 Diff 算法原理?
- 虚拟 DOM:提高性能,通过更新虚拟 DOM 而不是直接操作真实 DOM;轻量级的 DOM 副本,提高渲染性能。
- Diff 算法:通过比较新旧虚拟 DOM,找到最小的变更并更新真实 DOM。
-
React Hooks 的使用场景(useState、useEffect、自定义 Hook)?
- useState:管理组件状态。
- useEffect:处理副作用,如数据获取、订阅。
- 自定义 Hook:复用逻辑。
import { useState, useEffect } from 'react'; function useFetch(url) { const [data, setData] = useState(null); useEffect(() => { fetch(url).then(res => res.json()).then(setData); }, [url]); return data; } -
组件通信方式(Props、Context、Redux 等)?
- Props:父组件向子组件传递数据。
- Context:跨层级组件通信;在组件树中共享数据,避免多层传递 props。
- Redux:全局状态管理;集中管理应用状态。
-
React 性能优化手段(Memo、useCallback、懒加载)?
- Memo:缓存组件;避免不必要的渲染。。
- useCallback:缓存回调函数,减少子组件的渲染。
- 懒加载:
React.lazy和Suspense;按需加载组件,减少初始加载时间。
-
React Fiber 架构的设计目标?
- 目标:提高渲染性能,支持异步渲染;提升 React 的性能和可扩展性,支持异步渲染,优化更新过程。
-
React的生命周期方法有哪些?
- 主要生命周期方法包括
componentDidMount,componentDidUpdate,componentWillUnmount,以及钩子函数如useEffect。
- 主要生命周期方法包括
-
解释Redux的工作原理和基本概念。
- Redux是一个状态管理库,使用单一的状态树(store)来存储应用的状态,使用
action来描述状态变化,reducer处理这些变化并返回新的状态。
- Redux是一个状态管理库,使用单一的状态树(store)来存储应用的状态,使用
2. Vue
-
响应式原理(Object.defineProperty vs Proxy)?
- Object.defineProperty:Vue2 使用,劫持对象属性;通过 getter/setter 拦截属性访问和修改。
- Proxy:Vue3 使用,劫持整个对象;使用代理对象,能够拦截更多操作,如数组方法。
-
Vue 生命周期钩子及使用场景?
- created:实例创建后(数据初始化),适合数据获取。。
- mounted:DOM 挂载后,适合操作 DOM。
- updated:数据更新后,适合执行依赖数据的操作。
- destroyed:实例销毁后;清理定时器,取消网络请求,解除事件监听,清理外部库实例,数据清理。
-
Vuex 和 Pinia 的状态管理机制?
- Vuex:集中式状态管理,
state、mutations、actions、getters;使用 store 管理全局状态。 - Pinia:轻量级状态管理,更简洁的 API;支持 Composition API。
- Vuex:集中式状态管理,
-
Vue3 的 Composition API 优势?
- 优势:提供更灵活的逻辑复用,更灵活的组织代码,更好的 TypeScript 支持,简化复杂组件的状态管理。
-
虚拟 DOM 与 Diff 算法的优化点?
- 优化:静态节点提升、事件缓存、动态节点标记。通过标记更新、节点复用、只更新变化的部分,减少不必要的 DOM 操作
-
Vue中的计算属性和侦听器有什么区别?
- 计算属性是基于依赖的 getter,缓存其值,只有在相关依赖变化时重新计算。而侦听器用于响应数据的变化,可以执行异步操作或开销大的操作。
3. 通用框架问题
MVVM 模式的理解?
- MVVM:Model-View-ViewModel,数据驱动视图;分离 UI 与业务逻辑,使用双向数据绑定。
前端路由的实现原理(Hash vs History API)?
- Hash:通过
#实现,兼容性好; 通过 URL 的 hash 部分实现路由,简单,但不友好。 - History API:通过
pushState和replaceState实现,URL 更美观。
SSR(服务端渲染)的优缺点及实现方案?
- 优点:SEO 友好,首屏加载快。
- 缺点:服务器压力大,开发复杂度高。
- 实现方案:Next.js(React)、Nuxt.js(Vue)支持 SSR。。
三、算法与手写代码
1. 常见算法题
-
数组去重、扁平化、排序(手写快排/归并)。
-
去重:
[...new Set(arr)]。const uniqueArray = arr => [...new Set(arr)]; -
扁平化:
arr.flat(Infinity)。const flatten = arr => arr.flat(Infinity); -
快排:
//方法一 function quickSort(arr) { // 如果数组长度小于或等于 1,直接返回该数组(因为它已经是有序的) if (arr.length <= 1) return arr; // 选择第一个元素作为基准(pivot) const pivot = arr[0]; // 初始化左侧数组,用于存放小于基准的元素 const left = []; // 初始化右侧数组,用于存放大于或等于基准的元素 const right = []; // 遍历数组,从第二个元素开始 for (let i = 1; i < arr.length; i++) { // 如果当前元素小于基准,将其放入左侧数组 if (arr[i] < pivot) { left.push(arr[i]); } // 否则,将其放入右侧数组 else { right.push(arr[i]); } } // 递归调用 quickSort,对左侧和右侧数组进行排序 // 最后合并排序后的左侧数组、基准元素和排序后的右侧数组 return [...quickSort(left), pivot, ...quickSort(right)]; } //方法二 const quickSort = arr => { // 如果数组长度小于或等于 1,直接返回该数组(因为它已经是有序的) if (arr.length <= 1) return arr; // 选择最后一个元素作为基准(pivot) const pivot = arr[arr.length - 1]; // 使用 filter 方法创建一个新数组,包含所有小于基准的元素 const left = arr.filter(x => x < pivot); // 使用 filter 方法创建一个新数组,包含所有大于基准的元素 const right = arr.filter(x => x > pivot); // 递归调用 quickSort,对左侧和右侧数组进行排序 // 将排序后的左侧数组、基准元素和排序后的右侧数组合并并返回 return [...quickSort(left), pivot, ...quickSort(right)]; };
-
-
链表操作(反转、环检测)。
-
反转链表
反转链表的过程是将链表中的节点顺序反转。以下是使用迭代方法反转链表的示例:
function reverseList(head) { let prev = null; // 初始化前一个节点为 null let curr = head; // 当前节点从头节点开始 while (curr) { const next = curr.next; // 保存下一个节点 curr.next = prev; // 反转当前节点的指针 prev = curr; // 移动 prev 指针到当前节点 curr = next; // 移动 curr 指针到下一个节点 } return prev; // 返回新的头节点 }class ListNode { constructor(value = 0, next = null) { this.value = value; this.next = next; } } function reverseLinkedList(head) { let prev = null; let current = head; while (current) { let nextNode = current.next; // 保存下一个节点 current.next = prev; // 反转当前节点的指针 prev = current; // 移动 prev 指针 current = nextNode; // 移动 current 指针 } return prev; // 返回新的头节点 } // 示例用法 const head = new ListNode(1, new ListNode(2, new ListNode(3))); const reversedHead = reverseLinkedList(head); -
环检测
环检测可以使用快慢指针的方法。以下是环检测的实现:
function reverseList(head) { let prev = null; // 初始化前一个节点为 null let curr = head; // 当前节点从头节点开始 while (curr) { const next = curr.next; // 保存下一个节点 curr.next = prev; // 反转当前节点的指针 prev = curr; // 移动 prev 指针到当前节点 curr = next; // 移动 curr 指针到下一个节点 } return prev; // 返回新的头节点 } // 示例用法 const node1 = new ListNode(1); const node2 = new ListNode(2); const node3 = new ListNode(3); node1.next = node2; node2.next = node3; node3.next = node1; // 创建环 console.log(hasCycle(node1)); // 输出: true
-
总结
-
反转链表:通过迭代改变指针方向。
-
环检测:使用快慢指针判断是否有相遇。
-
-
-
二叉树遍历(前序、中序、后序)。
-
前序:
//按顺序访问节点,顺序为:根 -> 左子树 -> 右子树。 function preorderTraversal(root) { const result = []; function traverse(node) { if (!node) return; // 如果节点为空,返回 result.push(node.val); // 访问当前节点 traverse(node.left); // 遍历左子树 traverse(node.right); // 遍历右子树 } traverse(root); // 从根节点开始遍历 return result; // 返回结果数组 } //简化版本;将递归函数与主函数合并,稍微简化代码结构: //通过立即调用函数表达式(IIFE)来减少代码量。 function preorderTraversal(root) { const result = []; (function traverse(node) { if (node) { result.push(node.val); traverse(node.left); traverse(node.right); } })(root); // 立即调用函数 return result; }
-
-
动态规划(爬楼梯、背包问题)。
-
爬楼梯:每次可以爬 1 或 2 级台阶,求到达第 n 级台阶的方法总数。
/* 使用动态规划,时间复杂度为 O(n),空间复杂度为 O(1)(简化版) */ function climbStairs(n) { // 如果楼梯的台阶数小于或等于 2,直接返回 n // 因为只有 1 或 2 台阶时,走法分别为 1 和 2 种 if (n <= 2) return n; // 初始化动态规划数组 dp,其中 dp[i] 表示到达第 i 台阶的方式总数 let dp = [1, 2]; // dp[1] = 1 种方式,dp[2] = 2 种方式 // 从第 3 台阶开始计算到达每个台阶的方式 for (let i = 3; i <= n; i++) { // 到达第 i 台阶的方式等于到达第 (i-1) 台阶和第 (i-2) 台阶的方式之和 dp[i] = dp[i - 1] + dp[i - 2]; } // 返回到达第 n 台阶的方式总数 return dp[n]; } // 进一步简化为仅使用两个变量,不需要数组: function climbStairs(n) { // 如果楼梯的台阶数小于或等于 2,直接返回 n // 因为只有 1 或 2 台阶时,走法分别为 1 和 2 种 if (n <= 2) return n; // 初始化变量 a 和 b // a: 表示到达第 (i-2) 台阶的方式数 // b: 表示到达第 (i-1) 台阶的方式数 let a = 1, b = 2; // 从第 3 台阶开始计算到达每个台阶的方式 for (let i = 3; i <= n; i++) { // 更新 a 和 b // a 变为 b(即 dp[i-2] 变为 dp[i-1]) // b 变为 a + b(即 dp[i] = dp[i-1] + dp[i-2]) [a, b] = [b, a + b]; } // 返回到达第 n 台阶的方式总数(即 b) return b; } -
背包问题:给定一组物品,每个物品有重量和价值,求在不超过最大重量的情况下,能获得的最大价值。
/* 采用一维数组动态规划,时间复杂度为 O(n * capacity)。 */ function knapsack(weights, values, capacity) { // 获取物品的数量 const n = weights.length; // 创建一个长度为 capacity + 1 的数组 dp,用于存储每个容量下的最大价值 const dp = Array(capacity + 1).fill(0); // 遍历每个物品 for (let i = 0; i < n; i++) { // 从背包的最大容量开始,向下遍历到当前物品的重量 // 这样可以确保每个物品只被计算一次 for (let w = capacity; w >= weights[i]; w--) { // 更新 dp[w],选择是当前容量下的最大价值 // 比较不选当前物品和选当前物品的价值 dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]); } } // 返回在给定容量下的最大价值 return dp[capacity]; } -
2. 手写代码
-
实现 Promise(包含 all、race 等方法)。
-
Promise:
class MyPromise { constructor(executor) { // 初始化状态为 pending,表示 Promise 尚未完成 this.status = 'pending'; // 存储成功时的值 this.value = undefined; // 存储失败时的原因 this.reason = undefined; // 存储成功回调函数的数组 this.onResolvedCallbacks = []; // 存储失败回调函数的数组 this.onRejectedCallbacks = []; // resolve 函数用于将 Promise 状态从 pending 改为 fulfilled const resolve = value => { // 只有在状态为 pending 时才能改变状态 if (this.status === 'pending') { this.status = 'fulfilled'; // 更新状态为 fulfilled this.value = value; // 保存成功的值 // 执行所有成功的回调函数 this.onResolvedCallbacks.forEach(fn => fn()); } }; // reject 函数用于将 Promise 状态从 pending 改为 rejected const reject = reason => { // 只有在状态为 pending 时才能改变状态 if (this.status === 'pending') { this.status = 'rejected'; // 更新状态为 rejected this.reason = reason; // 保存失败的原因 // 执行所有失败的回调函数 this.onRejectedCallbacks.forEach(fn => fn()); } }; // 执行传入的 executor 函数,并传递 resolve 和 reject 函数 executor(resolve, reject); } then(onFulfilled, onRejected) { // 如果 Promise 已经成功,直接调用成功回调 if (this.status === 'fulfilled') { onFulfilled(this.value); } // 如果 Promise 已经失败,直接调用失败回调 if (this.status === 'rejected') { onRejected(this.reason); } // 如果 Promise 仍在 pending 状态,将回调函数存入数组 if (this.status === 'pending') { this.onResolvedCallbacks.push(() => { onFulfilled(this.value); }); this.onRejectedCallbacks.push(() => { onRejected(this.reason); }); } } static all(promises) { return new MyPromise((resolve, reject) => { let result = []; // 用于存储所有成功的结果 let count = 0; // 计数器,跟踪成功的 Promise 数量 promises.forEach((promise, index) => { promise.then(value => { result[index] = value; // 保存每个 Promise 的成功结果 count++; // 计数器加一 // 当所有 Promise 都成功时,调用 resolve if (count === promises.length) resolve(result); }, reject); // 如果有任何一个 Promise 失败,调用 reject }); }); } static race(promises) { return new MyPromise((resolve, reject) => { promises.forEach(promise => { // 只要有一个 Promise 成功或失败,就调用相应的 resolve 或 reject promise.then(resolve, reject); }); }); } }
-
-
手写防抖(Debounce)与节流(Throttle)。
-
防抖(Debounce):防抖是指在一定时间内,只有最后一次触发的事件才会被执行。如果在这个时间内再次触发事件,之前的调用会被取消。
function debounce(func, delay) { let timer; return function(...args) { const context = this; clearTimeout(timer); // 清除之前的定时器 timer = setTimeout(() => { func.apply(context, args); // 在指定延迟后调用函数 }, delay); }; } //使用 const handleResize = debounce(() => { console.log('Window resized'); }, 300); window.addEventListener('resize', handleResize); -
节流(Throttle):防抖是指在一定时间内,只有最后一次触发的事件才会被执行。如果在这个时间内再次触发事件,之前的调用会被取消。
function throttle(func, limit) { let lastFunc; let lastRan; return function(...args) { const context = this; if (!lastRan) { func.apply(context, args); // 立即执行 lastRan = Date.now(); } else { clearTimeout(lastFunc); // 清除之前的定时器 lastFunc = setTimeout(() => { if ((Date.now() - lastRan) >= limit) { func.apply(context, args); // 在限制时间内执行 lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; } 使用 const handleScroll = throttle(() => { console.log('Scrolling...'); }, 1000); window.addEventListener('scroll', handleScroll); -
-
实现深拷贝(处理循环引用)。
-
深拷贝:
function deepClone(obj, map = new WeakMap()) { // 如果 obj 是基本类型或 null,直接返回 if (obj === null || typeof obj !== 'object') return obj; // 如果 obj 已经被克隆过,直接返回克隆的对象以处理循环引用 if (map.has(obj)) return map.get(obj); // 创建一个新对象或数组 const clone = Array.isArray(obj) ? [] : {}; // 将原对象和克隆对象的映射存入 WeakMap map.set(obj, clone); // 处理 Date 对象 if (obj instanceof Date) { return new Date(obj); } // 处理 RegExp 对象 if (obj instanceof RegExp) { return new RegExp(obj); } // 遍历对象的每个属性进行递归克隆 for (const key in obj) { // 确保只处理对象自身的属性 if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key], map); } } return clone; // 返回深拷贝后的对象 }
// 使用const original = { name: 'Alice', age: 30, hobbies: ['reading', 'traveling'], meta: { created: new Date(), }, }; // 添加循环引用 original.self = original; const cloned = deepClone(original); console.log(cloned); console.log(cloned.self === cloned); // true, 验证循环引用处理 ``` -
-
手写 call/apply/bind。
``` Function.prototype.myCall = function (context, ...args) { context = context || globalThis; // 如果没有提供上下文,使用 globalThis context.fn = this; // 将函数赋值给上下文的临时属性 const result = context.fn(...args); // 调用函数并传入参数 delete context.fn; // 删除临时属性 return result; // 返回结果 }; Function.prototype.myApply = function (context, args) { context = context || globalThis; // 如果没有提供上下文,使用 globalThis context.fn = this; // 将函数赋值给上下文的临时属性 const result = context.fn(...(args || [])); // 调用函数并传入参数数组 delete context.fn; // 删除临时属性 return result; // 返回结果 }; Function.prototype.myBind = function (context, ...args) { const fn = this; // 保存原函数 return function (...newArgs) { return fn.myCall(context, ...args, ...newArgs); // 返回一个新函数,调用原函数 }; }; ```使用实例
function greet(greeting, punctuation) { return `${greeting}, ${this.name}${punctuation}`; } const person = { name: 'Alice' }; console.log(greet.myCall(person, 'Hello', '!')); // "Hello, Alice!" console.log(greet.myApply(person, ['Hi', '.'])); // "Hi, Alice." const boundGreet = greet.myBind(person, 'Hey'); console.log(boundGreet('!')); // "Hey, Alice!"-
总结
myCall和myApply方法用于改变函数的执行上下文,并传入参数。myBind方法用于创建一个新的函数,固定上下文并允许后续参数传递。
-
-
实现一个发布-订阅模式(EventEmitter)。
-
EventEmitter:
class EventEmitter { constructor() { this.events = {}; // 存储事件及其监听器的对象 } // 注册事件监听器 on(event, listener) { if (!this.events[event]) { // 如果事件不存在,初始化为数组 this.events[event] = []; // 创建事件数组 } this.events[event].push(listener); // 将监听器添加到事件数组 } // 触发事件 emit(event, ...args) { if (this.events[event]) { // 检查事件是否有监听器 this.events[event].forEach(listener => { // 遍历所有监听器 listener(...args); // 调用监听器并传入参数 }); } } // 移除事件监听器 off(event, listener) { if (!this.events[event]) return; // 如果事件不存在,直接返回 this.events[event] = this.events[event].filter(l => l !== listener); // 过滤掉要移除的监听器 } // 只触发一次的监听器 once(event, listener) { const wrapper = (...args) => { // 包装函数以实现一次性调用 listener(...args); // 调用原监听器 this.off(event, wrapper); // 移除包装函数 }; this.on(event, wrapper); // 注册包装函数 } } // 使用示例 const emitter = new EventEmitter(); function responseToEvent(data) { console.log(`Received: ${data}`); // 打印接收到的数据 } emitter.on('event1', responseToEvent); // 注册事件监听器 emitter.emit('event1', 'Hello, World!'); // 触发事件,输出 "Received: Hello, World!" emitter.off('event1', responseToEvent); // 移除事件监听器 emitter.emit('event1', 'This will not be received'); // 不会有输出 emitter.once('event2', (data) => { console.log(`Once: ${data}`); // 只会打印一次 }); emitter.emit('event2', 'First'); // 输出 "Once: First" emitter.emit('event2', 'Second'); // 不会有输出
-
四、性能优化 & 前端工程化
1.性能优化
-
如何优化前端页面的加载速度?
- 使用延迟加载(lazy loading)、压缩和合并资源、使用CDN、优化图片和使用浏览器缓存。
-
什么是懒加载(Lazy Loading)?如何实现?
- 懒加载是指仅在需要时才加载资源。可通过
Intersection ObserverAPI 或者在路由中实现动态导入。
- 懒加载是指仅在需要时才加载资源。可通过
-
解释CDN的作用及其优势。
- CDN(内容分发网络)将资源分发到全球各地的多个服务器,减少加载时间和带宽消耗,提高可用性和冗余性。
-
如何减少 JavaScript 的执行时间?
-
避免不必要的计算:
- 将经常使用的计算结果缓存到变量中,避免重复计算。
-
使用 Web Workers:
- 将 CPU 密集型任务放在 Web Worker 中执行,避免阻塞主线程。
-
减少 DOM 操作:
- 批量更新 DOM,尽量减少对 DOM 的访问次数。
- 使用文档片段(DocumentFragment)进行批量插入。
-
使用防抖和节流:
- 对于频繁触发的事件(如滚动、输入),使用防抖或节流技巧优化性能。
-
-
如何优化 CSS 选择器?
-
简化选择器:
- 避免使用过于复杂的选择器,尽量使用类选择器和 ID 选择器。
- 避免在选择器中使用通配符(如
*)和后代选择器(如div p)。
-
使用 CSS 预处理器:
- 使用 SCSS 或 LESS 等预处理器,使样式更清晰易维护,减少选择器的复杂性。
-
避免过多的嵌套:
- CSS 嵌套层级过深会导致选择器的性能下降,保持选择器的扁平化。
-
-
Web Vitals
-
了解核心 Web 指标
-
LCP(Largest Contentful Paint) :
-
衡量页面加载速度,指用户看到的最大内容元素(如图像、视频或文本块)渲染所需的时间。
-
优化方法:
- 使用优化的图像格式(如 WebP)。
- 预加载关键资源,确保快速加载。
-
-
FID(First Input Delay) :
-
测量用户首次与页面交互(如点击链接、按钮)到浏览器能够响应的延迟。
-
优化方法:
- 减少 JavaScript 的执行时间,优化事件处理函数。
- 使用 Web Workers 将重任务移出主线程。
-
-
CLS(Cumulative Layout Shift) :
-
测量页面布局的稳定性,指在加载过程中可见内容的意外偏移。
-
优化方法:
- 为图像和视频设置明确的宽高属性,避免加载时内容位置变化。
- 使用占位符或骨架屏,确保内容在加载过程中保持位置稳定。
-
-
2. 前端工程化
-
模块化
-
CommonJS
-
定义:CommonJS 是一套规范,主要用于服务器端的模块化,最典型的实现是 Node.js。
-
导出与导入:
- 导出:使用
module.exports或exports。 - 导入:使用
require()函数。
- 导出:使用
-
特点:
- 同步加载:适合服务器端,因为模块在文件系统中直接读取。
- 运行时加载:模块在执行时加载,无法进行静态分析。
-
-
AMD(Asynchronous Module Definition)
-
定义:AMD 是一种异步模块定义规范,适用于浏览器端,最典型的实现是 RequireJS。
-
导出与导入:
- 导出:使用
define()函数。 - 导入:通过
require()函数。
- 导出:使用
-
特点:
- 异步加载:模块在需要时才加载,不会阻塞其他代码执行。
- 适合浏览器环境,能显著提升加载速度。
-
-
ES6 Module
-
定义:ES6 模块是 JavaScript 原生支持的模块化方案,通过
import和export关键字实现。 -
导出与导入:
- 导出:使用
export关键字。 - 导入:使用
import语法。
- 导出:使用
-
特点:
- 静态加载:模块在编译时确定依赖关系,支持静态分析。
- 支持异步加载:可以与动态
import()结合,实现按需加载。
-
区别总结
特性 CommonJS AMD ES6 Module 加载方式 同步 异步 异步(静态) 导出方式 module.exportsdefine()export导入方式 require()require()import使用场景 Node.js 浏览器 浏览器及 Node.js -
-
打包工具
-
Webpack 基本配置
-
安装:
npm install --save-dev webpack webpack-cli -
基本配置(webpack.config.js) :
const path = require('path'); module.exports = { entry: './src/index.js', // 入口文件 output: { filename: 'bundle.js', // 输出文件名 path: path.resolve(__dirname, 'dist'), // 输出路径 }, module: { rules: [ { test: /.js$/, // 处理 JavaScript 文件 exclude: /node_modules/, use: { loader: 'babel-loader', // 使用 Babel 转换 }, }, ], }, mode: 'development', // 开发模式 }; -
-
优化打包性能
-
代码分割:
-
使用
SplitChunksPlugin实现代码分割,按需加载模块。
optimization: { splitChunks: { chunks: 'all', }, },-
Tree Shaking:
- 确保使用 ES6 模块化,Webpack 会自动去除未使用的代码。
- 在
package.json中设置"sideEffects": false来标记不需要副作用的文件。
-
压缩与优化:
- 使用
TerserPlugin压缩 JavaScript 代码。 - 配置
MiniCssExtractPlugin提取 CSS 文件,减少重复加载。
- 使用
-
-
版本控制
-
Git 基本操作
- 初始化仓库:
git init-
常用命令:
- 添加文件:
git add <file> - 提交更改:
git commit -m "commit message" - 查看状态:
git status - 查看历史:
git log
- 添加文件:
-
分支管理
- 创建分支:
git branch <branch-name>- 切换分支:
git checkout <branch-name>-
合并分支:
git merge <branch-name> -
合并冲突
-
当两个分支修改同一文件的同一部分时,合并会产生冲突。
-
Git 会标记冲突位置,需要手动解决。
-
解决后,标记为已解决:
git add <file> git commit -m "Resolved merge conflict" -
-
Rebase
-
定义:将一个分支的更改应用到另一个分支的基础上,保持提交历史的整洁。
-
使用:
git checkout <feature-branch> git rebase <base-branch>- 注意:在与他人分享的分支上要谨慎使用
rebase。
-
-
-
CI/CD
-
持续集成(CI)和持续部署(CD)
- 持续集成(CI) :频繁地将代码集成到主干,通过自动化测试验证集成的有效性。
- 持续部署(CD) :将经过测试的代码自动部署到生产环境,确保代码始终可用。
-
配置 CI/CD 流程
- 选择 CI/CD 工具:如 GitHub Actions、Travis CI、CircleCI 等。
- 编写配置文件(例如
.github/workflows/ci.yml):
name: CI on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Node.js uses: actions/setup-node@v2 with: node-version: '14' - name: Install dependencies run: npm install - name: Run tests run: npm test - name: Build run: npm run build
-
五、项目经验与系统设计
1. 项目深挖
-
你做过最复杂的项目是什么?技术难点如何解决?
-
项目描述:描述项目的功能、技术栈。
-
技术难点:如性能优化、状态管理、跨域问题等,具体解决方案。
- 示例:一个实时聊天应用,技术难点包括 WebSocket 连接管理、消息队列处理、性能优化等。
-
-
如何优化首屏加载速度?(具体指标与工具)
- 优化手段:代码分割、懒加载、CDN、图片优化、服务端渲染。
- 工具:Lighthouse、Webpack Bundle Analyzer。 进行性能监测。
-
如何设计一个组件库?需要考虑哪些问题?
-
考虑点:组件复用性、可定制性、文档、测试、版本管理。
-
实现方式:使用 Storybook、Rollup 等工具进行构建和展示。
-
-
是否处理过内存泄漏?如何定位和解决?
- 定位:使用 Chrome DevTools 的 Memory 面板或使用 Chrome DevTools 的内存快照。
- 解决:及时释放不再使用的引用,避免循环引用(清理不再需要的事件监听、定时器等)。
2. 系统设计题
-
设计一个实时聊天功能(WebSocket、消息队列)。
-
方案:使用 WebSocket 实现实时通信,消息队列处理高并发消息。
-
技术选型:Node.js 后端、Redis 作为消息队列。
-
-
如何实现一个前端埋点监控系统?
-
设计思路:收集用户行为数据(点击、浏览等),发送到后端。
-
实现方式:使用
fetch或navigator.sendBeacon或XMLHttpRequest发送日志,后端存储和分析。
-
-
设计一个支持拖拽的可视化搭建平台。
-
设计思路:使用 HTML5 的 Drag and Drop API 或第三方库(如 React DnD);结合 React 或 Vue 实现组件拖拽和布局。
-
实现方式:定义可拖拽的组件,管理状态以记录布局变化。
-
六、工程化与工具
-
Webpack 的构建流程及常用优化手段(Tree Shaking、Code Splitting)?
- 构建流程:入口 -> 加载器 -> 插件 -> 输出。(解析依赖图,编译、打包、输出文件)
- 优化手段:
Tree Shaking去除未使用代码,Code Splitting分割代码(按需加载模块,减少初始加载时间。)。
-
Babel 的作用及插件开发原理?
- 作用:将 ES6+ 代码转换为 ES5(将现代 JavaScript 转换为兼容性更好的版本)。
- 插件开发:通过 AST(抽象语法树)转换代码。
-
如何配置 ESLint 和 Prettier 保证代码规范?
-
ESLint:配置规则,使用
.eslintrc文件。 -
Prettier:配置格式化规则,使用
.prettierrc文件。 -
集成:使用
eslint-config-prettier解决冲突。 -
配置:
.eslintrc和.prettierrc文件,结合husky和lint-staged实现提交前检查。
-
-
CI/CD 在前端项目中的应用?
-
定义:持续集成和持续交付,自动化测试和部署。
-
工具:使用 GitHub Actions、Travis CI 等实现自动化流程。
-
应用:自动化测试、构建、部署,常用工具如 Jenkins、GitLab CI、GitHub Actions。
-
七、综合能力
-
如何学习新技术?最近关注的前端趋势?
- 学习新技术的策略
-
学习方法:官方文档、开源项目、技术博客、实践项目。
-
定期阅读技术博客和文档:关注一些知名的前端开发博客(如 CSS-Tricks、Smashing Magazine、MDN)和官方文档(如 React、Vue、Angular 的文档)。
-
在线课程和视频:利用平台(如 Udemy、Coursera、YouTube)上的课程进行系统学习。
-
实践项目:通过构建小项目或参与开源项目来巩固所学技术,确保理论与实践结合。
-
社区参与:加入开发者社区(如 Stack Overflow、GitHub、Discord),参与讨论,向他人学习,分享经验。
-
- 最近关注的前端趋势:
- 前端趋势:WebAssembly、Progressive Web Apps(PWA)、Serverless、微前端。
- 微前端架构:一种将大型应用拆分成多个小型、独立可部署的应用的架构。
- Server Components:React 及其他框架中的服务端组件,简化数据获取和页面渲染。
- TypeScript 的普及:越来越多的项目和库采用 TypeScript,提高代码的可维护性和可读性。
- Jamstack 架构:将前端与后端解耦,使用静态生成、API 和 CDN 来构建快速、可扩展的应用。
- 学习新技术的策略
-
遇到技术分歧时如何与团队沟通?
-
沟通方式:基于数据和事实,尊重他人意见,寻求共识。
-
保持开放的心态:
- 尊重不同的观点,理解团队成员的立场和考虑因素。
- 以建设性的方式表达自己的意见,避免情绪化。
-
数据驱动的讨论:
- 使用数据和实际案例支持自己的观点,展示技术选择的优缺点。
- 进行 A/B 测试或原型验证不同方案的效果,提供实证依据。
-
寻求共识:
- 在讨论中寻找共同点,明确团队的目标和需求,促成团队达成一致。
- 如果可能,提出折衷方案,结合不同意见形成最佳解决方案。
-
文档化和总结:
- 将讨论结果和决策过程记录下来,确保团队成员都能查阅。
- 通过文档促进透明度,减少未来类似问题的发生。
-
-
未来的职业规划?
-
规划:深入前端技术,学习后端知识,向全栈发展,或专注于前端架构、性能优化等领域。
-
短期目标(1-2年) :
- 深入掌握当前使用的前端框架(如 React 或 Vue),提升开发技能。
- 学习 TypeScript、GraphQL 等新技术,增强技术栈的广度。
- 参与开源项目,贡献代码,提升自己的影响力和技术认可度。
-
中期目标(3-5年) :
- 担任技术领导或架构师角色,负责技术方向和架构设计。
- 深入了解前端性能优化和用户体验设计,提升产品质量。
- 可能考虑转型为全栈开发,拓宽自己的职业发展空间。
-
长期目标(5年以上) :
- 成为行业内的技术专家,分享经验,撰写技术文章或演讲。
- 参与技术社区或开设技术课程,帮助其他开发者成长。
- 如果有兴趣,探索创业机会,建立自己的产品或公司。
-
八、准备建议
-
针对性复习:根据目标公司 JD(Job Description)调整重点(如 React/Vue 偏向)。
-
实战演练:
- 刷题平台:LeetCode(算法)、Codewars(JavaScript)。
- 模拟面试:使用 Pramp 或找同行模拟。
-
项目复盘:梳理过往项目,用 STAR 法则(情境、任务、行动、结果)总结亮点。
-
资源推荐:
- 书籍:《JavaScript 高级程序设计》《React 设计原理》。
- 文档:MDN、React/Vue 官方文档。
- 博客:掘金、Medium 上的技术文章。