面试题手册

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

前端阅读 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空闲时间来执行垃圾回收的工作,以避免影响到程序的执行效率。
计算机基础阅读 972024年8月5日 12:52

什么是A/B测试?

A/B测试,也称为拆分测试,是一种统计学方法,用于比较两个或多个变体,以确定哪个变体在特定性能指标上表现得更好。在A/B测试中,A通常是当前使用的版本(控制组),而B则是改变了某些变量的版本(实验组)。通过将样本随机分配到A组和B组,并对结果进行分析来决定哪个版本更优。例如,在网页设计中,如果想测试一个新的按钮颜色是否会提高用户的点击率,那么可以创建两个版本的网页:一个是带有原始颜色按钮的A版本(控制组),另一个是带有新颜色按钮的B版本(实验组)。然后,将访问者随机分配到两个版本中的一个,并跟踪每个组的点击率。通过统计分析可以判断哪个按钮颜色能够带来更高的点击率。A/B测试的优点在于其简单性和高效性,能够直接对照比较,从而使决策者能够基于实际数据做出更加明智的选择。此外,A/B测试还有助于减少变化带来的风险,因为它允许在较小的用户群中测试改变,而不是全部用户。如果B版本效果更好,则可以将其推广到所有用户;如果效果更差或没有显著差异,则可以保持A版本,或者尝试其他变体进行进一步的测试。
前端阅读 1122024年8月5日 12:52

JavaScript 中对象 instanceOf 属性的原理是什么?

instanceof 是 JavaScript 中的一个二元操作符,用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。当我们使用语法 object instanceof Constructor 时,instanceof 会沿着 object 的原型链向上查找,检查是否有原型对象等于 Constructor.prototype。如果找到了这样的原型,instanceof 就会返回 true;如果一直查找到原型链的顶端(也就是 null),依然没有找到,就会返回 false。原型链是 JavaScript 中实现继承的一种机制。每个对象都有一个内部链接指向另一个对象,即它的原型。那个原型对象也有自己的原型,以此类推,直到一个对象的原型为 null。常见的 Object.prototype 就是很多对象原型链的终点。这里举一个例子来说明 instanceof 的工作原理:function Car(make, model) { this.make = make; this.model = model;}const myCar = new Car('Toyota', 'Corolla');console.log(myCar instanceof Car); // 输出:trueconsole.log(myCar instanceof Object); // 输出:true在这个例子中,myCar instanceof Car 返回 true,因为 Car.prototype 在 myCar 的原型链上。myCar instanceof Object 也返回 true,尽管我们没有直接设置 myCar 的原型为 Object.prototype。但由于 JavaScript 中几乎所有对象的原型链的最终都会指向 Object.prototype,因此任何对象基本上都是 Object 的一个实例。总的来说,instanceof 操作符提供了一种检查对象类型的方法,并且能够识别对象继承关系中的原型。
前端阅读 1422024年8月5日 12:52

JavaScript 中的原型&原型链是怎么工作的?

JavaScript中的原型(prototype)和原型链(prototype chain)是其面向对象编程的基础。这两个概念是理解JavaScript中对象之间的关系和继承机制非常关键的部分。原型(Prototype)在JavaScript中,每一个函数创建时都会有一个名为 prototype的属性,这个属性是一个对象,它包含了可以由特定类型的所有实例共享的属性和方法。这意味着你可以使用原型来添加或者分享功能,而不需要在每个实例中重新定义这些功能。例如,假设我们定义了一个构造函数:function Person(name) { this.name = name;}Person.prototype.sayName = function() { console.log(this.name);};以下是通过 Person构造函数创建的两个实例:var person1 = new Person("Alice");var person2 = new Person("Bob");这里的 person1和 person2都没有自己的 sayName方法。当你调用 person1.sayName()时,JavaScript会查找 person1是否有这个方法。由于 person1没有,它会通过原型链查找 Person.prototype是否有 sayName方法。这样,person1和 person2实际上共享了 Person.prototype上的 sayName方法。原型链(Prototype Chain)原型链是JavaScript实现继承的机制。每个对象都有一个内部链接到另一个对象,即它的原型。这个原型对象本身也有一个原型,以此类推,直到某个对象的原型为 null。按照规范,null的原型没有原型,这通常被视为原型链的末端。当你尝试访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript引擎会沿着原型链向上查找,直到找到这个属性或者到达原型链的末端。继续上面的例子,我们可以看到原型链是如何工作的:person1.sayName(); // 输出: Alice首先,JavaScript引擎检查 person1自身是否有 sayName属性。发现 person1没有,引擎接着检查 person1的原型(即 Person.prototype)。Person.prototype有 sayName方法,所以这个方法被调用。如果我们有一个对象的原型是另一个对象,那么第二个对象的属性和方法也可以被第一个对象访问。这个过程可以一直持续下去,形成了一个“链”。通过原型链实现继承我们可以使用原型链来实现继承。例如,如果我们有一个 Employee构造函数,它应该继承 Person:function Employee(name, title) { Person.call(this, name); // 继承属性 this.title = title;}Employee.prototype = Object.create(Person.prototype);Employee.prototype.constructor = Employee;Employee.prototype.sayTitle = function() { console.log(this.title);};var employee1 = new Employee("Charlie", "Developer");employee1.sayName(); // 输出: Charlieemployee1.sayTitle(); // 输出: Developer在这里,Employee.prototype被设置为一个新对象,这个新对象的原型是 Person.prototype。这样,Employee的所有实例都继承了 Person的方法。我们还修正了 Employee.prototype.constructor属性,确保它指向正确的构造函数。通过上面的代码,我们创建了一个 Employee的实例 employee1,它能够调用继承自 Person的 sayName方法,同时还有自己特有的 sayTitle方法。
前端阅读 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的一个基本工作流程。它的强大之处在于可扩展性,通过配置文件和插件系统,可以适应各种复杂的项目需求。
前端阅读 1362024年8月5日 12:48

