JavaScript面试题手册

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

前端阅读 322024年6月24日 16:43

详细说明闭包原理以及应用场景

闭包是一个函数以及创建该函数的词法环境的组合。闭包使得一个函数可以访问到它被定义时的作用域中的变量,即使该函数在其定义环境外被执行。这个概念在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 函数。结论闭包是函数编程中的一个强大特性,它不仅允许你访问定义函数时的作用域,而且能够在数据隐藏、封装和柯里化中发挥重要作用。了解和利用闭包,可以让你编写出更加灵活和强大的代码。
前端阅读 182024年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中管理数据及其引用非常重要。不可变对象提供了值的稳定性,而可变对象提供了灵活性。在编写代码时,理解这些概念可以帮助避免一些常见的错误,例如因直接修改对象或数组而导致的意外副作用。
计算机基础阅读 392024年6月24日 16:43

实现一个函数,判断输入是不是回文字符串

回文字符串是一个正向和反向都相同的字符串。比如 "madam" 或者 "racecar" 就是回文字符串。以下是一个用Python编写的简单函数,用于检测一个字符串是否是回文:def is_palindrome(s): # 首先,我们将字符串转为小写,并移除非字母字符 clean_s = ''.join(c for c in s.lower() if c.isalnum()) # 然后我们比较字符串与其翻转后的版本是否相同 return clean_s == clean_s[::-1]在这个函数中,我们首先将输入字符串转换为全部小写,并且移除了所有非字母和非数字字符,这样我们就能只关注字母和数字,忽略掉标点和空白。然后我们简单地将处理过的字符串与其自身的倒序版本进行比较,来判断它是否是回文。让我们用一些例子来测试这个函数:print(is_palindrome("Madam")) # 应该输出: Trueprint(is_palindrome("racecar")) # 应该输出: Trueprint(is_palindrome("hello")) # 应该输出: Falseprint(is_palindrome("A man, a plan, a canal, Panama")) # 应该输出: True在最后一个例子中,尽管原始字符串包含了空格和标点符号,但是在我们的 is_palindrome函数中,这些字符都被移除了,所以最终验证的字符串是"amanaplanacanalpanama",这是一个回文字符串。
前端阅读 222024年6月24日 16:43

事件的触发过程是怎么样的?知道什么是事件代理吗?

事件的触发过程事件的触发过程,通常指的是在Web浏览器中,当用户与网页上的元素交互时(如点击按钮、移动鼠标等),将会触发相应的事件(如click, mousemove等)。此过程遵循一个特定的模式,称为“事件流”,它描述了从浏览器到DOM元素再回到浏览器的过程。事件流有两种模型:事件冒泡和事件捕获。事件捕获(Capturing): 事件开始于window对象,然后向下传递到目标元素的父元素,最终到达目标元素自身。这个过程是从外向内逐层捕获事件的过程。目标阶段(Targeting): 事件到达目标元素,执行绑定在该元素上的事件处理器。事件冒泡(Bubbling): 在目标阶段完成后,事件又会从目标元素开始,逐层向上冒泡,直到window对象。举个例子,假设我们有一个按钮元素,它位于一个表单内,该表单又位于HTML页面的body元素内。当用户点击按钮时,如果所有这些元素都对点击事件定义了处理函数:在捕获阶段:首先window对象检查是否有onclick事件处理器,然后是body元素,接着是form元素,最后是按钮本身。目标阶段:事件到达按钮元素,触发绑定在按钮上的点击事件处理器。在冒泡阶段:事件从按钮开始向上冒泡,先到form元素,然后是body元素,最后是window对象。开发者可以通过JavaScript控制事件监听器是在捕获阶段还是冒泡阶段触发。事件代理事件代理是一种常用于减少内存使用并避免为多个子元素绑定监听器的技术。事件代理的基本原理是利用了事件冒泡的特性。而不是在每个子元素上单独设置事件监听器,我们在其父元素上设置单个监听器,以监控所有子元素上的事件。在这个监听器中,我们可以使用 event.target属性来获取实际触发事件的元素,并据此执行相应的事件处理逻辑。事件代理的主要优势在于:内存效率:不必为每个子元素创建和维护独立的事件监听器,减少了内存的占用。动态元素:对于在运行时动态添加到DOM中的元素,我们不需要再单独为它们添加监听器,因为父元素上的代理监听器已经能够处理。简化管理:当有许多子元素需要相同的事件处理逻辑时,通过在父元素上设置单一监听器,简化了事件管理。举个例子,假设我们有一个待办事项列表(<ul>元素),它下面有多个列表项(<li>元素)。如果我们想要为每个列表项添加点击事件,使用事件代理的方式如下:// 假设ul元素有id="todo-list"var todoList = document.getElementById('todo-list');// 为ul元素添加点击事件监听器todoList.addEventListener('click', function(event) { if (event.target.tagName.toLowerCase() === 'li') { // 这里可以处理点击事件 console.log('你点击了列表项:' + event.target.textContent); }});
前端阅读 662024年6月24日 16:43

