乐闻世界logo
搜索文章和话题

JavaScript面试题手册

JavaScript 执行过程分为哪些阶段?

JavaScript 的执行过程大致可以分为以下几个阶段:1. 解析(Parsing)在这一阶段,JavaScript 引擎会读取源代码,并将其解析成抽象语法树(AST)。抽象语法树是一种深层次的、结构化的代码表达方式,能够以树形结构表现代码中的每个语句、表达式等元素。解析过程中,如果遇到语法错误,会抛出错误,停止进一步执行。2. 编译(Compilation)JavaScript 引擎(如V8)通常会将解析后的代码进行即时编译(JIT)。编译器会先生成字节码,这是一种低级的、比源代码更接近机器语言的代码。随后根据程序的执行情况,编译器可能会把热点代码(经常执行的代码)编译成优化的机器码,提高执行效率。3. 执行(Execution)编译后得到的字节码或机器码被送到 JavaScript 引擎的执行环境中执行。在执行过程中,会进入下面的子阶段:创建执行上下文(Execution Context):首先会创建全局执行上下文,随后每当调用一个函数时,就会为该函数创建一个新的执行上下文。执行上下文包括变量对象、作用域链和 this 引用等信息。变量提升(Hoisting):在执行代码前,函数声明和变量(声明)会被提升到它们各自的执行上下文的顶部。变量会初始化为 undefined,而函数则会完整地提升。执行代码(Running Code):按照执行上下文中的代码逐行执行,进行变量赋值、函数调用等。垃圾回收(Garbage Collection):在执行过程中,引擎会进行内存管理,自动释放那些不再被需要的内存空间。4. 优化(Optimization)在代码执行的过程中,某些代码可能会被执行多次,引擎会尝试对这些频繁执行的代码进行优化。例如,在V8引擎中,有一个称为“TurboFan”的优化编译器,它可以根据代码执行的特点对代码进行优化,提高性能。如果优化假设失败了(即出现了“去优化” deoptimization),引擎还可以将代码回退到一个较少优化的版本。5. 回收(Deoptimization & Garbage Collection)对于那些不再需要的数据和优化,JavaScript 引擎会进行去优化和垃圾回收,以保证内存的高效使用。例子:假设我们有这样一个简单的 JavaScript 函数:function sum(a, b) { return a + b;}let result = sum(5, 3);首先,该函数会被解析成 AST。然后,它可能会被编译成字节码,当我们调用 sum(5, 3) 时,会创建一个新的执行上下文,包含 a 和 b 的参数以及任何局部变量。在这个上下文中,a 和 b 被赋予了值 5 和 3,函数执行,并返回结果 8。这个过程中可能还包括了对 sum 函数的优化,如果函数被频繁调用。最后,当执行上下文离开作用域,如果没有其他引用指向其中的数据,垃圾回收器最终会清理掉这些对象。
阅读 29·2024年6月24日 16:43

JavaScript 为什么要区分微任务和宏任务?

