前端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 375月27日 01:03

什么是 XSS 攻击?

XSS(跨站脚本攻击)的本质是攻击者把自己的 JS 代码注入到你的网页里,在真实用户的浏览器上执行。能做的坏事包括:偷 cookie、篡改页面内容、劫持用户操作。三种类型:存储型:恶意脚本存到了服务器(数据库、评论、留言),用户访问页面时被返回执行。危害最大,不需要诱导点击。反射型:恶意脚本放在 URL 参数里,服务器直接拼到响应页面中。需要诱导用户点击构造好的链接。DOM 型:纯客户端问题——前端 JS 把 URL 参数或用户输入直接用 innerHTML 插到 DOM 里,没有经过后端。防御三件套:输出转义:所有用户输入在输出到 HTML 前做转义(< → < 等),不信任任何用户数据CSP(内容安全策略):HTTP 头告诉浏览器只允许执行特定来源的脚本HttpOnly Cookie:关键 cookie 标记 HttpOnly,JS 读不到,即使被 XSS 也偷不走追问XSS 和 CSRF 有什么区别?XSS 是在受害者浏览器里执行恶意 JS,利用的是对网站的信任。CSRF 是诱导受害者发请求到已登录的网站,利用的是网站对浏览器的信任。CSP 怎么配置?Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'。核心思想:白名单机制,不在白名单里的脚本一律不执行。但 CSP 配置需要充分测试,容易把正常功能挡掉。富文本编辑器怎么防 XSS?不能用简单的转义(会把格式也干掉)。正确做法:白名单过滤标签和属性(如 DOMPurify),只允许 <b>、<i>、<a> 等安全的标签,移除 onclick、onerror 等事件处理器和 javascript: 协议。
前端阅读 175月27日 00:59

Vite 为什么启动这么快?

Vite 之所以快,核心就两点:开发阶段不打包 + 按需编译。传统打包工具(Webpack 等)在启动时会把整个应用打包一次,项目越大越慢。Vite 反过来,利用浏览器原生支持的 ES Module,让浏览器按需请求模块——只有被用到的文件才会被编译。加上 Vite 用 esbuild(Go 写的)做依赖预构建,速度比 JS 打包器快几十倍。HMR 方面也是模块级别的:改了哪个文件,只重新编译那个文件和依赖它的链,不是整体刷新。一句话:传统打包工具是把整个项目"煮好"再上菜,Vite 是你点什么它现做——启动当然快。追问Vite 生产环境还是用 Rollup 打包的,那开发快有什么用?开发体验直接影响效率。你一天可能启动几十次 dev server、改几百次代码,每次快几秒加起来就是大量时间。生产打包只在 CI 里跑一次,慢点无所谓。而且 Vite 生产构建用的是 Rollup,本身也比 Webpack 快。esbuild 这么快,为什么生产环境不用它打包?esbuild 快是因为它功能少——不支持 AST 操作、不支持插件生态里那些复杂转换。Vite 用 esbuild 做依赖预构建(只需要转译),用 Rollup 做生产打包(需要 Tree-Shaking、代码分割、插件等),各取所长。实际项目从 Webpack 迁移到 Vite 遇到过什么坑?CommonJS 依赖可能不兼容,需要配置 optimizeDeps.includerequire.context 这种 Webpack 特有的 API 不能用了CSS Modules 的导入方式有差异(.module.css 后缀强制要求)环境变量从 process.env.X 变成 import.meta.env.X为什么 Vite 不用 webpack 的 bundleless 方案(如 Snowpack)?Snowpack 已经停止维护了。Vite 和 Snowpack 思路类似,但 Vite 胜在:依赖预构建(esbuild)、开箱即用的 HMR、更完善的插件体系(兼容 Rollup 插件)。生态和社区活跃度完全不在一个量级。
前端阅读 255月27日 00:54

fetch 和 XHR 有什么区别?

fetch 和 XHR 都能在浏览器发 HTTP 请求,但 fetch 是现代 API,基于 Promise,语法更简洁。XHR 是回调时代的设计,API 复杂但老浏览器兼容性好。关键区别:fetch 默认不携带 cookie(需要 credentials: 'include'),XHR 默认携带。fetch 只有网络错误才 reject,HTTP 404/500 算成功需要手动判断 response.ok,XHR 靠 onerror 和 status 判断。fetch 可以中断(AbortController),XHR 靠 xhr.abort()。fetch 支持 ReadableStream 流式读取,XHR 可以监听 progress 事件——下载进度条场景 XHR 反而更方便。另外 fetch 不支持超时设置(要配合 AbortController 模拟),也没有原生的上传进度回调。Service Worker 里只能用 fetch,XHR 不可用。追问什么场景下 XHR 比 fetch 更好?需要上传/下载进度条时,XHR 有 progress 事件,fetch 没有原生的。需要超时控制时 XHR 有 timeout 属性。老浏览器兼容也选 XHR。fetch 怎么取消请求?AbortController:controller.abort() 发出取消信号,fetch 收到后抛出 AbortError。注意这只是前端取消,服务端可能还在处理。fetch 怎么带 cookie?fetch(url, { credentials: 'include' })。跨域时服务端还要设 Access-Control-Allow-Credentials: true 且 origin 不能用 *。
前端阅读 245月27日 00:54

html meta 标签如何把 HTTP 换成 HTTPS?

