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

前端面试题手册

Promise 和 async/await 和 Callback 有什么区别?

Promise、async/await 和 Callback 都是在 JavaScript 中处理异步操作的机制。每种技术都有其特点和适用场景。CallbackCallback 是一种较老的异步编程技术,它是将一个函数作为参数传递给另一个函数,并在那个函数执行完毕后调用。它最常见的用途是在进行文件操作或者请求网络资源时。优点:简单易懂,易于实现。缺点:容易导致 "回调地狱"(Callback Hell),即多个嵌套的回调函数使代码难以阅读和维护。错误处理不方便,需要在每个回调中处理错误。例子:fs.readFile('example.txt', 'utf8', function(err, data) { if (err) { return console.error(err); } console.log(data);});PromisePromise 是异步编程的一种解决方案,比传统的解决方案 —— 回调函数和事件 —— 更合理和更强大。它表示一个尚未完成但预期将来会完成的操作的结果。优点:提供了更好的错误处理机制,通过 .then() 和 .catch() 方法链。避免了回调地狱,代码更加清晰和易于维护。支持并行执行异步操作。缺点:代码仍然有些冗长。可能不够直观,特别是对于新手。例子:const promise = new Promise((resolve, reject) => { fs.readFile('example.txt', 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } });});promise.then(data => { console.log(data);}).catch(err => { console.error(err);});async/awaitasync/await 是建立在 Promise 上的语法糖,它允许我们以更同步的方式写异步代码。优点:代码更加简洁、直观。更容易理解,特别是对于习惯了同步代码的开发者。方便的错误处理,可以用传统的 try/catch 块。缺点:可能会导致性能问题,因为 await 会暂停函数的执行,直到 Promise 解决。在某些情况下不够灵活,例如并行处理多个异步任务。例子:async function readFileAsync() { try { const data = await fs.promises.readFile('example.txt', 'utf8'); console.log(data); } catch (err) { console.error(err); }}readFileAsync();总结来说,Callback 是最基本的异步处理形式,Promise 提供了更强大的控制能力和错误处理机制,而 async/await 是在 Promise 基础上提高代码可读性和减少样板代码的语法糖。
阅读 44·2024年6月24日 16:43

JavaScript 中什么是暂时性死区?

在JavaScript中,暂时性死区(Temporal Dead Zone,简称TDZ)是一个术语,用来描述在代码块中使用let或const声明变量后,和这些变量实际可以被访问之前的区域。在这个区域内访问这些变量会导致一个ReferenceError。暂时性死区的存在是因为let和const声明的变量不会像使用var声明的变量那样在代码执行前进行提升。变量提升(hoisting)是一个过程,其中变量和函数声明被移动到它们所在作用域的顶部。但对于let和const声明的变量,它们会被绑定到它们所在的块级作用域中,并且不会在声明之前存在于该作用域中。这里有一个例子来说明暂时性死区:console.log(a); // 这会抛出 ReferenceError,因为变量a还处于TDZ中let a = 3;console.log(a); // 这时不会有错误,因为变量a已经声明且初始化完毕在上面的代码中,在声明变量a之前就尝试打印它,这会导致一个ReferenceError,因为在那个点上变量a还在TDZ中。只有在let a = 3;这行代码执行后,变量a才会离开TDZ,从那时起它就可以被安全地访问了。TDZ的设计主要是为了捕获编程中的错误,如在变量声明之前就使用它们的情况,这有助于开发者发现潜在的问题,并使代码更加可靠。
阅读 23·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,并认为他们是等价的。如果使用===,就需要写更多的代码来处理类型检查和转换。不过,除非有非常清晰的理由,一般还是推荐使用===,因为它能让代码的行为更加可预测和清晰。
阅读 43·2024年6月24日 16:43

JavaScript 异步解决方案的发展历程以及优缺点