如何做 CSS 的性能优化

CSS 性能优化是 web 项目性能优化中的重要部分。以下是一些策略来帮助优化 CSS 的性能:减少冗余代码为类或元素重复写入相同的 CSS 规则会浪费带宽和浏览解析时间。实用工具如 PurgeCSS 可帮助删除无用的 CSS。CSS 压缩CSS 压缩可以移除所有多余的字符,包括空格、换行符和注释。使用CSS 压缩工具如 CSSO 或 clean-css。使用 CSS 雪碧图CSS 雪碧图合并了一系列的小图片到一张大的图片中。这可以减少HTTP请求的数量,提高加载速度。CSS 对象模型(CSSOM) 和 渲染树浏览器通过解析 HTML 和 CSS 成 CSSOM 和 DOM ,然后结合他们形成渲染树。因此,应该尽量把 CSS 放在 HTML 文档的顶部,以加快渲染速度。避免使用过于复杂的选择器复杂的选择器可能会导致浏览器使用更多的资源来解析它们,优先使用类和 ID 选择器。使用 CSS 预处理器CSS 预处理器如 Sass 或 Less 可以使 CSS 更易于维护,同时可以使用变量,嵌套,混入 (Mixins) 等高级特性。使用硬件加速利用 GPU 来提供高效渲染,例如 transform 或 opacity。避免使用 @import@import 可能会导致更多的 HTTP 请求,使页面加载速度变慢。应该尽量使用命令行工具或构建系统的导入功能,以便在构建过程中进行文件合并。按需加载 CSS只加载需要立即使用的 CSS。缩小 CSS 的范围例如, instead of using * {margin: 0; padding: 0;}, 用类似 .myClass {margin: 0; padding: 0;} 更好。
前端阅读 1212024年8月5日 12:48

Composition API 如何实现逻辑复用

在Vue.js的Composition API中,逻辑复用是通过使用可组合函数(composables)来实现的。可组合函数是可以封装和重用Vue组件逻辑的函数。Composition API引入了一种新的组织和重用组件逻辑的方式,它提供了更灵活的代码组织结构,使得函数的复用变得更加简单和清晰。要实现逻辑复用,你可以按照以下步骤操作:创建可组合函数(composables):你可以创建一个独立的JavaScript函数,这个函数利用Composition API中的ref, reactive, computed, watch, watchEffect等响应性API来创建和管理状态或逻辑。在组件中使用可组合函数:在Vue组件的setup函数中,你可以引入和使用这些可组合函数。这样,你就可以在多个组件之间共享和重用相同的逻辑,而无需复制代码。传递参数和返回值:可组合函数可以接受参数并返回一些响应式引用、方法或其他值,这使得它们可以与组件进行交互并根据组件的需要进行调整。下面我将通过一个简单的例子来说明这一过程:假设我们有一个用于处理用户信息的逻辑,这部分逻辑需要在多个组件中复用。我们可以创建一个名为useUser的可组合函数来封装这部分逻辑。// useUser.jsimport { ref } from 'vue';export function useUser() { const user = ref(null); const isLoading = ref(false); async function loadUser(userId) { isLoading.value = true; try { const response = await fetch(`/api/users/${userId}`); user.value = await response.json(); } catch (error) { console.error('Failed to load user', error); } finally { isLoading.value = false; } } return { user, isLoading, loadUser };}在上面的例子中,useUser函数创建了一个用户信息的响应式引用user和一个加载状态的响应式引用isLoading。它还提供了一个异步函数loadUser来加载用户数据。现在,我们可以在组件中使用这个可组合函数了:// UserProfile.vue<template> <!-- 使用user和isLoading渲染UI --></template><script>import { onMounted } from 'vue';import { useUser } from './useUser';export default { setup() { const { user, isLoading, loadUser } = useUser(); onMounted(() => { loadUser('123'); // 假设'123'是用户ID }); return { user, isLoading }; }};</script>在UserProfile.vue组件的setup函数中,我们引入并调用useUser可组合函数,并在组件被挂载时调用loadUser函数来加载用户数据。这样,user和isLoading就可以在组件的模板中直接使用了。这种方法不仅使得代码更加清晰和易于维护,而且还提高了代码的复用性。通过这种方式,我们可以将逻辑抽离出来,并在多个组件之间共享。
前端阅读 1082024年8月5日 12:48

