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

前端面试题手册

讲一下 import 的原理,与 require 有什么不同?

import 的原理在JavaScript中,import语句用于从模块中导入绑定(即函数、对象、原始类型等)。这是ES6规范(即ECMAScript 2015)引入的模块化特性的一部分。import的工作原理基于ECMAScript模块(ESM)系统。当你使用 import语句时,JavaScript引擎执行以下步骤:解析模块标识符:确定要导入的模块的位置及其文件路径。模块加载:如果模块尚未加载,JavaScript引擎会加载模块文件。编译模块:引擎会对模块代码进行编译,检查语法并进行优化。执行模块代码:在私有的模块作用域内执行模块代码,以初始化导出的绑定。缓存模块:模块的导出会被缓存,这意味着每个模块只会被执行一次,之后的导入会重用同一份导出的实例,保持状态的一致性。import 与 require 的不同import和 require都是JavaScript中用于加载模块的语句,但它们之间存在几个关键差异:语法规范:import是ES6中引入的模块化语法,而 require则来自于CommonJS规范,后者主要用于Node.js环境中。模块类型:import用于加载ESM模块,而 require用于加载CommonJS模块。加载方式:import声明是静态的,意味着它必须位于模块的顶部,不能动态运行或按条件导入模块。require是动态的,可以在代码的任何地方调用,支持条件加载和运行时动态计算路径。异步与同步:import可以支持异步模块的导入,通过 import()函数进行动态导入,返回一个Promise对象。require的加载是同步的,当调用 require时,代码会停止执行,直到模块被加载和返回。性能优化:由于 import是静态的,它允许JavaScript引擎进行更强大的性能优化,比如死代码消除和模块的静态分析。导出绑定的可变性:使用 import导入的绑定是活动的,也就是说如果导出的模块变量值发生变化,导入的绑定也会更新。使用 require导入的值是导出值的拷贝,一旦导入,无论源模块如何变化,导入的值都不会改变。例子使用 import:// ES6模块导入语法import { myFunction, myVariable } from './myModule.js';// 使用导入的函数和变量myFunction();console.log(myVariable);使用 require:// CommonJS模块导入语法const myModule = require('./myModule.js');// 使用模块的属性和方法myModule.myFunction();console.log(myModule.myVariable);在处理前端项目时,我们可能更倾向于使用 import,因为它与现代JavaScript模块化标准一致,而在Node.js环境中,尽管现在已经支持ESM,require依然被广泛使用,特别是在老项目中。
阅读 40·2024年6月24日 16:43

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

事件的触发过程在Web开发中,事件的触发过程通常遵循以下几个步骤:捕获阶段:事件开始由最外层的document对象向事件目标节点传播的阶段。这个阶段不是所有事件都会有。目标阶段:事件到达目标元素,即实际触发事件的元素。冒泡阶段:事件从目标元素向外传播到document对象的阶段,事件可以在这个阶段的任意元素上被监听和处理。例如,假设我们有一个按钮(<button>元素),它位于一个段落(<p>元素)内,该段落又位于一个页面(document)。如果用户点击了按钮,那么在捕获阶段,事件会从document开始,经过<p>,直到达到<button>。此时,事件进入目标阶段,通常是在这里触发任何与按钮直接相关的事件监听器。之后,事件会进入冒泡阶段,途径<p>元素,最后到达document。在这个过程中,开发者可以选择在捕获阶段或冒泡阶段的任何点上处理事件。事件代理事件代理(Event Delegation)是一种常用的事件处理模式,它利用了JavaScript中事件的冒泡机制。在这种模式下,我们不是直接在目标元素(例如一个按钮)上设置事件监听器,而是在其父元素上设置一个事件监听器,监听其所有子元素的事件。当子元素上的事件被触发并冒泡到父元素时,父元素上的监听器会捕捉到这些事件,并根据事件的来源执行相应的事件处理函数。事件代理的优势在于:减少内存消耗:不需要为每个子元素都添加事件监听器,只需要在父元素上添加一个监听器即可。动态内容的事件管理:对于动态添加到页面中的元素,不需要重新绑定事件监听器,已有的事件代理依然有效。简化事件管理:通过在一个中心位置管理事件,使事件的添加、删除和修改变得更加容易。例子:假设我们有一个任务列表,每个任务项都有一个删除按钮,我们要给这些按钮添加点击事件来删除对应的任务项。如果使用事件代理,我们会在任务列表的容器上添加一个点击事件监听器:document.getElementById('taskList').addEventListener('click', function(event) { if (event.target.className === 'delete-btn') { // 如果点击的是删除按钮,则删除对应的任务项 event.target.closest('.task-item').remove(); }});在这个例子中,无论何时新增任务项,其删除按钮点击事件都会被容器上的事件监听器捕获和处理,而不需要单独给每个删除按钮绑定事件监听器。这就是事件代理的概念。
阅读 7·2024年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); }});
阅读 17·2024年6月24日 16:43