JavaScript的异步编程解决方案从早期的回调函数(Callbacks)开始发展,随后引入了Promises,再到现在的Async/Await。以下是每种解决方案的发展历程以及它们的优缺点: 回调函数(Callbacks)发展历程:最初,JavaScript中的异步编程依靠的是回调函数。这是一种将函数作为参数传递给另一个函数,并在后者完成异步操作后调用的技术。优点:简单直观:对于简单的异步操作,回调提供了一种直接的解决方案。广泛支持:所有的JavaScript环境都支持回调。缺点:回调地狱(Callback Hell):在复杂的应用中,回调可能嵌套得非常深,导致代码难以读懂和维护。难以处理错误:错误处理需要在每个回调中单独处理,不能很好地进行错误传递。例子:fs.readFile('example.txt', function(err, data) { if (err) { console.error('Error reading file'); return; } console.log(`File content: ${data}`);});Promises发展历程:为了解决回调地狱的问题,Promises被提出并在ES6中成为标准。Promise是一个代表着异步操作最终完成或失败的对象。优点:链式调用:能够通过 .then()和 .catch()进行链式调用,使得异步流程更加直观。错误处理:通过 .catch()方法可以集中处理错误。更好的控制:提供了更丰富的控制异步操作的方法,如 Promise.all()。缺点:代码可能还是有些冗长,特别是在多个异步操作依赖同一条件时。仍需要一定的学习曲线,特别是对于初学者。例子:new Promise((resolve, reject) => { fs.readFile('example.txt', (err, data) => { if (err) { reject('Error reading file'); } else { resolve(data); } });}).then(data => console.log(`File content: ${data}`)).catch(error => console.error(error));Async/Await发展历程:Async/Await是建立在Promises之上的,被引入在ES2017中。它们让异步代码看起来和同步代码更为相似。优点:代码可读性强:看起来就像是阻塞式的同步代码,虽然它是非阻塞的。易于理解和维护:使用 try/catch可以用同步代码的方式处理错误。简化了错误处理和条件语句。缺点:需要理解Promises,并且Babel转译对老JavaScript环境的支持可能有限。需要注意不要在非异步函数中使用 await,否则会导致编译错误。例子:async function readFileAsync() { try { const data = await fs.promises.readFile('example.txt'); console.log(`File content: ${data}`); } catch (error) { console.error('Error reading file'); }}readFileAsync();每一种异步解决方案都有其用武之地,随着JavaScript的发展,我们可以根据具体场景和个人偏好选择合适的方式来编写异步代码。
阅读 12·2024年6月24日 16:43

介绍事件代理以及优缺点,主要解决什么问题

事件代理是一种常用于处理事件监听的技术,通过这种技术,我们可以将事件的监听器(Event Listener)绑定到一个父级元素上,而不是直接绑定到实际的目标元素上。这样,当子元素的事件冒泡到父级元素时,就可以在父级元素上处理这些事件。优点:减少内存消耗: 由于只需要在父元素上绑定单一的事件监听器,而不需要为每个子元素单独绑定,减少了内存使用,提高了性能。动态内容的事件处理: 对于动态添加到页面的元素,比如通过Ajax加载的内容,无需重新绑定事件监听器,事件代理可以自动处理这些新元素的事件。简化事件管理: 当有大量元素需要相同的事件处理时,事件代理可以大大简化事件管理工作。缺点:事件冒泡依赖: 事件代理依赖于事件冒泡机制,如果事件不冒泡或者被中间某个节点的事件处理函数停止冒泡,则事件代理将不会生效。可能的性能影响: 如果父元素中有非常多的子元素或者事件处理逻辑较为复杂时,每次事件冒泡到父元素都需要进行事件对象的属性检查,可能会对性能造成影响。限制了某些事件的使用: 并不是所有的事件都能冒泡,例如focus、blur和load等,因此并不能利用事件代理处理这些事件。主要解决问题:事件代理主要解决了以下几个问题:内存和性能问题: 如上所述,减少了绑定在多个子元素上的事件监听器数量,从而节省了内存消耗,提高了性能。动态元素事件监听问题: 对于后来动态添加到页面的元素,不需要单独为它们添加事件监听器,因为它们会自然地冒泡到已经设置了事件代理的父元素。事件管理复杂性问题: 简化了事件处理逻辑,特别是当你有大量需要相似事件处理的元素时,只需要维护一个公共的事件监听器即可。例子:假设我们有一个任务列表,每个任务项都有一个"删除"按钮,可以删除对应的任务项。如果没有使用事件代理,我们可能需要为每个"删除"按钮绑定一个点击事件监听器。但是,如果使用事件代理,我们可以在任务列表的容器元素上绑定一个点击事件监听器,然后检查点击事件的目标是否是"删除"按钮,如果是,则执行删除操作。这样就无论任务列表如何变化,我们都只需要一个事件监听器。// HTML 结构<ul id="taskList"> <li>任务 1 <button class="delete-btn">删除</button></li> <li>任务 2 <button class="delete-btn">删除</button></li> // 更多任务项...</ul>// JavaScript 代码document.getElementById('taskList').addEventListener('click', function(event) { if (event.target.className === 'delete-btn') { event.target.parentNode.remove(); }});这个例子展示了如何实现一个简单的事件代理来处理动态内容。
阅读 7·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 } 作为第三个参数传递,确保了监听器在捕获阶段执行,并且只执行一次。
阅读 35·2024年6月24日 16:43

cros 的简单请求和复杂请求的区别是什么?