JavaScript 区分微任务(microtasks)和宏任务(macrotasks)主要是为了有效地管理异步操作的执行时机和顺序。这两种类型的任务允许 JavaScript 引擎维持一个控制异步操作何时何地执行的精细化调度机制。宏任务 (Macrotasks)宏任务通常是浏览器的主要任务,包括但不限于:setTimeoutsetIntervalI/O 操作UI 渲染事件处理(如点击、滚动事件)每次执行栈为空时,事件循环都会从任务队列中取出一个宏任务执行。微任务 (Microtasks)微任务通常是需要快速响应的任务,执行时机在每个宏任务之后,以及JavaScript执行环境准备好以后。微任务包括:Promise 回调(例如.then、.catch和.finally)MutationObserver 的回调queueMicrotask函数执行顺序在每个宏任务执行完毕后,在执行下一个宏任务之前,JavaScript 引擎会处理所有队列中的微任务。这意味着微任务总是在当前宏任务结束和下一个宏任务开始之间执行,并在新的UI渲染前完成。这样可以确保异步操作的快速响应,同时因为微任务的延迟更小,所以适合高优先级的任务。为什么区分区分微任务和宏任务的原因包括:性能优化: 通过微任务,JavaScript 可以在不影响用户界面渲染的情况下,快速执行简单的操作,如承诺的解决。这提高了应用程序的响应速度和性能。控制异步操作顺序: 微任务和宏任务的区分允许开发者控制异步操作的执行顺序。例如,一个由Promise产生的微任务可以确保其在下次UI渲染之前解决,而setTimeout可能会推迟到下一个宏任务。避免阻塞: 对于需要快速执行的代码,使用微任务可以避免阻塞宏任务队列,这有助于避免长时间运行的任务阻塞UI更新。示例假设我们有以下代码:console.log('宏任务开始');setTimeout(() => { console.log('宏任务');}, 0);Promise.resolve().then(() => { console.log('微任务');});console.log('宏任务结束');执行顺序会是这样的:打印"宏任务开始"宏任务结束时,设置了一个setTimeout打印"宏任务结束"当前宏任务已结束,开始执行微任务队列中所有任务打印"微任务"微任务队列为空,开始下一个宏任务打印"宏任务"通过这个例子,我们可以看到微任务总是在当前执行栈清空后立即执行,而宏任务的执行则可能会因为任务队列中的其他宏任务而延迟。这种机制允许JavaScript有效地处理异步事件,同时保持对执行顺序的细粒度控制。
阅读 43·2024年6月24日 16:43

jsonp 为什么不支持 post 方法?

JSONP(JSON with Padding)是一种利用<script>标签不受同源策略限制的特性来实现跨源请求的技术。因为<script>标签的初衷是加载静态的JavaScript文件,所以<script>标签仅支持GET方法来请求资源,它并不支持POST方法。这就是为什么JSONP不支持POST方法的根本原因。当使用JSONP进行通信时,您会将请求参数包含在URL中,并通过动态创建<script>标签的方式将其发出。服务器接收到GET请求后,将数据包裹在一个函数调用中,并将其作为响应返回。客户端定义好回调函数后,这段包裹着JSON数据的JavaScript被执行,回调函数便会被调用并处理返回的数据。例如,假设您的页面需要从http://example.com获取一些用户数据,您可能会发送如下的JSONP请求:<script type="text/javascript"> // 定义回调函数 function handleResponse(data) { console.log('Received data:', data); }</script><script type="text/javascript" src="http://example.com/data?callback=handleResponse"></script>服务器端需要接收到callback参数后,把数据包装在该函数调用中:// 服务器端响应handleResponse({ "user": "Alice", "age": 25 });如上所述,JSONP请求的本质是一个GET请求,它是通过<script>标签的src属性来发起的。因此,它不能使用POST方法,后者通常用于传输大量数据或者发送需要安全传输的数据。如果您需要进行跨域的POST请求,可以考虑使用更现代的技术,如CORS(跨源资源共享),它允许在各种HTTP方法中使用跨源请求,同时提供了更好的安全性。
阅读 26·2024年6月24日 16:43

Ajax 如何处理请求跨域问题?CORS 如何设置?