[Event Loop] 浏览器和nodejs事件循环有什么区别?

在浏览器和Node.js中,事件循环是实现非阻塞I/O操作的核心机制,尽管它们在高层面上非常相似,但具体实现上有几个主要区别。以下是我将回顾的几点关键差异及其例子:1. 任务源和处理方式浏览器:浏览器的事件循环主要处理来自Web API的任务,这些可以是DOM事件、Ajax回调、setTimeout等。它使用了宏任务(macro tasks)和微任务(micro tasks)的概念。宏任务包括script(整体代码)、setTimeout、setInterval和I/O,而微任务主要包括Promise.then、MutationObserver。在一个事件循环中,每次只会从宏任务队列中取出一个任务执行,然后执行所有可用的微任务。Node.js:Node.js的事件循环由libuv库实现,包括了多个阶段,如timers、I/O callbacks、poll、check、close callbacks等。Node.js中处理任务更为复杂,各个阶段几乎都有自己的队列。timers阶段处理setTimeout和setInterval回调,poll阶段负责I/O事件回调,而setImmediate的回调会在check阶段执行。例子:在浏览器中,Promise.resolve().then()会在当前宏任务完成后立即执行,因为微任务总是在宏任务之后清空。在Node.js中,由于事件循环的阶段性,可能会在执行微任务时插入其他类型的任务,例如,如果在I/O操作完成后添加了一个setImmediate,邑可能在当前阶段的微任务和下一阶段的微任务之间执行。2. 定时器的精度浏览器:浏览器的定时器(如setTimeout和setInterval)的精度相对较低,早期定时器至少有4ms的延迟(根据HTML5标准规定),而现代浏览器偶尔会有更高的延迟,以帮助减少后台标签页的能耗。Node.js:Node.js定时器的精度通常更高,因为服务器端的环境对实时性和性能有更高的要求。Node.js的事件循环可以精确到毫秒。例子:在浏览器中设置 setTimeout(fn, 1)可能实际上在4ms后才执行回调,而在Node.js中,相同的设置会尽量接近1ms执行回调。3. 默认行为和扩展性浏览器:浏览器的事件循环通常是不可见和不可控制的,由浏览器内核管理。Node.js:Node.js的事件循环可以通过C++插件和核心模块进行扩展,给开发者提供了更多控制。例如,使用libuv库,开发者能够接触到底层的事件循环机制。例子:Node.js的开发者可以编写本地插件,通过直接与libuv交互来修改或增强事件循环的行为,而这在浏览器端是做不到的。4. 性能和优化浏览器:浏览器的事件循环是为了优化用户界面和用户互动设计的,因此,许多优化都是围绕用户体验和界面响应性进行的。Node.js:Node.js的事件循环是针对I/O密集型操作进行优化的,特别是网络和文件系统操作。
前端阅读 322024年6月24日 16:43