Composition API和Options API 之间的区别是什么

Composition API 和 Options API 是 Vue.js 框架中用于创建和组织组件的两种不同的API。Vue.js 是一个流行的前端JavaScript框架,用于构建用户界面和单页应用程序。下面我将详细说明它们之间的区别:Options APIOptions API 是 Vue.js 最初提供的接口,它是基于一个包含描述组件选项的对象的概念。这些选项包括了data、methods、props、computed、watch、lifecycle hooks等属性。这种API的特点是将组件的不同方面划分到这些选项中,代码按功能组织。例子:export default { data() { return { message: 'Hello Vue!', }; }, props: { user: String, }, computed: { normalizedUser() { return this.user.trim().toLowerCase(); }, }, methods: { sayHello() { alert(this.message); }, },};在这个例子中,data是组件的状态,props是外部传入的属性,computed是计算属性,methods是组件的方法。优点:易于理解和上手,特别是对于初学者。由于选项类型的组织方式,IDEs 和静态类型检查工具通常可以提供更好的支持。缺点:在大型和复杂的组件中,相互关联的逻辑会被拆分到不同的选项中,导致代码维护和理解上的困难。当组件变得庞大时,相同功能的代码可能散布在不同的选项中,难以追踪和组织。Composition APIComposition API 是在 Vue.js 3 中引入的,旨在解决 Options API 在构建大型应用时遇到的问题。它提供了更加灵活的方式来组织和重用代码。使用Composition API,开发者可以更容易地将组件逻辑基于功能划分和抽象成可复用的函数。例子:import { ref, computed } from 'vue';export default { setup(props) { const message = ref('Hello Vue!'); const normalizedUser = computed(() => props.user.trim().toLowerCase()); function sayHello() { alert(message.value); } return { message, normalizedUser, sayHello, }; }, props: { user: String, },};在这个例子中,setup函数是组件中所有Composition API逻辑的起点。通过导入ref和computed,我们可以定义响应式状态和计算属性。setup 函数返回的对象将定义组件的响应式状态和方法。优点:更好的逻辑复用和抽象,便于开发者根据功能组织代码,使得代码更加模块化。更容易控制变量的作用域和生命周期。更好地与TypeScript集成,提升类型推断的能力和开发体验。缺点:学习曲线相对较陡峭,特别是对于那些习惯于 Options API 的开发者。尽管它提供了更大的灵活性,但在小型项目或简单组件中可能会引入不必要的复杂性。结论Options API 和 Composition API 都是 Vue.js 提供的强大工具,它们各有优势。选择哪种API取决于项目的需求、组件的复杂性以及开发团队的偏好。Composition API 在处理大型项目和复杂组件时优势明显,而Options API 在小型项目或对于新手更
前端阅读 1472024年8月5日 12:48

什么是 React 的受控组件和非受控组件?