Ajax 处理请求跨域问题Ajax(Asynchronous JavaScript and XML)本身是不支持跨源请求的,这是因为浏览器出于安全考虑实施的同源策略(Same-Origin Policy)。同源策略限制了来自不同源的文档或脚本对当前文档读取或设置某些属性的能力。不过,有几种方法可以绕过这个限制来实现跨源请求:JSONP(JSON with Padding):这是一种老的技巧,它利用 <script>标签不受同源策略限制的特点来进行跨域请求。服务器返回的响应拼接一个函数调用作为JavaScript代码。这种方法的缺点是它只能用于GET请求,并且存在一定的安全隐患。CORS(Cross-Origin Resource Sharing):这是现在推荐的方法,它允许服务器显式地指定哪些源可以访问该服务器上的资源。CORS是一个 W3C 标准,它通过在服务器上设置特定的HTTP头来工作。代理服务器:使用一个服务器充当中间人的角色,接受来自客户端的跨源请求,然后转发请求到目标服务器。代理服务器接收到响应后,再将数据返回给客户端。这种方法需要额外的服务器端配置。WebSockets: WebSockets提供了一个全双工的通信渠道,可以在浏览器和服务器之间建立持久连接。虽然WebSocket协议与HTTP不同,但它可以绕过同源策略,从而实现跨域通信。CORS 设置当你控制服务器时,可以通过设置HTTP响应头来启用CORS。这些头部主要包括:Access-Control-Allow-Origin: 指定了哪些网站可以参与跨域资源共享。例如,设置为 *代表允许所有域进行跨域请求,但出于安全考虑通常会指定明确的域名。Access-Control-Allow-Methods: 指定了允许的HTTP请求方法,如 GET, POST, PUT, DELETE, OPTIONS等。Access-Control-Allow-Headers: 在实际请求中允许自定义的HTTP头部字段列表。Access-Control-Allow-Credentials: 表示是否允许发送Cookie。如果服务器端设置了这个值为 true,那么前端请求的时候也必须将 withCredentials属性设置为 true。Access-Control-Expose-Headers: 允许客户端访问的服务器白名单头部字段。Access-Control-Max-Age: 表明在多少秒内,不需要再发送预检验请求(对于某一资源的预检请求结果的有效期)。下面是一个简单的例子,展示了如何在Node.js的Express框架中设置CORS响应头:const express = require('express');const app = express();app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'https://example.com'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); res.header('Access-Control-Allow-Credentials', true); if (req.method === 'OPTIONS') { res.header('Access-Control-Max-Age', '86400'); return res.status(200).send(); } next();});// 你的其他路由和逻辑app.listen(3000, () => { console.log('Server running on port 3000');});
阅读 35·2024年6月24日 16:43

base64 为什么能提升性能?

Base64编码通常不是直接用来提升性能的,而是用来确保二进制数据可以通过仅支持文本格式的传输层安全地传输。Base64将二进制数据编码为ASCII字符串,这使得它可以在不支持二进制数据的系统(如电子邮件)中传输。虽然Base64编码的数据比原始二进制数据大约增加了33%,但在某些情况下,它可以间接地提升性能:减少HTTP请求:在Web开发中,Base64编码通常用于将小的图片或其他文件直接嵌入到HTML或CSS中。这样做的好处是可以减少浏览器发起的HTTP请求的数量,因为所有的资源都包含在了主文档中。少了额外的请求,网页加载时间就会缩短,间接提高了用户体验和性能。举个例子,如果一个网页中有多个小图标,通常的做法可能是每个图标一个HTTP请求来获取图像文件。如果将这些图标的图片用Base64编码后嵌入到CSS中,就可以将多个HTTP请求合并为一个请求,从而减少了服务器的请求负载和网络延迟,提高了页面的加载速度。数据URI方案:Base64编码可以使用数据URI方案在Web页面中直接嵌入图像数据,这样可以避免服务器配置对小文件的较慢响应时间。服务器对小文件的处理往往不如大文件高效,因为涉及到磁盘I/O等开销。通过避免这些小文件请求,可以在服务器端节省资源,从而提升性能。安全和兼容性:有些系统不支持二进制数据的传输,或者在传输过程中可能会因为某些字符(如NUL byte)的存在而出现问题。在这种情况下,Base64编码提供了一种可靠的方法来处理和传输数据,避免了潜在的数据损坏和传输错误,从而确保系统的顺畅运行和性能。总之,Base64编码本身增加了数据量,理论上会降低传输效率,但通过减少HTTP请求的数量、充分利用缓存、避免小文件请求开销以及提高数据的安全性和兼容性,它可以在特定场景下间接提升系统的整体性能。
阅读 75·2024年6月24日 16:43

== 和 === 的区别是什么?什么情况下用 == 相等?