addEventListener 和 attachEvent 的区别是什么?

addEventListener 和 attachEvent 都是用于在 Web 开发中将事件监听器绑定到 DOM 元素上的方法,但它们之间存在一些关键差异:浏览器兼容性:addEventListener 是 DOM Level 2 Events 规范的一部分,它被所有现代浏览器(包括 IE9 及以上版本)支持。attachEvent 是微软为 IE8 及以下版本的浏览器提出的一个方法,不是标准的 DOM 事件处理方法。语法:addEventListener 采用以下语法: javascript element.addEventListener(event, function, useCapture); 其中 event 是事件名称(不带 on 前缀),function 是事件处理函数,而 useCapture 是一个布尔值,用于指定事件是在捕获阶段处理还是在冒泡阶段处理。attachEvent 采用不同的语法: javascript element.attachEvent(eventWithOn, function); 其中 eventWithOn 是带有 on 前缀的事件名称,function 是事件处理函数。事件流:addEventListener 允许你指定事件处理函数是在捕获阶段还是冒泡阶段被调用。attachEvent 只支持事件冒泡,不支持事件捕获。this 关键字:在 addEventListener 中绑定的事件处理函数里,this 指向绑定事件的元素。在 attachEvent 中绑定的事件处理函数里,this 指向全局对象 window,而不是绑定事件的元素。多个事件监听器:使用 addEventListener,你可以为同一个事件添加多个事件监听器,它们会按顺序执行。而 attachEvent 有可能会导致多个事件监听器的执行顺序不一致,而且同一个事件处理函数如果添加多次,也会被执行多次。移除事件监听器:addEventListener 配对的是 removeEventListener,可以用于移除事件监听器。attachEvent 配对的是 detachEvent,用于移除事件监听器,但需要确保传递给 detachEvent 的参数与 attachEvent 时的参数完全一致。由于 attachEvent 方法是一个非标准方法且仅在老版本的 IE 浏览器中可用,所以通常推荐使用 addEventListener,因为它是跨浏览器兼容的并遵循现代的 Web 标准。当需要支持老版本 IE 浏览器时,开发者可能需要编写额外的代码来兼容 attachEvent。
前端阅读 472024年6月24日 16:43

页面意外崩溃,这时候 JS 线程都已经崩溃了,如何传递通知呢?

在Web应用中,当JavaScript线程崩溃导致页面无法正常工作时,确实需要一种机制来通知用户或者开发者。在这种情况下,由于主JavaScript线程已经崩溃,传统的错误捕获如 try-catch或者 window.onerror事件监听可能都不起作用。但是,我们仍然可以采用以下几种策略:1. 使用Web WorkersWeb Workers运行在与主JavaScript线程分离的后台线程中。即使主线程崩溃,Web Workers可能仍然保持运行状态。因此,可以在页面加载时,启动一个监控Worker,用来检测主线程的心跳。如果主线程心跳停止(例如,可以通过设置定时消息来实现心跳),Worker可以尝试通知服务器或者更新UI来告诉用户发生了错误。2. 利用 window.onunload和 window.onbeforeunload事件可以在 window.onunload或 window.onbeforeunload事件中进行错误上报。如果浏览器支持 navigator.sendBeacon方法,即使在页面卸载的情况下也可以向服务器发送数据。虽然这两个事件不是为错误处理设计的,但它们可以用于在页面关闭时发送一些信息,可能包括崩溃通知。3. 使用Service WorkersService Workers作为一种在浏览器后台运行的脚本,可以用来拦截和缓存网络请求,推送通知等。如果设置了Service Worker,并且页面发生崩溃的情况下,它仍然能够接收到fetch请求或者推送事件,从而可以实现一定程度上的错误处理或状态报告。4. 外部心跳系统通过外部系统定时检测Web应用的状态,例如可以通过服务器定时发送请求到客户端,检测页面的响应。如果在预定时间内没有收到响应,或者收到错误响应,服务器则可以记录这类事件并采取相应的措施。5. 自动化监控和错误上报工具使用像Sentry、LogRocket这样的第三方错误监控服务,它们可以帮助在JavaScript出现未捕获异常时自动上报错误。虽然在某些崩溃情况下这些工具也可能失效,但它们仍然是一种有效的自动监控手段。6. 客户端存储在客户端使用如localStorage或sessionStorage,可以在检测到问题时写入状态标志。然后在页面重载或重新打开时检查这些标志,如果发现有异常状态,可以采取相应的通知措施。7. 使用全局异常处理器在可能的情况下,可以注册全局异常处理器 window.addEventListener('error', function() {...}),尽管在某些崩溃情况下可能不会被调用,但它提供了一个捕获异常并尝试通知的机会。示例举一个使用Web Workers进行心跳检测的简单示例:假设我们在主线程中有一个定期运行的函数,模拟心跳:function sendHeartbeat() { if (worker) { worker.postMessage('heartbeat'); }}// 定期发送心跳到WorkersetInterval(sendHeartbeat, 5000);
前端阅读 372024年6月24日 16:43