meta 标签本身不能切换 HTTP 到 HTTPS,HTTPS 必须在服务器端配置。但 meta 标签可以辅助:用 <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> 让浏览器自动把页面里的 HTTP 资源请求升级成 HTTPS,避免混合内容警告。服务器端配置 HTTPS 需要三步:申请 SSL/TLS 证书(Let's Encrypt 免费)、在 Nginx/Apache 配置证书和私钥、强制 HTTP 跳转 HTTPS(301 重定向或 HSTS)。HSTS(HTTP Strict Transport Security)比 301 更安全——浏览器记住后,下次访问直接走 HTTPS,不会先发 HTTP 请求。设置方式:Strict-Transport-Security: max-age=31536000; includeSubDomains。注意一旦启用 HSTS 就很难回退。追问upgrade-insecure-requests 和 HSTS 有什么区别?前者是 CSP 指令,浏览器自动升级页面内资源的请求;后者是 HTTP 响应头,让浏览器记住该域名只能走 HTTPS。两者配合使用效果最好。301 重定向和 HSTS 选哪个?都要。301 处理首次访问,HSTS 让回访用户不再发 HTTP 请求。只用 301 的话,第一次 HTTP 请求已经发出去了,中间人可以劫持。混合内容警告怎么处理?页面 HTTPS 但加载了 HTTP 图片/脚本。方案:upgrade-insecure-requests 自动升级,或者代码里统一用 // 协议相对 URL。
前端阅读 02月7日 16:44

说一下 splice 和 slice 的功能用法

splice() 和 slice() 都是 JavaScript 中用来处理数组的方法,但它们的功能和用法有所不同。splice()splice() 方法通过删除或替换现有元素或在数组中添加新元素来改变数组的内容。其基本语法如下:array.splice(start[, deleteCount[, item1[, item2[, ...]]]])start: 指定修改的开始位置(数组索引)。deleteCount: (可选)整数,表示要从数组中删除的元素数量。item1, item2, …: (可选)要添加进数组的新元素。示例:let myArray = ['a', 'b', 'c', 'd'];myArray.splice(1, 2, 'x', 'y'); // 从索引1开始删除2个元素,并添加'x'和'y'console.log(myArray); // 输出: ['a', 'x', 'y', 'd']slice()slice() 方法则返回一个新的数组,包含从开始到结束(不包括结束)选择的数组的一部分。原始数组不会被修改。其基本语法如下:array.slice(begin[, end])begin: 提取起始处的索引(从该索引开始提取元素)。end: (可选)提取结束处的索引(到该索引之前的元素会被提取)。示例:let myArray = ['a', 'b', 'c', 'd'];let newArray = myArray.slice(1, 3); // 提取从索引1到索引2的元素console.log(newArray); // 输出: ['b', 'c']console.log(myArray); // 原数组不变,输出: ['a', 'b', 'c', 'd']总结来说,splice() 是一个可以在任何位置添加或删除元素的方法,这会改变原数组,而 slice() 用于创建一个新的数组,包含原数组的一部分,原数组不会改变。
前端阅读 02月7日 16:36

介绍 AST(Abstract Syntax Tree)抽象语法树

AST(抽象语法树)是源代码的抽象符号和语法结构的树状表示。它是编译器设计中的一个重要概念,用于表示编程语言中的程序结构,而不包括其实际的语法细节(如括号和语法糖)。在解析阶段,编译器会读取源代码,进行词法分析生成令牌(Token),然后这些令牌会被进一步分析并构造成AST。每个节点代表程序中的一个构造,如表达式、声明或控制流语句。AST使得编译器能够执行更多的分析和优化任务,例如类型检查、作用域解析、内存分配以及代码生成等。此外,AST也被用于静态代码分析工具中,以帮助开发者找到代码中的错误或进行代码复杂度分析。例如,对于简单的数学表达式 3 + 4 * 5 的AST,根节点可能是一个加法表达式,它有两个子节点:左子节点是数字 3,右子节点是乘法表达式,乘法表达式又有两个子节点,分别是数字 4 和 5。
前端阅读 02月7日 16:36

详细介绍 babel 的工作流程

Babel 的工作流程主要包括以下几个步骤:解析(Parsing):Babel 首先将输入的 JavaScript 代码转换成一个抽象语法树(AST)。这一过程分为两个主要阶段:词法分析(将代码字符串拆解成有意义的代码块,称为 tokens)和语法分析(将 tokens 转换成表示程序结构的 AST)。转换(Transforming):转换阶段是 Babel 工作流程的核心。在这个阶段,Babel 使用各种插件来处理 AST。插件可以访问、分析、替换、添加或删除 AST 的节点。常见的转换包括语法扩展(如 JSX、TypeScript 转换为 JavaScript)、ES6+ 代码转换为向后兼容的 ES5 代码等。生成(Code Generation):经过转换的 AST 然后被转换回 JavaScript 代码。此过程包括根据 AST 的结构重新构造代码,同时还可能包括源代码映射(source maps)的生成,用于调试目的。输出(Output):最终生成的 JavaScript 代码作为 Babel 的输出。这段代码已经被转换,可以在旧版浏览器和环境中运行,而无需担心兼容性问题。通过这些步骤,Babel 允许开发者使用最新的 JavaScript 语言特性,而不必担心目标环境是否支持这些新特性。
前端阅读 02月7日 13:47

JS 数组有哪些方法? 讲讲它们的区别跟使用场景

JavaScript数组作为核心数据结构,掌握其方法能显著提升代码效率和可维护性。本文将系统分析常用数组方法,深入探讨它们的区别、适用场景及最佳实践,帮助开发者写出更简洁、高性能的代码。所有方法基于ECMAScript标准,重点聚焦于函数式方法,避免常见陷阱。对于更详细的信息,可参考MDN文档。常见数组方法分类数组方法可大致分为以下几类,每类服务于特定需求:迭代方法:用于遍历和转换数组,如 map、filter、reduce、forEach,适合声明式编程。变更方法:直接修改原数组,如 push、pop、shift、unshift、splice、sort,适用于栈操作或原地修改。生成方法:创建新数组或字符串,如 slice、concat、join,常用于数据处理。其他方法:如 fill、from、includes、indexOf,提供额外功能。 关键提示:函数式方法(如 map、filter)返回新数组,不修改原数组,而变更方法(如 push)直接操作原数组。选择时需权衡性能和可读性。迭代方法详解迭代方法是数组处理的核心,强调纯函数特性,避免副作用。map作用:创建新数组,其中每个元素是调用回调函数的结果。不修改原数组。参数:回调函数(item, index, array)使用场景:数据转换,如数字列表转字符串、计算倍数。避免在回调中修改原数组,保持函数式纯度。代码示例:const numbers = [1, 2, 3];const doubled = numbers.map(num => num * 2);console.log(doubled); // [2, 4, 6]// 原数组未被修改console.log(numbers); // [1, 2, 3]filter作用:创建新数组,包含通过测试的元素。不修改原数组。参数:回调函数(item, index, array)使用场景:数据过滤,如筛选偶数、有效对象。与map对比:map转换所有元素,filter仅保留满足条件的元素。代码示例:const numbers = [1, 2, 3, 4];const evens = numbers.filter(num => num % 2 === 0);console.log(evens); // [2, 4]// 原数组未被修改console.log(numbers); // [1, 2, 3, 4]reduce作用:将数组元素归约成单个值(如总和、最大值)。不修改原数组。参数:回调函数(accumulator, currentValue, index, array),初始值可指定(如 0)。使用场景:聚合计算、链式操作。性能提示:对大型数组,避免嵌套循环,reduce 更高效。代码示例:const numbers = [1, 2, 3, 4];const sum = numbers.reduce((acc, num) => acc + num, 0);console.log(sum); // 10const max = numbers.reduce((acc, num) => Math.max(acc, num), -Infinity);console.log(max); // 4forEach作用:对数组每个元素执行回调,但不返回新数组。不修改原数组。参数:回调函数(item, index, array)使用场景:遍历操作,如DOM修改。避免使用:因无返回值,不适合链式操作,仅用于副作用。代码示例:const items = ['a', 'b', 'c'];items.forEach(item => { console.log(`Item: ${item}`);});// 输出: Item: a// Item: b// Item: c// 原数组未被修改console.log(items); // ['a', 'b', 'c']变更方法详解变更方法直接修改原数组,适用于原地操作,但可能破坏函数式纯度。push/pop作用:push添加元素到末尾,pop移除末尾元素(栈操作)。参数:push接收多个值;pop无参数。使用场景:栈实现、队列操作。性能提示:对于频繁操作,避免在循环中使用,考虑slice等替代方案。代码示例:const stack = [];stack.push('item1', 'item2');console.log(stack); // ['item1', 'item2']const last = stack.pop();console.log(last); // 'item2'console.log(stack); // ['item1']splice作用:插入、删除或替换数组元素,返回被移除的元素。参数:start索引,deleteCount,items(可选)。使用场景:动态数组修改。注意事项:修改原数组,可能导致意外副作用。代码示例:const arr = [1, 2, 3, 4];const removed = arr.splice(1, 2, 'a', 'b');console.log(removed); // [2, 3]console.log(arr); // [1, 'a', 'b', 4]sort作用:对数组元素排序,默认按字符串规则(需显式指定比较函数)。参数:可选比较函数(a, b)。使用场景:数据排序。性能提示:对大型数组,使用Array.prototype.sort可能慢,优先使用Array.from和稳定排序。代码示例:const nums = [3, 1, 4, 2];nums.sort((a, b) => a - b);console.log(nums); // [1, 2, 3, 4]// 对字符串排序const names = ['Alice', 'Bob', 'Charlie'];console.log(names.sort()); // ['Alice', 'Bob', 'Charlie']生成方法详解生成方法返回新数组或字符串,不修改原数组,适合数据处理。slice作用:返回新数组,包含从start到end(不含)的元素。参数:start(索引,负值表示倒数),end(可选,索引)。使用场景:复制数组片段、避免原地修改。关键区别:slice vs splice——slice不修改原数组,splice会修改。代码示例:const arr = [1, 2, 3, 4];const sub = arr.slice(1, 3);console.log(sub); // [2, 3]console.log(arr); // [1, 2, 3, 4] // 原数组未被修改concat作用:连接多个数组或值,返回新数组。参数:一个或多个数组/值。使用场景:合并数组、拼接数据。性能提示:对大型数组,避免嵌套concat,使用[...arr1, ...arr2]更高效。代码示例:const arr1 = [1, 2];const arr2 = [3, 4];const merged = arr1.concat(arr2);console.log(merged); // [1, 2, 3, 4]join作用:将数组元素连接成字符串,用指定分隔符。参数:分隔符(默认',')。使用场景:生成字符串、日志输出。注意事项:对大型数组,可能产生内存问题,避免过度使用。代码示例:const fruits = ['apple', 'banana', 'cherry'];const str = fruits.join(', ');console.log(str); // 'apple, banana, cherry'方法选择指南掌握方法区别后,需根据场景选择最优方案:map vs filter:map用于转换所有元素(如[1,2,3] → [2,4,6]),filter用于过滤(如[1,2,3,4] → [2,4])。选择建议:数据转换用map,数据筛选用filter。避免副作用:forEach适合遍历副作用(如DOM操作),但不适合链式操作;map和filter返回新数组,适合纯函数式代码。性能优化:对大型数组,优先使用slice(不修改原数组)而非splice(修改原数组)。计算聚合时,reduce 比 for 循环更高效且可读。避免在循环中使用push,改用array.map().push() 或 array.concat()。安全实践:始终优先使用函数式方法(map、filter),避免for循环,提升代码可测试性。对原地操作(如splice),确保数据副本,防止意外副作用。 实践建议:在开发中,使用console.log验证数组行为,例如:结论JavaScript数组方法是前端开发的核心工具。本文系统分析了关键方法的区别与使用场景,强调函数式方法的优势(如map、filter)和变更方法的适用性。最佳实践:优先使用声明式方法,避免副作用;性能敏感场景,选择高效操作;持续学习新特性(如Array.from和Array.of)。掌握这些方法,能显著提升代码质量,使开发更高效、可维护。记住:数组方法的正确选择是性能优化和代码健壮性的关键。 延伸阅读:在现代JavaScript中,数组方法与迭代器结合,可实现更高级的流式处理。例如,使用Array.from转换可迭代对象:​
前端阅读 3002024年8月9日 17:42

JavaScript 原型中 prototype 和proto区别是什么?

在JavaScript中,prototype属性和__proto__属性(通常读作"proto")是有关于对象原型链的概念,但它们在使用和目的上有所不同。prototype属性prototype是函数对象(Function objects)的一个属性。当你使用构造函数创建一个新对象时,这个新对象的内部[[Prototype]](也就是它的__proto__属性)会被赋值为构造函数的prototype属性。这意味着,使用同一个构造函数创建的所有对象都会共享同一个prototype对象。举个例子,如果我们有一个构造函数:function Person(name) { this.name = name;}Person.prototype.sayHello = function() { console.log(`Hello, my name is ${this.name}`);};当我们创建一个Person实例时:var person1 = new Person("Alice");person1对象的[[Prototype]](即__proto__)会指向Person.prototype,这使得person1能够访问到sayHello方法。__proto__属性__proto__是每个JavaScript对象都拥有的一个内部属性,它指向该对象的原型。这是一个从对象指向其构造函数的prototype属性的链接。根据ECMAScript标准,__proto__是[[Prototype]]的实现,而[[Prototype]]是对象的内部属性。在现代JavaScript开发中,通常推荐使用Object.getPrototypeOf(obj)来获取对象的原型,而不是直接使用__proto__,因为__proto__并不是所有JavaScript环境中都得到支持。再次拿刚才的例子,person1.__proto__会指向Person.prototype,因为person1是由Person构造函数创建的。小结prototype是函数特有的属性,用于当作构造函数时为实例对象指定原型。__proto__是每个对象都有的属性,指向该对象的原型。在实践中,prototype用来实现基于原型的继承和共享属性/方法,而__proto__提供了一种访问和操作对象原型链的方式。然而,直接操作__proto__被视为不太安全的做法,尤其是在现代JavaScript编程中,应该利用Object.getPrototypeOf()和Object.setPrototypeOf()等方法来替代__proto__的直接使用。
前端阅读 1662024年8月5日 12:52

script 标签的 defer 和 async 有什么区别?

当您在 HTML 文档中使用 <script> 标签引入 JavaScript 时,defer 和 async 属性可以控制脚本的加载和执行方式,它们之间的区别主要在于脚本加载的时间以及执行的时机。defer 属性使用 defer 属性的 <script> 标签会让脚本在文档解析期间异步下载,但是会延迟到整个文档解析完毕之后、DOMContentLoaded 事件触发之前执行。这意味着带有 defer 的脚本总是在文档解析完成之后执行,保证了执行时 DOM 已经完全构建好。例子:<script src="example.js" defer></script>如果您有多个带有 defer 属性的脚本,它们将按照在文档中出现的顺序执行,即便有些脚本可能会比其他脚本更早下载完成。async 属性而 async 属性也允许脚本在文档解析时异步下载,但是它一旦下载完成就会立即执行,这可能会在文档的其余部分尚未解析完毕时发生。因此,使用 async 的脚本不能保证按照在页面中出现的顺序执行,也无法保证 DOM 完全构建完成。例子:<script src="example.js" async></script>async 适用于那些不依赖于其他脚本且不依赖于 DOM 的脚本,例如,广告加载或者埋点脚本。总结defer 确保脚本在文档完全解析和 DOM 构建完成后,但在 DOMContentLoaded 事件之前执行。async 确保脚本在下载完成后尽快执行,但可能会打断文档的解析过程。没有这两个属性的 <script> 标签会立即下载并阻塞文档解析直到脚本执行完成。在实际应用中,选择 defer 或 async 取决于脚本对文档解析的依赖性,以及脚本之间的依赖关系。如果您需要确保脚本按照顺序执行,并且在 DOM 完全构建后执行,那么 defer 是更好的选择。如果脚本的执行顺序不重要,并且想尽快获取并执行脚本,可以使用 async。
前端阅读 1622024年8月5日 12:52

javascript 中垃圾回收的方法有哪些?

JavaScript中的垃圾回收(garbage collection)是一种自动内存管理机制,它帮助开发者不需要手动释放分配的内存。在JavaScript中,垃圾回收主要采用了以下几种方法:1. 标记清除(Mark and Sweep)这是最常见的垃圾回收算法。当变量进入环境时,就“标记”这个变量为“进入环境”。当变量离开环境时,则“标记”这个变量为“离开环境”。垃圾收集器会定期运行,它会检查所有的变量,以及它们引用的其他变量是否还在环境中。如果一个变量已经不再环境中,且没有任何其他变量引用它,那么这个变量占用的内存就会被回收。例子:function processData() { var data = { /* 大量数据 */ }; // 使用data进行处理}processData();// processData执行完毕后,data变量离开环境,变成无法访问的状态,会被标记为可回收。2. 引用计数(Reference Counting)引用计数是另一种垃圾回收机制。在这个系统中,每一个值都有一个“引用数”,表示有多少变量或资源引用这个值。如果引用数变为0,则表示该值不再需要,其占用的内存可以被回收。这种方法的一个问题是循环引用:如果两个对象互相引用,即便它们已经不再需要,它们的引用数也不会降到0,导致内存无法被回收。例子:function referenceCycle() { var objectA = {}; var objectB = {}; objectA.other = objectB; objectB.other = objectA;}referenceCycle();// 即使referenceCycle函数执行结束,objectA和objectB因为相互引用,它们的引用数都不为0,造成内存泄漏。3. 分代收集(Generational Collection)分代收集是基于对象存活时间的假设,将对象分为两组:“新生代”和“老生代”。新创建的对象属于新生代,对象如果存活足够长的时间,就会被移动到老生代。通常新生代使用标记-复制(mark-copy)算法,老生代使用标记-清除(mark-sweep)或标记-整理(mark-compact)算法。4. 标记-整理(Mark-Compact)这种方法是对标记-清除的改进。在标记阶段,标记所有活动对象,然后在整理阶段,将所有活动的对象移动到内存的一端,然后清理掉边界之外的内存。5. 增量收集(Incremental Collection)增量收集是将垃圾回收分成小片段执行,每次只处理一部分对象,然后暂停,让程序执行。这种方式可以减少垃圾收集过程中的停顿时间。6. 闲时收集(Idle-time Collection)某些JavaScript引擎会利用CPU空闲时间来执行垃圾回收的工作,以避免影响到程序的执行效率。
前端阅读 3302024年8月5日 12:52

React 如何做性能优化?有哪些常见手段?

React 在性能优化方面提供了多种策略和工具,以确保用户界面高效、平滑且响应迅速。以下是一些常用的性能优化手段:1. 使用 shouldComponentUpdate 和 React.PureComponent在类组件中,通过实现 shouldComponentUpdate 方法可以控制组件是否需要更新。当组件的状态或属性改变时,此方法会被调用,并根据返回的布尔值决定是否进行渲染。javascriptshouldComponentUpdate(nextProps, nextState) { // 只有当特定的属性或状态改变时才更新组件 return nextProps.id !== this.props.id || nextState.count !== this.state.count;}对于那些拥有不可变的属性和状态的组件,可以使用 React.PureComponent,它通过浅比较 props 和 state 来减少不必要的渲染。2. 使用 Hooks(如 React.memo 和 useMemo)对于函数组件,React.memo 是一个高阶组件,它仅在组件的 props 发生变化时才会重新渲染组件。javascriptconst MyComponent = React.memo(function MyComponent(props) { /* 只有props改变时,组件才会重新渲染 */});useMemo 和 useCallback 钩子可以用来缓存复杂计算的结果和回调函数,避免在每次渲染时都重新计算和创建新的函数实例。javascriptconst memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);const memoizedCallback = useCallback(() => { // 一个依赖特定props的回调函数}, [props]);3. 避免不必要的 DOM 更新当操作 DOM 时,应尽量减少更新次数和范围。可以使用虚拟列表(比如 react-window 或 react-virtualized)来仅渲染可视区域的元素,从而提高长列表的性能。4. 懒加载组件和路由使用 React.lazy 可以实现组件级别的代码拆分,这样可以将不同的组件打包成单独的代码块,并在需要时才加载它们。同时,结合 React Router 的 Suspense 组件,可以实现路由级别的懒加载,仅当路由被访问时才加载对应的组件。javascriptconst OtherComponent = React.lazy(() => import('./OtherComponent'));function MyComponent() { return ( React.Suspense fallback={div>Loading.../div>}> OtherComponent /> /React.Suspense> );}5. 使用 Web Workers对于复杂或计算密集型任务,可以使用 Web Workers 在后台线程中执行,避免阻塞主线程导致用户界面卡顿。6. 优化条件渲染避免不必要的渲染,例如,可以将条件渲染逻辑移到可能更改状态的事件处理函数中,而不是在渲染方法中进行。7. 状态升级将子组件的本地状态提升到父组件中,这样可以减少不必要的子组件渲染,因为状态的变化会集中处理。8. 使用不可变数据结构使用不可变数据可以更容易地检测到状态和属性的变化,这使得组件的更新检查更高效。库如 Immutable.js 可以用来帮助创建不可变数据。9. 使用生产版本的 React开发中通常使用的是开发版本的 React,它包含了许多有用的警告和错误信息。但在生产中,应该使用经过压缩和优化的生产版本,它删除了这些额外的警告和检查,以减少库的大小并提升性能。10. 分析和监控使用性能分析工具,如 React DevTools 中的 Profiler,可以帮助识别渲染性能瓶颈。它可以记录组件的渲染时间,并帮助你找到可以优化的部分。11. 避免内联对象和数组的传递对于那些接收对象或数组作为 props 的组件,应避免在渲染方法中直接创建新的内联对象或数组,因为这会导致 props 始终不相等,从而触发不必要的渲染。<MyComponent items={[1, 2, 3]} /> // 每次渲染都会创建一个新的数组,不推荐这样做// 更好的做法是在组件外部定义这个数组const items = [1, 2, 3];<MyComponent items={items} />12. 使用键(keys)来优化列表渲染当渲染列表时,应该为每个列表项指定一个唯一的 key。这有助于 React 确定哪些项已更改、添加或删除,从而提高列表渲染的效率。data.map((item) => <ListItem key={item.id} {...item} />)13. 使用 Context 时的优化当使用 React Context API 时,应该注意其可能对性能的影响。Context 的变动会导致所有消费该 Context 的组件重新渲染。为了避免不必要的渲染,可以分割 Context 或是使用 useMemo 跟 useCallback 来传递稳定的上下文值。14. 避免过度渲染和过度传递 props审视组件间的 props 传递,确保不会传递额外的 props。如果一个组件不需要某个 prop,那么就不应该传递它,因为这可能会导致不必要的组件渲染。15. 服务器端渲染 (SSR)服务器端渲染可以加快首次页面加载时间,并提升搜索引擎优化(SEO)。通过在服务器上生成 HTML,可以减少客户端的工作量,从而提升性能。实施示例假设我们有一个用户列表组件,其中包含大量用户数据。我们可以应用以下优化:使用 React.memo 封装用户列表项组件,仅在 props 变化时重新渲染。通过 useMemo 缓存用户列表计算,避免在每次渲染时重新计算。使用虚拟列表库,如 react-window,仅渲染可视区域内的用户,以优化长列表性能。如果用户列表是通过路由导航到达的,可以使用 React.lazy 和 Suspense 实现路由懒加载。通过 React DevTools 的 Profiler 分析用户列表的渲染性能,找出任何潜在的性能瓶颈进行优化。通过应用这些优化技巧,我们可以显著提高大型 React 应用的性能和用户体验。
前端阅读 1572024年8月5日 12:52

什么是闭包?什么场景需要使用闭包?

什么是闭包?在计算机科学中,闭包(Closure)是指一个函数绑定了其外部作用域的变量,因此这个函数可以在其定义环境之外被调用时仍能访问到那些绑定的变量。简单来说,闭包让你可以从一个函数内部访问到其外部函数作用域的变量。闭包的特点是:函数嵌套:通常闭包包含一个函数内定义的另一个函数。环境捕获:内部函数会捕获定义它的外部函数的作用域中的变量。作用域链:内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。在 JavaScript 中,闭包是一种非常常见和强大的特性,因为 JavaScript 是词法作用域的语言,函数的作用域在函数定义时就已经确定了。什么场景需要使用闭包?闭包通常用于以下几种场景:数据封装和私有化:使用闭包可以创建私有变量,这些变量只能被特定的函数访问和修改,从而模拟出类似私有属性的效果。这在模块模式中尤其常见。例子:一个简单的计数器函数,利用闭包可以隐藏计数器的值,只能通过特定的函数来操作。 function createCounter() { let count = 0; return { increment: function() { count += 1; }, decrement: function() { count -= 1; }, getCount: function() { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.getCount()); // 输出 1回调函数:在异步编程中,闭包常用于回调函数中,以确保异步操作完成时能够访问到定义回调时的环境状态。例子:在一个异步请求中使用闭包记住请求开始时的状态。 function fetchData(url, callback) { // 假设这里发起了一个异步请求 setTimeout(() => { // 模拟异步操作 const data = 'fetched data'; // 假设这是响应数据 callback(data); }, 1000); } function requestData() { const requestTimestamp = Date.now(); fetchData('https://example.com/data', function(data) { console.log(`Request took ${Date.now() - requestTimestamp} ms`); console.log(`Data received: ${data}`); }); } requestData();函数工厂:闭包可以用来创建可以记住和操作环境状态的函数,这些函数根据不同的参数创建出来,具有不同的行为。例子:根据不同的倍数创建乘法函数。 function createMultiplier(multiplier) { return function(x) { return x * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 输出 10 console.log(triple(5)); // 输出 15节流和防抖:在 JavaScript 的 DOM 事件处理中,为了优化性能,防止过多的事件处理函数被频繁触发,会使用闭包来实现函数节流(throttle)和防抖(debounce)。例子:使用防抖确保事件处理函数在特定时间内只执行一次。
计算机基础阅读 1152024年8月5日 12:51

HTTP 协议 1.0 和 1.1 和 2.0 有什么区别?

HTTP(超文本传输协议)是 Web 上交换数据的基础协议,随着 Web 技术的发展,HTTP 也经历了多个版本的迭代。下面我会详细介绍 HTTP 1.0、1.1 和 2.0 这三个版本的区别:HTTP 1.0无状态连接:HTTP 1.0 是无状态的,也就是说每次请求都需要建立一个新的TCP连接,完成数据传输后连接就会关闭。这种方式在每次请求都需要经历 TCP 连接的建立和断开过程,导致性能上的不足。限制性能:由于每次请求都要建立新的连接,所以并发多个请求会导致大量的延迟和性能问题。无宿主名(Host)字段:HTTP 1.0 不支持 Host 头部。这意味着同一个物理服务器上无法托管多个域名的网站。HTTP 1.1持久连接:HTTP 1.1 默认采用持久连接(也称为“keep-alive”),允许在一个TCP连接上发送和接收多个HTTP请求/响应,从而减少了TCP连接的开销。管线化:HTTP 1.1 引入了请求的管线化,理论上客户端可以在收到前一个响应之前发送下一个请求,减少了请求的延迟。但实际上,由于某些浏览器和服务器的实现问题,这个特性并未广泛使用。新增头部字段:例如 Host(它允许在同一物理服务器上虚拟托管多个域名)、Etag(实体标签,可以协助缓存验证)、Accept-Encoding(指定客户端可以接收的内容编码类型)等。缓存控制:更复杂和灵活的缓存控制机制,使得客户端和服务器可以更有效地协商数据的缓存,减少不必要的数据传输。分块传输编码:允许服务器开始发送响应而不需要先知道全部内容的总大小。HTTP 2.0二进制协议:HTTP/1.x 是文本协议,而 HTTP/2 是二进制协议,提供了更高效的解析和网络传输。多路复用:在同一个连接上并行交错地发送多个请求和响应,而不会互相影响,极大地提高了传输效率和减少了延迟。流优先级:可以为 HTTP/2 连接上的流设置优先级,允许更重要的资源先被发送。服务器推送:服务器可以对一个客户端请求发送多个响应,允许服务器主动推送资源给客户端,进一步提升页面加载效率。头部压缩:HTTP/2 引入了 HPACK 压缩格式,用于减小头部大小,以减少传输延迟。举例来说,一个明显的性能改进是在使用HTTP/2时浏览一个网站:由于多路复用和头部压缩等特性,相比于 HTTP/1.1,网页的加载时间可以显著减少,尤其是在网络条件较差或加载资源较多的场景下。此外,HTTP/2 的服务器推送功能允许服务器预先推送静态资源,比如 CSS 或 JavaScript 文件,这可以进一步提高加载速度,因为浏览器不必等待解析 HTML 再去请求这些资源。
前端阅读 1432024年8月5日 12:50

浏览器有哪些缓存策略?

浏览器缓存策略主要是用于提高网页加载速度,减少服务器压力以及节省带宽。以下是几种主要的浏览器缓存策略:强缓存(Strong Cache)Expires:这是HTTP/1.0中使用的头信息,用来指定资源到期的时间。如果请求的时间小于Expires的时间,浏览器会直接使用缓存中的资源,而不会向服务器发起请求。Cache-Control:在HTTP/1.1中引入,比Expires更灵活。常用的指令包括max-age(资源最大有效时间)、no-cache(每次都要向服务器确认)、no-store(完全不缓存),等等。若设置了max-age,且缓存时间未过期,则浏览器会直接使用本地缓存。协商缓存(Negotiation Cache)Last-Modified和If-Modified-Since:服务器在响应中加入Last-Modified标头指明资源最后修改时间,浏览器再请求时通过If-Modified-Since将这个值发送给服务器,由服务器判断资源是否有更新。ETag和If-None-Match:ETag是资源的唯一标识符,当资源有变动时ETag也会变。浏览器存储资源的ETag,并在下次请求时通过If-None-Match发送给服务器,以检查资源是否有更新。若在协商缓存中服务器确认内容没有更新,则服务器会返回304状态码,浏览器就会使用本地缓存;如果内容更新了,则会返回200状态码和新的资源内容。预缓存(Pre-Caching)Service Workers:通过Service Workers可以拦截网络请求,并动态地缓存或者恢复资源。这允许创建有效的离线体验,并且可以精细控制缓存策略。内存和硬盘缓存浏览器通常将资源缓存在内存或硬盘中:内存缓存:缓存存储在内存中,访问速度快,但只在浏览器会话期间有效。硬盘缓存:缓存存储在硬盘上,访问速度慢一些,但即使关闭浏览器后依然可以使用。举个例子,假设您访问了一个前端的网站,网站的CSS文件设置了强缓存,Cache-Control设置为max-age=3600,这意味着在接下来的一个小时内,如果您再次访问该网站,浏览器就会直接使用本地缓存的CSS文件,而不需要再次请求服务器,这样就能加快页面的加载速度。而对于网站的新闻部分,可能会使用协商缓存,每次访问时通过ETag或者Last-Modified信息检查内容是否有更新,以确保用户总是看到最新的内容,同时在内容没有更新的情况下减少不必要的资源传输。
前端阅读 1712024年8月5日 12:50

Web 端应用如何做移动的适配

为了确保Web应用能够在移动设备上良好运行,我们需要关注几个关键点:1. 响应式设计(Responsive Design)响应式设计是适配移动端的核心。通过使用媒体查询(Media Queries)和相对单位(如百分比,em,rem等),我们可以确保网页布局和元素能够根据不同设备屏幕尺寸和分辨率自动调整。例如,Bootstrap框架提供了一系列预定义的响应式类,可以帮助开发者更快地实现响应式设计。例子:@media screen and (max-width: 768px) { .container { width: 100%; }}上面的媒体查询表明,当屏幕宽度小于或等于768像素时,.container 类的宽度将被设置为100%。2. 触控优化(Touch Optimization)移动设备通常是通过触控进行操作的,因此需要确保所有的交互元素(如按钮、链接、表单控件等)都足够大,以便手指点击,并有足够的间距,以防误触。HTML5为表单元素提供了适用于移动端的类型,如 <input type="email">,这会调用适合输入电子邮件的虚拟键盘。例子:<button class="touch-optimized-button">Submit</button>.touch-optimized-button { padding: 15px; margin: 10px; font-size: 18px;}3. 视口配置(Viewport Configuration)通过设置 <meta>标签中的 viewport属性,我们可以控制视口的尺寸和比例。这告诉浏览器如何控制页面的尺寸和比例,以匹配不同设备的屏幕。例子:<meta name="viewport" content="width=device-width, initial-scale=1.0">4. 性能优化(Performance Optimization)移动设备相比于桌面设备,其网络连接可能更不稳定,处理能力可能更弱。因此,要特别注意减少代码体积、图片大小,以及优化加载时间。使用压缩工具、图片懒加载、缓存策略等,都是性能优化的常见做法。例子:使用WebP格式的图片,它比传统的JPEG或PNG格式具有更好的压缩率,可以更快地加载。5. 使用框架和工具(Frameworks and Tools)现代前端框架和库,如React、Vue、Angular等,通常都有考虑到移动设备的适配。使用这些框架和它们的UI组件库(如Material-UI, Vuetify等),可以更快地开发出适合移动设备的界面。例子:import { Button } from '@material-ui/core';<Button variant="contained" color="primary"> Click me</Button>综上所述,要做好移动端的Web应用适配,需要综合考虑布局、交互、视口设置、性能优化以及合适的开发工具。通过以上这些方法,我们可以提供更好的移动用户体验,并确保应用在不同设备上都能够良好地运行。
前端阅读 1542024年8月5日 12:50

React hook 使用需要注意哪些?

在使用 React Hooks 时需要遵循一些最佳实践和注意事项,以确保代码的可维护性与功能的正确性。以下是一些关键点:1. 遵守Hooks规则不要在循环、条件或嵌套函数中调用HooksHooks 应该始终在组件的顶层被调用,这样可以保证 Hooks 在每次渲染时都以相同的顺序被调用,这对于 React 的内部状态追踪机制非常重要。只在React函数中调用Hooks应该仅在React的函数组件或自定义 Hooks 中调用 Hooks。不要在普通的 JavaScript 函数中调用。2. 使用 useState时的注意事项初始化状态对于复杂的状态逻辑,可以通过传递一个函数给 useState 来惰性初始化,这样可以避免在每次渲染时重新创建初始状态。const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState;});状态更新函数的身份稳定setState 函数是身份稳定的,这意味着你可以在其他 Hooks 中安全地引用它,而不用担心它会在重新渲染时改变。3. 使用 useEffect时的注意事项清理副作用在 useEffect 中创建的订阅、定时器、监听事件等副作用,应该在返回的清理函数中进行清除,以避免内存泄漏。useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清理订阅 subscription.unsubscribe(); };}, [props.source]);依赖列表的完整性确保依赖列表包含了所有外部作用域中被 useEffect 使用到的值,这样才能正确响应这些值的变化。如果忽略了依赖,可能会导致旧的闭包中的值被捕获,从而引发错误。useEffect(() => { function doSomething() { console.log(someProp); } doSomething();}, [someProp]); // 确保所有使用到的变量都被包含在依赖列表中4. 避免在 useEffect中进行不必要的操作节流和防抖如果 useEffect 中的操作非常昂贵,考虑使用节流(throttling)或防抖(debouncing)技术来减少操作的频率。5. 自定义Hooks代码复用当你发现需要在不同组件之间复用状态逻辑时,可以将其抽离成自定义 Hooks。这有助于减少代码冗余并增强逻辑的可维护性。例如,使用自定义 useForm Hook 来处理表单:function useForm(initialValues) { const [values, setValues] = useState(initialValues); const handleChange = (event) => { setValues({ ...values, [event.target.name]: event.target.value, }); }; return [values, handleChange];}6. 性能优化useMemo 和 useCallback在有必要的情况下,使用 useMemo 和 useCallback 来避免不必要的渲染或计算。useMemo 可以用来缓存复杂计算的结果,useCallback 可以用来缓存函数,这在将函数传递给子组件时特别有用,可以避免不必要的子组件重渲染。
前端阅读 1392024年8月5日 12:50