::before 和 :after 中双冒号和单冒号有什么区别

在 CSS 中,::before 和 :before,::after 和 :after是伪元素,它们被用来向选择的元素添加一些内容。其中,::before 和 ::after 是在 CSS3 才出现的,并且是推荐的用法。双冒号 :: 是用来区分伪元素和伪类的。伪元素用于向元素的特定部分添加样式。例如,::first-line、::first-letter、::before、::after 等。单冒号 : 是 CSS1 和 CSS2 中的写法,在 CSS3 中仍然维持了对它的支持,主要是为了维持向后兼容。单冒号 : 用来表示伪类,例如 :hover、:active、:focus 等。举例来说的话,如果你写的样式需要兼容一些较为老旧的浏览器,那么可能需要使用 :before 和 :after。在现代浏览器中,推荐使用 ::before 和 ::after。总结如下:双冒号 :: 用于 CSS3 的伪元素。单冒号 : 用于 CSS2 的伪元素以及所有的伪类,同时保持向后的兼容。目前很多浏览器都已经支持了双冒号,但如果你在开发时需要兼容老版本的浏览器,那么还是建议使用单冒号。
阅读 88·2024年6月24日 16:43

异步加载JS的方式有哪些?

在Web开发中,异步加载JavaScript是一种提高页面加载性能的常用技术,因为它允许网页在加载和解析JavaScript文件的同时,继续加载页面上的其他内容。以下是几种异步加载JavaScript的方法:1. 使用 async 属性HTML5提供了async属性,可以直接在<script>标签中使用。当浏览器遇到带有async属性的<script>标签时,它会继续加载网页的其他部分,同时异步加载脚本。一旦脚本下载完毕,它会被执行,但不会阻塞DOM的解析。<script async src="path/to/your-script.js"></script>例子:假设你有一个用于分析用户行为的脚本,这个脚本不需要在页面加载时立即执行,你就可以使用async属性来加载它。2. 使用 defer 属性与async类似,defer属性也可以在<script>标签中使用,以指示浏览器异步加载脚本。不同的是,使用defer属性的脚本会在整个文档解析完成后、DOMContentLoaded事件触发前执行。<script defer src="path/to/your-script.js"></script>例子:如果你的脚本需要访问DOM,但又不是紧急的,比如一个幻灯片效果,使用defer属性可以确保DOM加载完毕后再执行脚本。3. 动态创建 <script> 元素可以使用JavaScript动态创建一个<script>元素,并将其插入DOM中来异步加载脚本。var script = document.createElement('script');script.src = "path/to/your-script.js";script.async = true; // 可以设置为async或省略此行来异步加载document.head.appendChild(script);例子:在使用单页应用框架(如Angular或React)时,可能需要根据路由动态加载模块,此时可以动态创建<script>元素来加载需要的JavaScript文件。4. 使用 XMLHttpRequest 或 fetch API可以使用XMLHttpRequest或现代的fetch API来异步请求脚本内容,并在请求完成后使用eval()或者新的Function构造函数来执行代码。例子(使用fetch API):fetch('path/to/your-script.js') .then(response => response.text()) .then(text => eval(text)) .catch(error => console.log(error));需要注意的是,使用eval()或者Function构造函数来执行脚本会有安全风险,因为它们会执行任意的代码。因此,应该只从可信的源加载脚本,并尽量避免使用这些方法。5. 使用模块加载器使用模块加载器和打包工具(如Webpack、RequireJS等)可以帮助开发者更加有效地管理和异步加载模块。例子:在Webpack中,可以使用import()语法来实现代码分割和动态导入模块。import(/* webpackChunkName: "my-chunk-name" */ 'path/to/module').then(module => { // 使用模块});所有这些方法都可以减少页面首次加载时的时间,并且可以提供更好的用户体验。适当选择异步加载技术还能根据实际需求进行优化,例如,对于核心功能优先加载,对于非核心功能可以延后加载或按需加载。
阅读 5·2024年6月24日 16:43

Proxy 的代理原理,以及 Proxy 是如何使用的?