attribute和property的区别是什么

HTML文档中,attribute(属性)和property(属性)这两个术语经常使用,它们在不同的上下文中有不同的含义。 对象导向编程中的Attribute和Property在对象导向编程(OOP)的上下文中,attribute和property通常指代与对象关联的数据,但它们的概念和用途有所不同。Attribute (属性):在OOP中,attribute通常指的是对象的内部状态,它们是类定义中的变量。这些是对象的数据成员,用于存储对象的信息。例如,假设我们有一个 Car类,那么 color和 model可能是 Car对象的attributes。Property (属性):Property在OOP中通常指的是提供对attribute的访问的一种特殊方法,这些方法通常是通过getter和setter方法暴露的。Property允许封装attribute,从而可以在读取或修改attribute时添加附加的逻辑,如验证或事件触发。例如,Car类可能有一个 mileage属性,它通过getter方法 get_mileage()和setter方法 set_mileage(value)来访问和修改里程信息,而不是直接公开一个 mileage attribute。HTML文档处理中的Attribute和Property在HTML和Web开发的上下文中,attribute和property也有不同的含义。Attribute (属性):HTML attribute是HTML标签的一部分,用于在HTML文档中为元素定义特定的配置或行为。Attributes在HTML源代码中明确定义,例如 <input type="text" value="Hello">中的 type和 value。Attributes的值通常是在页面加载时定义的静态值。Property (属性):HTML元素在浏览器中表示为JavaScript对象,这些对象具有properties。这些properties与JavaScript运行时相连,表示DOM元素的当前状态。Properties可以在运行时动态改变,例如,通过JavaScript改变input元素的 value property,document.getElementById('myInput').value = 'New Value';。示例:考虑HTML中的一个 <input>元素,其初始HTML可能如下所示:<input id="myInput" type="text" value="Initial">这里,id、type和 value都是HTML attributes。当页面加载后,我们可以通过JavaScript访问 <input>元素的property,比如:var inputElement = document.getElementById('myInput');console.log(inputElement.value); // 输出: Initial在这个时候,value attribute和 value property都是“Initial”。然而,如果用户在input框中输入新的文本,比如“Hello”,那么 value property将改变,而 value attribute仍然为“Initial”。
前端阅读 332024年6月24日 16:43

JS内存泄露如何检测?场景有哪些?