CORS,即跨源资源共享(Cross-Origin Resource Sharing),是一种允许在一个源(origin)上的网页获取访问另一个源上资源的机制。它是一种安全特性,可以让网站的前端代码安全地进行跨域请求,而不会暴露用户数据。CORS 请求分为两类:简单请求(simple requests)和复杂请求(preflighted requests)。它们之间的区别主要体现在请求的方式和所发送内容上。简单请求简单请求满足以下条件:请求方法是以下三种方法之一:GETPOSTHEADHTTP的头信息不超出以下几种字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)简单请求的例子:GET /some/resource HTTP/1.1Host: api.example.comAccept-Language: en-USContent-Type: text/plain当浏览器判断一个请求为简单请求时,它会直接发起跨域请求,并在请求中携带Origin头部信息。服务端会检查这个Origin,决定是否允许这个跨域请求。复杂请求复杂请求通常指不满足以上简单请求条件的所有其他请求,例如:使用了 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH 等 HTTP 方法。发送的 HTTP 头部信息超出了简单请求允许的范围。Content-Type 的值不属于简单请求中允许的三个值。在发送复杂请求之前,浏览器会先发起一个 OPTIONS 请求,这被称为“预检”请求(preflight request),用来确认真正的请求是否安全可被服务器接受。预检请求的例子:OPTIONS /data/resource HTTP/1.1Host: api.example.comOrigin: http://example.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: X-Custom-Header如果服务器允许这样的请求,它会在响应的 HTTP 头部中包含Access-Control-Allow-Origin、Access-Control-Allow-Methods和Access-Control-Allow-Headers等字段,明确告知客户端是否可以进行实际的请求。之后,浏览器才会发送实际的请求。总结简单来说,简单请求是对CORS更宽容的请求,直接发起并通过Origin头部判断是否允许跨域;而复杂请求需要先进行一次额外的预检通信以确认安全性,只有在预检通过后,实际的请求才会发起。这个机制确保了敏感操作(如对数据的修改)在跨域时能够得到恰当的安全检查。
阅读 23·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循环会更合适。
阅读 23·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 可以。
阅读 17·2024年6月24日 16:43

分别介绍事件冒泡、事件代理、事件捕获,以及它们的关系?

事件冒泡 (Event Bubbling)事件冒泡是一种事件传播机制,在这种机制下,当一个元素上的事件被触发时,这个事件会从触发元素开始,逐级向上传播至最外层的父元素。这种传播方式允许在父元素上监听并处理来自子元素的事件。事件冒泡通常用于减少事件处理器的数量,并且简化事件管理。例子: 假设我们有一个按钮(<button>)位于一个段落(<p>)元素内,该段落又位于一个容器(<div>)元素内。如果用户点击了按钮,那么点击事件会首先在按钮元素上触发,然后依次向上冒泡至段落元素,最终到达容器元素。事件代理 (Event Delegation)事件代理是一种借助事件冒泡机制实现的事件处理模式。它通过在父元素上设置一个事件监听器来管理所有子元素的同类事件。这样可以避免在每个子元素上单独设置事件监听器,从而提高效率和性能,尤其是在动态添加或删除子元素的情况下。例子: 假如我们有一个任务列表,列表中的每一项任务都需要一个点击事件监听器。使用事件代理,我们可以在任务列表的容器元素上设置一个点击事件监听器,而不是在每个任务项上单独设置。当点击事件发生并冒泡到容器元素时,我们可以检查事件的目标元素(event.target)来确定是哪个任务项被点击,并进行相应的处理。事件捕获 (Event Capturing)事件捕获是DOM事件流的另一部分,与事件冒泡相对应。在事件捕获阶段,事件从最外层的父元素开始传递,一直向下直到触发元素。主要的区别在于事件的传播方向:事件捕获是从外到内,而事件冒泡是从内到外。例子: 再次使用上面的按钮、段落和容器元素的场景,当用户点击按钮时,在事件捕获阶段,点击事件会首先到达最外层的容器元素,然后到达段落元素,最后到达按钮元素。它们的关系事件捕获和事件冒泡是DOM事件流的两个阶段。在实际的事件处理中,浏览器首先经过捕获阶段,从最外层的父元素向下传递到目标元素,然后是目标元素上的事件处理,接着是冒泡阶段,事件从目标元素开始向上逐级传播。事件代理则是利用了事件冒泡原理来简化事件管理。通过在父元素上监听事件,可以管理所有子元素的事件,而无需在每个子元素上单独绑定事件监听器,使得代码更加简洁高效。在使用 addEventListener 方法时,我们可以指定第三个参数为 true 或 false 来明确选择是在捕获阶段还是冒泡阶段处理事件,默认值为 false,即在冒泡阶段处理。总之,事件捕获和事件冒泡共同构成了事件传播的完整过程,而事件代理则是一种利用这种传播机制的高效事件处理策略。
阅读 7·2024年6月24日 16:43