代理原理:Proxy,也称为代理服务器,其基本原理是作为客户端和服务器之间的中介角色。当客户端请求网页或其他网络服务时,代理服务器会接收这个请求,然后代表客户端向真正的服务器发送请求。代理服务器收到服务器的响应后,再将数据转发给客户端。这个过程实现了数据的中转,可以用于多种用途,如内容缓存、过滤请求、负载均衡、隐私保护等。使用Proxy的方式:Proxy 的使用可以根据不同的需求和场景分为几种方式:客户端配置:在客户端(如浏览器、操作系统)中设置代理服务器的地址和端口,使得所有通过客户端发出的请求都经过代理服务器。例如,在浏览器的网络设置中,可以填入代理服务器的IP地址和端口,之后浏览器发出的所有请求都会通过这个代理服务器。系统级别的代理:在操作系统中设置代理,那么所有的网络请求都可以被代理。在Windows系统中,可以通过“Internet选项”设置全局代理。在UNIX-like系统中,可以设置环境变量http_proxy和https_proxy。应用级别的代理:某些应用程序允许用户配置代理,仅该应用程序的数据流量会通过代理服务器。例如,可以在Telegram客户端中单独设置代理。透明代理:客户端不需要做特别配置,所有的网络流量都会自动通过代理服务器,通常是由网络管理员在网络的出口处设置。这通常用在企业或学校网络中,用于监控和过滤流量。反向代理:对于服务端而言,反向代理服务器接收来自客户端的请求,然后转发到内部服务器,并将服务器的响应返回给客户端。反向代理常用于负载均衡、SSL终结和缓存静态内容。例如,Nginx和Apache都可以配置为反向代理。编程中使用Proxy:在编写软件时,可以使用编程语言提供的库通过代理发送请求。例如,在Python中,可以使用requests库并配置proxies参数来发送请求。示例:假设我们有一台位于美国的代理服务器,其IP地址是12.34.56.78,端口是8080。如果我们在中国,并希望通过这台美国的代理服务器来访问一个通常在中国无法访问的网站,我们可以这样做:在浏览器设置中,我们输入代理服务器的IP地址和端口。我们尝试访问目标网站,如https://www.example.com.浏览器不会直接连接到example.com,而是将请求发送到代理服务器12.34.56.78的8080端口。代理服务器接收到请求后,将其代理到example.com.example.com将响应发送回代理服务器。代理服务器再将这些信息转发回我们的浏览器。通过这种方式,我们就可以访问原本无法访问的网站,同时也隐藏了我们的真实IP地址。
阅读 124·2024年6月24日 16:43

HTML 中 script 标签的位置是否会影响网站首屏的显示时间?

<script> 标签的位置确实会影响网站首屏的显示时间。当浏览器解析到 HTML 文档中的 <script> 标签时,它会暂停文档的解析,去加载和执行脚本。这意味着,如果 <script> 标签放置在文档的 head 部分,浏览器必须先加载并执行完这些脚本,才能继续解析 HTML 文档的剩余部分。如果脚本很大或者需要从慢速服务器加载,这将延迟页面内容的渲染,从而增加首屏显示的时间。例如,假设我们有一个简单的 HTML 页面,其中在 head 部分包含了一个大型的外部 JavaScript 文件:<!DOCTYPE html><html><head> <title>My Page</title> <script src="big-external-script.js"></script></head><body> <!-- 页面内容 --></body></html>在上述情况中,用户必须等待 big-external-script.js 完全加载并执行后,才能看到页面内容,即首屏的显示会受到影响。作为对比,如果我们将 <script> 标签放在 HTML 文档的 body 部分的最后:<!DOCTYPE html><html><head> <title>My Page</title></head><body> <!-- 页面内容 --> <script src="big-external-script.js"></script></body></html>在这种结构中,浏览器会先渲染页面内容,用户可以更快地看到首屏,而脚本将在页面内容加载之后异步加载和执行,从而不会阻塞首屏的显示。为了进一步提高性能,还可以使用 async 或 defer 属性,这两者都允许浏览器异步加载脚本:使用 async 时,脚本会在加载完成后尽快执行,但不保证执行顺序;使用 defer 时,脚本会在整个文档解析完成后,但在 DOMContentLoaded 事件之前按照它们在文档中出现的顺序执行。<!DOCTYPE html><html><head> <title>My Page</title> <script src="external-script.js" defer></script></head><body> <!-- 页面内容 --></body></html>在实际开发中,为了优化首屏显示时间,开发者通常会将非必须的脚本移动到文档底部,或者使用 async 或 defer 属性,以确保关键渲染路径(Critical Rendering Path)不会因脚本加载和执行而受阻。
阅读 13·2024年6月24日 16:43

浏览器中的事件循环的工作流程