在React中,受控组件(Controlled Components)和非受控组件(Uncontrolled Components)都是处理表单输入的方式,但它们处理数据的方式不同。受控组件(Controlled Components)受控组件是React的一种模式,在这种模式下,表单数据是由React组件的状态管理的。这意味着每次字段的值发生变化时,我们都会通过一个事件处理函数(通常是 onChange)来更新组件的状态。然后,组件的状态被用作输入字段的值,确保组件的状态是数据的唯一来源。示例:假设我们有一个受控的 <input>元素:class ControlledComponent extends React.Component { constructor(props) { super(props); this.state = {value: ''}; } handleChange = (event) => { this.setState({value: event.target.value}); } render() { return ( <form> <label> Name: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> </form> ); }}在上面的例子中,<input>的值始终由 this.state.value决定,而且每当用户输入时,handleChange函数都会被调用,更新状态,因此界面显示的内容总是和状态同步。非受控组件(Uncontrolled Components)非受控组件是另一种模式,在这种模式下,表单数据是由DOM本身处理的,而不是由React状态管理。这就像传统的HTML表单工作方式。在非受控组件中,我们通常使用 ref来从DOM节点获取表单数据,而不是为每个状态变化编写事件处理函数。示例:下面是一个非受控组件的例子:class UncontrolledComponent extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); } handleSubmit = (event) => { alert('A name was submitted: ' + this.inputRef.current.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" ref={this.inputRef} /> </label> <button type="submit">Submit</button> </form> ); }}在上面的例子中,<input>不是通过状态来控制其值,而是通过 ref来访问DOM节点获取其值。 总结受控组件允许你更好地控制表单的行为,因为组件的状态充当了数据的真实来源。非受控组件舍弃了对表单状态的即时控制,使得组件的代码更简洁,但可能会更难管理表单的状态,尤其是在复杂的表单交互时。在实际的开发实践中,受控组件通常是首选方法,因为它们更加符合React的数据流概念,使得状态的管理更加清晰和可预测。然而,对于一些简单的表单或者集成第三方DOM库时,非受控组件也可能是一个不错的选择。
前端阅读 1032024年8月5日 12:48

手写 javascript 中 new 的实现过程

当我们在JavaScript中使用new操作符创建一个新对象时,实际上会发生以下几个步骤:创建一个新对象。 使用new操作符时,JavaScript会自动为我们创建一个新的空对象。设置原型链。 新对象内部的[[Prototype]](或者__proto__)属性会被赋值为构造函数的prototype属性,这样新对象就可以访问到构造函数原型上的属性和方法。绑定this并执行构造函数。 构造函数内部的this将会被绑定到新创建的对象上,然后执行构造函数中的代码,这样新对象就可以具有构造函数中定义的属性和方法。返回新对象。 如果构造函数返回的是一个对象,则返回该对象;如果没有返回对象或者返回的不是一个对象,那么将返回步骤1创建的新对象。如果我们要手写一个new的实现,可以定义一个函数来模拟这个过程。以下是一个例子:function myNew(constructor, ...args) { // 步骤1:创建一个空对象,并设置原型链 const obj = Object.create(constructor.prototype); // 步骤2:将构造函数的this绑定到新对象上,并执行构造函数 const result = constructor.apply(obj, args); // 步骤3:根据返回值判断 return result instanceof Object ? result : obj;}// 测试用例function Person(name, age) { this.name = name; this.age = age; this.sayHello = function() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); };}// 使用自定义的myNew来替代new操作符const person = myNew(Person, 'Alice', 30);person.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.以上代码中,myNew函数模拟了new操作符的所有关键步骤,能够模拟出通过new操作符创建对象的效果。
前端阅读 832024年8月5日 12:48

Redux 如何实现自定义中间件

在Redux中,中间件是一种强大的机制,允许开发者在action被发送到reducer之前插入自己的逻辑。创建自定义的Redux中间件涉及到编写一个函数,该函数按照Redux中间件API的规格返回一个满足特定签名的函数。我将向您展示如何自定义实现一个简单的日志中间件,该中间件的作用是在action被派发时在控制台输出日志信息。以下是自定义Redux中间件的基本步骤:编写一个函数,该函数接收store的dispatch和getState方法。该函数返回一个接收下一个中间件的next函数的函数。返回的函数再返回一个接收action的函数。在最内层的函数体内,可以执行自定义的逻辑,然后调用next(action)将action传递给链中的下一个中间件或reducer。下面是一个自定义日志中间件的例子:// 自定义日志中间件const loggerMiddleware = store => next => action => { // 自定义的逻辑:在当前action被处理之前输出日志 console.log('dispatching', action); // 调用链中的下一个中间件或reducer let result = next(action); // 自定义的逻辑:在action被处理后输出新的状态 console.log('next state', store.getState()); // 返回result,因为middleware的链需要从next(action)获取返回值 return result;};export default loggerMiddleware;在上述的中间件代码中:store: Redux store实例,它包含了dispatch和getState方法。next: 是一个将action传递给链中下一个处理者(中间件或reducer)的函数。action: 是当前正在处理的action对象。使用这个中间件的典型方式是在创建Redux store时应用它:import { createStore, applyMiddleware } from 'redux';import rootReducer from './reducers';import loggerMiddleware from './middleware/loggerMiddleware';// 使用applyMiddleware来增强store,添加自定义的loggerMiddlewareconst store = createStore( rootReducer, applyMiddleware(loggerMiddleware));export default store;在这个例子中,任何派发到store的action都会先经过loggerMiddleware这个中间件,在控制台输出action信息,然后继续沿中间件链传递,直到最终被reducer处理。这只是自定义中间件的一个简单例子,但您可以根据需要在中间件中实现更复杂的逻辑,例如异步操作、路由导航或其他您想要的任何自定义行为。