在JavaScript中,== 和 === 都用于比较运算符,但它们在比较值时使用不同的方式。=== 称为严格等于或恒等运算符,它比较两个值的类型和值是否完全相同。如果比较的两边数据类型不同,则直接返回false,不会进行类型转换。只有当数据类型及值都相同时,=== 才返回true。例子:3 === 3 // true,因为类型和值都相同3 === '3' // false,因为一个是数字类型,另一个是字符串类型== 称为宽松等于或等于运算符,它在比较时会进行类型转换,如果两个值类型不同,它会尝试将它们转换为相同类型,然后再进行值的比较。例子:3 == 3 // true,类型和值都相同3 == '3' // true,尽管类型不同(一个是数字,一个是字符串), // 但'3'会在比较之前转换为数字3,然后进行比较通常在编码中建议使用===来避免由于类型转换导致的意外结果,这也是代码质量工具和最佳实践的推荐。然而,有些情况下,如果你确切知道类型转换的机制,并且想利用这个特性来简化代码,可以使用==。比如:// 这里我们知道x的值可能是数值0或者"0",且两者我们视为等同的情况function checkZero(x) { return x == 0;}checkZero(0); // truecheckZero("0"); // true,因为"0"会被转换为数字0然后比较在上述代码中,使用==可以接受字符串'0'和数字0,并认为他们是等价的。如果使用===,就需要写更多的代码来处理类型检查和转换。不过,除非有非常清晰的理由,一般还是推荐使用===,因为它能让代码的行为更加可预测和清晰。
阅读 48·2024年6月24日 16:43

addEventListener 的第三个参数的作用是什么?