JavaScript内存泄露是指在应用程序中不再需要使用的内存由于某些原因没有被释放或回收,导致可用内存逐渐减少,最终可能会导致应用程序或系统性能下降,甚至崩溃。 如何检测JS内存泄露?检测JavaScript内存泄露通常可以通过以下途径进行:浏览器开发者工具:大多数现代浏览器都提供了内置的开发者工具,可以用来监视内存使用情况。例如,Google Chrome的开发者工具中有"Performance"和"Memory"面板,允许开发者记录和分析网站的运行时性能和内存使用情况。堆快照(Heap Snapshots):通过浏览器的开发者工具,可以捕获堆快照,它会展示内存分配的静态视图。通过比较连续的堆快照,可以观察到哪些对象被分配内存后没有被释放。时间线记录(Timeline Profiling):这个工具可以帮助我们理解内存是如何随着时间的推移而增加的。我们可以使用浏览器工具的时间线功能记录一个时间段内的内存使用情况,寻找内存使用上升的趋势。代码审查(Code Review):定期进行代码审查以查找常见的内存泄露模式,如未取消的事件监听器、闭包的滥用、未清除的定时器等。内存泄露场景内存泄露可能出现在多种不同的场景中,以下是一些常见的场景:全局变量:意外地创建全局变量会导致这些变量不被回收,例如,忘记使用 var、let或 const关键字。事件监听器未移除:如果在DOM元素上添加了事件监听器,但在不需要时没有正确移除,它们会持续占用内存。闭包:不当使用闭包可能会导致父作用域中的变量无法被释放。DOM引用:JavaScript中的变量如果引用了已经从DOM中移除的元素,如果引用一直保持,那么这部分内存也不会被回收。定时器:设置了定时器(如 setInterval)而没有清除(clearInterval),可能会导致内部回调函数和相关变量长期占用内存。第三方库:使用的第三方库如果存在内存泄露,同样也会影响到使用它的应用程序。举个具体的例子:在开发一个单页应用时,我注意到随着页面的使用时间增加,页面的响应速度逐渐变慢。我使用Chrome开发者工具中的Performance面板进行了记录,发现内存使用量呈现持续上升的趋势。通过分析和比较不同时间点的堆快照,我发现存在一个大量DOM元素对应的监听器没有在元素被移除时一并清理。修复这个问题后,应用的性能得到了显著的提升。
前端阅读 442024年6月24日 16:43

如何实现Promise的resolve?

在JavaScript中,Promise 对象是异步编程的一种解决方案。一个 Promise 在创建时处于 pending(等待)状态,可以通过其 resolve 方法转变为 fulfilled(成功)状态,或通过其 reject 方法转变为 rejected(失败)状态。要实现 Promise 的 resolve,通常是在异步操作成功完成时调用。下面是一个简单的例子说明如何使用 Promise 的 resolve 方法:function asyncOperation() { // 创建一个新的Promise对象 return new Promise((resolve, reject) => { // 执行异步操作 setTimeout(() => { const operationWasSuccessful = true; // 假设这是基于异步操作结果的条件 if (operationWasSuccessful) { resolve('Operation successful'); // 如果操作成功,调用resolve并传递结果 } else { reject('Operation failed'); // 如果操作失败,调用reject并传递错误信息 } }, 1000); // 假设这个异步操作需要1秒钟 });}asyncOperation() .then(result => { console.log(result); // 打印成功结果 }) .catch(error => { console.error(error); // 打印错误信息 });在上述代码中,asyncOperation 函数返回一个新的 Promise 对象。在这个 Promise 的构造函数中,有两个参数:resolve 和 reject。这两个参数也是函数,它们被用来分别处理异步操作的成功和失败情况。在异步操作(这里使用 setTimeout 模拟)完成后,根据操作的结果调用 resolve 或 reject。如果异步操作成功(在这个例子中,我们假设 operationWasSuccessful 为 true),则调用 resolve 函数并传递结果消息 'Operation successful'。这将使得 Promise 对象的状态变为 fulfilled,并将结果传递给随后的 .then 方法的回调函数。如果异步操作失败,就调用 reject 函数并传递错误消息 'Operation failed'。这将使得 Promise 对象状态变为 rejected,并将错误信息传递给随后的 .catch 方法的回调函数。