封装一个可以设置过期时间的localStorage存储函数

实现一个具有过期时间功能的localStorage存储函数,需要定义一个函数,它会将数据和过期时间一起存储在localStorage中。 下面是一个简单的实现示例:/** * 设置带过期时间的localStorage * @param {string} key - 存储的键名 * @param {*} value - 要存储的值,可以是任何可序列化的数据 * @param {number} ttl - 过期时间(毫秒) */function setLocalStorageWithExpiry(key, value, ttl) { const now = new Date(); // 创建一个包含数据和过期时间的对象 const item = { value: value, expiry: now.getTime() + ttl, }; // 将对象序列化之后存储到localStorage中 localStorage.setItem(key, JSON.stringify(item));}/** * 获取localStorage存储的值 * @param {string} key - 存储的键名 * @returns {*} 存储的值或者当值不存在或过期时返回null */function getLocalStorageWithExpiry(key) { const itemStr = localStorage.getItem(key); // 如果没有找到对应的存储项 if (!itemStr) { return null; } const item = JSON.parse(itemStr); const now = new Date(); // 检查过期时间 if (now.getTime() > item.expiry) { // 如果已过期,删除存储并返回null localStorage.removeItem(key); return null; } // 如果未过期,返回存储的值 return item.value;}// 示例使用// 存储一个名为 'myData' 的数据,过期时间为1小时(3600000毫秒)setLocalStorageWithExpiry('myData', { a: 1, b: 2 }, 3600000);// 获取存储的数据const myData = getLocalStorageWithExpiry('myData');console.log(myData); // 如果还未过期,则会打印出存储的对象 { a: 1, b: 2 }在这个封装的函数中,我们通过 setLocalStorageWithExpiry函数存储数据的时候,会额外添加一个过期时间戳到对象中,并将该对象序列化后保存在localStorage里。当通过 getLocalStorageWithExpiry函数获取数据的时候,我们会先检查当前时间是否已经超过了存储时设置的过期时间戳,如果已经过期,则从localStorage中删除该项,并返回 null;如果未过期,则返回保存的值。
前端阅读 702024年8月5日 12:49

三元表达式中“三元”这个词代表什么?

三元表达式是一种在多种编程语言中广泛使用的条件语句,它由三个部分组成:一个条件、一个结果表达式1和一个结果表达式2。"三元"这个词就是指这种表达式由三个部分构成。其基本形式为:条件 ? 结果表达式1 : 结果表达式2当条件为真(true)时,整个三元表达式的结果就是结果表达式1;当条件为假(false)时,表达式的结果就是结果表达式2。这里举一个具体的例子来说明三元表达式的使用:int x = 10;int y = 20;int max = x > y ? x : y;在这个Java代码示例中,我们使用三元表达式来决定max变量的值。条件是x > y,如果这个条件为真,则max会被赋值为x的值;如果条件为假,则max会被赋值为y的值。在这个例子中,因为x小于y,条件为假,所以max的值会是y的值,也就是20。
前端阅读 822024年8月5日 12:48

Webpack 的详细工作流程

Webpack是一个现代JavaScript应用程序的静态模块打包器,它主要的工作就是分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(如TypeScript等),并将其转换和打包为合适的格式供浏览器使用。 Webpack的工作流程主要分为以下几个阶段:初始化在Webpack启动后,它会从配置文件(默认是 webpack.config.js)中读取配置的参数,合并命令行传递过来的参数,形成最终的配置对象。编译(Compiling)Webpack开始编译整个项目。在这个阶段,Webpack会根据配置中的入口(Entry)开始递归解析所有依赖项。配置文件中的 entry属性用于定义入口文件,可能是一个或多个。构建(Building)针对每个依赖项,Webpack会使用对应的loader去处理文件,例如使用 babel-loader来处理JavaScript文件,css-loader处理CSS文件,file-loader处理图片等资源。Loaders的定义让Webpack能够去处理非JavaScript文件(Webpack本身只理解JavaScript)。生成(Output)经过加载和转换,Webpack会根据配置中的 output部分,把处理过的文件生成到文件系统中。通常是在项目的 dist目录下生成 bundle.js或者其他自定义名称的文件。优化(Optimizing)在生成出来的文件中,Webpack可以进行代码压缩、分割代码以实现按需加载等优化操作。这通过配置 plugins来实现,比如 UglifyJsPlugin、SplitChunksPlugin等。输出(Emitting)将所有的资源文件输出到指定目录下,此时,Webpack的工作就算是完成了。示例例如,您有一个项目,其入口文件是 src/index.js。Webpack会解析这个文件,并解析出这个文件依赖的模块。假如 index.js中依赖了 src/print.js,Webpack会继续解析 print.js的依赖。假设 index.js中还使用了ES6的语法和 .scss样式文件,那么在构建阶段,Webpack会使用 babel-loader将ES6代码转换为ES5代码,使用 sass-loader将SCSS文件转换为CSS文件,并且结合 css-loader和 style-loader将CSS代码注入到JavaScript中,这样就可以通过JavaScript将样式添加到DOM上。在优化阶段,可能会有插件去检查代码,去除未引用的代码(dead code),压缩混淆输出的文件,以减少文件大小和提高加载速度。最终,在输出阶段,Webpack会在 dist目录下输出 bundle.js,其中包含了所有的应用程序代码,以及所有的样式打包成的CSS代码。这就是Webpack的一个基本工作流程。它的强大之处在于可扩展性,通过配置文件和插件系统,可以适应各种复杂的项目需求。