前端面试题手册
详细说明闭包原理以及应用场景
闭包是一个函数以及创建该函数的词法环境的组合。闭包使得一个函数可以访问到它被定义时的作用域中的变量,即使该函数在其定义环境外被执行。这个概念在JavaScript等支持一等函数的编程语言中尤为重要。闭包的原理当你在JavaScript中创建一个函数时,该函数会记住它被创建时候的环境。在函数中定义的变量,以及它的父作用域中的变量,都会被闭包保留。在函数执行时,如果它访问了这些外部变量,即便父作用域已经执行完毕,这些变量依然可以被访问,因为闭包中保留了它们的引用。应用场景闭包在JavaScript编程中非常有用,它们有许多的应用场景:数据封装闭包可以用于创建私有变量,这样你就可以封装数据,只暴露必要的操作接口。function createCounter() { let count = 0; return { increment() { count++; }, get() { return count; }, };}const counter = createCounter();counter.increment();console.log(counter.get()); // 输出 1在上面的例子中,count 是一个私有变量,通过闭包的形式封装在 createCounter 函数中。外部代码无法直接访问 count 变量,只能通过 increment 和 get 方法间接操作它。回调函数与异步操作异步操作,如定时器、网络请求或事件处理中,闭包常用于保持对某个变量的引用。function delayedGreeting(name) { setTimeout(function() { console.log('Hello, ' + name); }, 1000);}delayedGreeting('Alice');在这个例子里,即使 delayedGreeting 函数的执行已经结束,传给 setTimeout 的匿名函数依然能够访问 name 变量。循环中创建闭包循环中创建函数时,闭包可以帮助每个函数记住它们各自的环境。for (var i = 0; i < 3; i++) { (function(index) { setTimeout(function() { console.log('Value is ' + index); }, 1000); })(i);}在这个例子中,立即执行的函数表达式(IIFE)创建了一个新的词法作用域,这样每次迭代都会保存各自的索引值 index。函数柯里化闭包可以用来实现函数柯里化,即创建已经设置了一些固定参数的新函数。function multiply(a, b) { return a * b;}function curriedMultiply(a) { return function(b) { return multiply(a, b); };}const double = curriedMultiply(2);console.log(double(5)); // 输出 10在这个例子中,curriedMultiply 函数通过闭包记住了参数 a,并返回了一个新函数,这个新函数接受参数 b 并调用原本的 multiply 函数。结论闭包是函数编程中的一个强大特性,它不仅允许你访问定义函数时的作用域,而且能够在数据隐藏、封装和柯里化中发挥重要作用。了解和利用闭包,可以让你编写出更加灵活和强大的代码。
阅读 24·2024年6月24日 16:43
介绍防抖节流原理区别以及应用
防抖(Debouncing)原理:防抖是一种控制方法,用于减少函数调用的频次。具体而言,当一个动作频繁触发时,防抖会确保该函数在指定的时间内只被执行一次。如果在这个延迟时间内再次触发了该动作,计时器会被重置,直到延迟时间结束后,才会执行函数。应用:举个例子,假设你在一个搜索框中键入文本,为了减少请求服务器的次数,你可以利用防抖技术。这意味着用户停止键入一段时间(比如500毫秒)之后,才会发送搜索请求。如果用户在500毫秒内继续输入,计时器会重置,直到用户停止输入后过了500毫秒,才会执行搜索。节流(Throttling)原理:节流是另一种控制方法,它同样用于减少函数调用的频次。与防抖不同的是,节流会强制函数以固定的时间间隔执行。即便动作频繁触发,函数也只会按照这个间隔执行,不会像防抖那样完全等待动作停止。应用:以滚动事件为例,如果我们需要在用户滚动页面时进行一些计算或更新界面元素,不加控制的话可能会导致性能问题。此时我们可以应用节流技术,比如设置每100毫秒执行一次更新操作,无论在这100毫秒内滚动事件被触发了多少次,更新函数都只会执行一次。区别触发频率:防抖函数会在动作结束后才执行,而节流函数会按照预定的频率执行,不管动作是否结束。目的:防抖通常用于确保某些计算密集型或高延迟的任务不会因为用户的高频操作而频繁执行,节流则用于控制函数的执行频率,保持性能的平稳。使用场景:防抖适用于诸如输入框内容自动完成、窗口大小调整(resize)、提交按钮(防止多次提交)等场景。节流适用于滚动加载、用户滚动监听、动画的持续触发等场景。总结来说,防抖和节流都是优化高频事件触发后的回调执行频率的技术。防抖是通过延迟执行来合并频繁的触发,而节流是通过减少执行的频率来降低触发的次数。根据具体的应用场景和需求选择合适的优化策略,可以显著提升应用的性能和用户体验。
阅读 10·2024年6月24日 16:43
JavaScript 中可变对象和不可变对象之间的区别是什么?
在JavaScript中,对象可以被分为可变对象和不可变对象两类。可变对象是指那些可以在创建后改变其内容和结构的对象。在JavaScript中,所有的对象(Object)、数组(Array)以及函数(Function)都是可变的。这意味着我们可以在创建这些对象后,添加新的属性或方法、改变其属性值、或者从对象中删除属性。例如,当我们创建一个数组时,我们可以通过各种方法来改变这个数组:let myArray = [1, 2, 3]; // 创建一个数组myArray.push(4); // 向数组添加一个新元素myArray[0] = 10; // 改变数组中第一个元素的值console.log(myArray); // 输出: [10, 2, 3, 4]在上面的例子中,我们创建了一个数组myArray,然后通过push方法添加了一个新元素,接着又修改了数组的第一个元素的值。这种改变数组内容的行为展示了数组是一个可变对象。不可变对象,相对地,是指那些一旦创建后其内容就不能更改的对象。在JavaScript中,原始数据类型(如Number、String、Boolean、Null、Undefined、Symbol和BigInt)是不可变的。这意味着这些类型的值一旦创建,就不能被改变;如果需要一个改变后的值,实际上是创建了一个新的值。举个例子,字符串的不可变性如下所示:let myString = "Hello"; // 创建一个字符串myString[0] = "M"; // 尝试改变字符串的第一个字符console.log(myString); // 输出: "Hello"在这个例子中,尽管我们尝试改变字符串myString的第一个字符,最终字符串仍然是原来的"Hello"。这表明尽管我们尝试对字符串进行了操作,但实际上字符串本身并没有改变,因为字符串是不可变的。如果我们想要一个不同的字符串,我们需要创建一个全新的字符串:let myString = "Hello";let newString = "M" + myString.substring(1); // 创建一个新字符串console.log(newString); // 输出: "Mello"在上面的例子中,newString是一个新的字符串,它是通过组合一个新的字符和原有字符串的一部分创建的,而原始的myString并未改变。可变对象和不可变对象之间的这种区别对于理解如何在JavaScript中管理数据及其引用非常重要。不可变对象提供了值的稳定性,而可变对象提供了灵活性。在编写代码时,理解这些概念可以帮助避免一些常见的错误,例如因直接修改对象或数组而导致的意外副作用。
阅读 10·2024年6月24日 16:43
在 Vue 中,watch和watchEffect的区别是什么?
在Vue中,watch和watchEffect是两种响应式侦听器,都能够对响应式状态的变化作出反应,但是它们的工作方式和使用场景有所不同。watchwatch API 允许我们侦听特定的数据源,并在数据源改变时执行回调函数。它是响应式系统的一部分,非常适合于执行异步操作或比较昂贵的操作,因为你可以精确控制何时以及如何响应状态的变化。精确性: watch允许你指定确切的响应式引用或计算属性来侦听。懒执行: watch默认情况下不会立即执行回调,它只会在侦听的响应式源发生变化时才执行。深度监听: watch可以配置为深度监听,侦听对象内部属性的变化。旧值和新值: watch回调提供新旧值,便于比较。停止监听: watch返回一个停止观察函数,你可以用它来停止监听。例子:<template> <div>{{ count }}</div></template><script>export default { data() { return { count: 0 } }, watch: { count(newVal, oldVal) { // 当 count 改变时,这个函数将被调用 console.log(`Count changed from ${oldVal} to ${newVal}`); } }}</script>watchEffectwatchEffect是一个响应式侦听器,它自动追踪它的函数内部使用的任何响应式状态,当这些状态改变时会重新执行该函数。自动追踪: watchEffect会自动侦听函数内部所有的响应式引用,并在引用更新时重新运行。立即执行: watchEffect在创建时会立即执行一次,确保响应式效果与当前的状态同步。无需指定侦听源: 不需要像watch那样指定侦听的具体状态,它会自动收集依赖。无旧值: watchEffect不提供旧值,因为它不侦听特定的数据源。停止监听: watchEffect同样返回一个停止监听的函数。例子:<template> <div>{{ count }}</div></template><script>import { ref, watchEffect } from 'vue';export default { setup() { const count = ref(0); watchEffect(() => { // 这个函数会在 setup() 时立即执行一次,并在 count 改变时再次执行 console.log(`Count is now: ${count.value}`); }); return { count }; }}</script>总结一下,watch更适用于当你需要侦听特定数据并在变化时进行比较或执行复杂逻辑时,而watchEffect则更适用于当你希望自动追踪并响应响应式状态变化,而不需要过多地控制侦听源和执行时机时。
React 项目中如何统一监听组件报错,并且处理报错情况?
在React项目中,统一监听组件报错并处理错误可以通过几种方式来实现,其中最常见和有效的方式是使用错误边界(Error Boundaries)。错误边界(Error Boundaries)错误边界是React 16引入的一种新的概念,它允许我们捕获后代组件树中JavaScript错误,并记录这些错误,并显示一个备用UI,而不是使整个组件树崩溃。实现方式创建一个错误边界组件:我们可以创建一个类组件,并在其中定义static getDerivedStateFromError()和componentDidCatch()这两个生命周期方法。class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新状态使下一次渲染能够显示降级后的UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误记录到一个错误报告服务 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的UI并渲染 return <h1>Something went wrong.</h1>; } return this.props.children; }}在应用中使用错误边界组件:错误边界组件可以包裹在任何你希望捕获其内部错误的组件外部。如果一个class组件内部发生了错误,但是它并没有被任何错误边界包裹,那么整个React组件树将会卸载。<ErrorBoundary> <MyComponent /></ErrorBoundary>错误报告服务在componentDidCatch方法中,我们不仅可以将错误渲染为备用UI,还可以将错误信息发送到服务器或错误监控服务,比如Sentry、LogRocket或者自己的错误收集系统中。实现方式componentDidCatch(error, errorInfo) { // Example: Sentry integration Sentry.captureException(error, { extra: errorInfo });}总结通过使用错误边界组件(Error Boundaries),我们可以在React应用中统一监听组件报错。每当组件树中的某部分发生JavaScript错误时,错误边界将会捕获这些错误,并能够显示备用UI来避免整个应用崩溃。同时,利用componentDidCatch可以处理错误,例如记录到日志服务中。这种方式不仅能够提高用户体验,还能帮助开发者及时发现并解决问题。不过需要注意,错误边界无法捕获以下场景中的错误:事件处理(了解更多请使用try/catch)异步代码(例如setTimeout或requestAnimationFrame回调函数)服务器端渲染它自身(错误边界组件自己的错误)抛出的错误在实践中,我们通常建议在高层组件(如路由层级)使用错误边界,以便能够捕获更多未预期的错误情况。
阅读 49·2024年6月24日 16:43
Vite 为什么启动非常快
Vite 之所以能快速启动,主要得益于它的两个核心设计理念:使用原生 ES 模块(ESM)和按需编译。原生 ES 模块加载(ESM):Vite 利用现代浏览器支持的原生 ES 模块特性来服务模块。在开发模式下,Vite 作为一个由浏览器原生支持的模块服务器,它不需要对代码进行打包和构建,而是通过 <script type="module"> 直接加载模块。这样可以快速启动服务器,并且在请求代码时,浏览器只需加载需要的模块,而不是整个应用程序的打包文件。按需编译:当使用传统的打包工具时,比如 Webpack,整个应用的所有依赖在启动时都需要先被打包,这包括了构建第三方库和应用代码。这个过程需要时间,特别是当应用规模庞大时。与此相反,Vite 在开发模式下采用了按需编译的策略。这意味着只有当请求某个文件时,Vite 才会对这个文件进行编译和处理。这种方式显著减少了启动时间,并且能够快速响应代码的热更新。缓存优化:Vite 对依赖的预构建过程进行了优化。它会在第一次运行时将所有的依赖项编译成 ESM,并将结果缓存起来。在随后的启动中,如果没有依赖变化,它会直接使用缓存的结果而不是重新编译,这进一步提高了启动速度。快速的 HMR(热模块替换):Vite 的热模块替换是模块级别的,当文件发生变化时,只有那个文件和依赖它的链会被重新加载和编译。这比 Webpack 这样的打包器进行整体刷新要快得多。高效的依赖预打包(Pre-bundling):对于某些复杂的依赖(如 CommonJS 或者依赖其他模块的库),Vite 使用 esbuild 预打包这些依赖。esbuild 是用 Go 语言编写的,它的打包速度远远超过基于 JavaScript 的打包器,如 webpack 和 Rollup。例如,假设你有一个具有数百个模块的大型应用程序。在传统的打包器中,即使你只修改了一个模块,也需要重新打包整个应用程序,这可能需要几十秒甚至更长时间。而在 Vite 中,如果你修改了一个模块,只有这个模块会被重新编译和返回,因此你几乎可以立即看到变化,从而极大提高了开发效率。总的来说,Vite 之所以能够实现快速的启动,是因为它摈弃了在开发阶段不必要的打包步骤,采用了更现代的模块加载方式,并通过智能的缓存和预编译策略来提高效率。这些设计理念使得 Vite 在开发过程中能够提供极致的快速响应体验。
阅读 8·2024年6月24日 16:43
如何广度优先遍历DOM树?
广度优先遍历(Breadth-First Traversal, BFT)是一种遍历或搜索树或图结构的算法,它从根节点开始,然后遍历所有邻近的节点,再对每个邻近节点做同样的处理,依此类推,直到遍历完所有可达的节点。在DOM树中应用广度优先遍历同样遵循这个原则,其中DOM树的根节点通常是document对象的documentElement属性,通常指向HTML文档的<html>元素。在JavaScript中执行DOM树的广度优先遍历,我们可以使用队列(Queue)这一数据结构来辅助实现。以下是一个简单的例子:function breadthFirstTraversal(root) { // 创建一个队列,并将根节点入队 let queue = [root]; // 当队列不为空时,循环执行 while (queue.length > 0) { // 出队一个节点并访问 let currentNode = queue.shift(); console.log(currentNode.tagName); // 打印当前节点的标签名 // 将当前节点的所有子节点入队 [].slice.call(currentNode.children).forEach(child => { queue.push(child); }); }}// 调用函数,传入document.documentElement作为遍历的起点breadthFirstTraversal(document.documentElement);在这个例子中,我定义了一个名为breadthFirstTraversal的函数,它接收一个DOM节点作为遍历的起点。然后,我使用一个数组作为队列来存放待访问的节点。在while循环中,我不断地从队列中取出节点,访问它,并将它的子节点加入队列末尾。通过这种方式,我能够按照广度优先的顺序访问整个DOM树的每一个节点。这个例子中,console.log(currentNode.tagName)是对当前节点的访问方式,实际应用中可以替换为其他操作,比如获取或修改节点信息等。此外,.slice.call(currentNode.children)是一种常见的技巧,用来将HTMLCollection或NodeList对象转换为数组,以便使用数组的forEach方法。在现代的JavaScript中,你也可以直接使用Array.from(currentNode.children)来进行转换。
阅读 14·2024年6月24日 16:43
React 的 setState是如何批量更新的?
html meta标签如何把http换成https的?
在HTML中,meta 标签本身并不直接将 HTTP 切换到 HTTPS。meta 标签通常用于定义网页文档的元数据,比如页面描述、关键字、文档的作者、最后修改时间以及其他元数据。这些元数据不会直接显示在页面上,但会被搜索引擎和浏览器用于处理网页数据。将网站从 HTTP 协议切换到更安全的 HTTPS 协议,通常需要在服务器层面上进行配置,而不是通过 HTML。这通常包括以下几个步骤:购买和安装 SSL/TLS 证书:首先,您需要为您的网站购买 SSL(Secure Sockets Layer)或 TLS(Transport Layer Security)证书。这些证书可以从证书颁发机构(CA)获得,它能够为你的网站提供加密,从而确保数据安全传输。配置 Web 服务器:安装证书之后,您需要配置您的 Web 服务器(比如 Apache、Nginx、IIS 等)来使用这个证书,并启动 HTTPS 协议。这通常涉及到编辑服务器配置文件来指定证书的位置,并设置服务器监听 443 端口(HTTPS 默认端口)的请求。重定向所有 HTTP 请求到 HTTPS:为了确保用户访问的是 HTTPS 版本的网站,您需要设置 HTTP 到 HTTPS 的重定向。在 Web 服务器配置中,您可以设置规则来自动将所有 HTTP 请求重定向到 HTTPS。例如,在 Apache 服务器中,您可以使用 .htaccess 文件来设置重定向规则,如下: RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]在上面的例子中,如果用户访问的是 HTTP 版本的网站,服务器将会发送一个 HTTP 状态码为 301(永久重定向)的响应,告诉浏览器该资源已经被永久地移动到了对应的 HTTPS URL。虽然 meta 标签不能用于切换 HTTP 到 HTTPS,但它有一个相关的用途,那就是设置 HTTP 的内容安全策略(Content Security Policy, CSP)。通过 CSP,您可以使用 meta 标签来增强网站的安全性。例如:<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">这个 meta 标签的作用是指示兼容的浏览器自动将页面上所有可用的不安全 URL(HTTP)请求升级为安全的 URL(HTTPS)。这不是将整个站点从 HTTP 切换到 HTTPS 的方法,而是一个辅助措施,用于提高页面中单独资源请求的安全性。
阅读 17·2024年6月24日 16:43
fetch 和 xhr 有什么区别?
Fetch API和XMLHttpRequest(XHR)都是用于在浏览器中发起HTTP请求的技术。 XMLHttpRequest (XHR):历史悠久: XHR出现的比较早,它是Ajax技术的基石,自2000年代初以来一直被广泛使用。复杂性: XHR的API相对复杂,使用时需要管理不同的事件和状态变化。支持状态: 它支持对请求的细粒度控制,比如可以在下载过程中监控进度事件。灵活性: XHR允许同步和异步通信。兼容性: 它的兼容性很好,支持老旧的浏览器。Fetch API:现代替代: Fetch是一个现代化的替代方案,提供了更简单、更强大的方式来发起网络请求。基于Promise: Fetch API基于Promise,这使得异步操作更加简洁,易于管理。语法简洁: Fetch提供了一个更加简洁和清晰的API,易于阅读和写作。无需额外的库: 与XHR配合一些库(如jQuery)使用相比,Fetch不需要额外的库或框架。默认异步: Fetch只支持异步操作,这有助于防止阻塞用户界面的问题。例子:使用XHR发起GET请求的代码可能如下:var xhr = new XMLHttpRequest();xhr.open('GET', 'https://api.example.com/data', true);xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { console.log(xhr.responseText); }};xhr.send();而使用Fetch API完成同样的任务的代码则更加简洁:fetch('https://api.example.com/data') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));Fetch API的优势在于它的简洁性和基于Promise的结构,这使得异步编码更加直观和易于维护。同时,Fetch还支持Request和Response接口,这有利于进一步控制请求和响应的细节。不过,对于一些需要细粒度控制的场合,或者在需要支持老旧浏览器的环境下,XHR仍然是一个可行的选择。
阅读 19·2024年6月24日 16:43