addEventListener 方法是 JavaScript 中常用来为元素添加事件监听器的方法。这个方法可以让开发者指定当某个事件在目标元素上触发时,应该调用的回调函数。addEventListener 方法通常接收三个参数:type: 字符串,表示监听事件类型的名称,比如 click, mouseover 等。listener: 函数,事件触发时浏览器调用的函数。options or useCapture: (可选)布尔值或者是一个对象。这是第三个参数,它指定了事件处理的更多选项。当第三个参数是布尔值时,它指的是 useCapture。如果 useCapture 设置为 true,则表示在捕获阶段触发事件处理函数;如果设置为 false 或者省略,则表示在冒泡阶段触发事件处理函数。在 DOM 事件处理中,事件传播分为三个阶段:捕获阶段、目标阶段和冒泡阶段。默认情况下,事件监听器只在冒泡阶段被调用。如果第三个参数是一个对象,它可以包含多个属性,如下所示:capture: 布尔值,和直接提供 useCapture 作为布尔值的效果一样。once: 布尔值,如果为 true,监听器会在添加之后第一次触发时自动移除。passive: 布尔值,如果为 true,表明监听器永远不会调用 preventDefault()。如果监听器确实调用了这个函数,客户端将会忽略它并且可能给出一个警告。例如,如果我们想要在用户第一次点击按钮时做出反应,并且希望在捕获阶段而不是冒泡阶段处理事件,我们可以这样写代码:const button = document.querySelector('#myButton');button.addEventListener('click', (event) => { // 处理点击事件 console.log('Button clicked!');}, { capture: true, once: true });在这个例子中,{ capture: true, once: true } 作为第三个参数传递,确保了监听器在捕获阶段执行,并且只执行一次。
阅读 37·2024年6月24日 16:43

JavaScript 的遍历方法中,在 map 和 for 中调用异步函数的区别是什么?

在JavaScript中,map和for循环是遍历数组的两种常见方法,但在处理异步函数时,它们的行为有显著差异。使用map调用异步函数map函数是Array原型上的一个方法,它对数组中的每个元素执行一个由你提供的函数,并返回一个新的数组,该数组是由原数组中每个元素调用处理函数得到的结果组成的。当你在map内使用异步函数时,每次迭代都会立即发起异步操作,但不会等待上一个完成,这意味着所有异步操作几乎是同时发起的。map不会等待异步函数的解决,它会立即继续到下一次迭代。例如,如果你使用map遍历数组,并在每个元素上调用一个返回Promise的异步函数:let promises = [1, 2, 3].map(async (num) => { let result = await someAsyncFunction(num); return result;});这里,promises数组将包含三个Promise对象,这些Promise对象是someAsyncFunction返回的,并且他们将并行执行。使用for循环调用异步函数使用传统的for循环,你可以更容易地控制异步函数的执行顺序。如果在循环内部使用await,你可以确保每次迭代都等待上一个异步操作完成再继续。例如,使用for循环顺序执行异步操作:let results = [];for (let num of [1, 2, 3]) { let result = await someAsyncFunction(num); results.push(result);}在这段代码中,someAsyncFunction会为数组中的每个元素顺序执行。第二次迭代会等待第一次迭代中的异步操作完成,以此类推。这意味着异步操作是串行执行的。总结使用map调用异步函数时,所有异步操作几乎同时开始,它们是并行的,最后你得到一个Promise对象的数组。使用for循环(或其他类型的循环,如for...of、for...in、while等)并结合await调用异步函数时,操作将按顺序一个接一个地执行,即串行执行。因此,选择哪种方法取决于你是否需要并行或串行执行异步操作。如果操作之间没有依赖,并且你想最大限度地提高效率,可以使用map。如果操作必须按照一定的顺序执行,或者一个操作的输出是另一个操作的输入,那么使用for循环会更合适。
阅读 29·2024年6月24日 16:43

for..of 和 for...in 是否可以直接遍历对象?

for...of 循环是在ES6中引入的,它专门用于遍历可迭代对象的元素,如数组、字符串、Map、Set 等这些实现了迭代器接口的数据结构。所谓的可迭代对象就是那些具有 Symbol.iterator 属性的对象。例如,数组是可迭代对象,可以使用 for...of 遍历其元素:let array = [10, 20, 30];for (let value of array) { console.log(value);}// 输出:// 10// 20// 30然而,普通对象不是可迭代的,没有实现 Symbol.iterator 方法,因此不能直接使用 for...of 遍历它的属性。尝试使用 for...of 直接遍历一个对象会导致一个错误:let obj = {a: 1, b: 2, c: 3};for (let value of obj) { console.log(value);}// TypeError: obj is not iterable另一方面,for...in 循环是用来遍历一个对象的所有可枚举属性的键,包括继承的可枚举属性。它不仅可以遍历普通对象的属性,还可以遍历数组(虽然通常不推荐这样做,因为它会返回数组索引,而且可能会遍历到原型链上的属性)。使用 for...in 遍历对象的例子:let obj = {a: 1, b: 2, c: 3};for (let key in obj) { console.log(key + ': ' + obj[key]);}// 输出:// a: 1// b: 2// c: 3总结一下,for...of 用于遍历可迭代对象的元素,而 for...in 用于遍历对象的所有可枚举属性的键。因此,for...of 不能直接遍历普通对象,而 for...in 可以。
阅读 22·2024年6月24日 16:43

setTimeout 与 setInterval 的区别是什么?

setTimeout 和 setInterval 都是 JavaScript 中用于控制时间和执行定时任务的函数,但它们的工作方式和用途有所不同。setTimeoutsetTimeout 函数用于设置一个定时器,该定时器将在指定的毫秒数后执行一次您指定的函数或代码块。一旦定时器完成任务(即执行了指定的函数或代码),它就会停止。用法示例:function sayHello() { console.log('Hello!');}// 调用 sayHello 函数,但是会在 2000 毫秒(2 秒)后执行setTimeout(sayHello, 2000);在这个例子中,sayHello 函数会在约 2 秒后执行一次,然后 setTimeout 就完成了它的任务。setInterval与 setTimeout 不同,setInterval 函数用于设置一个定时器,该定时器会无限次地以指定的时间间隔重复执行您指定的函数或代码块,除非您明确停止它。用法示例:function sayHelloRepeatedly() { console.log('Hello again!');}// 每隔 2000 毫秒(2 秒),调用一次 sayHelloRepeatedly 函数const intervalId = setInterval(sayHelloRepeatedly, 2000);// 当你想停止定时器时,可以调用 clearInterval// clearInterval(intervalId);在这个例子中,sayHelloRepeatedly 函数会每隔 2 秒执行一次,这将一直持续下去,直到调用 clearInterval(intervalId) 才会停止这个定时器。总结差异setTimeout 是执行一次延迟操作的函数。setInterval 是重复执行操作的函数,直到清除定时器。setTimeout 定时器执行完毕后自动清除。setInterval 定时器会持续运行,直到你调用 clearInterval。实际应用中,选择哪一个函数取决于你的具体需求:如果你需要延迟执行一次操作,使用 setTimeout;如果你需要以固定的时间间隔重复执行操作,使用 setInterval。
阅读 30·2024年6月24日 16:43