事件循环 (Event Loop) 是浏览器用于处理非同步事件的一种机制。它确保了 JavaScript 的执行能够同步进行,即使 JavaScript 是单线程运行的。下面是事件循环的工作流程:执行栈(Call Stack): 当一段JavaScript代码开始执行时,它首先会进入到执行栈中。如果这段代码是一个函数,那么这个函数就会被放到栈的顶部。Web APIs: 当遇到非同步操作(如:setTimeout, XMLHttpRequest 等)时,该操作会被浏览器的Web APIs接管,执行栈会继续执行下一行代码,不会停下等待异步操作的结果。任务队列(Task Queue): 一旦异步操作完成了(比如说 setTimeout 中指定的时间已过),回调函数就会被放入任务队列中。任务队列就是一个等待执行栈清空后执行的回调函数的列表。事件循环: 事件循环的职责是监控执行栈和任务队列。如果执行栈为空,它就会检查任务队列。如果任务队列中有待执行的回调函数,事件循环就会将其从队列中取出,放到执行栈中去执行。渲染队列(Render Queue): 当浏览器准备进行渲染时(通常是每16.7毫秒,对应于60fps),它会有自己的渲染队列来处理重绘和回流事件。如果执行栈和任务队列都是空的,事件循环会从渲染队列取出任务执行,以确保用户界面能够及时更新。微任务队列(Microtask Queue): 除了常规的任务队列,还有一种叫作微任务(Microtask)的任务,比如Promise的回调。微任务队列的特点是在当前执行栈清空后,就会立即执行微任务队列中的所有任务,即使任务队列中有等待的任务。只有当微任务队列为空时,事件循环才会查看任务队列。例子假如我们有以下代码片段:console.log('脚本开始');setTimeout(function() { console.log('setTimeout');}, 0);Promise.resolve().then(function() { console.log('promise1');}).then(function() { console.log('promise2');});console.log('脚本结束');执行顺序将会是这样的:'脚本开始' 被打印到控制台,因为它是第一行同步代码。setTimeout 的回调被 Web APIs 接管,执行栈继续往下执行。Promise.resolve() 创建了一个Promise,它的回调被放入了微任务队列。'脚本结束' 被打印到控制台,因为它是同步代码。当前的同步代码已经执行结束,执行栈被清空。事件循环首先检查微任务队列,发现有Promise的回调。'promise1' 和 'promise2' 依次被打印到控制台。微任务队列清空,事件循环现在检查任务队列。setTimeout 的回调现在被事件循环从任务队列移到执行栈,打印 'setTimeout'。
阅读 23·2024年6月24日 16:43

详细说明浏览器的缓存机制

浏览器的缓存机制主要是为了提高网页的加载速度,减少服务器的负载,并优化用户的浏览体验。浏览器缓存可以分为以下几种类型: 强缓存(HTTP Cache Control)强缓存不会向服务器发送请求,直接从缓存中读取资源。这是通过设置HTTP响应头中的 Cache-Control和 Expires实现的。Cache-Control: 这个响应头可以设置多个值,比如:max-age=xxx:表示资源在xxx秒后过期。no-store:不允许缓存,每次都要向服务器请求。no-cache:资源不会被缓存,每次都会发请求到服务器验证资源是否有更新。Expires: 这是一个绝对时间,表示资源的过期时间。协商缓存(Validation Cache)当强缓存失效后,浏览器会向服务器发送请求,询问资源是否有更新。这是通过 Last-Modified/If-Modified-Since和 ETag/If-None-Match这两对HTTP头实现的。Last-Modified/If-Modified-Since: 服务器响应时通过 Last-Modified标识资源最后修改时间,浏览器下次请求时带上 If-Modified-Since,服务器比较后如果没有变化则返回304状态码,浏览器继续使用缓存。ETag/If-None-Match: ETag是资源的一个唯一标识,类似于指纹。浏览器在请求时带上 If-None-Match,服务器对比ETag,如果没有变化则返回304状态码,浏览器继续使用缓存。Web Storage(本地存储)包括 localStorage和 sessionStorage,它们提供了在客户端存储键值对数据的能力。localStorage数据在浏览器关闭后依然存在,而 sessionStorage的数据在页面会话结束时被清除。IndexedDB是一种在浏览器中保存大量结构化数据的方式,可以创建、读取、遍历和搜索数据库中的记录。IndexedDB操作基于事件响应,与Web Storage相比,它可以提供更复杂的数据操作功能。Service WorkersService workers可以拦截请求,并可以使用缓存API来管理请求的响应。开发者可以编写自己的缓存策略,例如,当网络不可用时,可以从缓存中提供备份内容。举个例子,假设用户第一次访问一个网页,浏览器会下载所有资源,并按照HTTP头信息决定哪些资源应当被缓存。当用户再次访问这个网页时,如果相关资源具备有效的强缓存设置,浏览器会直接从缓存中加载资源,不经过服务器请求,这样可以极大提高页面加载速度。如果强缓存过期,浏览器会使用协商缓存机制与服务器通信,确认资源是否更新,从而决定是重新下载资源,还是继续使用缓存版本。
阅读 33·2024年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密集型操作进行优化的,特别是网络和文件系统操作。
阅读 48·2024年6月24日 16:43