概念

Q1:什么是DOM跟BOM

难度:⭐

答案

DOM(文档对象模型)和 BOM(浏览器对象模型)都是由浏览器提供的两个重要的 API(应用程序接口),用于处理和操纵网页内容以及浏览器窗口等相关功能。

DOM(文档对象模型):

  1. 定义:
    • DOM 是一种对文档的结构化表示,以树状的方式呈现 HTML 或 XML 文档,使开发者可以通过脚本语言(通常是 JavaScript)动态地访问和修改文档的内容、结构和样式。
  2. 主要功能:
    • 提供了一种将文档表示为树形结构的方式,每个 HTML 或 XML 元素都是树中的一个节点。
    • 允许开发者通过脚本语言操作文档的内容,例如添加、删除、修改元素。
    • 提供了一系列的 API,使开发者能够动态地操作页面的结构和样式,响应用户的交互。

BOM(浏览器对象模型):

  1. 定义:
    • BOM 是浏览器提供的一组对象,用于表示浏览器窗口和浏览器本身的各种信息,而不是文档的结构。
  2. 主要对象:
    • window 对象:表示浏览器窗口,包含了有关窗口的信息和方法。
    • navigator 对象:包含有关浏览器的信息,如浏览器类型和版本。
    • screen 对象:包含有关用户屏幕的信息,如屏幕宽度和高度。
    • location 对象:包含有关当前文档 URL 的信息,可以用于导航到其他页面。
    • history 对象:包含用户在浏览器窗口中访问的 URL 历史记录。

区别:

  • DOM 关注文档的内容和结构,提供了一种访问和操作文档的方式。
  • BOM 关注浏览器窗口和浏览器本身的信息,提供了一种管理浏览器窗口和与浏览器交互的方式


Q2:说说你对轮询的理解

难度:⭐

答案

什么是轮询?

轮询是一种用于获取信息或监视状态的计算机编程技术。在轮询中,程序会定期检查一个或多个资源的状态,以确定它们是否有可用数据或发生了特定事件。

长短轮询有什么区别?

  • 长轮询(Long Polling):客户端发送一个请求到服务器,服务器保持连接打开,直到有数据更新或超时才响应请求。如果超时,则客户端会立即发起新的请求。
  • 短轮询(Short Polling):客户端定期向服务器发送请求,询问是否有数据更新。服务器会立即响应,并在每次响应后,客户端都会立即发起新的请求。

实现轮询的方式:

  1. 传统轮询:客户端定期发送请求询问服务器是否有更新。
  2. 长轮询:客户端发送请求到服务器,服务器保持连接,直到有数据更新或超时才响应。
  3. WebSocket:通过双向通信通道实现实时数据传输,避免了轮询的延迟和资源浪费。

轮询的优缺点对比(使用表格方式):

优点缺点
- 简单易实现
- 兼容性强
- 灵活性高
- 资源浪费(频繁轮询可能造成服务器负担增加)
- 延迟高(不能立即检测到状态变化)
- 不适用于实时应用场景

如何避免轮询的缺点?

  1. 使用长轮询或WebSocket:长轮询和WebSocket可以降低延迟,提高实时性。
  2. 优化轮询频率:根据应用场景的需要调整轮询频率,避免不必要的资源浪费。
  3. 使用推送技术:使用服务器推送技术(如Server-Sent Events、WebSocket)可以在发生事件时立即将数据推送给客户端,避免了轮询的延迟和资源浪费。


Q3:给一个dom同时绑定两个点击事件,一个用捕获,一个用冒泡,说下会执行几次事件,然后会先执行冒泡还是捕获?

难度:⭐

答案
  1. 捕获阶段点击事件触发
  2. 冒泡阶段点击事件触发

这两个事件都会被执行一次。

引申

事件传播分为三个阶段:捕获阶段、目标阶段和冒泡阶段。当一个事件发生在 DOM 元素上时,它会经历这三个阶段。以下是对每个阶段的详细解释:

  1. 捕获阶段(Capturing Phase)
    • 事件从顶层的根节点向下传播到目标节点之前的阶段。
    • 在捕获阶段中,事件首先被捕获到最顶层的父节点,然后逐级向下传播到目标节点。
  2. 目标阶段(Target Phase)
    • 事件到达目标节点后的阶段。
    • 在目标阶段中,事件到达目标节点并在目标节点上触发。
  3. 冒泡阶段(Bubbling Phase)
    • 事件从目标节点开始向上冒泡到顶层的根节点的阶段。
    • 在冒泡阶段中,事件从目标节点开始,逐级向上传播到根节点。

事件传播过程中,如果某个阶段的处理函数调用了 stopPropagation() 方法,则会阻止事件继续传播到下一个阶段。如果某个阶段的处理函数返回 false,也会阻止事件继续传播到下一个阶段。否则,事件会继续传播到下一个阶段。

在实际开发中,可以利用事件传播的特性来实现事件委托(Event Delegation),即将事件绑定到父元素上,通过事件冒泡机制来处理子元素上的事件,从而提高性能和减少代码量。

总结:

  • 捕获阶段:从根节点向目标节点传播。
  • 目标阶段:在目标节点上触发。
  • 冒泡阶段:从目标节点向根节点传播。


Q4:直接在script标签中写 export 为什么会报错?

难度:⭐

答案

<script> 标签中直接写 export 会导致语法错误,因为 export 是 ECMAScript 模块的语法,而 <script> 标签内的代码通常被视为普通的 JavaScript 代码,不是模块。

如果要使用 export,需要将 JavaScript 代码放在一个模块中,并通过 <script type="module"> 标签引入,例如:

1
2
3
4
<script type="module">
// 此处可以使用 export
export const greeting = "Hello";
</script>

或者将 export 语句放在独立的 JavaScript 文件中,然后通过 <script src="your-script.js" type="module"></script> 引入。


Q5:mouseover 利 mouseenter 有什么区别?

难度:⭐

答案

mouseovermouseenter 是 DOM 事件中常见的鼠标事件,它们在触发时有一些区别:

  1. mouseover
    • mouseover 事件在鼠标指针从一个元素的外部移入到该元素或其子元素时触发。
    • 当鼠标指针进入目标元素的任何子元素时,也会触发 mouseover 事件。
    • 这个事件会冒泡,当鼠标指针穿过目标元素的多个子元素时,会在每个子元素上触发。
  2. mouseenter
    • mouseenter 事件在鼠标指针从一个元素的外部移入到该元素时触发,但不会在进入其子元素时触发。
    • 即使鼠标指针进入目标元素的子元素,也不会触发 mouseenter 事件。
    • 这个事件不会冒泡,只有在鼠标指针直接从外部移入目标元素时才会触发。

综上所述,主要区别在于 mouseover 事件在鼠标穿过目标元素的子元素时也会触发,而 mouseenter 事件只在鼠标直接从外部移入目标元素时触发,并且不会在鼠标进入目标元素的子元素时触发。


Q6 :offsetWidth/offsetHeight,clientWidth/clientHeight 与

scrollWidth/scrollHeight 的区别?

难度:⭐

答案

这些属性都是用于获取元素的尺寸信息,但它们之间有一些区别:

  1. offsetWidth/offsetHeight
    • offsetWidthoffsetHeight 分别返回元素的宽度和高度,包括元素的边框(border)和内边距(padding),以及垂直滚动条、水平滚动条(如果存在的话)的宽度和高度。
    • 这些值通常是相对于父元素的内容框(content box)的。
    • offsetWidth = width + border + padding + 滚动条宽度offsetHeight = height + border + padding + 滚动条高度
  2. clientWidth/clientHeight
    • clientWidthclientHeight 返回元素的内容框(content box)的宽度和高度,不包括边框和滚动条。
    • clientWidth = width + paddingclientHeight = height + padding
    • 这些值通常是相对于视口(viewport)的,即视口的宽度和高度。
  3. scrollWidth/scrollHeight
    • scrollWidthscrollHeight 返回元素的内容区域的总宽度和总高度,包括了元素内容区域的实际宽度和高度以及被隐藏部分的宽度和高度(如果有的话)。
    • 当元素内容区域大于其可视区域时,可以通过滚动来查看被隐藏的内容,此时 scrollWidthscrollHeight 将会大于 clientWidthclientHeight
    • 通常情况下,scrollWidthscrollHeight 会大于或等于 clientWidthclientHeight

综上所述,这些属性提供了不同类型的尺寸信息,其中 offsetWidth/offsetHeight 包含了元素的边框、内边距和滚动条的尺寸,clientWidth/clientHeight 只包含了元素的内容区域的尺寸,而 scrollWidth/scrollHeight 则包含了元素内容的实际宽度和高度以及被隐藏的部分。


Q7:JS中怎么阻止事件冒泡和默认事件?

难度:⭐

答案
  1. 阻止事件冒泡

    • 使用 event.stopPropagation() 方法来停止事件冒泡。这会阻止事件进一步传播到父元素或其他祖先元素。

    • 示例:

      1
      2
      3
      4
      element.addEventListener('click', function(event) {
      event.stopPropagation();
      // 这里的代码不会触发父元素的 click 事件处理程序
      });
  2. 阻止默认事件行为

    • 使用 event.preventDefault() 方法来阻止事件的默认行为。例如,阻止链接的默认点击行为、表单提交等。

    • 示例:

      1
      2
      3
      4
      element.addEventListener('click', function(event) {
      event.preventDefault();
      // 阻止链接的默认行为
      });
  3. 同时阻止事件冒泡和默认行为

    • 如果需要同时阻止事件的冒泡和默认行为,可以先调用return false 。在某些情况下,使用 return false 可以达到同时阻止事件冒泡和默认行为的目的,但需要注意这种方式并不是严格意义上的标准做法。

    • 示例:

      1
      2
      3
      4
      element.addEventListener('click', function(event) {
      return false;
      // 阻止事件冒泡和默认行为
      });


Q8:

难度:⭐

答案


Q9:谈谈你对事件冒泡和捕获的理解

难度:⭐

答案

事件冒泡(Event Bubbling)和事件捕获(Event Capturing)是 DOM 事件传播的两种不同的阶段。理解它们对于编写复杂的交互式网页应用程序至关重要。

事件冒泡(Event Bubbling):

事件冒泡是指当在 DOM 树中触发某个事件时,该事件将从最具体的元素(事件目标)开始逐级向上传播到最不具体的元素(document)。换句话说,事件首先触发在目标元素上,然后向上冒泡直到根节点(document)。

  • 事件从目标元素开始向上传播到父元素、祖父元素,一直到根节点。
  • 大多数事件都会冒泡,包括点击事件、键盘事件等。

事件捕获(Event Capturing):

事件捕获是事件传播的另一个阶段,它在事件冒泡之前发生。事件捕获从根节点(document)开始,逐级向下直到达到事件的实际目标元素。

img

  • 在事件捕获阶段,事件从根节点向下传播到目标元素。
  • 在事件捕获阶段触发的事件处理程序会在目标元素的祖先元素上触发。

事件传播的三个阶段:

  1. 捕获阶段(Capture Phase):事件从根节点向下传播到目标元素。
  2. 目标阶段(Target Phase):事件在目标元素上触发。
  3. 冒泡阶段(Bubble Phase):事件从目标元素向上传播到根节点。

事件处理:

当一个元素上触发了某个事件时,事件首先在捕获阶段触发该元素的事件处理程序,然后在目标阶段触发该元素上的事件处理程序,最后在冒泡阶段触发该元素的父级元素的事件处理程序。

使用场景:

  • 事件代理(Event Delegation):事件代理通常利用事件冒泡机制,将事件处理程序绑定在父元素上,以处理大量子元素的事件,提高性能和代码简洁性。
  • 控制事件的传播:了解事件冒泡和捕获可以更好地控制事件的传播行为,有助于编写更灵活、高效的交互式应用程序。

综上所述,事件冒泡和捕获是理解 DOM 事件传播机制的关键概念,掌握它们可以更好地处理和控制事件流。


Q10:ES6有什么新特性

难度:⭐⭐

答案
  1. 箭头函数

    箭头函数提供了更简洁的函数定义语法,并且自动绑定了 this 关键字

  2. let 和 const

    let 和 const 关键字用于声明变量,let 声明的变量具有块级作用域,而 const 声明的变量是常量,其值无法再次赋值

  3. 模板字符串

    使用反引号 `` 包裹的字符串,可以在其中插入变量和表达式,实现更灵活的字符串拼接

  4. 解构赋值

    可以通过解构赋值语法,从数组或对象中提取值并赋给变量,使代码更简洁

  5. **扩展运算符和剩余参数:

    使用 ... 来表示扩展运算符和剩余参数,用于处理可变长度的参数列表或数组

  6. 类和继承

    ES6 引入了类的概念,使得在 JavaScript 中可以更方便地使用面向对象的编程风格,并支持类的继承

  7. Promise

    Promise 是一种处理异步操作的方式,可以更清晰地表示异步操作的完成或失败,并使用链式调用来处理异步操作的结果

  8. 模块化

    ES6 引入了模块化的语法,可以更好地组织和管理 JavaScript 代码,使其更易于维护和重用

  9. 新的数据结构

    ES6 提供了新的数据结构,如 Set、Map、WeakSet、WeakMap,用于存储数据并支持高效的查找和操作

  10. Iterator 和 Generator

    Iterator 是一种统一的遍历接口,而 Generator 是一种通过函数简化迭代器的定义的方式。

  11. Symbol

    Symbol 是一种新的基本数据类型,用于创建唯一的标识符,可以用作对象的属性名,从而避免属性名冲突

  12. Proxy 和 Reflect

    Proxy 对象用于定义自定义的行为,如拦截对象的读取、写入和删除等操作,而 Reflect 对象提供了一组与 Proxy 对象相关的方法


Q11:xml和json什么区别

难度:⭐

答案

XML(可扩展标记语言)和 JSON(JavaScript 对象表示法)都是用于表示和传输数据的格式,它们有以下区别:

  1. 语法:
    • XML 使用标签来描述数据结构,每个标签包含开始标签和结束标签,例如 <name>John</name>
    • JSON 使用键值对的形式来描述数据结构,键和值之间使用冒号分隔,键值对之间使用逗号分隔,最外层通常是一个对象或数组。
  2. 可读性:
    • JSON 更加简洁和易读,因为它使用了更轻量级的语法,适合于数据交换和传输。
    • XML 的语法相对冗长,标签名、属性、值等都需要以文本形式表示,不如 JSON 直观。
  3. 数据类型:
    • JSON 支持对象(键值对)、数组、字符串、数字、布尔值和 null。
    • XML 支持更多的数据类型,包括字符串、数字、布尔值、日期、时间、文本、元素、属性等。
  4. 扩展性:
    • XML 是可扩展的,允许使用者自定义标签和属性,适用于定义复杂的数据结构和领域特定语言。
    • JSON 的结构相对固定,不支持自定义标签和属性,只能通过对象和数组来组织数据。
  5. 解析和处理:
    • 在 JavaScript 中,JSON 更容易解析和处理,因为它可以直接转换为 JavaScript 对象。
    • XML 需要使用 DOM 解析器或 SAX 解析器进行解析,相对来说处理起来更加繁琐。

综上所述,JSON 更加轻量、简洁、易读,并且更适合于数据交换和传输。而 XML 则更加灵活,支持更多的数据类型和自定义标签,适用于定义复杂的数据结构和领域特定语言。选择使用 JSON 还是 XML 取决于具体的应用场景和需求。


Q12:为什么JavaScript是单线程?

难度:⭐

答案

JavaScript 是一种单线程语言,这意味着它在任何给定的时间只能执行一个任务或代码块。这种设计是由 JavaScript 的最初用途所决定的,即作为网页上交互式脚本的语言。下面是一些原因解释为什么 JavaScript 是单线程的:

  1. 简单性和一致性:单线程模型使得 JavaScript 的行为更加简单和可预测。它不需要开发者担心多线程之间的竞态条件、死锁等复杂的并发问题。
  2. 网页交互:JavaScript 最初是为了在浏览器中操作网页元素而设计的。在这种情况下,多线程并发可能会导致混乱和不可预测的结果,例如多个脚本同时尝试修改同一个 DOM 元素。
  3. 防止阻塞:JavaScript 在浏览器中是由浏览器引擎负责解释和执行的。如果 JavaScript 是多线程的,那么一个线程的阻塞可能会影响到其他线程的执行,从而导致用户界面的卡顿和不流畅。

虽然 JavaScript 本身是单线程的,但是通过使用事件循环和异步编程模型,JavaScript 可以利用回调函数、Promise、async/await 等机制来处理异步操作,从而实现非阻塞的并发执行。这样的设计使得 JavaScript 在处理网络请求、定时器、用户输入等异步任务时可以更高效地利用资源,同时保持了简单性和可预测性。

引申

JavaScript 的事件循环(Event Loop)是一种处理程序执行、事件和调度消息的过程

它允许 JavaScript 在执行长时间的任务时,仍然可以处理其他的事件,例如用户输入、脚本加载等

在深入事件循环之前,首先需要了解 JavaScript 的运行环境是单线程的

这意味着在同一时间内只能执行一个任务

为了协调这些任务,使得高延迟操作不会阻塞线程,事件循环得以产生

事件循环的机制如下:

  1. 调用栈(Call Stack)
    • 执行的所有代码块(函数调用)都按顺序进入一个“调用栈”
    • 当 JS 引擎首次执行脚本时,全局代码作为一个主要的块首先被推送到栈中
    • 每当一系列函数调用发生时,它们会按照调用顺序被推入栈中,并且当函数执行完毕,返回结果后,它们会从栈中被弹出
  2. 任务队列(Task Queue)
    • 当异步事件完成时,例如:HTTP 请求、文件读取、setTimeout 等,相应的回调函数会被添加到“任务队列”中
    • 如果调用栈为空,即所有当前任务已经执行完成,事件循环就会从任务队列中取出回调函数并推入调用栈来执行
    • 有哪些宏任务?
      • setTimeout
      • setInterval
      • setImmediate (主要用在 Node.js)
      • I/O 操作(如文件读写、网络请求等)
      • UI 渲染事件 (例如 requestAnimationFrame,在浏览器环境中)
      • MessageChannel
      • 主脚本的执行 (HTML 页面加载完毕后,浏览器中的全局执行上下文)
  3. 微任务队列(Microtask Queue)
    • 微任务队列是一个处理比任务队列优先级更高的任务的队列
    • 当一个 Promise 被解决或拒绝时,相应的 .then().catch().finally() 处理程序会被添加到微任务队列中
    • 微任务队列在每个宏任务执行完毕后会完全清空
    • 有哪些微任务?
      • Promise.thenPromise.catchPromise.finally 处理函数
      • queueMicrotask (这是一个显式将任务排入微任务队列的方法)
      • MutationObserver (浏览器中,用于观察 DOM 变化的回调)
      • process.nextTick (在 Node.js 中)

任务队列的执行逻辑是这样的:

  • 当一个宏任务执行完毕后,JavaScript 引擎会查看是否有微任务需要执行

    如果微任务队列中有任务,那么它们会被依次执行,直到微任务队列为空

  • 如果微任务执行过程中产生新的微任务,这些新的微任务也会被加入微任务队列并在本轮循环中执行完毕

  • 微任务全部执行完毕后,渲染进程(在浏览器中)将有机会更新渲染,然后事件循环将进入下一个宏任务执行

    在此之前,可能会处理其他的UI事件、操作等

  • 接着,下一个宏任务开始执行,之后该宏任务的微任务,依此类推

整个这个循环确保了异步任务的有效执行,同时允许通过微任务和宏任务


事件循环的过程简化如下:

  1. 执行全局脚本

  2. 执行调用栈中的同步代码

  3. 如果调用栈为空,检查微任务队列

    如果微任务队列不为空,执行微任务,直到队列为空

  4. 取出任务队列中的下一个任务,推入调用栈中执行

  5. 重复上述流程

由于微任务的优先级高于普通任务,所以在任何新的宏任务被处理之前,微任务队列会被完全清空

这包括在微任务中创建的微任务

而对于宏任务来说,每执行完一个宏任务,都会检查并清空微任务队列,然后再执行下一个宏任务

最后,由于 JavaScript 的这个单线程非阻塞的性质,它适合处理 I/O 密集型的操作,而不是 CPU 密集型的操作,因为复杂的计算可能会长时间占据 JS 线程,造成界面不流畅或卡顿

在 Web 应用中,影响用户体验是需要避免的

因此理解和合理地利用事件循环,对于编写高效的 JavaScript 代码至关重要


Q13:”严格模式”是什么?

难度:⭐

答案

“严格模式”(Strict Mode)是 ECMAScript 5 引入的一种 JavaScript 执行模式,它提供了更加严格的语法和错误检查,有助于编写更安全、更规范的 JavaScript 代码。启用严格模式可以帮助开发者避免一些常见的错误,并且提高代码质量和性能。

启用严格模式的方法是在代码的顶部(全局作用域)或者函数体的开头(函数作用域)添加如下语句之一:

1
"use strict";

或者,在函数作用域内使用:

1
2
3
4
function myFunction() {
"use strict";
// 函数体
}

严格模式对 JavaScript 的一些行为做了限制,其中一些主要的变化包括:

  1. 禁止使用未声明的变量:在严格模式下,使用未声明的变量会抛出 ReferenceError 错误。
  2. 删除不可删除的属性:在严格模式下,删除不可删除的属性会抛出 TypeError 错误。
  3. 禁止使用八进制表示法:在严格模式下,八进制数值的表示方式会被视为错误。
  4. 禁止对只读属性赋值:在严格模式下,对只读属性赋值会抛出 TypeError 错误。
  5. 函数参数名唯一性:在严格模式下,函数参数名不能重复。
  6. 禁止使用 with 语句:在严格模式下,禁止使用 with 语句,因为它会导致作用域链被修改,增加代码的不可预测性。
  7. 保留关键字:在严格模式下,一些在非严格模式下可以使用的关键字变成了保留字,不能作为变量名、函数名或参数名等标识符使用。

启用严格模式的好处包括:

  • 帮助开发者捕获更多的错误,提高代码的健壮性。
  • 使得 JavaScript 引擎可以更有效地优化代码,提高性能。
  • 促使开发者遵循更严格的编程规范,减少不规范的代码写法。
  • 为将来的 ECMAScript 版本引入新特性提供了更好的准备。

因此,建议在 JavaScript 代码中尽可能使用严格模式,以获得更好的代码质量和执行性能。


Q14:Node跟Element是什么关系?

难度:⭐⭐

答案

在DOM(文档对象模型)中,Node 和 Element 是两个重要的接口,它们之间有一定的层次关系。

  1. Node(节点):
    • Node 是 DOM 树中的基本构建块,代表文档树中的一个节点。Node 接口定义了所有节点类型的通用属性和方法。文档中的所有元素、属性、文本等都是节点。
    • ElementNode 的子接口,因此每个 Element 对象也是 Node 对象。Node 接口提供了操作文档树的通用方法,比如查找父节点、子节点,添加、删除节点等。
  2. Element(元素):
    • Element 接口表示文档中的元素,如 <div><p><span> 等。Element 继承自 Node,因此具备了 Node 接口的所有属性和方法。
    • Element 接口还提供了一些专门用于处理元素的属性和方法,如获取元素的标签名、设置和获取元素的属性、获取元素的子元素等。

在层次结构中,ElementNode 的一种特殊情况。所有的元素都是节点,但并非所有的节点都是元素。其他类型的节点包括文本节点、注释节点、文档节点等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Node and Element</title>
</head>
<body>

<!-- Example Element -->
<div id="exampleElement">This is an example element.</div>

<script>
// Accessing the element using JavaScript
var elementNode = document.getElementById('exampleElement');

// Checking if it's a Node
if (elementNode instanceof Node) {
console.log('This is a Node.');
}

// Checking if it's an Element
if (elementNode instanceof Element) {
console.log('This is an Element.');
}
</script>

</body>
</html>

在上面的示例中,document.getElementById('exampleElement') 返回的是一个 Element 对象,它同时也是一个 Node 对象。通过 instanceof 运算符,我们可以检查对象是否是 NodeElement 的实例。


Q15:说说你对DOM树的理解

难度:⭐⭐⭐

答案

文档对象模型(Document Object Model,简称DOM)是一种表达和操作HTML、XML等文档结构的方式。DOM 将文档解析为一个由节点组成的树状结构,每个节点代表文档中的一个元素、属性、文本等。

以下是对DOM树的一些理解:

  1. 树状结构: DOM 将文档表示为一个树状结构,其中树的根是文档节点,树的每个分支代表文档的不同层次结构,叶子节点代表文档中的具体元素或内容。
  2. 节点: 树中的每个元素都是一个节点。节点可以是元素节点、属性节点、文本节点等。元素节点表示HTML或XML中的标签,属性节点表示元素的属性,文本节点表示元素包含的文本内容。
  3. 层次结构: DOM 树按照文档的层次结构组织,每个节点有父节点、子节点和兄弟节点。根节点是文档节点,它没有父节点,而其他节点通过层次关系相互连接。
  4. 实时性: DOM 是动态的,可以通过脚本语言(如JavaScript)来操作。可以通过脚本动态地添加、修改或删除节点,从而改变页面的结构和内容。
  5. 接口: DOM 提供了一种通过编程方式访问和操作文档的接口。通过这些接口,开发者可以获取节点、修改节点的属性和内容、添加新节点等。
  6. 跨平台性: DOM 是与平台和编程语言无关的标准,因此可以在各种环境和语言中使用。在浏览器中,通过JavaScript可以直接访问和操作DOM。
  7. 事件模型: DOM 提供了事件模型,允许开发者对用户交互和其他事件进行监听和响应。例如,可以通过DOM来捕获用户的点击事件、键盘事件等。
引申
  1. 分词器将字节流转化为 Token:
  • 字节流: HTML 文档以字节流的形式从服务器传输到浏览器。
  • 分词器工作: 浏览器使用分词器(Tokenizer)将字节流转换为一系列 Token。Token 是解析过程的基本单元,包括标签 Token 和文本 Token。分词器根据 HTML 规范逐个读取字符,生成相应的 Token。
  1. Token解析为 DOM 节点
  • DOM 节点: Token 被解析为 DOM 节点,每个 Token 对应一个节点。节点包括元素节点、文本节点、属性节点等。
  • 构建 DOM 树: 浏览器通过将 Token 解析为相应的 DOM 节点来构建 DOM 树。元素节点表示 HTML 元素,文本节点表示元素包含的文本内容,属性节点表示元素的属性。
  1. 将 DOM 节点添加到 DOM 树中:
  • DOM 树的构建: 在解析过程中,浏览器逐步构建 DOM 树,树的根节点是 <html> 元素,它有子节点 <head><body>
  • 节点关系: 构建过程中,建立节点之间的父子关系。每个元素节点都成为其父节点的子节点,文本节点则成为相应元素节点的子节点。
  • 完整的 DOM 树: 构建完成后,得到一个完整的 DOM 树,表示了 HTML 文档的结构和层次关系。

这三个阶段是解析和构建 DOM 树的关键步骤,DOM 树的构建是为了将文档结构化表示,以便浏览器进一步处理、布局和渲染到用户界面。


Q16:Javascript跟Css是怎么阻塞DOM树构建的?

难度:⭐⭐⭐

答案
  1. JavaScript 阻塞 DOM 树构建:
    • 当浏览器解析到 <script> 标签时,它会立即停止 HTML 解析,然后下载并执行 JavaScript 代码。
    • 如果 JavaScript 代码位于文档的头部(即在 <head> 中),它可能会阻塞 DOM 树的构建,因为浏览器会等待 JavaScript 代码执行完成才能继续解析 HTML。
    • 这种情况下,用户可能会看到一个白屏或加载延迟,因为 DOM 树的构建被阻塞,直到 JavaScript 执行完毕。
  2. CSS 阻塞 DOM 树构建:
    • 如果浏览器解析到外部样式表(通过 <link> 标签或 @import)或在文档头部的内联样式(在 <style> 标签中),它会开始下载和解析 CSS 文件。
    • 如果 CSS 文件很大或者在网络上加载耗时,它可能会阻塞 DOM 树的构建,因为浏览器希望尽早获取和应用样式信息以正确渲染页面。
    • 类似于 JavaScript 阻塞,这也可能导致页面加载延迟。

为了解决这些阻塞问题,可以采取以下措施:

  • 将 JavaScript 放在底部:<script> 标签放在文档底部,确保 HTML 解析不会因为 JavaScript 而阻塞。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    e<!DOCTYPE html>
    <html>
    <head>
    <!-- CSS 样式表 -->
    <link rel="stylesheet" href="styles.css">
    </head>
    <body>
    <!-- 页面内容 -->

    <!-- JavaScript 脚本 -->
    <script src="script.js"></script>
    </body>
    </html>
  • 使用 asyncdefer 属性: 对于 <script> 标签,可以使用 asyncdefer 属性,使 JavaScript 异步加载和执行,减少对 DOM 树构建的阻塞。

    1
    2
    3
    <script src="script.js" async></script>
    <!-- 或 -->
    <script src="script.js" defer></script>
  • 优化 CSS 文件加载: 尽可能减小 CSS 文件的大小,考虑使用浏览器缓存等策略,以减轻对 DOM 树构建的影响。


Q17:什么是变量提升

难度:⭐⭐

答案

变量提升(Hoisting)是 JavaScript 中的一种特性,它会将变量声明提升到当前作用域的顶部,但不会提升变量的赋值。

在 JavaScript 中,变量提升的规则如下:

  1. 变量声明(以及函数声明)会被提升到当前作用域的顶部,但赋值操作不会被提升。这意味着在声明变量之前就可以访问这些变量,但访问时会返回 undefined
  2. 变量提升只影响声明本身,不会影响作用域内的代码执行顺序。
  3. 如果变量名重复声明,后面的声明会覆盖前面的声明。

例如,以下代码演示了变量提升的行为:

1
2
3
4
5
6
7
console.log(a); // undefined
var a = 10;

// 上面的代码实际上被 JavaScript 引擎理解为:
var a;
console.log(a); // undefined
a = 10;

在上面的例子中,变量 a 在声明之前被访问,但由于变量提升的影响,代码不会报错,而是打印出 undefined。这是因为变量声明 var a; 被提升到了作用域的顶部,但赋值操作 a = 10; 并未被提升。


Q18:JavaScript中的事件模型有哪些

难度:⭐⭐

答案

JavaScript 中的事件模型通常指的是 DOM 事件模型,它描述了在网页中处理和触发事件的机制。在 DOM 事件模型中,事件分为捕获阶段、目标阶段和冒泡阶段,而 JavaScript 提供了一些方法来注册和处理这些事件。以下是常见的几种事件模型:

  1. DOM0级事件模型
    • 在 DOM0 级事件模型中,事件处理程序直接赋值给 DOM 元素的属性。
    • 示例:element.onclick = function() { // 事件处理逻辑 }
    • 特点:简单直接,适用于单一的事件处理。
  2. DOM2级事件模型
    • 在 DOM2 级事件模型中,通过 addEventListenerremoveEventListener 方法来注册和移除事件处理程序。
    • 示例:element.addEventListener('click', function() { // 事件处理逻辑 }, false)
    • 特点:支持多个事件处理程序,可以为同一个元素的同一种事件注册多个处理程序;支持事件捕获和冒泡;更加灵活和标准化。
  3. IE 事件模型
    • IE 浏览器早期采用了与标准不同的事件模型,通过 attachEventdetachEvent 方法来注册和移除事件处理程序。
    • 示例:element.attachEvent('onclick', function() { // 事件处理逻辑 })
    • 特点:与 DOM2 级事件模型不同,IE 事件模型不支持事件捕获,只支持事件冒泡;事件处理程序的执行顺序与注册顺序相反。
  4. 事件委托模型
    • 事件委托模型是利用事件冒泡的特性,将事件处理程序绑定在父元素上,通过判断事件目标来执行相应的逻辑。
    • 特点:减少了事件处理程序的数量,提高了性能;适用于需要处理大量相似事件的情况,比如列表或表格中的事件处理。


Q19: 什么是同步和异步

难度:⭐⭐

答案

在 JavaScript 中,同步异步是两个描述代码执行模式的术语,它们用于处理代码的执行流程。理解这两个概念对编写高效、响应迅速的应用程序非常重要。

同步(Synchronous):

  • 定义:同步代码是指代码按照编写的顺序执行,前一个任务完成后,才会执行后一个任务。这种模式下,代码会阻塞在一个任务上,直到任务完成。
  • 执行方式:JavaScript 是单线程的,因此在同步代码中,所有代码在同一个线程中按顺序执行。如果某个任务(例如计算密集型任务或 I/O 操作)耗时较长,它会阻塞后续代码的执行,导致用户界面卡顿或响应迟钝。
  • 示例:例如,如果你在代码中执行一个循环,而循环内有一个密集计算操作,整个循环执行期间程序将一直阻塞在这个操作上。
1
2
3
4
5
console.log('开始');
for (let i = 0; i < 1000000000; i++) {
// 密集计算任务
}
console.log('结束');

在这个示例中,密集计算任务会阻塞程序,导致第二个 console.log 语句的执行延迟。

异步(Asynchronous):

  • 定义:异步代码是指不按照顺序执行任务,而是让任务在后台进行。程序可以在等待任务完成的同时继续执行其他代码,这样的机制可以避免阻塞。
  • 执行方式:JavaScript 使用事件循环来管理异步任务。这意味着在执行同步代码的同时,异步任务可以在后台进行。一旦异步任务完成,它将通过回调函数、Promise、async/await 等方式来通知主线程。
  • 示例:例如,使用 setTimeout 来延迟执行某个代码块。
1
2
3
4
5
console.log('开始');
setTimeout(() => {
console.log('异步任务完成');
}, 1000);
console.log('结束');

在这个示例中,console.log('结束') 会在 setTimeout 的回调函数执行之前立即执行。因此,”结束” 会先于 “异步任务完成” 输出。

异步编程的常用方式:

  • 回调函数:使用函数作为参数传递给异步操作,当异步操作完成时调用。
  • Promise:一种表示异步操作最终完成或失败的对象,允许链式处理异步任务。
  • async/await:基于 Promise 的异步语法糖,使异步代码更类似于同步代码。


Q20:CSS动画和JS实现的动画分别有哪些优缺点?

难度:⭐⭐

答案

CSS 动画和 JavaScript 实现的动画是两种常用的网页动画方式,它们各有优缺点

理解它们的特性和适用场景可以帮助你在项目中做出最佳的选择

CSS 动画

优点:

  1. 性能优越:CSS 动画通常会被 GPU 加速,这可以减少 CPU 的负担,提高动画的流畅度。
  2. 简单易用:CSS 动画通过定义样式来实现,代码简洁明了,不需要过多的 JavaScript 逻辑。
  3. 自动化:CSS 动画可以在元素进入和离开 DOM 时自动触发,例如通过 :hover:focus 等伪类。
  4. 不阻塞页面渲染:CSS 动画不会阻塞页面渲染,因为它们是异步执行的。
  5. 一致性:不同浏览器对 CSS 动画的实现一致,提供了良好的跨浏览器兼容性。

缺点:

  1. 控制有限:CSS 动画的功能较为简单,不如 JavaScript 实现的动画灵活
  2. 事件监听受限:CSS 动画很难直接与 JavaScript 的事件监听器互动(可以通过监听 animationend 事件解决)
  3. 调试困难:CSS 动画的调试和调整通常比 JavaScript 实现的动画更困难

JavaScript 实现的动画

优点:

  1. 高度灵活:JavaScript 实现的动画可以控制动画的细节和行为,提供了更高的灵活性
  2. 交互性强:JavaScript 动画可以与其他事件(如点击、鼠标移动等)和数据源(如 API)结合,实现更复杂的交互
  3. 自定义:可以通过 JavaScript 控制动画的整个生命周期,包括开始、暂停、恢复、停止等
  4. 链式调用:JavaScript 动画库(如 GSAP)通常支持链式调用,允许你方便地创建复杂的动画序列

缺点:

  1. 性能可能较差:如果处理不当,JavaScript 动画可能导致性能问题,如 CPU 负载高、动画不流畅等
  2. 代码复杂度高:JavaScript 实现的动画通常需要更多的代码和逻辑来管理,代码复杂度较高
  3. 动画可能阻塞页面渲染:如果 JavaScript 动画在主线程中运行,它可能会阻塞页面渲染,导致用户体验下降

选择的权衡

  • 对于简单的动画(例如元素的淡入淡出、滑动效果等),并且不需要高度灵活性,CSS 动画通常是最佳选择
  • 对于需要高度交互和定制化的动画,或者需要与其他 JavaScript 逻辑密切配合的动画,JavaScript 是更好的选择
  • 无论选择哪种方式,确保动画的性能和流畅性是关键。此外,可以结合两者的优势,CSS 用于简单的动画效果,而 JavaScript 用于更复杂的逻辑和交互


Q21:堆与栈有什么区别

难度:⭐⭐

答案

栈(Stack)

  1. 管理方式

    栈由编译器自动管理,无需程序员手动控制

  2. 内存分配和回收

    遵循后进先出(LIFO,Last In First Out)原则,意味着最后分配的内存块会被最先释放

  3. 速度

    栈内存的分配和回收速度非常快

  4. 大小限制

    栈的大小在程序启动时已经定好,因此空间有限

  5. 用途

    主要用于存放局部变量、函数参数和返回地址等

  6. 生命周期

    栈内存中的对象通常在其定义的代码块执行结束后即失效

堆(Heap)

  1. 管理方式

    堆内存的分配和释放需要程序员通过代码手动管理(或通过垃圾回收机制自动管理,例如在 Java、Python 等语言中)

  2. 内存分配和回收

    内存分配更为灵活,可以在任何时候申请和释放内存

  3. 速度

    相比于栈,堆内存的分配和回收速度较慢

  4. 大小限制

    堆的大小受到系统可用内存的限制,理论上比栈要大得多

  5. 用途

    主要用于存储程序运行中动态分配的大块内存,比如用来存储由new操作符创建的对象和数组

  6. 生命周期

    堆内存中的对象生命周期不由代码块控制,而是依赖于引用和垃圾回收机制

核心区别

  • 管理方式

    栈是由系统自动分配释放,而堆则需由程序员控制或依赖于垃圾回收机制

  • 性能

    栈操作更快但有大小限制,而堆内存更灵活但分配和回收速度较慢

  • 用途差异

    栈通常用来存储执行线程的临时变量,而堆则用来存储程序运行时动态分配的内存


Q22:Service worker是什么

难度:⭐⭐

答案

Service Worker 是一种在Web浏览器中运行的脚本,它充当客户端(例如Web应用)与网络之间的代理服务器

它可以帮助你通过以下方式控制网络请求、缓存资产、以及提供能够在网络不可用时仍能正常使用网站或应用的能力

这些特性在PWA(渐进式Web应用)中尤为关键

下面是 Service Worker 的一些主要功能和特点:

  1. 离线体验

    Service Worker 可以拦截并缓存网络请求,使得网站能够在无网络状态下运行,从而提供更好的离线体验

  2. 背景同步

    即使页面关闭后,Service Worker 仍然可以同步数据,等你下次上线时更新状态或进行通知

  3. 网络请求优化

    通过缓存一些重复的资源请求,Service Worker 能够优化应用的加载时间和性能

  4. 推送通知

    Service Worker 有能力接收服务器的推送消息,并将这些消息作为系统通知展示给用户,即使网页没有打开

  5. 生命周期独立

    Service Worker 与 Web 页面的生命周期是独立的,它可以在 Web 页面关闭后仍然活跃,与服务器进行通信

  6. 不直接操作DOM

    Service Worker 运行在其自己的上下文中,无法直接操作DOM。它通过发送消息与页面通信

  7. 安全性

    Service Worker 由于其强大的能力,要求在 HTTPS 环境下才能工作,以确保传输内容的安全性

  8. 可编程网络代理

    开发者可以编写 Service Worker 中的事件响应器来自定义处理所有浏览器的网络请求

由于 Service Worker 在浏览器后台作为一种独立的脚本运行,因此它能在你访问网页时启动,并且即使用户关闭了网页,它还可以打开新的网页

这为Web应用提供了更强的背景处理能力,大幅度扩展了Web的功能和可用性


Q23:JSBridge是什么

难度:⭐⭐

答案

JSBridge 是一种在移动应用的 WebView 中使用的技术,它允许 JavaScript(通常运行在 WebView 中的前端代码)与原生 app 代码(如 AndroidiOS)进行通信

Bridge 即是桥梁,指的是它作为前端代码和原生代码之间的桥梁,使得这两部分可以互相调用功能和数据

在开发过程中,通常会遇到一些网页功能无法直接实现或效率不高的问题,这时候就需要原生的支持

然而,网页端的 JS 无法直接访问手机系统的原生功能,如相机、文件系统等

JSBridge 提供了这样的一种机制,允许网页端通过定义的协议发出调用指令,由原生端接收这个指令,执行相应的原生操作,并将结果返回给网页端

举个例子,如果你想在 WebView 中实现一个按钮,点击后打开手机相册并选择图片,你可能需要通过 JSBridge 来调用手机系统的相册功能

JavaScript 中,你可能会调用一个特定的函数或发送一个特定的消息,并通过 JSBridge,这个请求被原生应用捕获并处理,最终原生应用可以打开相册,用户选择图片后,原生代码再把选中的图片通过桥梁传递回 JS

JSBridge 核心包含以下几个步骤:

  1. 消息发送

    网页端 JS 发起调用,发送消息

  2. 消息拦截

    原生端拦截这些特定的消息/调用请求

  3. 执行原生方法

    原生端执行对应的原生操作

  4. 回调

    原生端处理完成后,通过桥梁将结果返回给网页端 JS,可能是通过回调函数或者是事件

这种机制在很多移动应用中都有广泛的应用,特别是在混合应用(Hybrid App)开发中,混合应用是指同时包含原生界面元素和 WebView 的应用程序

使用 JSBridge 可以使得开发者既可以利用 web 技术的便捷和跨平台特性,又可以让应用有更丰富的功能,更好的性能和用户体验

引申
  • JSBridge的实现原理是什么

    • URL Scheme

      这是最早期的实现方式之一,通过拦截WebView的URL请求实现

      • JavaScript调用原生

        JavaScript端通过修改location.href为特定的URL Scheme(如myapp://functionName?param1=value1&param2=value2),然后原生代码可以通过WebView的代理方法拦截到这个URL请求,解析出要调用的函数和参数,执行相应的原生操作

      • 原生调用JavaScript

        原生代码执行完毕后,可以通过调用WebView的方法(如Android的webView.loadUrl("javascript:methodName(params)"),iOS的webView.evaluateJavaScript("methodName(params)", completionHandler: nil))来执行JavaScript端的方法,以此来传递结果或者触发页面的更新

    • JavaScriptInterfaceAndroid特有)

      在Android中,可以通过向WebView添加JavaScript的接口来实现JSBridge

      即通过WebView.addJavascriptInterface(Object, String)方法,将一个Java对象映射到JavaScript环境中,JavaScript代码就可以直接调用这个Java对象的方法

      • JavaScript调用原生

        定义一个Java对象,其中的方法使用@JavascriptInterface注解标注,然后添加到WebView中。JavaScript通过映射的对象直接调用这些方法

      • 原生调用JavaScript

        同上,通过webView.loadUrl("javascript:...")webView.evaluateJavaScript方法调用

    • MessageChannelpostMessage (现代方法)

      这是一种更现代的、基于HTML5的交互方式,适用于进行更复杂的数据交换

      • JavaScript调用原生

        通过WebView的postMessage方法发送消息,原生代码通过相应的监听器接收并处理这些消息

      • 原生调用JavaScript

        原生代码处理完业务逻辑后,可以通过调用postMessage方法向JavaScript发送消息,JavaScript端监听并处理这些消息

不同的实现方式各有优缺点,比如URL Scheme简单但有安全风险,JavaScriptInterface易用但只限于Android,而MessageChannel或postMessage方式则更为安全和灵活,但需要较新的浏览器支持

在实践中,开发者需要根据自己的具体需求和目标平台的特点选择合适的实现方式


Q24: JavaScript 中内存泄漏有哪几种情况

难度:⭐⭐

答案

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存

并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

image-20240510182435283

在JavaScript中,内存泄漏可能有以下几种常见情况:

  1. 全局变量意外赋值

    未经声明就赋值的变量会自动成为全局对象的属性,这意味着它们不会被当作局部变量进行垃圾收集

  2. 被遗忘的计时器或回调函数

    如果你设置了定时器或者间隔(如 setTimeoutsetInterval)并忘记清除它们,或者绑定了事件监听器并且没有适当移除,它们会一直存在,并且保持对它们回调函数中变量的引用

  3. 脱离了DOM的引用

    如果你保存了一些DOM元素的引用然后删除了这些元素,除非这些引用也被明确地设置为null,否则它们不会被垃圾回收器回收

  4. 闭包

    不正确或者不必要地使用闭包,可以造成父级函数作用域链中的变量无法被释放,特别是在闭包被长期保持的时候

  5. 循环引用

    在早期的IE浏览器中,JavaScript中的对象和DOM对象之间的循环引用会导致内存无法释放

    现代浏览器通过改进垃圾收集器来处理此问题,但在某些情况下循环引用仍然可能是问题

为了避免内存泄漏,建议进行以下几个步骤

  • 明确生命周期

    了解和规划你的代码和对象的生命周期以确保及时释放

  • 使用工具和分析

    利用浏览器提供的开发工具来监控和分析内存使用情况

  • 减少全局变量使用

    尽可能通过局部作用域和模块化来避免全局变量

  • 清理定时器和监听器

    确保用不到的时钟或者事件监听被清理掉

  • 管理DOM引用

    移除DOM元素时注意也要清理掉对应的JavaScript引用

引申

什么是垃圾回收机制

垃圾回收(Garbage Collection,GC)是一种自动内存管理的形式

它的作用是回收程序中不再使用的内存空间,防止内存泄漏导致的资源浪费

大多数现代编程语言,包括JavaScript,都提供了某种形式的垃圾回收机制

垃圾回收主要基于这样一个事实:在程序运行过程中,有些对象会变得不再可达,即不存在任何方式来引用它们。这通常是因为对象已经超出了其作用域,或者没有任何变量或属性引用它

JavaScript中的垃圾回收通常由以下几个策略和算法实现:

  1. 标记-清除(Mark-and-Sweep)
    这是最常见的垃圾回收算法。当变量进入环境时,垃圾收集器会将它“标记”(通常设置一个位),以说明这个变量目前是活跃的。随后,收集器会去“扫描”内存中的所有变量,并标记那些还在被引用或者在作用域中可达的变量。之后,清除阶段启动,此时收集器会进行再次扫描,销毁那些在扫描阶段没被标记为活跃的变量,并回收它们占据的内存
  2. 引用计数(Reference Counting)
    这是一种较早的垃圾回收算法,在该算法中,每个对象都有一个引用计数器。当有一个变量引用该对象时,引用计数增加;当引用被移除时,计数减少。如果一个对象的引用计数变为0,则意味着对象不再被引用,可以将其内存回收。然而,引用计数有一个主要问题,即循环引用。如果两个对象相互引用,即使它们都已不可访问,它们的引用计数也不会是0,导致内存无法释放
  3. 分代收集(Generational Collection)
    这种算法是基于这样的事实:大多数对象都是短暂的。因此,它会将对象分为两组,新生代(Young Generation)和老年代(Old Generation)。新生代中的对象经常进行垃圾回收,因为许多对象都很快就不再需要了,而老年代中的对象不那么频繁进行回收,因为它们通常存活时间更长。在实际操作中,如果一个新生代的对象在多次收集后依然存活,它可能会被移至老年代
  4. 增量收集(Incremental Collection)
    由于GC会暂停所有代码的执行,如果堆内存很大,这可能会导致明显的暂停。增量收集算法会将垃圾收集分成小片段执行,减少每次收集导致的停顿时间。这样,垃圾回收与应用程序代码可以交替执行
  5. 空闲时间收集(Idle-time Collection)
    某些垃圾回收系统只会在CPU空闲时执行,减少对程序执行的影响

JavaScript的垃圾收集是自动的,而我们作为开发者通常无需直接管理内存

但了解和遵循良好的编码习惯可以减少内存泄漏和过早的垃圾收集,从而提高应用程序的性能


Q25:什么是事件代理

难度:⭐⭐

答案

事件代理是一种利用事件冒泡原理来简化事件处理器管理的技术

在DOM中,如果一个元素发生了事件(比如点击),这个事件会依次冒泡至其所有的父元素,直至文档的根元素

事件代理就是在父元素上设置一个事件监听器,监听来自子元素的事件


文字例子

想象你在一个图书馆里,有一个专门的儿童阅读区,这个区域里有很多本绘本供孩子们阅读

每当一个孩子对某本书感兴趣,并想要借阅时,他们都会举手示意

为了管理这些请求,图书管理员不会挨个去询问每一个举手的孩子想要阅读哪本书,而是只在儿童区设置一个“服务点”

每当有孩子举手时,他们就会到这个服务点告诉管理员他们想要的书

这样,无论儿童区有多少孩子想要借书,管理员只需要在一个地方就能管理所有的请求


代码例子

假设我们有一个按钮列表,每个按钮点击时都会触发一个事件

不使用事件代理,我们可能需要为每个按钮单独添加事件监听器

使用事件代理,我们只需要在它们的父元素上添加一个事件监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul id="buttonList">
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
</ul>

<script>
document.getElementById('buttonList').addEventListener('click', function(event) {
// 检查事件来源是否是按钮
if (event.target.tagName === 'BUTTON') {
alert(event.target.textContent + ' 被点击了');
}
});
</script>

好处

  • 减少内存使用:不需要为每个子元素单独绑定事件监听器,只在共同的父元素上绑定一个监听器即可
  • 方便代理动态元素:对于动态添加到父元素中的子元素,无需再次手动添加事件监听器

局限性

  • 不是所有事件都能冒泡:适合事件委托的事件有:clickmousedownmouseupkeydownkeyupkeypress。某些事件(如focusblur等)不会冒泡,对于这些事件,事件代理不适用
  • 事件对象的target属性可能需要额外处理:在某些情况下,需要确切知道事件具体是在哪个子元素上被触发,这时可能需要对event.target进行额外的检查和处理
  • 细粒度控制较困难:如果只想对特定的子元素进行事件处理,而这些元素并没有共同的父元素,用事件代理的方式就比较困难
引申

Javascript事件流

JavaScript中的事件流描述了从页面中接收事件的顺序

事件流可以分为两种主要的模型:事件冒泡(Event Bubbling)和事件捕获(Event Capturing)

这两种事件流模型的主要区别在于事件触发的顺序


事件冒泡(Event Bubbling)

事件冒泡是最常见的事件流模型,在这个模型中,当一个事件触发在DOM中的某个元素上时,这个事件会逐级向上传播至其所有的祖先元素直到文档的根元素(通常是document对象)

例如,如果你点击了一个按钮,那么首先这个点击事件会在按钮上触发,然后冒泡到按钮的父元素,然后是更高一级的父元素,一直冒泡到document对象


事件捕获(Event Capturing)

事件捕获则是另一种事件流模型

事件捕获的顺序与事件冒泡相反,当一个事件发生后,浏览器首先会从document对象开始捕获事件,然后通过DOM树向下传递到事件实际发生的位置

事件捕获的目的是在事件到达预定目标前先捕获它


DOM标准事件流的三个阶段

DOM事件标准定义了事件处理的三个阶段:

  1. 捕获阶段(Capturing phase):从document对象传导到事件目标的路径上的对象开始捕获事件
  2. 目标阶段(Target phase):实际的事件目标对象对事件作出反应
  3. 冒泡阶段(Bubbling phase):从事件目标对象传导回document对象的路径上的对象开始处理事件


使用addEventListener进行事件处理

使用addEventListener方法添加事件处理程序时,你可以指定第三个参数来决定是在捕获阶段还是冒泡阶段触发该处理程序

如果第三个参数设置为true,那么事件处理程序将在捕获阶段触发,如果设置为false(或者不设置,因为默认值就是false),事件处理程序将在冒泡阶段触发

1
2
3
4
5
6
7
8
9
// 在冒泡阶段触发
element.addEventListener('click', function(event) {
// handle click event
}, false);

// 在捕获阶段触发
element.addEventListener('click', function(event) {
// handle click event
}, true);

了解事件流对于开发者来说至关重要,因为它直接影响事件处理程序的行为和程序的整体性能

通过合适的使用事件捕获和冒泡,开发者可以更精确地控制事件处理的策略


Q26:什么是作用域、作用域链

难度:⭐⭐

答案
  • 作用域

    作用域是指程序中定义变量的区域,该位置决定了变量的可见性和生命周期

    它限定了一个变量的使用范围

    在JavaScript中,作用域可以大致分为两种:全局作用域和局部作用域

    • 全局作用域

      在代码中任何位置都能访问到的变量,都处于全局作用域

      一般来说,在最外层定义的变量或者未经声明直接赋值的变量(这是不推荐的做法),都是全局作用域的变量

    • 局部作用域

      只能在定义它的函数内部访问到的变量

      局部作用域可以进一步分为函数作用域和块级作用域(ES6新增)

      • 函数作用域

        在函数内部声明的变量,只能在该函数内部被访问

      • 块级作用域

        {}包括的区域定义的作用域,使用letconst声明的变量在相应的块级作用域中有效

  • 作用域链

    当代码在一个环境中执行时,会创建变量的作用域链

    作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问

    作用域链的前端,始终都是当前执行的代码所在环境的变量对象,如果这个环境是函数,则将其活动对象作为变量对象

    活动对象在最开始时只包含一个变量,即arguments对象(这个对象包含了调用函数时传入的所有参数)

    作用域链向上逐级查询直到全局执行环境,全局环境的变量对象始终是作用域链的最后一个对象

    • 作用域链的工作机制

      当代码需访问一个变量时,JavaScript引擎首先会尝试在当前执行环境的变量对象中查找标识符

      如果没找到,继续在上一层作用域的变量对象中查找

      一直向上直到全局执行环境

      如果在整个作用域链中都没有找到标识符,则表明该变量未声明

  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var globalVar = "global"; // 全局变量

    function outer() {
    var outerVar = "outer"; // 局部变量,函数outer的作用域

    function inner() {
    var innerVar = "inner"; // 局部变量,函数inner的作用域

    console.log(innerVar); // "inner"
    console.log(outerVar); // "outer"
    console.log(globalVar); // "global"
    }

    inner();
    }

    outer();

    inner函数中,尝试访问innerVarouterVarglobalVar

    首先,在inner的作用域中找到了innerVar

    由于在inner作用域中找不到outerVar,它会去上一级作用域,即outer函数的作用域中查找,找到了outerVar

    同样的,globalVarinnerouter的作用域中都找不到,所以它会继续向上查找直到全局作用域,最后在全局作用域中找到了globalVar

    这个过程就是作用域链的体现:从当前作用域开始,逐级向上查找变量,直到找到为止,或者最终在全局作用域结束查找

    这确保了在函数嵌套的情况下,内部函数可以访问到外部函数以及全局变量


Q27:说一下对于SPA的理解

难度:⭐⭐⭐

答案

单页面应用程序(Single Page Application,SPA)是一种浏览器中运行的应用程序模型

在这种模型中,用户在浏览器中加载一个HTML页面,然后所有的交互都通过JavaScript完成,这些JavaScript代码会动态更新页面上的部分,而不是加载新页面

这种模式提供了一种更接近原生应用的用户体验

当你打开一个SPA,大部分资源(HTML + CSS + Scripts)都会在一次加载中完成

之后的每次操作,例如点击一个链接或者提交表单,都不会引发页面的重新加载

相反,JavaScript将运行,执行必要的操作(可能是AJAX请求到服务器获取新数据),并在当前页内动态更新HTML

这种页面渲染方式避免了页面间切换时常规的网络请求,大大提高了页面交互的速度和效率

典型的SPA框架有React、Angular、Vue.js等,他们提供了构建用户界面的集合工具,帮助你处理页面的动态更新等问题

然而,SPA不是没有缺点

它对于SEO(搜索引擎优化)不太友好,因为搜索引擎可能会不理解或者执行你的JavaScript代码,导致某些内容无法被搜索引擎抓取到

而且,由于它需要一次性加载所有的代码和资源,可能会导致初次加载速度比较慢

最后,SPA的安全性也需要额外的关注,因为大部分代码都在客户端执行,因此可能存在安全风险


优点

  1. 改进的用户体验

    由于用户在使用SPA时无需等待页面重新加载,因此可以提供平滑的用户体验,类似于使用桌面应用程序或原生移动应用程序

  2. 减少服务器负载

    由于服务器不需要重新处理HTML页面和发送响应,所以服务器只需处理数据请求,降低了服务器的负载和带宽使用

  3. 快速的响应时间

    页面不需要重新加载所有资源,只需加载新数据,这通常可以提供比传统多页应用更快的响应时间

  4. 前后端分离

    SPA架构促进了前后端分离发展,前端负责用户界面和用户体验,后端负责数据管理和业务逻辑。这使得开发工作可以更好地协作分工

  5. 简化的调试过程

    在SPA中,开发者可以只关注单一页面,使用Chrome开发者工具等辅助工具进行调试变得更加容易

  6. 流畅的跨设备体验

    SPA能够适应不同的设备,而不要专门为移动设备或桌面设备开发不同的版本

  7. 易于维护

    由于应用由模块化、互相独立的组件构建,开发者更容易添加新功能或者更新现有功能

  8. 缓存效率

    SPA通过发送单一页面,可以有效地缓存本地数据或资源,因此可以离线使用

  9. 实时交互

    SPA适合需要实时数据更新的应用,如游戏、绘图应用或社交网络,可以提供较好的实时交互体验


缺点

  1. SEO(搜索引擎优化)问题

    传统的SPA往往难以被搜索引擎有效索引,因为它们的内容是动态通过JavaScript加载的,这可能导致搜索引擎抓取工具难以抓取到所有内容

  2. 首次加载时间较长

    尽管SPA在页面间的切换上非常快,但其首次加载时,需要加载应用的所有脚本和资源,这可能导致相比于多页应用(MPA)更长的加载时间

  3. 浏览器的前进/后退按钮可能不工作

    由于SPA在单个页面内动态更改内容,没有加载新页面,所以对浏览器的前进和后退按钮支持可能不那么直观。开发者需要额外实现这些功能,以保证用户体验

  4. 内存利用问题

    单页应用可能会因为长时间运行而消耗大量浏览器内存,尤其是当应用未正确管理内存时(如未清理定时器或事件监听器)

  5. 安全性问题

    SPA可能更容易受到某些类型的安全攻击,如跨站脚本(XSS)攻击,因为所有页面逻辑都由客户端JavaScript控制

  6. JavaScript依赖性

    如果用户禁用了浏览器的JavaScript,或者浏览器不支持当前使用的JavaScript特性,那么SPA将无法正常工作

  7. 难以调试

    SPA由于大量用到JavaScript和异步请求,可能在出现问题时难以追踪和调试

  8. 用户体验一致性

    SPA的表现和行为完全依赖于客户端设备的性能,这导致不同用户可能会有不同的体验,特别是在老旧或性能较低的设备上

引申

多页应用(MPA)是传统的Web应用模式,在这种模式中,每当页面跳转发生时,服务器将会提供一个新的页面

这跟单页面应用(SPA)不同,在SPA中,页面不会重新加载,只会动态地替换内容

以下是MPA的一些具体特点:

  1. 全页刷新
    • 在MPA中,用户的每一个操作,如链接点击、表单提交等,都可能导致整个页面的重新加载或跳转到一个新页面
    • 这可能会导致更明显的用户等待时间,因为客户端每次都需要从服务器加载新的HTML、CSS和JavaScript
  2. SEO友好
    • 因为每个页面都有独立的链接,搜索引擎可以很容易地爬取和索引每一个页面
    • 这是MPA的一个强大优势,尤其适合那些需要良好搜索引擎优化的大型网站
  3. 前后端耦合
    • 在MPA应用中,后端不仅负责业务逻辑和数据库操作,还负责控制页面的渲染
    • 这种耦合方式简化了开发流程,适合小的团队和小到中端的项目
  4. 开发工具和框架
    • 开发MPA可以采用各种后端语言(如PHP、Java、Ruby等)和框架(如Express.js、Django
特点SPAMPA
单一页面加载
用户体验一般
SEO优化需要优化
首次加载速度一般,取决于项目大小
页面切换速度一般
前后端分离可以实现
缓存管理一般
浏览器历史导航需要优化
初始开发复杂性较高一般
状态管理可以实现
服务器负载
依赖JavaScript一般


Q28:原型、原型链是什么

难度:⭐⭐⭐

答案

原型(Prototype)

在JavaScript中,原型是一种让对象继承属性和方法的机制。每个JavaScript对象(除了null)都具有一个特殊的内置属性,称为[[Prototype]],但在代码中通常通过__proto__属性的形式来访问(虽然这种方式现在被JavaScript社区视为过时和不推荐。在ES6中,Object.getPrototypeOf()方法可以更标准地获取对象的原型

更正式地,所有JavaScript对象都是通过引用一个原型对象来继承属性和方法的当你创建一个新对象时,你可以选择某个对象作为它的原型。JavaScript提供了Object.create方法来创建一个新对象,同时让你指定这个对象的原型

原型链(Prototype Chain)

原型链是JavaScript的一种基本工作机制,用于在对象之间共享属性和方法。当你尝试访问一个对象的某个属性时,如果这个对象本身没有这个属性,JavaScript会自动去其原型(即__proto__属性指向的对象)中查找。如果这个原型对象也没有这个属性,那么JS将继续搜索这个原型的原型,以此类推,直至找到该属性或者到达原型链的末尾(Object.prototype的原型是null)。

原型链的这种机制让对象可以共享方法和属性,极大地节省了内存资源,因为这意味着JavaScript中的所有对象实例可以共享它们的构造函数的原型上的属性和方法,而不是在每个对象实例中复制一份。

image-20240511173637440

示例

为了更好地理解,让我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
function Person() {
}

Person.prototype.sayHello = function() {
console.log('Hello');
};

var person1 = new Person();
var person2 = new Person();

person1.sayHello(); // 输出:Hello
person2.sayHello(); // 输出:Hello

在这个例子中:

  • Person是一个构造函数

  • 通过Person.prototype.sayHelloPerson的原型添加了一个sayHello方法

  • person1person2都是Person的实例

    它们是通过new Person()创建的,因此它们的原型都是Person.prototype

  • 当调用person1.sayHello()时,JS首先检查person1自身有没有sayHello方法

    没有找到时,它会沿着原型链往上查找,即查找Person.prototype是否有这个方法

    找到后就执行这个方法


Q29:对JS模块化方案的理解

难度:⭐⭐⭐

答案

JavaScript模块化方案是寻求在逻辑上划分代码,并通过公开和获取进口接口来组合这些代码块的一种方法

这意味着,我们可以将复杂的代码分离成可维护的小文件,并且这些文件可以在不同的项目之间轻松重用

引申

在JavaScript中,有几种不同的模块化标准:

  1. CommonJS

    • 概念

      CommonJS是一个在服务器端对模块的定义,特别是为了Node.js设计的

      它的目标是弥补JavaScript没有标准库的缺陷,以及提供一个模块化的标准,以便JavaScript也能在服务器端运行

    • 核心思想

      使用require函数来同步加载依赖,使用module.exportsexports来导出模块

      由于是设计给服务器端使用的,因此其同步的本质不会造成问题,因为服务器端的文件通常都是本地可访问的,而无需像在浏览器端那样从网络上异步加载

    • 这是Node.js使用的模块标准,它允许使用require语句来加载模块,并通过module.exportsexports导出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // lib.js
    function libMethod() {
    return 'This is a library method';
    }
    module.exports = libMethod;

    // app.js
    const libMethod = require('./lib');
    console.log(libMethod());
  2. AMD (Asynchronous Module Definition)

    • 概念

      AMD即异步模块定义,它是一种针对浏览器而设计的模块化方案

      AMD采用异步方式加载模块,是为了解决浏览器环境下模块代码可能需要从服务器异步加载的问题

    • 核心思想

      通过define函数定义模块,require函数来异步加载依赖

      AMD最大的特点就是支持浏览器端的异步加载,且允许指定回调函数,以便在所有需要的模块都加载完成后进行操作

    • 主要用于异步加载模块,并在浏览器端使用,RequireJS是实现AMD规范的著名库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // lib.js
    define([], function() {
    function libMethod() {
    return 'This is a library method';
    }
    return libMethod;
    });

    // app.js
    require(['lib'], function(libMethod) {
    console.log(libMethod());
    });
  3. ES Modules (ESM)

    • 概念

      ESM是ECMAScript标准的一部分,是JavaScript的官方模块系统

      它设计之初就考虑到了代码的静态分析,允许浏览器和服务器(如Node.js)对模块进行优化载入

    • 核心思想

      通过importexport语句来导入导出模块

      ESM支持静态导入也支持动态导入(通过import()表达式)

      ESM的模块是单例的,且导入导出是实时绑定的,并且ESM模块是异步解析的

    • 这是现代浏览器支持的原生JavaScript模块系统,利用importexport关键字实现模块化

    1
    2
    3
    4
    5
    6
    7
    8
    // lib.js
    export function libMethod() {
    return 'This is a library method';
    }

    // app.js
    import { libMethod } from './lib.js';
    console.log(libMethod());
  4. UMD

    • 概念

      UMD是一种兼容CommonJS和AMD的模块定义方式,它使得模块可以在AMD环境、CommonJS环境以及全局变量使用场景下运行

    • 核心思想

      UMD通过一个立即执行的函数表达式(IIFE)来检测当前环境支持哪模块方案,并相应地初始化模块

      它试图提供一个在前后端都可以通用的方案,实现代码的最大兼容性

    • UMD是一种支持两种模块化标准(CommonJS和AMD)的模式,以便模块能够在AMD环境、CommonJS环境和全局变量的使用环境中使用

    • UMD尤其适合那些要在浏览器和服务器(比如Node)上运行的模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // lib.js
    (function(root, factory) {
    if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define([], factory);
    } else if (typeof exports === 'object') {
    // Node, CommonJS-like
    module.exports = factory();
    } else {
    // Browser globals (root is window)
    root.returnExports = factory();
    }
    }(typeof self !== 'undefined' ? self : this, function() {
    // Actual module code
    function libMethod() {
    return 'This is a library method';
    }
    return libMethod;
    }));

    // app.js (CommonJS)
    const libMethod = require('./lib');
    console.log(libMethod());

    // app.js (AMD)
    require(['lib'], function(libMethod) {
    console.log(libMethod());
    });

    // app.js (Browser Global)
    console.log(returnExports());

ESM是目前推荐的JavaScript模块化标准,因为它是JavaScript语言的官方标准,得到了现代浏览器和Node.js的原生支持,无需额外的工具或编译

模块化使得开发者能够构建大型、复杂的应用程序,通过模块划分可以让不同的功能和组件彼此隔离,变得更易于理解和调试

同时也促进了社区共享代码,例如通过npm上的包

特点/标准CommonJSAMDESMUMD
环境Node.js浏览器现代浏览器和Node.js浏览器和Node.js
异步加载不支持支持支持支持(依条件)
导入语法requiredefine/requireimport根据环境使用require或define
导出语法module.exportsreturnexportmodule.exports或return
本地变量支持支持支持支持
循环依赖支持有限支持支持支持
标准非官方标准非官方标准ECMA标准综合式非官方标准
加载方式同步异步同步和异步适应性
浏览器支持通常需要打包工具通常需要RequireJS原生支持,可能需要编译无需特定库或工具
执行时机立即执行延时执行导入时执行根据模块系统选择立即执行或延时执行


Q30:JavaScript对象里面的可枚举性是什么

难度:⭐⭐⭐

答案

在JavaScript中,对象属性(包括数据属性和访问器属性)有一个名为“可枚举性”的特性(enumerable)

可枚举性决定了一个属性是否能够出现在对象的枚举属性中,也就是说,它是否可以通过某些循环或方法(如for...in循环、Object.keys()方法、JSON.stringify()函数等)来访问

如果一个属性的可枚举性特性为true,那么它就可以被这些操作发现或访问

反之,如果可枚举性为false,则这些操作会忽略该属性

这里有一个例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建一个对象
var obj = {};

// 在对象上定义一个不可枚举的属性
Object.defineProperty(obj, 'nonEnumerableProperty', {
value: 'This is non-enumerable',
enumerable: false,
});

// 在对象上定义一个可枚举的属性
Object.defineProperty(obj, 'enumerableProperty', {
value: 'This is enumerable',
enumerable: true,
});

// 输出对象的所有可枚举属性
for (let key in obj) {
console.log(key + ': ' + obj[key]);
}
// 输出结果: "enumerableProperty: This is enumerable"

在这个例子中,enumerableProperty的可枚举性为true,所以它可以在for...in循环中被访问到

nonEnumerableProperty的可枚举性为false,所以它在循环中被忽略

需要注意的是,尽管可枚举性决定了属性是否可以通过for...in循环或Object.keys()方法等方式访问,但并不阻止通过点标记法(.)或方括号([])来直接访问属性

即使属性的可枚举性为false,我们仍然可以直接获取或设置其值

1
2
console.log(obj.nonEnumerableProperty); 
// 输出结果:"This is non-enumerable"

简而言之,可枚举性特性控制了一些枚举操作如何与属性交互,而不是控制访问属性的能力


Q31:对函数式编程的理解

难度:⭐⭐⭐

解析
答案

函数式编程(Functional Programming, FP)是一种编程范式,它将计算视为数学函数的评估,并避免状态以及可变数据

与面向对象编程强调对象包含数据和行为不同,函数式编程强调了应用函数和函数之间的组合,以创建更复杂的函数和过程

优点:

  1. 易于测试和调试

    因为纯函数遵循相同输入相同输出的原则,不依赖于程序中的其他状态,这使得纯函数更容易进行单元测试。

  2. 更好的模块化

    函数式编程鼓励将大问题分解为小问题,通过函数组合的方式将小函数组合起来解决复杂的问题。

  3. 更少的副作用

    函数式编程的纯函数减少了意外的副作用,使得程序的状态更加可控,降低了程序运行过程中出现bug的风险。

  4. 并行处理

    不可变数据和无副作用函数意味着没有线程之间的冲突,可以更安全地进行并行代码的编写。

  5. 代码的可读性

    通过函数组合可以写出声明式的代码,意图更加明确,可读性更好。

缺点:

  1. 学习曲线

    对于习惯了命令式编程的开发者来说,需要时间适应函数式编程的思维方式

  2. 性能问题

    递归和不可变数据结构可能导致性能下降,尤其是在那些不自动进行尾调用优化的环境中

  3. 内存使用

    纯粹函数式编程使用不可变数据,可能会增加内存的使用量,因为每次数据修改都会创建一个新的数据副本

  4. 语言支持

    不是所有编程语言都原生支持函数式编程概念,有些语言可能只能通过库来实现,这可能会制约函数式编程的应用

  5. 递归的复杂性

    在一些场景下,使用递归比迭代循环更难以理解,且如果递归深度过大也可能导致栈溢出

  6. 资源消耗

    某些函数式编程操作,比如尤其在组合多个函数操作大数据集时,可能会导致计算资源(CPU/内存)的高消耗

引申

函数式编程的一些关键特征和概念:

  1. 不可变性(Immutability)

    在函数式程序中,状态是不可变的。一旦创建了数据结构(如对象、数组等),它就不能被修改。任何“修改”操作都会产生一个新的数据结构,而原有的数据结构保持不变。

  2. 纯函数(Pure Functions)

    函数的输出仅由输入决定,不依赖于程序的状态(无状态)、不修改程序状态,也不具有可观察的副作用,使得它们更易于推理与测试

  3. 函数组合(Function Composition)

    小函数可以组合成更复杂的函数,就像在数学中的函数组合(f o g)

    这种方式可以构建出复杂的操作,而每一个操作都由简单清晰的函数构成

  4. 高阶函数(Higher-Order Functions)

    函数可以作为参数或返回值传递。这使得抽象和重复的逻辑能够被轻易地重用

  5. 惰性评估(Lazy Evaluation)

    计算会被推迟直到绝对需要结果,这可以提升性能,通过避免不必要的计算,并能处理无限数据结构(比如无限列表)

  6. 递归(Recursion)

    在函数式编程中,循环被递归结构所替代

    因为没有可变状态,函数式编程利用递归来执行重复任务或迭代数据结构

  7. 模式匹配(Pattern Matching)

    这通常用于FP中,可以简化对数据结构的解构和分析,是一种更直观的数据访问与处理方式

  8. 尾调用优化(Tail Call Optimization)

    尾调用是指函数的最后一个动作是返回另一个函数调用

    尾调用优化降低了递归函数的空间复杂度,从而避免栈溢出

函数式编程的目标是使用这些原则和技术来创造更可预测、更少出错并易于测试的软件

然而,实际应用时,完全的不可变性或纯函数可能难以实现或低效

因而,现代函数式编程语言和库通常提供了一些实用主义的策略,以便更好的融入实际问题解决之中

其中,JavaScript并不是一种纯粹的函数式编程语言,但是它支持许多函数式编程的特性,并且社区内有大量的库来应用函数式编程概念


命令式编程(Imperative Programming)是一种计算机编程范式,它通过详细描述计算机需要执行的步骤来更改程序状态

这一范式是通过编写一系列指令来告诉计算机如何达到期望的结果,这些指令会改变程序的状态

命令式编程可以视为一系列的命令,指导计算机完成特定的操作,类似于烹饪食谱中的步骤,每个步骤告诉你如何做

特征

详细性: 在命令式编程中,开发人员需要给出获取输出的确切步骤指令

状态变化: 程序由一系列的状态组成,各个命令将导致状态的变化

控制结构: 包括循环、条件分支、顺序结构等,控制程序的执行流程

变量: 功能上类似于存储数据的 “容器”,它们的值可以改变

迭代: 重复执行同一块代码,直到满足特定条件

常见语言

许多流行的编程语言是支持命令式编程的:

  • C
  • C++
  • Java
  • Python(虽然它同时也支持其他编程范式)

优点与缺点

优点:

  • 直观性: 思考和解决问题的方式更符合我们进行日常任务时的顺序逻辑
  • 控制性: 开发者可以控制程序的每一步,使得可以进行微观管理

缺点:

  • 复杂性: 当应用程序变得复杂时,管理和理解所有的状态变化变得困难
  • 可维护性: 代码重复和难以追踪的状态可能导致程序难以维护
  • 并发性: 变化的状态使得处理并发任务更加复杂

命令式编程和函数式编程最主要的区别在于,命令式编程关注如何执行,而函数式编程关注什么需要被执行

在函数式编程中,状态的变化被尽量避免,而命令式编程则大量依赖于状态变化

在实际的工作中,很多语言和项目会融合多种编程范式,以适合不同的用例和优化开发体验


Q32:对闭包的理解

难度:⭐⭐⭐

答案

闭包(Closure)在编程中是一个非常重要的概念,尤其是在JavaScript等支持词法作用域的编程语言中

闭包的定义

闭包是函数和声明该函数的词法环境的组合

它允许一个函数访问并操纵函数外部的变量

即使外部函数执行完毕,闭包仍然能记住和访问函数外部的变量

闭包的优点包括:

  • 数据封装性

    闭包可以帮助创建私有变量,其他代码不能直接访问这些变量,只能通过提供的方法来操作

  • 持久性

    常规的局部变量在函数执行完后会被销毁,但闭包中的数据可以维持状态,即使外部函数调用结束后仍然存在

  • 记忆状态

    闭包可以记住它被创建时的环境,这使得它非常适合在如实现迭代器或生成器等场景

闭包的缺点包括:

  • 内存消耗

    因为闭包可能会长时间保存变量,所以它可能会导致比普通函数更大的内存消耗

  • 复杂性

    对于不熟悉闭包的开发者来说,闭包可能会带来不必要的复杂性和理解障碍

  • 调试困难

    由于闭包的特殊作用域,有时候调试它们的状态和行为可能会比较困难

闭包的使用场景:

  • 事件处理

    在JavaScript中,闭包广泛用于事件监听和处理中,以便在事件回调中使用在外部函数中定义的数据

  • 模拟私有方法和变量

    在JavaScript中模拟对象的私有成员

  • 函数柯里化

    闭包可以用于函数柯里化,创建一个新函数,这个新函数持有一些已设定的参数

  • 模块化

    创建模块,闭包能够将一组相关的功能封装起来,形成一个模块,公开一些方法,隐藏其他状态和方法

例如,下面的代码展示了一个创建闭包的例子,其中的makeAdder函数创建了一个闭包,包括函数adder和该函数能记住的自由变量x

1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
return function(y) {
return x + y;
};
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

在这个例子中,add5add10是闭包,它们分别记忆了x为5和10

即使makeAdder函数的执行上下文结束后,这两个闭包仍然可以访问和操纵它们自己的x变量

引申

函数柯里化(Currying)是函数式编程中的一个重要概念,它是指将一个接受多个参数的函数转换成接受单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数且返回结果的新函数的技术

基本原理

柯里化的核心是闭包

利用闭包可以存储函数的状态,还可以继续接收剩余的参数,直到所有参数都被传递完毕,最后统一处理这些参数

柯里化的作用

柯里化主要有以下几个作用:

  • 参数复用

    柯里化可以将一个多参数的函数转换成多个单参数函数,这样部分参数可以被复用

  • 延迟计算/执行

    通过柯里化可以延迟函数的执行,只有在接收了所有需要的参数之后,才执行原函数

  • 动态生成函数

    可以根据传入的参数,动态地生成具有特定功能的新函数

一个简单的柯里化函数示例

来看一个简化的柯里化函数的例子,用以加深理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function curry(fn) {
return function curried(...args) {
// 如果提供的参数数量足够,则直接调用原函数
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// 否则,返回一个接受剩余参数的函数
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}

// 原始的未柯里化的函数
function sum(a, b, c) {
return a + b + c;
}

// 柯里化后的函数
const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3)); // 输出 6
console.log(curriedSum(1, 2)(3)); // 输出 6
console.log(curriedSum(1, 2, 3)); // 输出 6

在上面的例子中,curry函数接收一个函数fn并返回一个新的函数curried

curried函数被调用,它检查传递给它的参数数量

如果参数数量不够,它返回一个新的函数,这个函数期待更多的参数

这个过程会一直进行直到收到了足够的参数,最终原始函数被调用

使用场景

常见的柯里化使用场景有事件处理、部分求值等需要预置某些参数的情况

注意事项

柯里化函数通常的实现依赖于闭包,这意味着柯里化可能会导致与闭包相同的一些问题,例如变量作用域混淆和内存消耗问题

柯里化是一种强大但稍显复杂的技术

在实际应用中,它可以帮助我们编写高度模块化和重用性强的代码

了解和掌握柯里化,可以提升你在函数式编程领域的技能,并能帮助你更好的理解JavaScript这门语言的函数式特性


Q33:深拷贝跟浅拷贝

难度:⭐⭐⭐⭐

答案
  1. 浅拷贝

    • 浅拷贝是创建一个新的对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

    • 实现方式

      • Object.assign

        1
        2
        3
        let obj1 = { a: 1, b: { c: 1 } };
        let obj2 = Object.assign({}, obj1);
        console.log(obj2); // { a: 1, b: { c: 1 } }
      • 扩展运算符

        当对象里面只有基础类型的时候,拓展运算符就会变成深拷贝

        1
        2
        3
        let obj1 = { a: 1, b: { c: 1 } };
        let obj2 = { ...obj1 };
        console.log(obj2); // { a: 1, b: { c: 1 } }
  2. 深拷贝

    • 深拷贝会拷贝所有的属性,并且会递归到所有层级的子属性,直到找到所有的基本类型为止。这样的话,一个对象改变不会影响另一个对象

    • 实现方式

      • JSON.parse(JSON.stringify(object))

        需要特别注意,JSON.parse(JSON.stringify(object))会忽略掉原对象中的undefinedfunctionsymbol

        并且,如果对象中存在循环引用的情况也无法正确处理

        1
        2
        3
        let obj1 = { a: 1, b: { c: 1 } };
        let obj2 = JSON.parse(JSON.stringify(obj1));
        console.log(obj2); // { a: 1, b: { c: 1 } }
      • 递归拷贝

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        function deepClone(obj) {
        if (typeof obj !== 'object' || obj === null) {
        return obj;
        }
        let copy = Array.isArray(obj) ? [] : {};
        for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
        copy[key] = deepClone(obj[key]);
        }
        }
        return copy;
        }
        let obj1 = { a: 1, b: { c: 1 } };
        let obj2 = deepClone(obj1);
        console.log(obj2); // { a: 1, b: { c: 1 } }
      • lodash 的 _.cloneDeep

        1
        2
        3
        4
        const _ = require('lodash');
        let obj1 = { a: 1, b: { c: 1 } };
        let obj2 = _.cloneDeep(obj1);
        console.log(obj2); // { a: 1, b: { c: 1 } }


类型

Q1:typeof NaN的结果是什么

难度:⭐

解析

NaN(Not a Number)是一种特殊的数值,表示一个本来要返回数值的操作未返回数值的情况。虽然 NaN 是一种特殊的数值,但它的数据类型仍然被归类为 'number'

这种设计的原因在于 JavaScript 的 typeof 操作符返回数据类型的字符串表示,而 'number' 是 NaN 实际的数据类型。因此,typeof NaN 返回 'number' 是符合语言规范的结果。

答案

typeof NaN 的结果是 'number'

引申

在 JavaScript 中,NaN 是一个特殊的值,表示 “Not a Number”,通常用于表示数学运算中产生的非数值结果。虽然 NaN 本身不是一个有效的数字,但是它仍然可以参与一些操作。

以下是一些 NaN 的用法示例:

  1. 检查是否为 NaN: 可以使用全局函数 isNaN() 来检查一个值是否为 NaN。这个函数会尝试将传入的值转换为数字,如果不能成功转换为数字,或者转换后的值是 NaN,则返回 true,否则返回 false

    1
    2
    3
    isNaN(NaN); // true
    isNaN(123); // false
    isNaN('abc'); // true
  2. 检查是否为有效数字: 由于 NaN 表示 “Not a Number”,因此可以使用 isNaN() 函数来检查一个值是否为有效的数字。

    1
    2
    3
    isNaN(123); // false
    isNaN('123'); // false
    isNaN('abc'); // true
  3. 数学运算中的 NaN: 当数学运算中出现非法操作时,通常会产生 NaN

    1
    2
    0 / 0; // NaN
    Math.sqrt(-1); // NaN
  4. 判断是否为 NaN: 可以使用 === 运算符直接比较一个值是否等于 NaN。注意,NaN 与任何其他值,包括自身,都不相等。

    1
    NaN === NaN; // false

总之,NaN 在 JavaScript 中代表着 “Not a Number”,通常用于表示数学运算中产生的非数值结果,可以通过 isNaN() 函数来检查一个值是否为 NaN,并且在一些操作中需要小心处理 NaN 的情况


Q2:如何区分数组和对象?

难度:⭐

答案
  1. Array.isArray() 方法

    1
    2
    3
    4
    5
    6
    const myArray = [1, 2, 3];
    if (Array.isArray(myArray)) {
    console.log('myArray 是一个数组');
    } else {
    console.log('myArray 不是一个数组');
    }
  2. typeof 操作符

    1
    2
    3
    4
    5
    6
    const myObject = {name: 'John', age: 30};
    if (typeof myObject === 'object' && !Array.isArray(myObject)) {
    console.log('myObject 是一个对象');
    } else {
    console.log('myObject 不是一个对象');
    }
  3. instanceof 操作符

    1
    2
    3
    4
    5
    6
    const myArray = [1, 2, 3];
    if (myArray instanceof Array) {
    console.log('myArray 是一个数组');
    } else {
    console.log('myArray 不是一个数组');
    }
  4. 检查构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const myArray = [1, 2, 3];
    const myObject = {name: 'John', age: 30};

    if (myArray.constructor === Array) {
    console.log('myArray 是一个数组');
    } else {
    console.log('myArray 不是一个数组');
    }

    if (myObject.constructor === Object) {
    console.log('myObject 是一个对象');
    } else {
    console.log('myObject 不是一个对象');
    }
  5. 检查原型链

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const myArray = [1, 2, 3];
    const myObject = {name: 'John', age: 30};

    if (Object.getPrototypeOf(myArray) === Array.prototype) {
    console.log('myArray 是一个数组');
    } else {
    console.log('myArray 不是一个数组');
    }

    if (Object.getPrototypeOf(myObject) === Object.prototype) {
    console.log('myObject 是一个对象');
    } else {
    console.log('myObject 不是一个对象');
    }


Q3:js中如何判断一个值是否是数组类型?

难度:⭐

答案
  1. instanceof 操作符:使用 instanceof 操作符可以检查一个值是否是某个对象的实例,对于数组来说,可以检查是否是 Array 类型的实例。

    1
    2
    var arr = [1, 2, 3];
    console.log(arr instanceof Array); // true
  2. 使用对象的 constructor 属性:每个 JavaScript 对象都有一个 constructor 属性,可以通过它来获取对象的构造函数。对于数组来说,其构造函数是 Array

    1
    2
    var arr = [1, 2, 3];
    console.log(arr.constructor === Array); // true
  3. 使用 Object.prototype.toString() 方法:通过调用 Object.prototype.toString() 方法,可以获取对象的字符串表示,对于数组来说,其返回的字符串格式是 [object Array]

    1
    2
    var arr = [1, 2, 3];
    console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
  4. 使用数组的原型链:数组是原型链的一部分,可以通过 Array.prototype 来检查一个值是否具有数组的方法和属性。如果一个值具有数组的方法和属性,那么它很可能是一个数组。

    1
    2
    var arr = [1, 2, 3];
    console.log(arr.__proto__ === Array.prototype); // true

以上方法都可以用来判断一个值是否是数组类型,但各有优劣。Array.isArray() 方法是最简单直接的方式,而其他方法更多是作为了解 JavaScript 内部工作原理的参考。


Q4:null和 undefined 有什么区别?

难度:⭐

答案

nullundefined 是 JavaScript 中的两种特殊的数据类型,它们之间有一些区别:

  1. undefined:

    • undefined 是表示未定义的值,用于表示变量声明了但没有赋值的情况,或者函数调用时没有提供参数的情况。

    • 在 JavaScript 中,未初始化的变量默认值为 undefined

    • 例如:

      1
      2
      var x;
      console.log(x); // 输出 undefined
  2. null:

    • null 是表示空值的特殊值,用于表示一个变量被显式地赋值为“空”。

    • null 值不代表任何对象,它是 JavaScript 中的关键字。

    • 例如:

      1
      2
      var y = null;
      console.log(y); // 输出 null

主要区别:

  • undefined 表示变量未定义或者属性不存在,而 null 表示变量已经定义并且赋值为“空”。
  • 在使用条件语句时,undefined 会被转换为 false,而 null 不会。
  • 当想要表示一个“空”值时,通常使用 null,而不是 undefined


Q5:’1’.toString()为什么不会报错?

难度:⭐

答案

在 JavaScript 中,基本数据类型(如数字、字符串和布尔值)并没有方法

然而,JavaScript 允许你像对待对象一样对待这些基本类型的值,比如调用方法

这是因为 JavaScript 会临时地将它们转换成对应的对象,这个过程称为“装箱”(Boxing)

之后,就可以在这个对象上调用方法了

其实在这个语句运行的过程中做了这样几件事情:

1
2
3
var s = new String('1'); 
s.toString();
s = null;

在这个例子中,当调用'1'.toString()时,JavaScript 内部会自动创建一个临时的String对象,然后在这个对象上调用toString方法

调用完成后,这个临时对象就被丢弃

这个过程对于开发者来说是透明的,开发者看到的只是原始类型值似乎拥有了调用方法的能力

整个过程体现了 基本包装类型 的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String

在JavaScript中,基本包装类型是一些特殊的内置构造函数,这些构造函数用于为简单的基本数据类型(如String, Number, Boolean)创建对应的对象

这样做使得基本数据类型可以像对象一样使用,且能访问一些方法去处理数据,比如字符串的拼接、大小写转换,数字的转换,以及布尔值的逻辑操作等

这三个基本包装类型如下:

  1. String:用于创建一个包含字符串的对象
  2. Number:用于创建一个包含数字的对象
  3. Boolean:用于创建一个包含布尔值(true或false)的对象

以下展示了如何使用基本包装类型:

1
2
3
var stringObject = new String("string text");
var numberObject = new Number(123);
var booleanObject = new Boolean(true);

这些基本包装类型的对象版和基本类型的值版(如直接使用字面量创建的字符串、数字或布尔值)在许多情况下行为很相似,但它们是不同的

对象是引用类型,而基本数据类型是直接值

这意味着基本类型的值存储是按值访问的,并且它们是不可变的

而包装对象则是按照引用访问的,它们有自己的内存地址,并且是可以添加属性的对象

大多数情况下,没有必要直接使用基本包装类型,因为JavaScript会自动进行装箱和拆箱操作

当对一个基本类型值调用方法时,JavaScript会临时使用相应的包装类型来使这一方法的调用成为可能

在方法执行完之后,这个临时创建的包装对象就会被销毁

这个过程是自动和隐形的,从而让基本数据类型看起来像是具有对象的性质

引申

这个现象根源于JavaScript语言的设计原则及其类型系统的构造

JavaScript是一种动态类型语言,它通过一套称为自动装箱(autoboxing)的机制使得原始数据类型(如字符串、数字和布尔值)能够调用与之对应的对象原型上的方法

这种设计使得基础数据类型在使用上更加灵活,同时保持了性能效率

自动装箱转换机制

在JavaScript中,当对一个原始值(比如字符串、数字或布尔值)执行对象的操作时,JavaScript会临时把这个原始值包装成一个相应类型的对象

这个过程是自动的,发生在原始值上调用方法的那一刻

例如,当你对一个字符串调用.toString()方法时,JavaScript内部会创建一个String对象,然后在这个临时对象上调用.toString()方法

方法调用完成后,这个对象会被丢弃,不会影响原始值

为何不能跨类型调用方法

原始数据类型包装对象的设计初衷是为了提供对相应类型相关操作的便捷性,并不意味着可以让数据类型之间无限制地互相调用对方的方法:

  1. 类型安全

    不同的数据类型支持不同的方法,这基于它们各自的用途和行为

    允许一个类型的原始值随意调用另一个类型的方法会破坏类型之间的边界,引入潜在的类型安全问题和逻辑错误

  2. 明确性和可维护性

    JavaScript的设计鼓励明确而直接的代码

    类型之间的明确界限让代码更容易理解和维护

    如果原始数据类型之间可以相互调用方法,这将大大增加理解和使用语言时的复杂度

  3. 性能问题

    自动的类型转换和方法调用如果不受限制地发生,可能会导致性能问题

    为了维护运行时的效率,避免不必要的类型转换和装箱操作是重要的

实用性

在实际应用中,如果你需要对一个数据类型执行不自然的操作(例如,把字符串当作数组处理),通常有专门的方法或者是模式来实现这个需求,比如使用split方法把字符串转换为数组,再对数组操作

这样的处理方式更加明确且高效

JavaScript提供了足够的工具和方法来在不同类型之间进行转换和操作,同时保持代码的清晰和效率

总的来说,虽然JavaScript通过自动装箱允许原始数据类型调用对象方法,这种机制的存在是为了增强编程的便利性和灵活性,而不是让不同数据类型之间的方法随意横跨调用


Q6:typeof 与instanceof 有什么区别

难度:⭐⭐

答案

typeofinstanceof 是 JavaScript 中用于检查数据类型的两种操作符,它们有以下区别:

  1. typeof
    • 用法:typeof 是一个一元操作符,可以用来检查一个值的数据类型。
    • 返回值:typeof 返回一个表示值的数据类型的字符串,包括 "undefined""boolean""number""string""bigint""symbol""function""object"
    • 特点:typeof null 返回 "object",这是一个历史遗留问题;typeof 对于函数和数组也返回 "function""object",无法准确区分。
    • 适用范围:通常用于检查基本数据类型(除了 null)和函数的数据类型。
  2. instanceof
    • 用法:instanceof 是一个二元操作符,用于检查对象是否属于某个类或其原型链上是否存在某个构造函数的 prototype 属性。
    • 返回值:如果对象是指定类的实例,则返回 true,否则返回 false
    • 特点:instanceof 通过原型链检查对象的构造函数是否存在于指定类的原型链上,因此可以准确地检查对象是否是指定类的实例。
    • 适用范围:通常用于检查对象是否是特定类的实例,特别适用于自定义对象或继承关系的检查。


Q7:JavaScript中的错误有哪几种类型?

难度:⭐⭐

答案
  1. 语法错误(Syntax Errors)

    也称为解析错误(Parsing Errors),是由于代码中的语法不正确而导致的错误

    这种错误通常在代码解析阶段就会被检测到,并在控制台中显示错误消息

  2. 类型错误(Type Errors)

    当操作或表达式的类型不符合预期时,会引发类型错误

    例如,对非函数对象进行函数调用、对未定义或空值进行属性访问、非法的操作符操作等

    这种错误通常会在运行时抛出异常

  3. 引用错误(Reference Errors)

    当代码尝试引用一个不存在的变量或属性时,会抛出引用错误

    例如,访问未定义的变量、调用未声明的函数、使用未定义的对象属性等

  4. 范围错误(Range Errors)

    当操作超出有效范围时,会引发范围错误

    例如,尝试使用超出数组长度的索引、使用负数作为参数传递给内置函数(如 Array 构造函数的 length 属性)等

  5. URI 错误(URI Errors)

    在处理统一资源标识符(URI)时发生的错误,例如使用 encodeURI()decodeURI() 函数时的错误


Q8:js中的undefined和 ReferenceError: xxx is not defined 有什么区别?

难度:⭐

解析

JavaScript 中的 undefinedReferenceError 的区别在于它们表示的含义和产生的原因不同。

  1. undefined

    • undefined 表示一个变量已被声明,但尚未被赋值,或者一个函数没有明确返回值时的默认返回值。

    • 当你访问一个已声明但未赋值的变量时,或者调用一个没有返回值的函数时,其结果会是 undefined

      1
      console.log(x); // 输出 undefined
  2. ReferenceError

    • ReferenceError 表示一个变量不存在,即在作用域中未声明或未定义该变量。

    • 当你访问一个未声明的变量时,或者尝试访问一个未在当前作用域内定义的变量时,会导致 ReferenceError

      1
      console.log(y); // ReferenceError: y is not defined
答案

undefined 是一个表示未赋值的特殊值,而 ReferenceError 表示变量或函数在当前作用域中未定义


Q9:null是对象吗?为什么

难度:⭐⭐

答案

在 JavaScript 中,null 被认为是一种特殊的对象类型,但实际上它并不是一个对象。null 表示一个空值或者不存在的对象,用于表示一个变量不指向任何对象。

尽管在 JavaScript 中,typeof null 的结果是 "object",但这其实是一个历史遗留问题,起源于 JavaScript 最初的设计。在 JavaScript 的早期版本中,表示对象的第一个字节的值为 000,而 null 的二进制表示是全 0,因此被错误地判断为对象类型。这个问题至今仍然保留了下来,为了保持向后兼容性,JavaScript 的设计者们没有修复这个问题。

因此,尽管 typeof null 返回 "object",但 null 实际上不是一个对象,而是一个原始值。可以使用 === 运算符来判断一个值是否为 null


Q10:数据类型检测的方式有哪些

难度:⭐

答案
  1. typeof运算符

    typeof是最常用的检测数据类型的方法,适用于基本类型(如String, Number, Boolean, Undefined, Symbol)的检测,但对于Array, Null, Object的检测则会返回"object",对于Function会返回"function"

    1
    2
    3
    4
    5
    6
    7
    8
    console.log(typeof "Hello World"); // "string"
    console.log(typeof 42); // "number"
    console.log(typeof true); // "boolean"
    console.log(typeof undefined); // "undefined"
    console.log(typeof Symbol('sym')); // "symbol"
    console.log(typeof null); // "object"
    console.log(typeof []); // "object"
    console.log(typeof function(){}); // "function"
  2. Array.isArray()方法

    由于typeof不能准确判断数组类型,Array.isArray()方法能够准确检测一个值是否为数组

    1
    2
    console.log(Array.isArray([])); // true
    console.log(Array.isArray({})); // false
  3. instanceof运算符

    instanceof可以检测一个对象是否是其原型链中某个构造函数的实例

    1
    2
    3
    4
    5
    function MyClass() {}
    const instance = new MyClass();
    console.log(instance instanceof MyClass); // true
    console.log([] instanceof Array); // true
    console.log({} instanceof Object); // true
  4. Object.prototype.toString.call()

    这个方法通过调用Object原型上的toString方法检测对象的类型,可以准确区分大部分内置对象类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    console.log(Object.prototype.toString.call("Hello World")); // [object String]
    console.log(Object.prototype.toString.call(42)); // [object Number]
    console.log(Object.prototype.toString.call(true)); // [object Boolean]
    console.log(Object.prototype.toString.call([])); // [object Array]
    console.log(Object.prototype.toString.call({})); // [object Object]
    console.log(Object.prototype.toString.call(new Date())); // [object Date]
    console.log(Object.prototype.toString.call(null)); // [object Null]
    console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
    console.log(Object.prototype.toString.call(function(){})); // [object Function]
    console.log(Object.prototype.toString.call(Symbol('sym'))); // [object Symbol]
  5. 构造函数名称(constructor.name)

    每个对象都有一个constructor属性,该属性指向创建该对象的构造函数

    通过检查constructor.name属性的值,可以得知对象类型的名称

    1
    2
    3
    console.log(({}).constructor.name); // Object
    console.log(([]).constructor.name); // Array
    console.log((function(){}).constructor.name); // Function

每种方法都有其优缺点,选择哪种方法取决于具体的应用场景和对准确度的要求

通常,Object.prototype.toString.call()提供了最准确的类型检测结果


Q11:isNaN 和 NumberisNaN 函数有什么区别

难度:⭐⭐

答案
  1. isNaN(全局函数)

    • isNaN函数将其参数转换成数字,然后判断该数字是否为NaN
    • 如果参数不能被转换成数字(比如一个字符串或对象),或者转换后的数字是NaN,则返回true
    • 也就是说,如果你传递给isNaN的参数在转换为数字过程中变成了NaN,那么isNaN就会返回true
    1
    2
    3
    4
    5
    isNaN(NaN);       // true
    isNaN("hello"); // true - "hello" 转换为数字失败,所以是 NaN
    isNaN(undefined); // true - undefined 转换为数字得到 NaN
    isNaN({}); // true - {} 转换为数字失败
    isNaN(37); // false - 37 是一个数字
  2. Number.isNaN(ES6中引入)

    • Number.isNaN不会将参数转换成数字,只有当参数类型为Number且值为NaN时,才返回true
    • 它是对NaN的直接判断,不会有任何类型转换,这意味着除了NaN本身,对于其他不是数字类型的参数,即使它们在类型转换后可能会是NaNNumber.isNaN也返回false
    1
    2
    3
    4
    5
    Number.isNaN(NaN);       // true
    Number.isNaN("hello"); // false - 这里不会尝试将字符串 "hello" 转换成数字
    Number.isNaN(undefined); // false - undefined 不是数字
    Number.isNaN({}); // false - {} 不是数字
    Number.isNaN(37); // false - 37 是一个数字

Number.isNaN是一个更严格的检查,它避免了isNaN因为类型强制转换导致的一些可能会引起混淆的情况

因此,在判断一个值是否为NaN时,通常推荐使用`Number.isNaN


对象

Q1:怎么获取到一个实例对象的原型对象?

难度:⭐

答案
  1. Object.getPrototypeOf(obj)

    1
    2
    3
    const obj = {}; // 你的实例对象
    const prototype1 = Object.getPrototypeOf(obj);
    console.log(prototype1);
  2. __proto__ 属性(非标准):

    1
    2
    3
    4
    const obj = {}; // 你的实例对象
    const prototype2 = obj.__proto__;
    console.log(prototype2);
    // 虽然在一些环境中有效,但不是标准的 JavaScript,而且在未来可能会被废弃
  3. 构造函数的 prototype 属性:

    1
    2
    3
    4
    function CustomObject() {}
    const obj = new CustomObject(); // 你的实例对象
    const prototype3 = CustomObject.prototype;
    console.log(prototype3);
  4. 通过实例对象的构造函数的 constructor 属性和 prototype 属性:

    1
    2
    3
    4
    function CustomObject() {}
    const obj = new CustomObject(); // 你的实例对象
    const prototype4 = obj.constructor.prototype;
    console.log(prototype4);


Q2:改变this指向的方法有哪些?

难度:⭐

答案

在 JavaScript 中,有几种方式可以改变函数执行时的 this 指向:

  1. 使用 bind 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    const obj = { name: 'Alice' };

    function sayHello() {
    console.log(`Hello, ${this.name}!`);
    }

    const boundFunction = sayHello.bind(obj);
    boundFunction(); // Hello, Alice!
  2. 使用箭头函数:

    箭头函数不会创建自己的 this,而是捕获当前所处上下文的 this

    1
    2
    3
    4
    5
    6
    7
    const obj = { name: 'Bob' };

    const sayHello = () => {
    console.log(`Hello, ${this.name}!`);
    };

    sayHello.call(obj); // Hello, Bob!

    请注意,箭头函数的 this 是在定义时确定的,而不是在运行时确定的。

  3. 使用 callapplybind 方法:

    这些方法可以在调用函数的同时传递一个对象作为 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const obj = { name: 'Charlie' };

    function sayHello() {
    console.log(`Hello, ${this.name}!`);
    }

    sayHello.call(obj); // Hello, Charlie!
    sayHello.apply(obj); // Hello, Charlie!

    const boundFunction = sayHello.bind(obj);
    boundFunction(); // Hello, Charlie!
引申

this对象在JavaScript中是一个特别重要的概念,它是在运行时基于函数的执行环境动态绑定的

this的值并不是在函数定义的时候被绑定的,而是在函数被调用的时候决定的

以下是this在不同情境下的一些基本行为:

  1. 全局上下文

    在全局执行上下文(不在任何函数内部)中,this指向全局对象

    在浏览器中,全局对象是window

    1
    console.log(this === window); // 在浏览器中返回true
  2. 函数上下文

    在函数内部,this的值取决于函数是如何被调用的。

    • 非严格模式下:未指定上下文的函数调用中,this指向全局对象
    • 严格模式下this的值为undefined
    • 使用callapply或者bind:可以将this显式地设置为第一个参数指定的对象
    1
    2
    3
    4
    5
    6
    7
    function func() {
    return this;
    }

    func(); // 在非严格模式下返回 window,在严格模式下返回 undefined

    func.call({ a: 'example' }); // 返回 { a: 'example' }
  3. 对象方法

    当函数作为对象的方法调用时,this指向该对象

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    method: function() {
    return this;
    }
    };

    obj.method(); // 返回 obj
  4. 构造函数

    在构造函数中,this指的是一个新创建的对象

    1
    2
    3
    4
    5
    6
    function Constructor() {
    this.a = 'example';
    }

    let instance = new Constructor();
    console.log(instance.a); // 返回 'example'
  5. 箭头函数

    箭头函数不绑定自己的this,它们继承了上层代码块的this

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    method: function() {
    return () => this;
    }
    };

    obj.method()(); // 返回 obj
  6. 事件处理器

    在事件处理函数中,this通常指向触发事件的元素

    1
    2
    3
    button.addEventListener('click', function() {
    console.log(this); // 指向按钮元素
    });

理解this的工作机制对于编写可预测和有效的代码非常重要

不同的调用方式和函数类型会影响this的绑定,正确的使用this可以使代码更简洁、逻辑更清晰


Q3:对ToPrimitive的理解

难度:⭐

答案

ToPrimitive 是 JavaScript 中的一个抽象操作符,用于将值转换为基本类型(primitive type)。这个操作通常由内置函数调用,例如进行运算或将对象转换为原始值的时候。

ToPrimitive 的具体规则如下:

  1. 如果一个对象有 Symbol.toPrimitive 方法,就调用该方法,返回其结果。
  2. 否则,如果 hint 参数是 “string”,尝试调用对象的 valueOftoString 方法,如果其中任意一个返回原始值,则返回该值。
  3. 否则,如果 hint 参数是 “number” 或默认(没有提供 hint 参数),尝试调用对象的 valueOftoString 方法,如果其中任意一个返回原始值,则返回该值。
  4. 否则,抛出 TypeError

以下是一些示例:

对象有 Symbol.toPrimitive 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
[Symbol.toPrimitive](hint) {
if (hint === 'string') {
return 'Hello';
}
if (hint === 'number') {
return 42;
}
}
};

console.log(String(obj)); // 输出 'Hello'
console.log(Number(obj)); // 输出 42

对象没有 Symbol.toPrimitive 方法:

1
2
3
4
5
6
7
8
9
10
11
const obj = {
valueOf() {
return 42;
},
toString() {
return 'Hello';
}
};

console.log(String(obj)); // 输出 'Hello',toString 返回原始值
console.log(Number(obj)); // 输出 42,valueOf 返回原始值

ToPrimitive 主要用于规范中描述对象转换为原始值的过程,通常在 JavaScript 引擎内部处理。开发者在日常编码中不经常直接使用这个抽象操作符


Q4:Object与Map有什么区别?

难度:⭐

答案

在 JavaScript 中,ObjectMap 都是用于存储键值对的数据结构,但它们有一些重要的区别:

  1. 键的类型
    • Object:对象的键必须是字符串或者 Symbol 类型。如果试图使用其他类型的值作为键,JavaScript 引擎会将其转换为字符串。
    • Map:Map 可以接受任意类型的值作为键,包括原始类型和对象引用。
  2. 键值对的顺序
    • Object:对象的键值对是无序的。尽管在一些实现中,属性的遍历顺序可能会按照添加的顺序,但 JavaScript 规范并不保证这一点。
    • Map:Map 保留键值对的插入顺序。当你迭代一个 Map 对象时,它会按照键值对被添加的顺序进行迭代。
  3. 大小
    • Object:对象的大小没有直接的属性或方法来获取。要获取对象的键值对数量,需要手动计算属性的数量或者使用 Object.keys(obj).length
    • Map:Map 对象有一个 size 属性,可以直接获取键值对的数量。
  4. 迭代
    • Object:要遍历对象的属性,可以使用 for...in 循环或者 Object.keys()Object.values()Object.entries() 方法。
    • Map:Map 对象实现了迭代器协议,可以使用 for...of 循环直接遍历 Map 对象。
  5. 内存占用
    • Object:对象的键值对在某些情况下可能会占用更多的内存,因为对象会维护原型链和额外的属性。
    • Map:Map 对象的实现通常会对内存进行优化,特别是在大型数据集上时。Map 可以更有效地存储大量的键值对。


Q5:cookie 的有效时间设置为 0会怎么样

难度:⭐

答案

将 cookie 的有效时间设置为 0 实际上是告诉浏览器在会话结束时将其删除。在 HTTP 协议中,如果 cookie 的过期时间设置为 0 或者省略,则 cookie 会成为一个会话 cookie,它仅在用户当前会话期间有效,一旦用户关闭浏览器,这个 cookie 就会被删除。

设置 cookie 的有效时间为 0 通常用于创建会话 cookie,这些 cookie 存储了在用户会话期间需要保持的数据,例如用户登录状态或者临时会话标识。当用户关闭浏览器时,这些 cookie 就会被删除,从而保护用户的隐私和安全。

总之,将 cookie 的有效时间设置为 0 表示这是一个会话 cookie,它只在用户当前会话期间有效,并在用户关闭浏览器时被删除。


Q6:Dom操作有哪些

难度:⭐⭐

答案

DOM(文档对象模型)操作是指对网页中的 HTML 元素进行增、删、改、查等操作,使得页面的结构、样式和内容能够动态地改变。以下是常见的 DOM 操作:

  1. 获取元素
    • document.getElementById(id):通过元素的 id 属性获取元素。
    • document.getElementsByClassName(className):通过元素的类名获取元素集合。
    • document.getElementsByTagName(tagName):通过元素的标签名获取元素集合。
    • document.querySelector(selector):通过 CSS 选择器获取第一个匹配的元素。
    • document.querySelectorAll(selector):通过 CSS 选择器获取所有匹配的元素集合。
  2. 创建元素
    • document.createElement(tagName):创建一个指定标签名的元素节点。
    • document.createTextNode(text):创建一个包含指定文本的文本节点。
  3. 添加、移除和替换元素
    • parentNode.appendChild(newNode):将一个新节点添加到指定节点的子节点列表的末尾。
    • parentNode.removeChild(node):从指定节点的子节点列表中移除一个子节点。
    • parentNode.replaceChild(newNode, oldNode):用一个新节点替换指定节点的子节点列表中的一个子节点。
  4. 修改元素的属性和内容
    • element.setAttribute(attribute, value):设置指定元素的属性值。
    • element.getAttribute(attribute):获取指定元素的属性值。
    • element.innerHTML:获取或设置指定元素的 HTML 内容。
    • element.innerText:获取或设置指定元素的文本内容。
  5. 添加和移除事件监听器
    • element.addEventListener(event, handler):为指定元素添加事件监听器。
    • element.removeEventListener(event, handler):移除指定元素的事件监听器。
  6. 样式操作
    • element.style.property = value:直接设置元素的样式属性。
    • element.classList.add(className):为元素添加类名。
    • element.classList.remove(className):移除元素的类名。
    • element.classList.toggle(className):切换元素的类名。
  7. 查询元素的位置和尺寸
    • element.getBoundingClientRect():返回元素的大小及其相对于视口的位置。
    • element.offsetParent:返回最近的包含该元素的定位元素。
    • element.offsetWidthelement.offsetHeightelement.offsetTopelement.offsetLeft:返回元素的宽度、高度、相对于父元素的顶部距离和左侧距离。
  8. 其他常用操作
    • document.createDocumentFragment():创建一个文档片段,用于高效地进行多个 DOM 操作。
    • element.scrollIntoView():使元素滚动到浏览器窗口的可视区域内。


Q7:ES6中对象新增了哪些扩展?

难度:⭐⭐

答案
  1. 对象字面量的简写语法

    • 可以在定义对象时使用更简洁的语法,不再需要写重复的属性名。

    • 示例:

      1
      2
      let x = 10, y = 20;
      let obj = { x, y }; // 等价于 { x: x, y: y }
  2. 计算属性名

    • 在对象字面量中可以使用计算属性名,使得属性名可以动态计算。

    • 示例:

      1
      2
      3
      4
      let propName = 'foo';
      let obj = {
      [propName]: 'bar'
      };
  3. 方法简写

    • 在对象字面量中定义方法时可以省略 function 关键字。

    • 示例:

      1
      2
      3
      4
      5
      let obj = {
      method() {
      // 方法逻辑
      }
      };
  4. Object.assign()

    • 可以用于将一个或多个源对象的属性复制到目标对象中。

    • 示例:

      1
      2
      3
      let target = { a: 1 };
      let source = { b: 2, c: 3 };
      Object.assign(target, source); // target 变为 { a: 1, b: 2, c: 3 }
  5. Object.keys()、Object.values() 和 Object.entries()

    • Object.keys():返回一个包含目标对象所有可枚举属性名的数组。

    • Object.values():返回一个包含目标对象所有可枚举属性值的数组。

    • Object.entries():返回一个包含目标对象所有可枚举属性键值对的数组,每个键值对以数组形式表示。

    • 示例:

      1
      2
      3
      4
      let obj = { a: 1, b: 2, c: 3 };
      Object.keys(obj); // 返回 ['a', 'b', 'c']
      Object.values(obj); // 返回 [1, 2, 3]
      Object.entries(obj); // 返回 [['a', 1], ['b', 2], ['c', 3]]
  6. 对象的扩展方法

    • Object.setPrototypeOf():设置一个对象的原型(即修改 __proto__ 属性)。
    • Object.getPrototypeOf():获取一个对象的原型。
    • Object.getOwnPropertyDescriptors():获取一个对象的所有属性的描述符。
  7. Symbol 类型

    • Symbol 类型是一种新的原始数据类型,表示独一无二的值。

    • 可以用作对象的属性名,用于创建唯一标识符。

    • 示例:

      1
      2
      3
      4
      const sym = Symbol();
      let obj = {
      [sym]: 'value'
      };


Q8:Proxy 能够监听到对象中的对象的引用吗?

难度:⭐⭐

答案

是的,Proxy 可以监听到对象中的对象引用的变化。

当使用 Proxy 对象对某个对象进行代理时,代理对象可以拦截对目标对象的各种操作,包括对对象属性的访问、修改、添加和删除等。如果目标对象的属性值是对象,那么对该属性的操作也会被拦截,包括对该属性值对象内部属性的修改。

下面是一个示例,演示了如何使用 Proxy 监听对象中的对象引用的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let target = {
nestedObj: {
key: 'value'
}
};

let handler = {
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
return Reflect.set(target, prop, value);
}
};

let proxy = new Proxy(target, handler);

proxy.nestedObj.key = 'new value'; // 输出 "Setting key to new value"

在上面的示例中,当通过 proxy 对象修改 nestedObjkey 属性时,set 拦截器会捕获到操作,并输出相关信息。这表明 Proxy 可以监听到对象中的对象引用的变化。


Q9 :导致 JavaScript 中 this 指向混乱的原因是什么?

难度:⭐⭐

答案

JavaScript 中 this 指向混乱的原因主要有以下几点:

  1. 函数调用方式不同:JavaScript 中函数的调用方式有多种,包括普通函数调用、方法调用、构造函数调用和箭头函数等。不同的调用方式会导致 this 的指向不同。
  2. 丢失绑定:当一个函数被单独调用时,即没有任何对象或上下文与之相关联时,this 将指向全局对象(在浏览器环境中通常是 window 对象)。这种情况下,如果函数内部使用了 this,则可能会出现意外结果。
  3. 隐式绑定丢失:当一个方法从对象中切割出来并作为独立函数调用时,隐式绑定将会丢失,导致 this 不再指向原对象。这往往发生在将对象方法作为回调函数传递给其他函数的情况下。
  4. 显式绑定问题:使用 .call().apply().bind() 方法可以显式地绑定函数的 this,但如果不小心使用或错误地使用这些方法,也可能导致 this 指向混乱。
  5. 箭头函数中的 this:箭头函数没有自己的 this 绑定机制,它会从外围作用域继承 this。这意味着箭头函数中的 this 与其定义时的上下文相关联,而不是调用时的上下文。
  6. 异步操作中的 this:在异步函数或回调函数中,this 的指向可能会发生变化,因为它们的执行上下文可能会改变。
引申

为了避免 this 指向混乱的问题,可以采取以下措施:

  • 使用箭头函数,它能够继承外部作用域的 this。
  • 使用 .bind().call().apply() 方法显式地绑定函数的 this。
  • 使用闭包将需要引用的 this 缓存起来。
  • 在方法调用时确保上下文正确。


Q10:e.target跟e.currentTarget有什么区别?

难度:⭐

解析
  1. e.target
    • e.target 表示触发事件的实际目标元素。
    • 对于事件冒泡,它是最深层次的元素,即实际接收到事件的元素。
    • 对于事件捕获,它是最外层的元素,即最先接收到事件的元素。
    • 通常用于获取用户实际与之交互的元素。
  2. e.currentTarget
    • e.currentTarget 表示当前正在处理事件的元素,即事件处理程序所附加到的元素。
    • 对于事件冒泡,它是在事件流上当前正在处理事件的元素。
    • 对于事件捕获,它是在事件流下当前正在处理事件的元素。
    • 通常用于在事件处理函数内部引用附加处理程序的元素。
答案

e.target 表示触发事件的实际元素

e.currentTarget 表示当前正在处理事件的元素

引申
1
2
3
4
5
6
7
<div id="a">
<div id="b">
<div id="c">
<div id="d">哈哈哈哈哈</div>
</div>
</div>
</div>

image-20240125161544126

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const a = document.getElementById('a')
const b = document.getElementById('b')
const c = document.getElementById('c')
const d = document.getElementById('d')
a.addEventListener('click', (e) => {
const {
target,
currentTarget
} = e
console.log(`target是${target.id}`)
console.log(`currentTarget是${currentTarget.id}`)
})
b.addEventListener('click', (e) => {
const {
target,
currentTarget
} = e
console.log(`target是${target.id}`)
console.log(`currentTarget是${currentTarget.id}`)
})
c.addEventListener('click', (e) => {
const {
target,
currentTarget
} = e
console.log(`target是${target.id}`)
console.log(`currentTarget是${currentTarget.id}`)
})
d.addEventListener('click', (e) => {
const {
target,
currentTarget
} = e
console.log(`target是${target.id}`)
console.log(`currentTarget是${currentTarget.id}`)
})


// 分别点击这几个之后的结果
// target是d currentTarget是d
// target是d currentTarget是c
// target是d currentTarget是b
// target是d currentTarget是a


Q11:说说你对 new.target 的理解

难度:⭐

解析
1
2
3
4
5
6
function Person() {
console.log(new.target);
}

const person1 = new Person(); // 输出: [Function: Person]
Person(); // 输出: undefined

在上面的示例中,Person() 构造函数中的 new.target 分别输出了 [Function: Person]undefined。这是因为第一个调用是通过 new 关键字调用的,所以 new.target 指向 Person 构造函数本身;而第二个调用是普通函数调用,所以 new.targetundefined

new.target 主要用于确定构造函数或者类是否通过 new 关键字调用,从而执行不同的逻辑,例如在类的构造函数中可以使用 new.target 来确保类只能通过 new 关键字调用。

答案

new.target 是 ECMAScript 6 引入的一个元属性,它在构造函数或者类的构造函数中表示通过 new 关键字调用的构造函数或者类的引用。

具体来说,new.target 返回一个指向正在执行的构造函数或者类的引用。如果构造函数或者类是通过 new 关键字调用的,则 new.target 会指向该构造函数或者类本身;如果构造函数或者类是通过普通函数调用的,则 new.target 会是 undefined


Q12:如何判断一个对象是不是空对象

难度:⭐⭐

答案
  1. Object.keys() 方法

    我们可以使用Object.keys()方法来获取对象的所有键,然后检查键的数量。如果键的数量为 0,意味着对象为空

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function isEmpty(obj) {
    return Object.keys(obj).length === 0;
    }

    const object1 = {};
    console.log(isEmpty(object1)); // 输出:true

    const object2 = {name: 'John'};
    console.log(isEmpty(object2)); // 输出:false
  2. for...in 循环

    通过 for...in 循环遍历对象。如果循环没有执行,那么对象就是空的

    其中 obj.hasOwnProperty(key) 用于确保属性是对象自己的属性,而非继承自原型链

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function isEmpty(obj) {
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) return false;
    }
    return true;
    }

    const object1 = {};
    console.log(isEmpty(object1)); // 输出:true

    const object2 = {name: 'John'};
    console.log(isEmpty(object2)); // 输出:false


Q13:正则表达式是什么

难度:⭐⭐

答案

“正则表达式”(Regular Expression)是一种强大的文本处理工具,用于字符串的搜索、替换、检索和拆分等操作

它通过定义一个规则来匹配一系列符合某个句法规则的字符串

正则表达式非常灵活且高效,广泛应用于文本编辑器、编程语言、数据库查询等多种领域

正则表达式由普通字符(例如字母和数字)以及特殊字符(称为”元字符”)组成

这些规则简洁但功能强大,可以非常精确地描述和匹配文本模式,包括:

  • 字面字符

    直接匹配文本中的指定字符

  • 元字符

    具有特殊含义的字符,例如用于表示空白字符、单词边界、字符集合等

  • 量词

    指明了模式出现的频率,如“一次或更多次”、“零次或一次”等

  • 位置匹配

    比如单词的开始和结束、字符串的开头和结尾

  • 分组和引用

    可以将模式的一部分组合在一起,以便可以后续引用或应用量词

引申

匹配规则

  1. 字符匹配
    • .:匹配除换行符 \n 之外的任意单个字符
    • \d:匹配任意一位数字,等同于 [0-9]
    • \D:匹配任意非数字字符,等同于 [^0-9]
    • \w:匹配任意字母、数字或下划线(Word Character),等同于 [a-zA-Z0-9_]
    • \W:匹配任意非单词字符,等同于 [^a-zA-Z0-9_]
    • \s:匹配任意空白字符,包括空格、制表符、换行符等
    • \S:匹配任意非空白字符
    • [abc]:字符集合,匹配包含的任一字符
    • [^abc]:否定字符集,匹配不在指定集合中的任一字符
    • [a-z][A-Z][0-9]:范围,匹配指定范围内的任意字符
    • ^:在字符集合中使用时,表示否定,如 [^a-e] 表示非 a 到 e 的字符。在其他时候表示行的起始
    • $:匹配行的结束
    • \b:匹配单词的边界
    • \B:匹配非单词边界
    • \:转义字符,用来匹配那些特殊字符,如 \\., \\$, \(
    • |:选择,匹配符号左边或者右边的表达式
    • (expr):分组符号,用来定义一个组
    • (?:expr):非捕获分组,该组匹配的内容不会被捕获,不分配组号
    • (?=expr):正向先行断言,表示之后的内容必须匹配表达式 expr
    • (?!expr):负向先行断言,表示之后的内容不匹配表达式 expr
    • expr1(?<=expr2):正向后行断言,表示 expr1 前面的内容必须匹配 expr2
    • expr1(?<!expr2):负向后行断言,表示 expr1 前面的内容不匹配 expr2
    • x*:匹配前面的模式 x 0 或多次
    • x+:匹配前面的模式 x 1 或多次
    • x?:匹配前面的模式 x 0 或 1 次
    • x{n}:匹配前面的模式 x 恰好 n
    • x{n,}:匹配前面的模式 x 至少 n
    • x{n,m}:匹配前面的模式 x 至少 n 次,但不超过 m
  2. 位置匹配
    • ^:匹配输入字符串的开始位置
    • $:匹配输入字符串的结束位置
    • \b:匹配一个单词边界,也就是指单词和空格间的位置
    • \B:匹配非单词边界,'er\B' 可以匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’
    • (?=p):匹配 ‘p’ 前面的位置
    • (?!p):匹配 ‘p’ 不在其后的位置
  3. 分组引用
    • (abc):匹配abc并捕获该匹配项
    • (?:abc):匹配abc但不捕获该匹配项(非捕获组)
    • \1:匹配之前第1个分组捕获的文本
  4. 量词
    • *:匹配前面的子表达式零次或多次
    • +:匹配前面的子表达式一次或多次
    • ?:匹配前面的子表达式零次或一次
    • {n}:匹配确定的n次
    • {n,}:至少匹配n次
    • {n,m}:最少匹配n次且最多匹配m次
  5. 选择
    • |:或运算符,匹配两项之一
  6. 转义字符
    • \:将下一个字符标记为特殊字符或字面值


模式

在正则表达式中,贪婪模式(Greedy Mode)和懒惰模式(Lazy Mode,也叫非贪婪模式或惰性模式)是与量词相关的两种匹配模式

它们决定了正则表达式匹配字符的方式,即是尽可能多地匹配(贪婪)还是尽可能少地匹配(懒惰)

  1. 贪婪模式

    贪婪模式是正则表达式的默认匹配模式,它会尽可能多地匹配字符

    也就是说,它会匹配尽可能多的字符,直到整个表达式能够匹配为止

    例如,在表达式 a.*b 中,它会匹配从第一个 "a" 到最后一个 "b" 之间的所有内容

  2. 懒惰模式

    懒惰模式或非贪婪模式,会尽可能少地匹配字符

    在量词后加上问号 ? 可以实现懒惰匹配

    例如,在表达式 a.*?b 中,它会匹配从第一个 "a" 到最近的一个 "b" 之间的内容,使用尽可能少的匹配尝试找到满足条件的匹配项

1
2
3
4
5
6
7
8
9
10
11
let text = "This is a <div>simple</div> div.";

// 贪婪匹配
let greedyRegex = /<.*>/;
let greedyResult = text.match(greedyRegex);
console.log("Greedy Result:", greedyResult[0]); // Greedy Result: <div>simple</div>

// 懒惰匹配
let lazyRegex = /<.*?>/;
let lazyResult = text.match(lazyRegex);
console.log("Lazy Result:", lazyResult[0]); // Lazy Result: <div>
  • 贪婪匹配的结果是 "<div>simple</div>"。它匹配了从字符串中的第一个 < 到最后一个 > 的整段文本。
  • 懒惰匹配的结果是 "<div>"。它只匹配了开头的 <div> 标签,因为懒惰模式会在找到第一个满足条件的 > 后停止匹配。


匹配方法

  1. exec()
    • 此方法由正则表达式对象调用
    • 它返回一个数组(包含匹配的信息)或在没有匹配项时返回null
    • 例子: /[a-e]/i.exec("Hello World")
  2. test()
    • 此方法同样由正则表达式对象调用
    • 它返回一个布尔值,表示是否存在匹配
    • 例子: /hello/i.test("Hello world")
  3. match()
    • 这个方法由字符串对象调用
    • 它返回一个包含匹配结果的数组或在没有匹配项时返回null
    • 例子: "Hello world".match(/[a-e]/i)
  4. matchAll()
    • 此方法由字符串对象调用
    • 它返回一个包含所有匹配的迭代器
    • 例子: "test1test2".matchAll(/\d/g)
  5. replace()
    • 由字符串对象调用
    • 它返回一个新字符串,其中的匹配项被替换
    • 例子: "Hello World".replace(/world/i, "Mars")
  6. replaceAll()
    • 类似于 replace(),但它会替换掉字符串中所有的匹配项
    • 例子: "2022/05/11".replaceAll(/\//g, "-")
  7. search()
    • 此方法由字符串对象调用
    • 返回第一个匹配项的索引,如果没有匹配则返回-1
    • 例子: "Hello World".search(/world/i)
  8. split()
    • 此方法由字符串对象调用
    • 它使用正则表达式或一个固定字符串来分割字符串,并返回一个数组
    • 例子: "Hello World".split(/\s/)


Q14:Javascript里面的继承方式有哪些?

难度:⭐⭐⭐

答案

在JavaScript中,继承是一种让一个对象获得另一个对象的属性和方法的机制。以下是主要的继承方式:

  1. 原型链继承

    每个对象都有一个指向另一个对象的链接称为原型(prototype)

    当你试图访问一个对象的属性时,如果对象本身没有这个属性,则会去它的原型(及原型的原型,依次类推)中查找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function Parent() {
    this.parentProperty = true;
    }

    Parent.prototype.getParentProperty = function() {
    return this.parentProperty;
    };

    function Child() {
    this.childProperty = false;
    }

    // 继承Parent
    Child.prototype = new Parent();

    var child = new Child();
    console.log(child.getParentProperty()); // true
  2. 构造函数继承

    通过在子类的构造函数中调用父类的构造函数,可以继承父类的属性,但没法继承父类原型的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
    }

    function Child(name) {
    Parent.call(this, name); // 继承属性
    }

    var child1 = new Child('child1');
    console.log(child1.name); // 'child1'
  3. 组合继承

    结合了原型链继承和构造函数继承,既能继承属性也能继承方法

    通常是在子类构造函数中调用父类构造函数,并将子类的原型设置为父类的实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
    }

    Parent.prototype.getName = function() {
    return this.name;
    };

    function Child(name, age) {
    Parent.call(this, name); // 继承属性
    this.age = age;
    }

    Child.prototype = new Parent(); // 继承方法
    Child.prototype.constructor = Child;

    var child1 = new Child('child1', 5);
    console.log(child1.getName()); // 'child1'
    console.log(child1.age); // 5
  4. 原型式继承

    是ECMAScript 5中通过Object.create方法实现的,可以创建一个新对象,用现有的对象来提供新创建的对象的__proto__

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var parent = {
    name: 'parent',
    colors: ['red', 'blue', 'green']
    };

    var child = Object.create(parent);
    child.name = 'child';

    console.log(child.name); // 'child'
    console.log(child.colors); // ['red', 'blue', 'green']
  5. 寄生式继承

    创建一个仅用于封装继承过程的函数来增强对象,然后返回这个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var parent = {
    name: 'parent',
    colors: ['red', 'blue', 'green']
    };

    function createAnother(original) {
    var clone = Object.create(original);
    clone.sayHi = function() {
    console.log('Hi');
    };
    return clone;
    }

    var child = createAnother(parent);
    child.sayHi(); // 'Hi'
    console.log(child.name); // 'parent'
  6. 寄生组合式继承

    为了解决组合继承调用两次父构造函数的问题,寄生组合式继承将父对象的原型赋给子对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
    }

    Parent.prototype.getName = function() {
    return this.name;
    };

    function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
    }

    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;

    var child1 = new Child('child1', 5);
    console.log(child1.getName()); // 'child1'
    console.log(child1.age); // 5
  7. ES6类继承 extendssuper

    ES6引入了类(class)概念,通过classextends关键字,提供了更清晰和易于理解的继承语法

    super关键字用于调用父类的构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Parent {
    constructor(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
    }

    getName() {
    return this.name;
    }
    }

    class Child extends Parent {
    constructor(name, age) {
    super(name);
    this.age = age;
    }
    }

    const child = new Child('child1', 5);
    console.log(child.getName()); // 'child1'
    console.log(child.age); // 5
引申

每种继承方式都有其特定的适用场景、优点和缺点

了解这些可以帮助你更好地选择适合你项目的继承策略:

  1. 原型链继承
    • 优点:简单易理解。
    • 缺点:来自原型的所有属性被所有实例共享,无法实现多继承。
  2. 构造函数继承
    • 优点:可以实现多继承,创建子类实例时可以向父类传递参数。
    • 缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
  3. 组合继承
    • 优点:融合了原型链继承和构造函数继承的优点,能够实现函数复用。
    • 缺点:调用了两次父类构造函数,生成了两份实例。
  4. 原型式继承
    • 优点:适用于不需要单独创建构造函数,但仍然需要在多个对象间共享信息的场景。
    • 缺点:包含引用类型的属性值始终会共享相应的值,这样的话就不适合单独实例。
  5. 寄生式继承
    • 优点:可以为对象添加新方法。
    • 缺点:使用函数创造对象,无法做到函数复用,效率较低。
  6. 寄生组合式继承
    • 优点:避免了组合继承的缺点,只调用一次父类构造函数,并且原型链保持不变。
    • 缺点:实现起来较为复杂。
  7. ES6类继承 (extendssuper)
    • 优点:语法更清晰简洁,更接近传统面向对象语言的写法,易于理解和使用。
    • 缺点:class关键字实质上还是原型继承的语法糖,不能完全摆脱原型链继承的限制。


Q15:base64编码图片,为什么会让数据量变大

难度:⭐⭐⭐

答案

Base64 是一种用64个字符来表示任意二进制数据的方法

它用 64 个可打印的 ASCII 字符来表示二进制数据

为什么 Base64 会使数据变大呢?

这是因为 Base64 编码设计的初衷是在无法直接处理二进制数据的情况下,将二进制数据转换为只包含ASCII字符的字符串,以便二进制数据可以以文本的形式在网络上进行传输或存储

然而,这种转换是有代价的

Base64编码过程中,每3个字节(24位)的数据,会被编码为4个字节的ASCII字符

这就导致了增加了约33%的数据量(4/3)

如果原始数据没有足够的数据(如不是3的倍数),那么 Base64 还会在末尾添加填充字符(‘=’),这可能会导致数据量进一步增加

因此,Base64编码并不适合用于压缩数据或减小数据大小,对于大量数据或者大文件,使用Base64会显著增加数据量

然而,如果你需要在无法直接发送二进制数据的环境中发送数据(例如在 JSON 中发送图像),或者需要将二进制数据存储为文本(如 CSS 中的内联图像),那么Base64是非常方便的工具

Base64编码的根本目的并不是为了减少数据大小,而是为了能在仅支持文本的环境中处理二进制数据


数组

Q1:将数组的length设置为0,获取它的第一个元素会返回什么

难度:⭐

解析

当将数组的 length 设置为0时,数组实际上被清空,不包含任何元素。在这种情况下,获取数组的第一个元素将返回 undefined

这是因为数组为空,没有任何元素可以被访问。当你尝试获取一个不存在的元素时,JavaScript 返回 undefined,表示值不存在或未定义。因此,如果数组的 length 被设置为0,任何尝试获取数组元素的操作都将返回 undefined

答案

undefined


Q2:什么是类数组对象

难度:⭐

答案

类数组对象(Array-like Object)是具有类似数组结构的对象,但并不是真正的数组。这些对象在结构上类似数组,通常有数值索引和 length 属性,但它们不继承自数组(Array)原型,因此不具备数组原型上的方法

引申

常见的类数组对象包括:

  1. arguments 对象: 在函数内部可用,包含了传递给函数的参数,具有数值索引和 length 属性。
  2. NodeList 对象: 代表文档中的节点列表,例如使用 document.querySelectorAllelement.childNodes 得到的对象。
  3. 字符串(String): 字符串也可以被视为类数组对象,因为它们有类似数组的索引和 length 属性。
  4. 函数的 arguments 对象: 函数内部的 arguments 对象也是一个类数组对象。

类数组对象在某些情况下非常有用,但由于它们不具备数组原型上的方法,例如 pushpopslice 等,因此在需要使用数组方法的场合,需要将其转换为真正的数组

有多种方法可以将类数组对象转换为数组。以下是几种常见的方法:

  1. 使用 Array.from()

    1
    2
    3
    4
    const arrayLikeObject = { 0: 'apple', 1: 'banana', length: 2 };
    const array = Array.from(arrayLikeObject);

    console.log(array); // ['apple', 'banana']
  2. 使用扩展运算符 (…)

    1
    2
    3
    4
    const arrayLikeObject = { 0: 'apple', 1: 'banana', length: 2 };
    const array = [...arrayLikeObject];

    console.log(array); // ['apple', 'banana']
  3. 使用 Array.prototype.slice.call()

    1
    2
    3
    4
    const arrayLikeObject = { 0: 'apple', 1: 'banana', length: 2 };
    const array = Array.prototype.slice.call(arrayLikeObject);

    console.log(array); // ['apple', 'banana']
  4. 使用 Array.prototype.concat.apply()

    1
    2
    3
    4
    const arrayLikeObject = { 0: 'apple', 1: 'banana', length: 2 };
    const array = Array.prototype.concat.apply([], arrayLikeObject);

    console.log(array); // ['apple', 'banana']
  5. 使用 forEach()

    1
    2
    3
    4
    5
    6
    7
    const arrayLikeObject = { 0: 'apple', 1: 'banana', length: 2 };
    const array = [];
    Array.prototype.forEach.call(arrayLikeObject, (element) => {
    array.push(element);
    });

    console.log(array); // ['apple', 'banana']
  6. 使用 for…of 循环

    1
    2
    3
    4
    5
    6
    7
    const arrayLikeObject = { 0: 'apple', 1: 'banana', length: 2 };
    const array = [];
    for (const element of arrayLikeObject) {
    array.push(element);
    }

    console.log(array); // ['apple', 'banana']


Q3:空数组调用reduce会发生什么

难度:⭐

解析

在空数组上调用 reduce 方法时,如果没有提供初始值(initialValue),会抛出 TypeError。这是因为 reduce 需要至少有一个元素来执行归约操作,而在空数组中没有元素可供归约

答案
1
2
3
4
5
6
const emptyArray = [];
const result = emptyArray.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
});

// Uncaught TypeError: Reduce of empty array with no initial value
引申

为了避免这个错误,可以在调用 reduce 时提供一个初始值:

1
2
3
4
5
6
const emptyArray = [];
const result = emptyArray.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
}, 0);

console.log(result); // 0


Q4:数组的reduce有什么用法

难度:⭐

答案

reduce 是数组方法之一,用于迭代数组的每个元素,并将它们汇总为单个值。下面列举了一些 reduce 方法的常见用途:

  1. 累加(Summation):

    1
    2
    3
    const numbers = [1, 2, 3, 4, 5];
    const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
    console.log(sum); // 15
  2. 累积(Product):

    1
    2
    3
    const numbers = [2, 3, 4];
    const product = numbers.reduce((accumulator, currentValue) => accumulator * currentValue, 1);
    console.log(product); // 24
  3. 查找最大值:

    1
    2
    3
    const numbers = [10, 5, 8, 3];
    const max = numbers.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue));
    console.log(max); // 10
  4. 查找最小值:

    1
    2
    3
    const numbers = [10, 5, 8, 3];
    const min = numbers.reduce((accumulator, currentValue) => Math.min(accumulator, currentValue));
    console.log(min); // 3
  5. 拼接字符串:

    1
    2
    3
    const words = ['Hello', ' ', 'World'];
    const sentence = words.reduce((accumulator, currentValue) => accumulator + currentValue, '');
    console.log(sentence); // 'Hello World'
  6. 统计元素出现次数:

    1
    2
    3
    4
    5
    6
    const fruits = ['apple', 'orange', 'apple', 'banana', 'orange', 'apple'];
    const countByFruit = fruits.reduce((accumulator, currentValue) => {
    accumulator[currentValue] = (accumulator[currentValue] || 0) + 1;
    return accumulator;
    }, {});
    console.log(countByFruit); // { apple: 3, orange: 2, banana: 1 }
  7. 扁平化数组:

    1
    2
    3
    const nestedArray = [[1, 2], [3, 4], [5, 6]];
    const flattenedArray = nestedArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
    console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]
  8. 将对象数组转换为对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const people = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Charlie' }
    ];
    const peopleMap = people.reduce((accumulator, person) => {
    accumulator[person.id] = person.name;
    return accumulator;
    }, {});
    console.log(peopleMap); // { 1: 'Alice', 2: 'Bob', 3: 'Charlie' }

这些只是 reduce 方法的一些用途,它的强大之处在于其灵活性,可以根据具体需求执行各种汇总操作


Q5:arguments 这种类数组,如何遍历类数组?

难度:⭐

答案

在 JavaScript 中,类数组是类似数组的对象,它们通常具有数字索引和 length 属性,但不具有数组原型链上的方法(例如 forEach、map 等)。在遍历类数组时,可以使用多种方法:

  1. for 循环

    1
    2
    3
    4
    for (var i = 0; i < arguments.length; i++) {
    // 对 arguments 中的每个元素执行操作
    console.log(arguments[i]);
    }
  2. Array.prototype.forEach()(需要将类数组转换为真正的数组):

    1
    2
    3
    4
    Array.prototype.forEach.call(arguments, function(item) {
    // 对每个元素执行操作
    console.log(item);
    });
  3. 转换为真正的数组后使用其他数组方法(例如 map、filter 等):

    1
    2
    3
    4
    5
    var argsArray = Array.from(arguments);
    argsArray.forEach(function(item) {
    // 对每个元素执行操作
    console.log(item);
    });
  4. 使用 ES6 中的扩展运算符

    1
    2
    3
    4
    [...arguments].forEach(function(item) {
    // 对每个元素执行操作
    console.log(item);
    });


Q6:Javscript数组的常用方法有哪些?

难度:⭐⭐

答案
  1. push():向数组末尾添加一个或多个元素,并返回修改后的数组的长度。

    1
    2
    let arr = [1, 2, 3];
    arr.push(4); // 返回值为 4,arr 变为 [1, 2, 3, 4]
  2. pop():移除数组末尾的元素,并返回移除的元素。

    1
    2
    let arr = [1, 2, 3];
    let removedElement = arr.pop(); // 返回值为 3,arr 变为 [1, 2]
  3. shift():移除数组的第一个元素,并返回移除的元素。

    1
    2
    let arr = [1, 2, 3];
    let removedElement = arr.shift(); // 返回值为 1,arr 变为 [2, 3]
  4. unshift():向数组的开头添加一个或多个元素,并返回修改后的数组的长度。

    1
    2
    let arr = [2, 3];
    arr.unshift(1); // 返回值为 3,arr 变为 [1, 2, 3]
  5. concat():将两个或多个数组合并成一个新数组,不修改原始数组。

    1
    2
    3
    let arr1 = [1, 2];
    let arr2 = [3, 4];
    let newArr = arr1.concat(arr2); // newArr 变为 [1, 2, 3, 4]
  6. slice():从数组中提取出指定范围的元素,返回一个新数组,不修改原始数组。

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let slicedArr = arr.slice(1, 3); // slicedArr 变为 [2, 3]
  7. splice():从数组中添加、移除或替换元素,会修改原始数组。

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let removedElements = arr.splice(1, 2); // 返回值为 [2, 3],arr 变为 [1, 4, 5]
  8. forEach():对数组的每个元素执行指定的操作。

    1
    2
    let arr = [1, 2, 3];
    arr.forEach(item => console.log(item)); // 依次输出 1、2、3
  9. map():对数组的每个元素执行指定的操作,并返回操作后的结果组成的新数组。

    1
    2
    let arr = [1, 2, 3];
    let newArr = arr.map(item => item * 2); // newArr 变为 [2, 4, 6]
  10. filter():筛选数组中满足条件的元素,并返回满足条件的元素组成的新数组。

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let filteredArr = arr.filter(item => item % 2 === 0); // filteredArr 变为 [2, 4]
  11. reduce():对数组中的所有元素执行指定的累加操作,返回累加结果。

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let sum = arr.reduce((acc, curr) => acc + curr, 0); // sum 变为 15
  12. every():检测数组中的所有元素是否都满足指定条件,如果所有元素都满足条件,则返回 true,否则返回 false

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let allEven = arr.every(item => item % 2 === 0); // 返回 false
  13. some():检测数组中是否有至少一个元素满足指定条件,如果有至少一个元素满足条件,则返回 true,否则返回 false

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let hasEven = arr.some(item => item % 2 === 0); // 返回 true
  14. find():查找数组中第一个满足指定条件的元素,并返回该元素;如果未找到满足条件的元素,则返回 undefined

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let found = arr.find(item => item > 3); // 返回 4
  15. findIndex():查找数组中第一个满足指定条件的元素的索引,并返回该索引;如果未找到满足条件的元素,则返回 -1

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let foundIndex = arr.findIndex(item => item > 3); // 返回 3
  16. includes():检测数组是否包含指定元素,如果包含则返回 true,否则返回 false

    1
    2
    let arr = [1, 2, 3, 4, 5];
    let hasElement = arr.includes(3); // 返回 true
  17. reverse():颠倒数组中元素的顺序,原地修改原始数组。

    1
    2
    let arr = [1, 2, 3, 4, 5];
    arr.reverse(); // arr 变为 [5, 4, 3, 2, 1]
  18. sort():对数组元素进行排序,默认按照字母顺序进行排序,原地修改原始数组。

    1
    2
    let arr = [3, 1, 2, 5, 4];
    arr.sort(); // arr 变为 [1, 2, 3, 4, 5]
  19. join():将数组中所有元素以指定的分隔符连接成一个字符串。

    1
    2
    let arr = ['a', 'b', 'c'];
    let str = arr.join('-'); // 返回 'a-b-c'
  20. toString():将数组转换为字符串,效果与 join() 方法一样,默认使用逗号 , 连接元素。

    1
    2
    let arr = ['a', 'b', 'c'];
    let str = arr.toString(); // 返回 'a,b,c'


函数

Q1:js的函数声明有几种方式?有什么区别?

难度:⭐

答案

在JavaScript中,函数声明有三种主要方式:函数声明(Function Declaration)、函数表达式(Function Expression)、箭头函数(Arrow Function)。它们之间的区别主要体现在语法和行为上。

  1. 函数声明(Function Declaration)

    1
    2
    3
    function functionName(parameters) {
    // 函数体
    }
    • 特点:
      • 在代码执行前进行预解析(hoisting),可以在声明之前调用。
      • 函数名是必须的,函数名会被添加到当前作用域(通常是全局作用域或函数作用域)。
      • 可以直接调用,不需要赋值给变量。
  2. 函数表达式(Function Expression)

    1
    2
    3
    var functionName = function(parameters) {
    // 函数体
    };
    • 特点:
      • 在代码执行到赋值语句时创建函数,因此不能在定义之前调用。
      • 函数名是可选的,可以省略,这种情况下函数被称为匿名函数。
      • 赋值给变量或作为参数传递给其他函数。
  3. 箭头函数(Arrow Function)

    1
    2
    3
    const functionName = (parameters) => {
    // 函数体
    };
    • 特点:
      • 箭头函数是ES6新增的一种函数定义方式,语法更简洁。
      • 不绑定this,会捕获所在上下文的this值。
      • 不能用作构造函数,不具备自己的this,不能使用arguments对象。


Q2:对“立即执行函数”的理解

难度:⭐

答案

立即执行函数(Immediately Invoked Function Expression,IIFE)是一种在定义后立即执行的 JavaScript 函数。它的语法形式是将函数声明或函数表达式包裹在圆括号中,然后紧接着使用另一对圆括号来立即调用这个函数。

下面是一个典型的立即执行函数的示例:

1
2
3
4
5
6
7
(function() {
// 函数体
})();

!function (test) { //使用!运算符,输出123
console.log(test);
}(123);

这个函数在声明后立即被调用执行。(),!,+,-,=等运算符都能起到立即执行的作用

立即执行函数的主要特点和用途包括:

  1. 作用域隔离:立即执行函数可以创建一个独立的作用域,其中的变量不会污染到全局作用域,避免了变量命名冲突。
  2. 模块化开发:通过立即执行函数,可以创建模块化的代码结构,将代码封装在独立的作用域中,提高了代码的可维护性和可重用性。
  3. 防止变量提升:立即执行函数中的变量会在函数执行完毕后被销毁,不会污染全局作用域,避免了变量提升可能带来的问题。
  4. 封装变量:可以将一些私有的变量或函数封装在立即执行函数中,只暴露需要暴露的接口,隐藏实现细节,增强了代码的安全性。
  5. 初始化代码:立即执行函数常被用于执行一些初始化代码,确保代码在加载后立即执行。


Q3:new fn与new fn()有什么区别吗?

难度:⭐

答案

在 JavaScript 中,new fnnew fn() 看起来很相似,但实际上它们之间有重要的区别:

  1. new fn: 这种写法是在调用构造函数 fn 时省略了括号。它会创建一个新的对象,并将构造函数的作用域绑定到这个新对象上,但不传递任何参数给构造函数。因此,如果构造函数 fn 没有参数,那么这两种写法会得到相同的结果。
  2. new fn(): 这是标准的构造函数调用语法,它会创建一个新的对象,并将构造函数的作用域绑定到这个新对象上,并传递括号内的参数给构造函数。即使构造函数不需要参数,也必须使用括号。

因此,主要区别在于是否传递参数给构造函数。如果构造函数需要参数,则必须使用 new fn(),而如果构造函数不需要参数,则可以使用 new fn,两者效果是一样的。


Q4:ES6中函数新增了哪些扩展?

难度:⭐⭐

答案
  1. 箭头函数(Arrow Functions)
    • 箭头函数是一种更简洁的函数定义语法,使用箭头(=>)来定义函数。
    • 箭头函数没有自己的 thisargumentssupernew.target,它们会从父作用域中继承这些值。
    • 示例:(param1, param2) => expression
  2. 默认参数(Default Parameters)
    • 可以为函数参数设置默认值,如果调用函数时未传递参数,则使用默认值。
    • 示例:function func(param1 = defaultValue) { }
  3. 剩余参数(Rest Parameters)
    • 使用剩余参数语法(...args)来捕获函数中的剩余参数,将它们放入一个数组中。
    • 示例:function func(param1, ...args) { }
  4. 扩展运算符(Spread Operator)
    • 使用扩展运算符(...)可以将数组展开为独立的参数。
    • 示例:function func(param1, param2, ...restParams) { }
  5. 函数的扩展方法
    • Function.prototype.bind() 方法现在支持在箭头函数上调用,可以创建绑定了指定 this 的箭头函数。
    • Function.prototype.toString() 方法现在返回函数原始代码,包括 ES6 中新增的箭头函数和方法简写。
  6. 模板字符串(Template Strings)
    • 模板字符串是一种新的字符串语法,使用反引号(``)来定义字符串,并支持嵌入表达式和多行字符串。
    • 示例:Hello, ${name}!
  7. 函数参数的解构赋值
    • 可以在函数参数中使用解构赋值语法,将传入的对象或数组解构为单独的变量。
    • 示例:function func({param1, param2}) { }
  8. arguments 对象的限制
    • 在箭头函数中,arguments 对象会继承自父作用域,而不是创建一个独立的 arguments 对象。


Q5:什么是尾调用优化和尾递归

难度:⭐⭐

答案
  1. 尾调用

    尾调用准确来说,是在一个函数的最后一部操作是另外一个函数的调用

    1
    2
    3
    function funcA(x) {
    return funcB(x);
    }

    在 funcA 中,最后一步操作是调用 funcB,所以这就是一个尾调用

  2. 尾调用优化 (Tail Call Optimization)

    在函数调用时,为了回到函数调用位置,及恢复所需环境,操作系统会在调用栈中维护所谓的“调用记录”或“帧”,这会占用一些内存

    对于一般的函数调用,新的函数调用需要维护一个新的帧,因此如果有大量的函数调用(例如在深度递归中),这个帧的堆栈可能会非常大,占用很多内存空间,甚至可能导致“栈溢出”

    然而对于尾调用来说,由于它是函数的最后一个操作,所以没有必要保留当前的帧

    尾调用优化就是利用这个性质来节约内存的一种技术

    在支持尾调用优化的环境中,如果一个函数的最后一步是尾调用,则解释器或编译器不会创建新的帧,而是复用当前的帧

  3. 尾递归

    尾递归是特殊的尾调用,它指的在函数的最后一步调用了函数自身

    1
    2
    3
    4
    5
    6
    function factorial(n, total = 1) {
    if (n === 1) {
    return total;
    }
    return factorial(n - 1, n * total);
    }

    在 factorial 函数这个例子中,我们实现了一个用于计算阶乘的尾递归函数

    在这个函数中,每次函数调用自身(也就是进行递归)的时候,都是在函数的最后一步,所以它就是个尾递归

    而且由于我们把 total 参数传递给下一次调用,使得下一次递归有了起始值,这样就能确保递归在结束条件满足时能立即得到结果

    这样的方法在编程中被称为累积传递风格(Accumulate Passing Style,简称APS),经常被用于实现尾递归

在支持尾调用优化的环境中,尾递归函数不会因为递归深度过大而导致栈溢出或者内存消耗过大,它的性能和循环差不多

不过需要注意的是,目前并不是所有的 JavaScript 环境都支持尾调用优化

引申
  • 尾调用优化如何帮助减少内存占用?

    尾调用优化(Tail Call Optimization,TCO)帮助减少内存占用的方式与其工作原理紧密相关

    一般来说,每当一个函数调用另一个函数时,计算机需要在内存中保留一些信息,如函数在哪里被调用的,该函数的参数,还有局部变量等等。这种信息记录被称为”栈帧”(stack frame)

    如果有一个函数在其函数体最后的语句中调用了另一个函数(不管是相同的函数还是不同的函数),这样的调用被称为”尾调用”。由于尾调用是函数的最后一步操作,函数的栈帧已经可以被废弃,因为我们已经没有其他的操作需要使用这个栈帧了

    在不进行尾调用优化的情况下,对每一个函数调用,系统都会为其创建一个新的栈帧,即使是尾调用。然而在开启尾调用优化的环境中,如果发生尾调用,不会创建新的栈帧,而是清除当前的栈帧并复用,因为我们知道老的栈帧已经不再需要了

    这样就实现了内存的优化,即使在大量的递归调用中,也仅需要维护一个栈帧,极大地优化了内存占用,因此就避免了内存溢出或者栈溢出的问题

  • 为什么目前并不是所有的 JavaScript 环境都支持尾调用优化

    1. 技术挑战:在 JavaScript 引擎中实现 TCO 要处理诸多技术问题和挑战。由于 JavaScript 的动态特性,很多情况下很难判断是否可以安全地进行尾调用优化。引擎必须确保优化不会对代码的预期行为产生副作用。
    2. 性能考量:虽然 TCO 在理论上能节省内存,并防止栈溢出的问题,但在实践中,并不是所有应用场景都能体现这种优化的效果。尤其是在目前大多数的 web 应用中,深层次的递归调用并不常见,因此浏览器厂商可能认为投入资源来实现这一特性的优先级不高。
    3. 规范更迭:Javascript 的规范在不断发展和更迭之中,一些特性可能在接下来的版本中被调整或替换。一部分实现者可能在等待规范的稳定,从而避免在未来需要重做相关的实现。
    4. 向后兼容性:引入 TCO 可能会影响到现有代码的功能,特别是那些依赖于栈追踪信息的代码,因此,实现 TCO 需要仔细考虑如何与旧代码兼容。
    5. 优先级与资源分配:浏览器和 JavaScript 环境的开发者可能有其他更紧急或更重要的优先级,这决定了他们如何分配时间和资源来实现语言规范的各个部分。


操作符

Q1:以下等式是成立的吗?1_000_000 === 1000000

难度:⭐

解析

在JavaScript中,下划线(_)在数字中的使用是为了提高数字的可读性,而不影响其值。这种表示法通常用于表示较大的数字,以便更容易阅读和理解数字的大小。

在你的例子中,1_000_0001000000 实际上表示相同的数值,因为下划线在JavaScript中被视为一个合法的数字分隔符,但在计算时会被忽略。这就意味着,无论你是用下划线分隔还是没有下划线,这两个表示法都代表相同的数字1,000,000。

这种语法的引入是为了方便阅读和书写较大的数字,而不改变其实际值。这样的可读性改进对于理解代码或数据中的大数字是很有帮助的。

答案

true


Q2:空值合并运算符是什么?有什么使用场景

难度:⭐

答案

空值合并运算符(nullish coalescing operator),通常表示为 ??,是 JavaScript 的一个逻辑运算符。它提供了一种简洁的方式来处理值为 nullundefined 时的默认值设置。

语法形式为:a ?? b,它的行为是:如果 anullundefined,则返回 b,否则返回 a

下面是一些示例说明空值合并运算符的使用场景:

设置默认值

1
2
3
4
5
6
7
8
9
10
const user = {
name: 'Alice',
age: null,
};

const userName = user.name ?? 'Guest';
const userAge = user.age ?? 18;

console.log(userName); // 'Alice'
console.log(userAge); // 18

防止使用空值运算符时,0'' 被认为是空值

1
2
3
4
5
6
7
8
const height = 0;
const minHeight = height || 100; // 如果 height 为 falsy(0),则使用默认值 100

console.log(minHeight); // 100

const minHeightWithNullish = height ?? 100; // 只有在 height 为 null 或 undefined 时才使用默认值 100

console.log(minHeightWithNullish); // 0

避免未定义属性引发错误

1
2
3
4
5
6
7
8
9
10
const config = {
server: {
host: 'localhost',
port: null,
},
};

const serverPort = config.server.port ?? 3000;

console.log(serverPort); // 3000

在上述示例中,如果 config.server.portnull,使用空值合并运算符可以安全地提供一个默认值,而不引发错误。

空值合并运算符与传统的逻辑运算符 || 相比,更明确地处理只有在值为 nullundefined 时才提供默认值的情况。这使得代码更容易理解,避免了一些潜在的意外行为


Q3:请简述 == 的机制

难度:⭐

答案

== 是 JavaScript 中的相等运算符,用于比较两个值是否相等。它的比较规则如下:

  1. 类型转换
    • 如果比较的两个操作数类型相同,则直接进行值的比较。
    • 如果比较的两个操作数类型不同,则 JavaScript 会尝试将它们转换为相同的类型再进行比较。
  2. 转换规则
    • 如果一个操作数是布尔值,则将其转换为数值进行比较。true 转换为 1false 转换为 0
    • 如果一个操作数是字符串,另一个是数字,则将字符串转换为数字再进行比较。
    • 如果一个操作数是对象,另一个不是,则将对象转换为原始值(通过 valueOf()toString() 方法),然后再进行比较。
    • 如果一个操作数是 null,另一个是 undefined,则它们被认为是相等的。
    • 如果一个操作数是 NaN,则它与任何其他值(包括自身)都不相等。
  3. 特殊情况
    • 当比较的操作数是 nullundefined 时,它们相等。但如果其中一个是 NaN,则它们不相等。
    • 如果其中一个操作数是对象,另一个是原始值(字符串、数字、布尔值),则将对象转换为原始值再进行比较。
  4. 注意事项
    • 由于 == 运算符进行类型转换,因此可能会导致一些意想不到的结果。为了避免这种情况,通常应该优先使用严格相等运算符 ===,它要求操作数的值和类型都相等才返回 true
    • 在条件语句中,使用 == 比较时要格外小心,确保理解其转换规则并且符合预期的结果。


Q4:==和 ===有什么区别,分别在什么情况使用?

难度:⭐⭐

答案

===== 都是 JavaScript 中的比较运算符,用于比较两个值是否相等,它们之间的区别如下:

  1. ==(相等运算符)
    • 使用 == 进行比较时,如果两个操作数的类型不同,会先进行类型转换,然后再比较值是否相等。
    • 如果操作数的类型不同,则会尝试将它们转换为相同的类型,再进行比较。
    • 如果操作数之一是对象,则比较它们是否引用了相同的对象,而不是对象的内容。
    • 示例:0 == '0' 返回 true,因为 '0' 被转换为数字 0 进行比较。
  2. ===(严格相等运算符)
    • 使用 === 进行比较时,不会进行类型转换,而是严格比较两个操作数的值和类型是否完全相等。
    • 只有在操作数的值和类型完全相等时,才会返回 true;否则返回 false
    • 示例:0 === '0' 返回 false,因为它们的类型不同。

使用情况

  • 通常情况下,推荐使用 === 进行严格相等比较,因为它不会进行隐式类型转换,更加安全和准确。
  • 当需要忽略类型的差异时,或者需要进行隐式类型转换时,可以使用 == 进行相等比较,但需要注意可能会产生的意外结果。
  • 在开发过程中,应根据具体的需求和情况选择合适的比较运算符。如果不确定应该使用哪个运算符,最好使用 === 进行严格相等比较


Q5:如果new一个箭头函数会怎么样

难度:⭐⭐

答案

如果你尝试用new操作符去实例化一个箭头函数,会抛出一个错误

这是因为箭头函数并没有自己的this,它们不绑定自己的this值,在箭头函数中的this实际上是继承自外围上下文

因此,箭头函数不能被用作构造函数

例如,以下代码将会抛出一个错误

1
2
const ArrowFunction = () => {};
const instance = new ArrowFunction(); // 错误 - ArrowFunction is not a constructor

当你运行上述代码时,JavaScript将会抛出一个TypeError,指出ArrowFunction不是一个构造函数

这是因为你无法用new关键字来实例化一个箭头函数

因此,如果你想要创建一个可以使用new操作符的函数,你应该使用传统的函数声明或者函数表达式,不要使用箭头函数

引申

new一个函数的时候,new操作符做了什么操作

使用new操作符创建一个函数的实例时,它执行了几个步骤来构建和返回一个新对象。以下是详细的步骤:

  1. 创建空对象

    new操作符首先创建一个空的JavaScript对象{}

  2. 设置原型

    新创建的对象的__proto__会被链接到构造函数的prototype对象,让新创建的对象可以访问构造函数原型链上的属性和方法

  3. 绑定this

    在构造函数中,this被改变并指向了新创建的对象

    因此,在构造函数内部,我们可以使用this来引用和初始化新对象的属性

  4. 运行构造函数中的代码(包括它定义的属性和方法)

    这将定义新对象的属性和方法

  5. 返回新对象

    如果构造函数没有显示地返回一个对象,那么new操作符将自动返回新创建的对象

    需要注意的是,如果构造函数返回非空对象(Object或者Array)则new操作符返回的是这个对象,而不是新创建的对象


Q6:Object.is()与比较操作符“===”、“==”的区别

难度:⭐⭐

答案
  1. Object.is()
    • Object.is()用于判断两个值是否为相同的值
    • 它与===非常相似,但在两个特殊情况下有所不同:
      • 对于NaNObject.is(NaN, NaN)返回true(而NaN === NaN返回false
      • 对于+0-0Object.is(+0, -0)返回false(而+0 === -0返回true
  2. ===(严格等于)
    • ===不会进行类型转换,如果两个值的类型不同,它直接返回false
    • 对于大多数值,===的行为和Object.is()一样
    • 除了上面提到的两个特殊情况,也就是NaN不等于自身,以及认为+0-0是相等的
  3. ==(抽象等于)
    • ==在比较前会进行类型转换,尝试将两个值转换为相同类型,然后再进行值的比较
    • 由于涉及到类型转换,使用==时可能出现一些非直观的结果,尤其是当比较不同类型的值时
    • 举个例子,'2' == 2会返回true,因为字符串'2'会被转换为数字2

在编写代码时,推荐使用===来避免意外的类型转换,从而使代码的行为更加可预测

Object.is()在需要区分+0-0,或者判断NaN的相等性时很有用

==由于其类型转换的特性,一般不推荐使用,除非明确需要这种行为


Q7:0.1+0.2为什么不等于0.3

难度:⭐⭐

答案

0.1 + 0.2 不等于 0.3 是浮点数精度问题的一个典型示例,这个问题存在于很多编程语言中,不仅仅是JavaScript。原因在于这些语言通常遵循IEEE 754标准来表示浮点数。

  1. 浮点数和精度

    在这个标准中,数字是以二进制格式存储的

    整数可以精确地表示为二进制数,但很多分数在二进制中是无限循环小数

    比如,1/101/5 在二进制中不能精确表示

    在十进制中:

    • 1/10 等于 `0.1``
    • `1/5 等于 0.2

    但在二进制中,它们是无限循环小数:

    • 1/10 的二进制形式近似为 `0.0001100110011001100110011001100…``
    • `1/5 的二进制形式近似为 0.00110011001100110011001100110011...

    由于计算机存储容量是有限的,这些小数点后无限循环的二进制数必须在某处切断,这意味着它们是近似值,而不是精确值

  2. 精确性问题

    当执行 0.1 + 0.2 这样的操作时,二进制中的近似值相加会产生一个比实际 0.3 略微不同的结果

    在十进制中,这个结果经常是 0.30000000000000004,而不是我们期望的 0.3

  3. 如何解决

    为了应对这个问题,在处理金融或精度重要的数学计算时,采取特别的策略:

    • 四舍五入

      JavaScript 提供了内置的 toFixed 方法,可以把浮点数四舍五入到指定的小数位

      比如,(0.1 + 0.2).toFixed(1) 将返回字符串 “0.3”

    • 使用整数进行计算

      如果我们知道我们正在处理的小数点后的位数,我们可以把浮点数转换为整数,进行整数运算,然后再转回浮点数

      比如,把 0.10.2 乘以10得到 12,然后相加得到 3,再除以10得到 0.3

    • 使用第三方库

      有许多JavaScript库,比如 decimal.js、bignumber.js 或者 math.js,这些库提供了更精确的计算,可以解决浮点数精度的问题


API

Q1:ES6中新增的Set跟Map两种数据结构该怎么理解?

难度:⭐⭐

解析

ES6 中新增的 Set 和 Map 是两种不同的数据结构,分别用于存储一组唯一的值和键值对。下面分别对 Set 和 Map 进行简要的理解:

Set(集合)

  1. 唯一性
    • Set 中的元素是唯一的,不能重复。
    • 如果试图向 Set 中添加重复的元素,它会被忽略。
  2. 元素的顺序
    • Set 中的元素按照插入的顺序排列,不会像对象那样根据键排序。
  3. API 方法
    • add(value):向 Set 中添加元素。
    • delete(value):删除 Set 中的指定元素。
    • has(value):检查 Set 中是否包含某个元素。
    • clear():清空 Set 中的所有元素。
  4. 应用场景
    • 用于存储一组唯一的值,比如去重。

Map(映射)

  1. 键值对
    • Map 中存储的是键值对,其中键可以是任意数据类型。
  2. 键的唯一性
    • Map 中的键是唯一的,每个键只能对应一个值。
  3. 元素的顺序
    • Map 中的元素按照插入的顺序排列,与 Set 类似,不会根据键的顺序排序。
  4. API 方法
    • set(key, value):向 Map 中添加键值对。
    • get(key):获取 Map 中指定键的值。
    • delete(key):删除 Map 中指定键的键值对。
    • has(key):检查 Map 中是否包含某个键。
    • clear():清空 Map 中的所有键值对。
  5. 应用场景
    • 用于存储键值对,提供更灵活的数据结构,例如保存对象的属性和值。
答案
  • Set 适合存储一组唯一的值,不关心键值对的情况。
  • Map 适合存储键值对,每个键对应一个值。
  • 它们都提供了高效的查找和删除操作,适用于各种场景,提供了更丰富的 API 方法。


Q2:Math.ceil跟Meth.floor有什么区别

难度:⭐

答案

Math.ceilMath.floor 是 JavaScript 中用于处理数字的 Math 对象的两个方法,它们分别用于向上取整和向下取整。

  1. Math.ceil(x)

    • Math.ceil 方法返回大于或等于传入数字 x 的最小整数。
    • 例如,Math.ceil(4.3) 返回 5,因为 5 是大于 4.3 的最小整数。
    1
    2
    const resultCeil = Math.ceil(4.3);
    console.log(resultCeil); // 输出 5
  2. Math.floor(x)

    • Math.floor 方法返回小于或等于传入数字 x 的最大整数。
    • 例如,Math.floor(4.9) 返回 4,因为 4 是小于或等于 4.9 的最大整数。
    1
    2
    const resultFloor = Math.floor(4.9);
    console.log(resultFloor); // 输出 4
引申
  • Math.abs(x) 返回给定数字 x 的绝对值。

    1
    2
    const absoluteValue = Math.abs(-5);
    console.log(absoluteValue); // 输出 5
  • Math.round(x) 返回给定数字 x 的四舍五入值。

    1
    2
    const roundedValue = Math.round(4.6);
    console.log(roundedValue); // 输出 5
  • Math.max(x, y, ...) 返回传入的一组数字中的最大值。

    1
    2
    const maxValue = Math.max(10, 5, 8, 12);
    console.log(maxValue); // 输出 12
  • Math.min(x, y, ...) 返回传入的一组数字中的最小值。

    1
    2
    const minValue = Math.min(10, 5, 8, 12);
    console.log(minValue); // 输出 5
  • Math.sqrt(x) 返回给定数字 x 的平方根。

    1
    2
    const squareRoot = Math.sqrt(25);
    console.log(squareRoot); // 输出 5


Q3:document.write和innerHTML有什么区别

难度:⭐

解析
  1. document.write

    • document.writeDocument 对象的方法,可以直接在文档中写入字符串。
    • 它通常用于在页面加载过程中动态生成内容,但不太推荐在页面加载后使用,因为它会覆盖整个文档。
    • 如果在文档已加载后使用 document.write,它将重写整个文档,可能导致文档结构被破坏。
    1
    document.write('<p>Hello, World!</p>');
  2. innerHTML

    • innerHTMLElement 对象的属性,允许你获取或设置元素的 HTML 内容。
    • 它更适用于在已存在的元素中动态插入或更新内容。
    • 使用 innerHTML 时,你只需操作需要更新的元素,而不会影响文档的其他部分。
    1
    2
    3
    4
    5
    // 获取元素的 HTML 内容
    const elementContent = document.getElementById('exampleElement').innerHTML;

    // 设置元素的 HTML 内容
    document.getElementById('exampleElement').innerHTML = '<p>New content</p>';
答案

主要区别:

  • document.write 是一个全局方法,它直接操作整个文档,可能导致文档结构被覆盖,而且在文档加载后使用时存在一些问题。
  • innerHTML 是元素的属性,更适用于操作元素的 HTML 内容,且更安全可控。


Q4:Math.max跟Math.min怎么用于数组里面取最值

难度:⭐

答案

Math.maxMath.min 分别用于获取数组中的最大值和最小值。这两个方法可以结合使用 apply 或扩展运算符 ... 来处理数组。以下是使用示例:

使用 apply

1
2
3
4
5
6
7
const numbers = [2, 5, 1, 8, 3];

const maxNumber = Math.max.apply(null, numbers);
const minNumber = Math.min.apply(null, numbers);

console.log(maxNumber); // 输出 8
console.log(minNumber); // 输出 1

使用扩展运算符 ...

1
2
3
4
5
6
7
const numbers = [2, 5, 1, 8, 3];

const maxNumber = Math.max(...numbers);
const minNumber = Math.min(...numbers);

console.log(maxNumber); // 输出 8
console.log(minNumber); // 输出 1

在这两个示例中,Math.maxMath.min 都被用于处理数组 numbers,得到了数组中的最大值和最小值。

请注意,在处理非常大的数组时,可能会遇到 apply... 的参数列表长度限制。在这种情况下,可以采用其他方法,如使用循环或使用 reduce 函数来找到最大值和最小值


Q5:对requestIdleCallback的理解

难度:⭐

答案

requestIdleCallback 是浏览器提供的一个用于在浏览器空闲时执行任务的 API。它的主要目的是充分利用浏览器的空闲时间,执行一些不紧急但耗时较长的任务,而不影响用户界面的响应性能。

使用方法:

1
window.requestIdleCallback(callback[, options]);
  • callback: 要在空闲时执行的函数。
  • options (可选): 一个对象,用于指定调用 callback 的条件,例如 timeout 表示最长等待时间。

工作原理:

  1. 当浏览器空闲时,会执行注册的回调函数。
  2. 如果回调函数执行时间较长,浏览器会在执行过程中中断它,以确保其他高优先级任务(例如用户交互)的及时响应。
  3. 在下一个空闲周期继续执行未完成的部分。

适用场景:

  • 异步操作: 执行一些不需要立即完成的异步操作,如图片加载、数据请求等。
  • 性能优化: 执行一些耗时较长的性能优化任务,如计算复杂布局、懒加载等。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function performIdleTask(deadline) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
const task = tasks.shift();
task();
}

if (tasks.length > 0) {
window.requestIdleCallback(performIdleTask);
}
}

const tasks = [];

function addTask(task) {
tasks.push(task);

if (tasks.length === 1) {
window.requestIdleCallback(performIdleTask);
}
}

// 使用示例
addTask(() => console.log('Task 1'));
addTask(() => console.log('Task 2'));
// ...

上述代码创建了一个简单的任务队列,并通过 requestIdleCallback 在浏览器空闲时执行队列中的任务。这可以用于将一些非紧急的任务推迟到浏览器处于空闲状态时执行,以提高性能。

requestIdleCallback 的作用就是将一些非关键性的任务从主线程中分离出来,等到浏览器闲置时再执行。这样就可以避免占用主线程,提高页面的响应速度和流畅度。

使用 requestIdleCallback 需要传入一个回调函数,该函数会在浏览器空闲时被调用。回调函数的参数是一个 IdleDeadline 对象,它包含有关浏览器还剩余多少时间可供执行任务的信息。根据该对象的时间戳信息,开发人员可以自行决定是否继续执行任务或推迟执行。

requestIdleCallback 可以帮助我们优化 Web 应用程序的性能和响应速度,减少资源的浪费。


Q6:map和和 filter 有什么区别?

难度:⭐

答案

map()filter() 都是 JavaScript 中常用的数组方法,用于对数组进行操作和转换,但它们的作用和使用方式有所不同。

区别:

  1. 作用
    • map() 方法用于对数组中的每个元素执行指定的操作,并返回一个新的数组,新数组的每个元素都是原始数组对应位置元素经过操作后的结果。
    • filter() 方法用于根据指定的条件过滤数组中的元素,并返回一个新的数组,新数组包含满足条件的元素。
  2. 返回值
    • map() 方法返回一个新的数组,包含每个元素经过操作后的结果。
    • filter() 方法返回一个新的数组,包含满足指定条件的元素。
  3. 使用方式
    • map() 方法接受一个回调函数作为参数,该回调函数会被传入数组中的每个元素和它们的索引,并返回操作后的结果。
    • filter() 方法接受一个回调函数作为参数,该回调函数会被传入数组中的每个元素和它们的索引,并根据回调函数返回的布尔值来决定是否保留该元素。

示例:

1
2
3
4
5
6
7
8
// 使用 map() 方法将数组中的每个元素乘以 2
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // 输出 [2, 4, 6, 8, 10]

// 使用 filter() 方法过滤出数组中的偶数
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // 输出 [2, 4]

在上面的示例中,map() 方法将数组中的每个元素乘以 2,并返回一个新的数组,而 filter() 方法则过滤出数组中的偶数,并返回一个新的数组。


Q7:map 和 forEach 有什么区别?

难度:⭐

答案

map()forEach() 都是 JavaScript 中用于处理数组的方法,但它们之间有一些重要的区别:

  1. 返回值
    • map() 方法返回一个新的数组,该数组包含了对原数组中每个元素调用回调函数后的返回值组成的数组。
    • forEach() 方法没有返回值(或者说返回值为 undefined),它仅用于迭代数组中的每个元素,执行回调函数但不会修改原数组。
  2. 使用场景
    • map() 适合用于需要对数组中的每个元素进行某种转换或映射的场景,例如将每个元素乘以 2、转换为大写等。
    • forEach() 适合用于需要对数组中的每个元素执行一些操作,但不需要返回新数组的场景,例如打印数组中的元素、向数组中添加新元素等。
  3. 对原数组的影响
    • map() 方法不会修改原数组,它会返回一个新数组。
    • forEach() 方法也不会修改原数组,但是可以在回调函数中对原数组进行修改,因为它在迭代过程中可以访问原数组的每个元素。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 map() 方法将数组中的每个元素乘以 2,并返回一个新的数组
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // 输出 [2, 4, 6, 8, 10]

// 使用 forEach() 方法打印数组中的每个元素
numbers.forEach(num => console.log(num)); // 输出每个元素的值

// 使用 forEach() 方法向数组中添加新元素
const newArray = [];
numbers.forEach(num => newArray.push(num * 2));
console.log(newArray); // 输出 [2, 4, 6, 8, 10]

在上面的示例中,map() 方法将数组中的每个元素乘以 2,并返回一个新的数组,而 forEach() 方法用于打印数组中的每个元素以及向新数组中添加新元素。


Q8:const声明了数组,还能push元素吗,为什么?

难度:⭐

答案

在 JavaScript 中,使用 const 声明的变量不允许重新分配(重新赋值),但是它并不限制对其引用的对象或数组进行修改。因此,虽然你不能给 const 声明的变量重新赋值,但是你可以修改其所引用的对象或数组的内容。

举例来说,你可以使用 const 声明一个数组,并且在之后调用数组的 push 方法向其中添加新元素。这是因为 const 保护的是变量本身的重新赋值,而不是其引用的对象或数组的内容。

1
2
3
const arr = [1, 2, 3];
arr.push(4); // 可以正常执行,向数组添加了一个新元素
console.log(arr); // 输出 [1, 2, 3, 4]

总之,const 保护的是变量的重新赋值,而不是变量所引用的对象或数组的内容,因此在使用 const 声明的数组中,你仍然可以修改数组的内容,包括使用 push 方法向数组中添加新元素。


Q9 :Math.ceil()、Math.round()、Math.floor()三者的区别是什么?

难度:⭐

答案
  1. Math.ceil()

    • Math.ceil() 方法返回大于或等于给定数字的最小整数。

    • 如果参数是一个整数,则返回该整数。

    • 如果参数是一个小数,则返回比它大的最小整数。

    • 例如:

      1
      2
      Math.ceil(5.3); // 输出 6
      Math.ceil(-5.3); // 输出 -5
  2. Math.round()

    • Math.round() 方法返回最接近给定数字的整数,四舍五入到最接近的整数。

    • 如果参数的小数部分大于或等于 0.5,则返回下一个更大的整数;否则返回前一个更小的整数。

    • 例如:

      1
      2
      Math.round(5.3); // 输出 5
      Math.round(5.6); // 输出 6
  3. Math.floor()

    • Math.floor() 方法返回小于或等于给定数字的最大整数。

    • 如果参数是一个整数,则返回该整数。

    • 如果参数是一个小数,则返回比它小的最大整数。

    • 例如:

      1
      2
      Math.floor(5.3); // 输出 5
      Math.floor(-5.3); // 输出 -6

总结来说,这三个方法都是用来对数字进行取整操作,但它们的取整规则略有不同。Math.ceil() 总是向上取整到最接近的整数,Math.round() 则是四舍五入到最接近的整数,而 Math.floor() 总是向下取整到最接近的整数。


Q10:toPrecision 和 toFixed 和 Math.round 有什么区别?

难度:⭐

解析

toPrecision(), toFixed(), 和 Math.round() 是 JavaScript 中用于处理数字的三种不同方法,它们有以下区别:

  1. toPrecision():
    • toPrecision() 是一个数字对象的方法,用于将数字转换为指定精度的字符串表示。
    • 该方法接受一个参数,该参数表示数字的有效位数(包括整数部分和小数部分),并返回一个字符串表示该数字。
    • 它可以处理较大或较小的数字,并以科学计数法的形式显示。
    • 例如:var num = 123.456789; num.toPrecision(4); // "123.5"
  2. toFixed():
    • toFixed() 是一个数字对象的方法,用于将数字转换为指定小数位数的字符串表示。
    • 该方法接受一个参数,表示保留的小数位数,然后返回一个带有指定小数位数的字符串表示。
    • 它会进行四舍五入并将结果舍入到指定的小数位数。
    • 例如:var num = 123.456789; num.toFixed(2); // "123.46"
  3. Math.round():
    • Math.round() 是一个全局对象 Math 的方法,用于将数字四舍五入到最接近的整数。
    • 该方法接受一个数字参数,并返回最接近的整数。
    • 它不会更改数字的小数部分,而是根据小数部分的值将数字舍入到最接近的整数。
    • 例如:Math.round(123.456789); // 123
答案
  • toPrecision()toFixed() 都返回字符串,而 Math.round() 返回数字。
  • toPrecision() 可以控制有效位数,包括小数点前后的位数。
  • toFixed() 用于固定小数位数。
  • Math.round() 用于简单的四舍五入到最接近的整数。


Q11:什么是 Polyfil ?

难度:⭐

答案

Polyfill(填充物)是用于模拟浏览器中缺失功能的代码块,通常用于使较新的Web技术能够在较旧的浏览器中正常运行。在Web开发中,不同的浏览器可能支持不同的JavaScript标准和API版本,这可能导致代码在某些浏览器中无法正常运行。

Polyfill 的目的是填补这些功能的缺失。通过在代码中包含 Polyfill,开发人员可以向较旧的浏览器提供与较新浏览器相同的功能,从而确保网站或应用程序在各种浏览器中具有一致的行为和体验。

Polyfill 通常是由开发者编写的 JavaScript 代码,其功能是检测浏览器是否支持某个特定的功能,如果不支持,则通过代码模拟这个功能。例如,如果某个浏览器不支持 Array.prototype.forEach() 方法,那么可以编写一个 Polyfill 来模拟这个方法的行为,使得在该浏览器中也可以使用 forEach()

Polyfill 可以是个别功能的填充,也可以是一个包含多个功能填充的库。它们通常在需要时被引入到项目中,以解决特定的兼容性问题。

引申

下面是一个使用 Polyfill 的简单例子,假设我们想要在不支持 Array.prototype.includes() 方法的浏览器中使用这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Polyfill for Array.prototype.includes()
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement /*, fromIndex*/) {
'use strict';
if (this == null) {
throw new TypeError('Array.prototype.includes called on null or undefined');
}

var O = Object(this);
var len = parseInt(O.length, 10) || 0;
if (len === 0) {
return false;
}

var n = parseInt(arguments[1], 10) || 0;
var k;
if (n >= 0) {
k = n;
} else {
k = len + n;
if (k < 0) {
k = 0;
}
}

var currentElement;
while (k < len) {
currentElement = O[k];
if (searchElement === currentElement ||
(searchElement !== searchElement && currentElement !== currentElement)) {
return true;
}
k++;
}

return false;
};
}

// 使用示例
var array = [1, 2, 3, 4, 5];

if (array.includes(3)) {
console.log('数组包含 3');
} else {
console.log('数组不包含 3');
}

在这个例子中,我们首先检查是否存在 Array.prototype.includes() 方法。如果不存在,我们将定义一个 Polyfill 来模拟这个方法的行为。然后我们使用这个方法来检查数组中是否包含特定的元素。这样,即使浏览器不支持 Array.prototype.includes() 方法,我们也可以在代码中使用它。


Q12:setTimeout 为什么不能保证能够及时执行?

难度:⭐

答案

setTimeout() 函数用于在指定的时间间隔之后执行一次 JavaScript 代码或函数。虽然在理论上 setTimeout() 应该在指定的时间间隔之后立即执行,但在实际应用中,它并不能保证能够及时执行的原因有几个:

  1. JavaScript 单线程模型:JavaScript 在浏览器中是单线程执行的,意味着所有的代码都是按顺序执行的。如果前面的代码需要花费大量时间来执行,那么 setTimeout() 中的代码就会被推迟执行,直到前面的代码执行完成。这可能会导致 setTimeout() 中的代码延迟执行,即使指定的时间间隔已经到了。
  2. 事件循环机制:在浏览器中,JavaScript 的执行是基于事件循环的。当执行栈中的任务执行完成后,浏览器会检查任务队列中是否有待执行的任务。如果有,它会将任务移到执行栈中执行。由于事件循环的机制,setTimeout() 中的代码可能需要等待前面的任务完成并且执行栈为空时才会被执行,这也会导致延迟执行的情况发生。
  3. 最小延迟时间setTimeout() 的精度受到浏览器和操作系统的限制,通常来说,setTimeout() 的最小延迟时间在几毫秒到几十毫秒之间。因此,如果你指定的延迟时间非常短,例如 1 毫秒,那么由于浏览器的限制,setTimeout() 也可能无法保证精确执行。

综上所述,虽然 setTimeout() 是用来在指定的时间后执行代码的方法,但由于上述原因,它并不能保证能够及时执行。在编写代码时,应该考虑到这些因素,并避免依赖于严格的时间控制。


Q13:Javascript字符串的常用方法有哪些?

难度:⭐

答案
  1. charAt(index): 返回指定索引位置的字符。

    1
    2
    let str = "Hello";
    console.log(str.charAt(1)); // 输出 "e"
  2. charCodeAt(index): 返回指定索引位置字符的 Unicode 值。

    1
    2
    let str = "Hello";
    console.log(str.charCodeAt(1)); // 输出 101
  3. concat(str1, str2, ...):连接两个或多个字符串,返回新的字符串。

    1
    2
    3
    let str1 = "Hello";
    let str2 = "World";
    console.log(str1.concat(", ", str2)); // 输出 "Hello, World"
  4. includes(searchString, position):判断字符串中是否包含指定的子字符串,返回 true 或 false。

    1
    2
    let str = "Hello World";
    console.log(str.includes("World")); // 输出 true
  5. indexOf(searchValue, fromIndex):返回指定值在字符串中首次出现的位置,如果没有找到则返回 -1。

    1
    2
    let str = "Hello World";
    console.log(str.indexOf("o")); // 输出 4
  6. lastIndexOf(searchValue, fromIndex):返回指定值在字符串中最后一次出现的位置,如果没有找到则返回 -1。

    1
    2
    let str = "Hello World";
    console.log(str.lastIndexOf("o")); // 输出 7
  7. slice(start, end):提取字符串的一个片段,并返回一个新的字符串。

    1
    2
    let str = "Hello World";
    console.log(str.slice(6, 11)); // 输出 "World"
  8. substring(start, end):提取字符串的一个子串,并返回一个新的字符串。

    1
    2
    let str = "Hello World";
    console.log(str.substring(6, 11)); // 输出 "World"
  9. substr(start, length):提取字符串中从指定位置开始的指定长度的子串,并返回一个新字符串。

    1
    2
    let str = "Hello World";
    console.log(str.substr(6, 5)); // 输出 "World"
  10. split(separator, limit):将字符串分割成子串,返回一个由子串组成的数组。

    1
    2
    let str = "Hello World";
    console.log(str.split(" ")); // 输出 ["Hello", "World"]
  11. toLowerCase():将字符串转换为小写。

    1
    2
    let str = "Hello World";
    console.log(str.toLowerCase()); // 输出 "hello world"
  12. toUpperCase():将字符串转换为大写。

    1
    2
    let str = "Hello World";
    console.log(str.toUpperCase()); // 输出 "HELLO WORLD"
  13. trim():去除字符串两端的空白字符。

    1
    2
    let str = "   Hello World   ";
    console.log(str.trim()); // 输出 "Hello World"
  14. replace(searchValue, replaceValue):替换字符串中的子串,并返回一个新的字符串。

    1
    2
    let str = "Hello World";
    console.log(str.replace("World", "Universe")); // 输出 "Hello Universe"
  15. startsWith(searchString, position):判断字符串是否以指定的子串开头,返回 true 或 false。

    1
    2
    let str = "Hello World";
    console.log(str.startsWith("Hello")); // 输出 true
  16. endsWith(searchString, position):判断字符串是否以指定的子串结尾,返回 true 或 false。

    1
    2
    let str = "Hello World";
    console.log(str.endsWith("World")); // 输出 true
  17. match(regexp):检索字符串内指定的值,返回一个或多个匹配的字符串。

    1
    2
    let str = "The rain in Spain falls mainly in the plain";
    console.log(str.match(/ain/g)); // 输出 ["ain", "ain", "ain"]
  18. search(regexp):检索与正则表达式相匹配的值,返回匹配的位置。

    1
    2
    let str = "The rain in Spain falls mainly in the plain";
    console.log(str.search(/ain/g)); // 输出 5
  19. repeat(count):复制字符串指定次数,并返回一个新的字符串。

    1
    2
    let str = "Hello";
    console.log(str.repeat(3)); // 输出 "HelloHelloHello"


Q14:addEventListener 第三个参数是干什么的

难度:⭐⭐

答案

addEventListener 方法中,第三个参数是一个布尔值或者一个对象,用于指定事件处理函数的配置。常见的配置包括:

  1. 捕获或冒泡:布尔值,默认为 false(冒泡阶段)。如果设置为 true,则在捕获阶段触发事件。
1
2
element.addEventListener('click', handler, true); // 在捕获阶段触发事件
element.addEventListener('click', handler, false); // 在冒泡阶段触发事件(默认)
  1. 可选参数对象:可以使用一个对象来指定更多的事件处理函数的配置,包括 captureoncepassive 等属性。
1
2
3
4
5
element.addEventListener('click', handler, {
capture: true, // 在捕获阶段触发事件
once: true, // 仅触发一次,然后移除监听器
passive: true // 告知浏览器事件处理函数不会调用 preventDefault()
});

其中:

  • capture:一个布尔值,表示事件是否在捕获阶段触发。默认为 false。
  • once:一个布尔值,表示事件是否仅触发一次,触发后即移除事件监听器。默认为 false。
  • passive:一个布尔值,表示事件处理函数是否调用了 preventDefault()。如果设置为 true,告知浏览器事件处理函数不会调用 preventDefault(),从而可以提高滚动性能。默认为 false。


Q15:Javascript本地存储的方式有哪些,有什么区别,及有哪些应用场景?

难度:⭐⭐

答案
存储方式区别优点缺点应用场景
Cookies浏览器提供的存储机制- 客户端与服务器端都可以读取和写入;
- 有大小限制,但可以设置过期时间;
- 支持跨域传输,但存在安全性限制。
- 大小限制,一般只能存储几 KB 数据;
- 每次请求都会自动发送,影响性能;
- 受同源策略和安全性限制。
存储少量的用户身份验证信息、会话标识、用户偏好设置等。
localStorageHTML5 提供的持久化本地存储机制- 可以存储大量的数据,大小限制一般在几 MB;
- 数据在浏览器关闭后仍然保留。
- 存储的数据仅限于字符串,不能存储复杂数据类型;
- 受同源策略限制,只能存储在同一个域名下;
- 可能受浏览器隐私模式影响。
长期保存在客户端的数据,比如用户偏好设置、本地缓存数据等。
sessionStorageHTML5 提供的临时本地存储机制- 可以存储大量的数据,大小限制一般在几 MB;
- 数据在会话结束后被清除,不会保留到下一次会话。
- 存储的数据仅限于字符串,不能存储复杂数据类型;
- 受同源策略限制,只能存储在同一个域名下;
- 可能受浏览器隐私模式影响
临时存储在客户端的会话相关数据,比如表单数据、单次会话的状态等。
IndexedDBHTML5 提供的客户端数据库系统- 支持存储大量结构化数据;
- 提供灵活的查询和索引功能;
- 支持事务和版本控制。
。- 相对复杂,学习曲线较陡;
- API 不够简洁易用;
- 可能受浏览器兼容性影响。
存储大量结构化数据,并需要进行复杂查询和索引的应用,比如离线 Web 应用、在线文件存储等。
WebSQLHTML5 提供的关系型数据库(已废弃)- 支持类似 SQL 的查询语言;
- 提供事务支持和复杂的查询功能。
- 技术规范已废弃,不推荐在新项目中使用;
- 可能受浏览器兼容性和安全性限制。
已废弃,不建议在新项目中使用;仅适用于需要在客户端进行复杂查询和数据处理的应用,且无需考虑兼容性和未来性的项目。


Q16:var、let、const之间有什么区别?

难度:⭐⭐

答案

varletconst 是 JavaScript 中用于声明变量的关键字,它们之间有以下区别:

  1. 作用域
    • var 声明的变量具有函数作用域(function scope),即在函数内部声明的变量在整个函数内部都可见。
    • letconst 声明的变量具有块级作用域(block scope),即在 {} 内部声明的变量只在该块内部可见,超出块作用域范围则无法访问。
  2. 变量提升
    • 使用 var 声明的变量会发生变量提升(hoisting),即变量声明会被提升到当前作用域的顶部,但初始化操作不会被提升。
    • 使用 letconst 声明的变量也存在变量提升,但在变量声明前访问该变量会抛出 ReferenceError 错误。
  3. 重复声明
    • 使用 var 可以重复声明同名变量,不会报错,但会覆盖之前的值。
    • 使用 letconst 在同一作用域内重复声明同名变量会导致 SyntaxError 错误。
  4. 赋值和重新赋值
    • 使用 varlet 声明的变量可以进行赋值和重新赋值。
    • 使用 const 声明的变量必须在声明时进行初始化,且初始化后不能再修改其值(常量)。
  5. 全局对象属性
    • 使用 var 声明的变量会成为全局对象的属性(在全局作用域下声明的变量)。
    • 使用 letconst 声明的变量不会成为全局对象的属性,它们仅在声明的作用域内可见。

综上所述,letconst 是 ES6 新引入的块级作用域变量声明方式,相较于 var 具有更加严格的作用域和行为。在实际开发中,推荐使用 letconst 来代替 var,以避免由于变量提升和作用域问题而引发的错误。同时,根据变量的特性选择合适的声明方式,如需要定义常量或避免重复赋值的情况下可以使用 const


Q17:Promise then 第二个参数和catch的区别是什么

难度:⭐⭐

解析

Promise.then()方法的第二个参数

  • Promise.then()方法接收两个参数,第一个参数是用于处理Promise成功解决时的结果(成功处理函数),第二个参数是用于处理Promise拒绝时的错误(失败处理函数)
  • 这意味着,如果Promise被拒绝,第二个参数(失败处理函数)将被调用,传递给它的参数是拒绝的原因
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise((resolve, reject) => {
// 触发拒绝
reject(new Error("Something went wrong"));
});

promise.then(
(result) => {
console.log("Resolved with:", result);
},
(error) => {
console.log("Rejected with:", error);
}
);

在这个例子中,如果Promise被拒绝,then()方法的第二个参数(失败处理函数)将被调用

Promise.catch()方法

  • Promise.catch()方法是专门用于处理Promise拒绝的错误的。它等同于Promise.then(null, failureHandler)
  • Promise.catch()接收一个参数(失败处理函数),当Promise被拒绝时会调用这个函数
  • 使用Promise.catch()可以使代码更清晰,因为它专门用于错误处理,而不是与then()方法的成功处理混在一起
  • 例子:
1
2
3
4
5
6
7
8
9
10
11
12
const promise = new Promise((resolve, reject) => {
// 触发拒绝
reject(new Error("Something went wrong"));
});

promise
.then((result) => {
console.log("Resolved with:", result);
})
.catch((error) => {
console.log("Rejected with:", error);
});

在这个例子中,catch()方法将处理Promise被拒绝的情况

答案

Promise.then()方法和Promise.catch()方法都是用于处理Promise对象的结果和错误的,但它们在处理错误方面有不同的用法和行为

区别

  • then()方法的第二个参数和catch()方法在功能上非常相似

    主要区别在于语义和可读性

    catch()明确表示只处理错误,而then()的第二个参数则是用于处理错误

  • 使用catch()可以让代码更清晰,因为它明确表示了只处理错误的情况


Q18:postMessage 有哪些使用场景

难度:⭐⭐

答案

postMessage 是浏览器环境中的一种用于跨文档(例如 iframe 和其父页面之间)或跨窗口(例如弹出窗口和其父窗口之间)进行消息传递的机制。它允许一个文档向另一个文档发送异步消息,并且可以指定接收消息的目标来源(origin)。以下是 postMessage 的一些常见使用场景:

  1. 跨域通信
    • 当你在一个页面内嵌入一个跨域的 iframe 时,你可以通过 postMessage 来与 iframe 进行通信。可以将数据从父页面发送到 iframe,也可以从 iframe 发送到父页面
  2. 父窗口和弹出窗口之间的通信
    • 当一个页面打开一个新的弹出窗口时,你可以通过 postMessage 在父窗口和弹出窗口之间传递信息。这可以用于在父窗口中控制弹出窗口的内容,或者从弹出窗口中返回数据给父窗口
  3. 与 Web Workers 的通信
    • Web Workers 是一种在浏览器中执行并行任务的方式。postMessage 是与 Web Workers 通信的主要方式。你可以向 Web Worker 发送数据,并接收来自 Web Worker 的消息
  4. 与 Service Workers 的通信
    • postMessage 也可以用于与 Service Workers 通信。这对于在离线模式下提供推送通知和缓存管理等功能非常有用
  5. 跨浏览器选项卡或窗口的通信
    • 在某些情况下,你可能需要在同一个浏览器中打开的不同选项卡或窗口之间进行通信。通过共享同一个窗口对象并使用 postMessage,可以实现跨选项卡或窗口的消息传递
  6. 单页应用程序(SPA)与嵌入式第三方内容的通信
    • 在单页应用程序中嵌入第三方内容(如广告或小部件)时,可以通过 postMessage 与这些内容进行通信,以确保数据的安全传递

使用 postMessage 的注意事项:

  • 目标来源postMessage 的第二个参数是目标来源(origin),即接收消息的目标文档的源。出于安全考虑,应该始终指定目标来源,以防止消息被发送到意外的接收方
  • 安全性:在接收 postMessage 消息时,注意验证消息的来源和内容,确保只处理来自可信源的消息,并且避免潜在的安全风险(例如跨站点脚本攻击)


Q19 :async/await 怎么进行错误处理

难度:⭐⭐

答案

在 JavaScript 中,async/await 是一种基于 Promise 的异步编程模式,它使异步代码的写法更接近于同步代码。为了进行错误处理,你可以使用 try...catch 语句来捕获 async/await 中可能出现的错误

下面是一些常见的错误处理方式:

1. try...catch 语句:

使用 try...catch 块来捕获异步函数中的错误。当异步函数中的 Promise 拒绝(即发生错误)时,错误会被抛出,并在 catch 块中被捕获,你可以在 catch 块中进行相应的错误处理

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
}

// 调用异步函数
fetchData('https://api.example.com/data');

在这个示例中,如果 fetch 请求失败或返回的状态码不是成功的状态(例如 404 或 500),会抛出错误,并在 catch 块中捕获。catch 块会输出错误信息

2. async 函数的返回值:

当你在一个异步函数中使用 async/await 时,函数的返回值将是一个 Promise。如果你想在异步函数调用时进行错误处理,可以使用 .then().catch() 方法来处理函数的返回值

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function fetchData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}

// 调用异步函数并处理返回的 Promise
fetchData('https://api.example.com/data')
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error fetching data:', error);
});

在这个示例中,异步函数 fetchData 返回的是一个 Promise,我们可以通过 .then().catch() 方法处理该 Promise 的成功和失败情况

总结:

  • 使用 try...catch 块在异步函数内进行错误处理
  • 在异步函数调用时,可以通过 .then().catch() 方法处理异步函数返回的 Promise
  • catch 块中,你可以记录错误、显示错误消息或采取其他错误处理措施
  • 合理的错误处理有助于确保你的应用程序在发生错误时不会意外崩溃,并且能够提供良好的用户体验


Q20:script标签内的async跟defer有什么区别跟作用?

难度:⭐⭐

解析

在 HTML 中会遇到以下三类 script:

1
2
3
<script src='xxx'></script>
<script src='xxx' async></script>
<script src='xxx' defer></script>

script标签用于加载脚本与执行脚本,直接使用script脚本时,html会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的DOM渲染。

比如现在大家习惯于在页面中引用各种第三方脚本,但如果第三方服务商出现了一些小问题,比如延迟之类的,就会使得页面白屏。

针对上述情况,script标签提供了两种方式来解决问题,就是加入属性async以及defer,这两个属性使得script标签加载都不会阻塞DOM的渲染。

1
2
defer:此布尔属性被设置为向浏览器指示脚本在文档被解析后执行
async:设置此布尔属性,以指示浏览器如果可能的话,应异步执行脚本
答案

defer:设置这个属性的script标签,浏览器将会异步下载该文件并不影响后续dom渲染,多个defer标签则会按顺序执行,在文档渲染完之后的DOMContentLoaded事件调用前执行

async:脚本将会异步加载并且不按顺序执行,谁加载的快就会执行谁


Q21:setTimeout的延时写成0,一般在什么场景下使用?

难度:⭐⭐⭐

解析

setTimeout 的延时设置为 0 的情况通常用于创建一个宏任务,让回调函数尽可能快地放入任务队列中等待执行。这种技术被称为“0 毫秒定时器”或“宏任务调度”。

需要注意的是,使用 0 毫秒延时并不能真正实现“立即执行”,而是将任务推迟到下一个宏任务。此外,滥用这种技术可能会导致性能问题,因此应该根据具体场景慎重使用。

答案

主要场景包括:

  1. UI 渲染优化
    • 在处理大量计算或操作时,通过将一部分任务延迟到下一个宏任务执行,可以优化 UI 渲染和响应速度,避免长时间的同步操作导致页面卡顿。
  2. 事件回调
    • 在事件回调中使用 0 毫秒延时,可以确保回调函数在当前事件循环的末尾执行,而不会阻塞后续事件的处理。这对于确保一些状态的及时更新是有帮助的。
  3. Promise 的微任务
    • 在处理 Promise 的 .then()async/await 时,通过 setTimeout 设置为 0,可以将后续的任务放入微任务队列,确保在当前任务执行完毕后立即执行。
  4. 模拟 nextTick
    • 在一些前端框架(如 Vue.js)中,nextTick 方法的实现通常使用了 setTimeout 设置为 0,用于在下一个宏任务中执行回调,以确保在 DOM 更新后执行。
  5. 事件循环
    • 通过 setTimeout 设置为 0,可以在当前事件循环的最后插入一个宏任务,用于处理一些异步操作,确保不会影响当前任务的执行。


Q22:webSocket如何兼容低浏览器

难度:⭐

答案
  1. Adobe Flash Socket:Flash 曾经是一种用于在浏览器中实现实时通信的常用技术,可以通过 Flash 插件来模拟 WebSocket 的行为。但需要注意的是,由于 Flash 的安全性问题和日益废弃的趋势,这不是一个长期可行的解决方案。
  2. ActiveX HTMLFile (IE):在早期版本的 Internet Explorer 浏览器中,可以使用 ActiveX 控件 HTMLFile 来实现长轮询技术,以模拟实时通信。这种方法只能在 IE 浏览器中使用,并且不适用于其他浏览器。
  3. 基于 multipart 编码发送 XHR:这种方法是通过使用 XMLHttpRequest 对象来模拟 WebSocket 的通信过程。数据会以 multipart 编码的方式发送,服务器需要进行解析。这种方法比较复杂,而且在性能上可能不如 WebSocket。
  4. 基于长轮询的 XHR:长轮询是一种通过不断发起 HTTP 请求来模拟实时通信的技术。客户端发送一个 HTTP 请求给服务器,服务器只有在有新消息时才会响应,否则保持连接处于挂起状态。一旦收到响应,客户端立即发送另一个请求。这种方法可以实现实时性,但是效率较低,并且可能会产生较高的服务器负载。


Q23:ajax、axios、fetch有什么区别

难度:⭐⭐

答案
特性/库AJAXAxiosFetch
基于XMLHttpRequest 对象XMLHttpRequest \Promise原生js的Fetch API
返回数据类型默认为纯文本默认为JSON默认为Response对象(需要转换为JSON)
优点- 在老版本的浏览器上有广泛支持
- 灵活,可以支持各种请求和内容类型
- 支持Promise - 请求拦截器、响应拦截器
- 自动转换JSON数据
- 客户端支持防御XSRF
- 基于Promise,语法简洁
- 可以很容易地通过一个API处理所有的HTTP请求
- 响应可以被多次读取
缺点- 不支持Promise,回调地狱
- 代码可能会比较复杂
- 不是浏览器内建,需要额外安装
- 不能取消请求 - 浏览器兼容性问题
- 默认不发送cookies
- 可能需要多步骤来处理响应
- 浏览器支持不如XHR
相同点- 都能发送HTTP请求
- 都能在客户端与服务器进行数据交换
- 都能发送HTTP请求
- 都能在客户端与服务器进行数据交换
- 都能发送HTTP请求
- 都能在客户端与服务器进行数据交换
不同点- 基于底层接口XMLHttpRequest- 封装了XMLHttpRequest,提供了更现代的API
- 提供拦截器,请求取消,全局的axios实例
- 基于Promise的新技术
- 更为简洁的API
- 与Service Worker的集成
开发封装- 通常直接使用,或者基于jQuery等库进一步封装- 独立库,提供现代的API和特性- 内建在现代浏览器中,不需外部库


Q24:for…in和for…of有什么区别

难度:⭐⭐

答案

for...in循环和for...of循环在JavaScript中用于遍历数据,但它们之间有一些重要的区别。下面是对这两种循环方式的主要区别的概述:

for…in循环

  • 用途for...in循环主要用于遍历对象的属性。
  • 行为:遍历对象的所有可枚举属性,包括继承的可枚举属性。
  • 迭代值:在每次迭代中,迭代变量存储的是对象属性的键(即属性名)。

示例:

1
2
3
4
5
const obj = { a: 1, b: 2, c: 3 };

for (const key in obj) {
console.log(key); // 输出 'a', 'b', 'c'
}

for…of循环

  • 用途for...of循环主要用于遍历可迭代对象的元素,如数组、字符串、Map、Set等。
  • 行为:直接遍历可迭代对象的值。
  • 迭代值:在每次迭代中,迭代变量存储的是元素的值。

示例:

1
2
3
4
5
const arr = [1, 2, 3];

for (const value of arr) {
console.log(value); // 输出 1, 2, 3
}

主要区别

  1. 遍历对象不同for...in遍历对象的属性(键),适用于对象;而for...of主要遍历可迭代对象的元素值,适用于数组、字符串等。
  2. 应用场景for...in更适合于遍历对象的属性;for...of提供了一种简洁的方式来遍历数组、Map、Set、字符串等可迭代对象的元素。
  3. 继承属性的遍历for...in也可以遍历对象原型链上的可枚举属性,而for...of不会遍历原型链,它只遍历当前对象的值。


Q25:Map跟WeakMap什么区别

难度:⭐⭐

答案

MapWeakMap 都是JavaScript中的集合类型,用于存储键值对,但它们在某些关键特性上有所不同。

  1. map

    Map 是ECMAScript 2015规范中引入的一种新的数据结构,它类似于对象,也是键值映射,但键的范围不限于字符串,可以是任何类型的值

    • 键的多样性: Map 的键可以是任意类型的值,包括函数、对象或任何基本类型
    • 有序性: Map 对象维护键插入的顺序
    • 大小可测: 通过 Map.prototype.size 属性可以直接获取一个 Map 的元素数量
    • 性能: 在频繁增删键值对的场景下,Map 有着比普通对象更好的性能
    1
    2
    3
    4
    let map = new Map();
    map.set('key1', 'value1');
    map.set('key2', 'value2');
    console.log(map.size); // 2
  2. WeakMap

    WeakMap 是ECMAScript 2015规范中一起引入的一种集合类型。它与 Map 的主要差别在于它的键必须是对象,不能是原始值,而且WeakMap 中的键是弱引用的

    • 键必须是对象: WeakMap 的键只能是对象引用
    • 弱引用: WeakMap 中的键所指向的对象是弱引用,这意味着如果没有其他引用和该对象相连,这些对象将会被垃圾回收机制回收。因此,WeakMap 适合做关联额外数据到对象上,而无需担心内存泄漏问题
    • 不可枚举: WeakMap 的内容不可枚举。这是出于垃圾回收机制的考虑。因为不能确切知道当前哪些键已经被回收,所以不可提供遍历其键或值的方法
    • 无法清空: WeakMap 没有 clear 方法,也无法获取大小,也就是说,没有办法直接清除 WeakMap 的所有键值对
    • 用途限定: 由于以上特性,WeakMap 多用于私有数据的存储,以及与对象生命周期绑定的信息存储
    1
    2
    3
    let weakMap = new WeakMap();
    let objKey = {};
    weakMap.set(objKey, 'value1');

    在这个例子中,如果 objKey 外部没有其他引用,它最终会被垃圾回收掉,相应地,weakMapobjKey 的引用不会阻止回收

总结

Map 更适合做数据集合,而 WeakMap 用于细粒度的对象级别的数据存储,它不阻止垃圾回收器清理键所引用的对象

了解这两者的差别,有助于选择适合不同场景的数据结构


Q26:如何确保你的构造函数只能被new调用,而不能被普通调用

难度:⭐⭐⭐

答案
  1. instanceof 运算符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function MyConstructor() {
    if (!(this instanceof MyConstructor)) {
    throw new Error('MyConstructor must be called with new');
    }
    // 在这里写你的构造函数代码
    }

    // 正确的调用方式,使用 new
    let newObj = new MyConstructor();

    // 错误的调用方式,没有使用 new,将会抛出错误
    MyConstructor();

    这段代码中使用的 instanceof 运算符检查 this 是否是 MyConstructor 的一个实例

    如果不是,那就意味着构造函数是被作为普通函数调用的,就会抛出一个错误

    所以当你尝试不使用 new 关键字调用 MyConstructor 时,就会触发这个错误

  2. class 关键字

    随着ES6的引入,现在更普遍和推荐的做法是使用 class 关键字来定义构造函数

    当试图执行不使用 new 的类构造方法时,JavaScript 会自动抛出错误

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass {
    constructor() {
    // 在这里添加构造函数代码
    }
    }

    const instance = new MyClass(); // 正确的方式
    const wrongInstance = MyClass(); // 这将会抛出TypeError: Class constructor MyClass cannot be invoked without 'new'

    在ES6类构造函数中,如果试图以普通函数的方式调用 MyClass(),JavaScript 会抛出 TypeError,因此在这种情况下不需要额外的检查

  3. new.target 属性

    《ECMAScript 6 入门》中讲到: ES6new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数

    如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined因此这个属性可以用来确定构造函数是怎么调用的

    new.target 在通过 new 调用函数时是定义过的,而通过普通函数方式调用时是 undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function MyFunction() {
    if (!new.target) {
    throw new Error('MyFunction must be called with new');
    }
    // 在这里写你的构造函数代码
    }

    // 正确的调用方式,使用 new
    let obj = new MyFunction();

    // 错误的调用方式,没有使用 new,将会抛出错误
    MyFunction();


Q27:怎么中断forEach循环

难度:⭐⭐⭐

答案

在JavaScript中,Array.prototype.forEach 方法是无法在循环过程中被中断的

这是因为按照其设计,Array.prototype.forEach 是用来对数组的每一项都执行一遍给定的函数,无法直接中断或者跳出


在出现错误的情况下,可以使用try-catch语句来停止 JavaScript Array.prototype.forEach中的循环,但这并非其设计初衷,如果需要在循环中实现某种条件的停止,不推荐使用这种方法

这是因为try-catch通常用于处理异常或错误,如果滥用try-catch来中断正常流程,可能会对代码的可读性和性能产生负面影响

例如,虽然我们能够通过抛出一个错误(throw)来退出forEach循环,然后在外层使用try-catch来捕捉这个错误,实现中止forEach循环的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
[1, 2, 3, 4, 5].forEach((el, i) => {
// If element meets condition, throw error
if (el === 3) throw new Error('Stop loop');
console.log(el);
});
}
catch (e) {
if (e.message === 'Stop loop') {
console.log('Loop stopped.');
} else {
// Real error, handle it
}
}

如上,假设我们在遇到元素 3 时中止循环,当元素 3 出现时,我们抛出一个错误并立即停止 forEach。错误然后被 catch 块捕捉,并打印出 'Loop stopped.'

但正如刚才我提到的,这并非try-catch的原始用途,如果只是想中止一个循环,应该选择更合适的循环结构,比如forwhile或者some/every等方法


替换方式

1. 使用 for 循环或 while 循环:
forwhile 两种循环在满足某种条件时,都可以使用 break 关键字来提前退出循环

1
2
3
4
5
for(let i = 0; i < arr.length; i++) {
if(arr[i] === target) {
break;
}
}

2. 使用 Array.prototype.someArray.prototype.every
这两个方法是用于判断数组中所有元素是否都满足(every)或者有至少一个元素满足(some)某个条件,如果一旦找到满足条件(或者不满足条件)的元素,就会立即停止迭代并返回

1
2
3
4
5
arr.some((item, index) => {
if(item === target) {
return true;
}
})


Q28:try…catch 可以捕获到异步代码中的错误吗

难度:⭐⭐⭐

答案

try...catch 结构在 JavaScript 中用于捕获和处理同步代码中的错误

对于异步代码,其行为会有所不同,这取决于异步代码的具体实现方式

  • 对于使用 Promiseasync/await 的异步代码,try...catch 可以有效地捕获异步操作中的错误

    这是因为 async/await 语法是基于 Promise 的,它允许你用看似同步的方式写异步代码

    在这种情况下,await 关键字会等待 Promise 完成,并且如果 Promise 被拒绝,await 会抛出一个错误,这个错误可以被同一个try...catch 块捕获

1
2
3
4
5
6
7
8
async function fetchData() {
try {
let data = await fetch("https://example.com");
// 处理数据
} catch (error) {
console.log("捕获到错误:", error);
}
}
  • 对于传统的异步回调(如使用 setTimeoutsetInterval 或者传统的异步回调模式),try...catch 无法直接捕获到异步操作中的错误

    这是因为当 try...catch 代码块执行时,异步代码可能尚未执行,错误也就不会在 try...catch 中被捕获

1
2
3
4
5
6
7
8
try {
setTimeout(function() {
throw new Error("错误!");
}, 1000);
} catch (error) {
// 无法捕获到错误
console.log("捕获到错误:", error);
}

在这个例子中,setTimeout 中的错误不会被 try...catch 捕获,因为 setTimeout 是异步执行的,当错误发生时,try...catch 块已经执行完毕了

总结来说,try...catch 可以捕获到使用 Promiseasync/await 等现代异步编程模式中的错误,但对于传统的异步回调模式,try...catch 无法直接捕获异步操作中的错误

对于这些情况,应该利用诸如 Promise.catch() 方法或者 async/await 结合 try...catch 来处理错误


Q29:Generator是怎么做到中断和恢复的

难度:⭐⭐⭐

答案

Generator 是 JavaScript ES6 引入的一种新的函数语法,它可以通过 yield 关键字来暂停函数的执行,然后通过外部控制恢复执行

一个 Generator 函数在初次调用时并不会执行,而是返回一个遵循迭代器协议的 Generator 对象

这个对象实际上是一个迭代器,它包含一个 next() 方法

每次调用迭代器的 next() 方法时,Generator 函数会执行到下一个 yield 表达式,并暂停,即“中断”的操作

此时,生成器代码的状态(包括变量的值和指令指针)都会被保留

在此状态下,代码外部可以处理当前 yield 出的值,然后再决定是否继续执行

以下是一个基本的 Generator 函数的例子

1
2
3
4
5
6
7
8
9
10
11
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}

const generator = numberGenerator(); // 获取到 Generator 对象,但函数尚未开始执行。

console.log(generator.next().value); // 输出 1,并暂停执行。
console.log(generator.next().value); // 从上一次暂停的地方恢复执行,输出 2。
console.log(generator.next().value); // 输出 3。

在上面的代码中:

  1. numberGenerator() 被调用时,它返回一个迭代器对象但不执行函数体内的代码
  2. 调用迭代器的 next() 方法时,Generator 函数开始执行,直到遇到第一个 yield
  3. yield 暂停函数的执行并将值返回给迭代器的 next() 方法调用
  4. 当再次调用 next() 时,Generator 函数会从上一次暂停的地方 yield 继续执行,直到遇到下一个 yieldreturn,或者到达函数体的末尾
  5. 可以重复此过程,直到 Generator 函数内没有更多的 yield,或者直到到达 return 语句,这会导致 Generator 完全终止

Generator 函数在可能涉及异步操作和需要暂停和恢复的场景中特别有用,因为它允许你编写看起来像同步代码的异步行为,并且在每个阶段都可以控制函数的执行


Q30:async/await、generator、promise 这三者的关联和区别是什么

难度:⭐⭐⭐

答案
  1. Promise

    Promise 是一个代表了异步操作最终完成或者失败的对象

    它是一个包装了异步操作结果的对象

    每个 Promise 有三种状态:pending(等待中)、fulfilled(已完成)或rejected(已失败)

    Promise的主要优点是可以链式调用(.then() 后可以继续 .then()),并且有统一的错误处理机制(.catch()

    但是,代码可能会因为 .then()的嵌套而变得难以阅读和维护

  2. Generator

    Generator 是 ES6 引入的一个特性,允许一个函数在执行过程中暂停,并在稍后重新开始,类似于线程的挂起和恢复

    Generator 函数返回了一个遍历器对象,可以通过 .next() 方法来得到一个 { value, done } 结构的对象,其中 value 是返回的结果,done 是一个布尔值表示函数是否执行完毕

    Generator 通常和 yield 关键字一起使用,yield 可以将函数的执行“暂停”,再次调用 .next() 时从上次“暂停”的地方开始

    Generator本身并没有异步处理能力,但可以配合 Promise 使用来处理异步操作

  3. Async/Await

    Async/Await 是 ES2017 引入,可以看作是 Generator 的语法糖,用于简化 Promise 的使用,并使异步的代码看起来就像同步的代码一样

    一个 async 函数内部可以使用 await 关键字等待一个 Promise resolve,然后获取它的结果

    这就好像是将异步代码暂停在那里,等待 Promise 完成

    实际上,async/await 内部就是通过类似 Generator 的方式,使得函数在等待 Promise 时“暂停”,在 Promise 完成时“恢复”

简单的关系是

Promise 提供了对异步操作的封装,解决了回调地狱的问题

Generator 提供了更进一步的流控制,使得代码可以在某个点“暂停”并在稍后“恢复”

而 Async/Await 是基于 Promise 和 Generator 的同步化处理异步操作的语法糖

同时,需要注意的是,虽然 async/await 让异步代码看起来就像同步代码一样,但实际上它们的行为仍然是异步的,只是语法变得更容易理解和阅读


Q31:如何让Promise.all在抛出异常后依然有效

难度:⭐⭐⭐

答案

在默认情况下,Promise.all 方法如果在等待所有promise解决(resolve)的过程中,有任何一个promise被拒绝(reject),那么它会立即结束,并返回一个拒绝(reject)状态的promise

如果你希望即便有一个或多个promise被拒绝,Promise.all 依然能够继续执行并返回所有promise的结果,你需要自己处理每个promise,确保它们不会抛出异常

这可以通过在每个promise后面附加一个 .catch() 方法来实现,这样可以捕获并处理错误,你也可以仅仅返回错误信息而不是抛出,这样外层的 Promise.all 就不会因为reject而立即停止了

1
2
3
4
5
let promises = [fetch('/api/endpoint1'), fetch('/api/endpoint2'), fetch('/api/endpoint3')];

Promise.all(promises.map(p => p.catch(e => e)))
.then(results => console.log(results))
.catch(e => console.log('Some promise failed: ', e));

上面的代码中,promises.map 方法遍历了所有的promise,并为每个promise附加了一个 .catch 方法

如果任何一个promise失败了,.catch 方法会处理异常,并且将错误信息作为结果返回

这会保证 Promise.all 方法得到的总是一个包含每个原promise结果的数组,无论它是成功的值还是错误

引申

使用 Promise.allSettled 替代 Promise.all()

Promise.allSettled()方法返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果


Q32:object.assign和扩展运算符是深拷贝还是浅拷贝,两者区别是什么

难度:⭐⭐⭐

解析

浅拷贝与深拷贝

  • 浅拷贝

    仅复制对象的第一层属性

    如果一个对象的某个属性值是引用类型(如对象或数组),浅拷贝会复制这个引用,而不是复制引用所指向的真实对象

    因此,原始对象与拷贝后的对象会共享这个引用类型的属性

  • 深拷贝

    复制对象的所有层级,创建完全独立的副本

    如果原始对象中包含引用类型的属性,深拷贝会递归地复制这些属性所指向的对象,确保拷贝后的对象与原始对象之间完全独立,修改一个不会影响另一个

答案

Object.assign()方法和扩展运算符(...)都是用于对象克隆或者合并对象的操作,但重要的是,它们都执行的是浅拷贝,而不是深拷贝

[!IMPORTANT]

当对象属性仅为第一层且全部为基本数据类型时,使用Object.assign()或扩展运算符(...)进行的实际上可以被视为深拷贝,因为这些基本类型的属性会被直接复制,而不是共享引用

因此,在这种特定情况下,修改拷贝对象的属性不会影响到原始对象,这表现得就像深拷贝一样

Object.assign()与扩展运算符的共同点

  1. 操作类型

    它们都可以用来克隆对象或合并对象

  2. 拷贝方式

    它们都是浅拷贝。在拷贝过程中,对象的第一层属性会被复制到新对象中

    如果属性值是引用类型,那么复制的将是这个引用,而不是引用所指向的值

不同点

尽管在拷贝行为上类似,Object.assign()和扩展运算符在语法上还是有所差异,主要体现在用途和功能上

  1. 语法与应用场景

    • Object.assign(target, ...sources)方法的第一个参数是目标对象,后续参数是一个或多个源对象

      它会将所有源对象的可枚举和自有属性复制到目标对象

    • 扩展运算符(...)用于将一个对象的所有可枚举属性,复制到了一个新的对象中

      Object.assign()不同,扩展运算符常用在变量解构(destructuring assignment)和数组合并等操作中

  2. 返回值

    • Object.assign()方法会直接修改目标对象,并返回这个修改后的目标对象
    • 使用扩展运算符创建的是一个新对象,源对象保持不变
  3. 表达式简洁性

    • 扩展运算符在写法上更为简洁,尤其是在需要克隆对象或与其他操作(如解构赋值)结合时


Q33:对window.requestAnimationFrame 的理解

难度:⭐⭐⭐

答案

window.requestAnimationFrame() 是一种高效率的,专门为浏览器绘制动画设计的方法

相比之下,传统的 setTimeoutsetInterval 方法不够精确和平滑,而且通常会耗费更多的CPU资源

以下是window.requestAnimationFrame()的一些要点:

  1. 精准的帧控制

    requestAnimationFrame()方法可以让浏览器在每次重绘之前调用指定的函数,这意味着你的动画将尽可能平滑

    而且这个函数的调用频率会自动调整为适合浏览器以及设备的最佳显示效果,一般而言,这个频率是每秒60次,也就是60帧

  2. CPU和电池友好

    requestAnimationFrame()在未激活的标签页,隐藏的iframe中会被暂停,这对于未处于聚焦状态的标签页十分有用

    在这种情况下,动画将不会浪费CPU进行渲染

    因此,requestAnimationFrame()` 在使用起来更加省电,同时也不会因动画导致风扇过度运转

  3. 使用方式

    调用 requestAnimationFrame() 需要传入一个函数作为参数,这个函数会在下次重绘之前调用

    更常见的是在调用的函数内部继续递归调用 requestAnimationFrame(),以此创建一个动画循环

1
2
3
4
5
6
7
8
9
function animate(timestamp) {
// 这里是你的动画代码...

// 根据动画的情况来决定是否需要继续下一帧的动画
requestAnimationFrame(animate);
}

// 开始动画
requestAnimationFrame(animate);

在这个例子中,animate() 函数会在浏览器每次重绘时被调用,从而实现了一个连续的动画效果


Q34:Object.defineProperty与 Proxy 的区别

难度:⭐⭐⭐

答案

Object.definePropertyProxy 都可以用来监视和干预对象的操作,但二者有一些重要区别

  1. Object.defineProperty

    Object.defineProperty 方法用于在对象上定义新属性,或者修改对象上的现有属性,然后返回对象

    通过它可以精确地添加或修改对象的属性

    你可以控制这些属性的值,是否可枚举,是否可配置,是否可写,甚至可以定义 get 和 set 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let obj = {};
    Object.defineProperty(obj, 'property', {
    value: 'value',
    writable: true,
    enumerable: true,
    configurable: true,
    get: function(){...},
    set: function(value){...}
    });

    然而,Object.defineProperty 有一些限制:

    • 它只能监视单个对象上的单个属性,这就意味着如果你想监视多个属性或者整个对象,你需要手动为每个属性调用 Object.defineProperty
    • 它无法监视数组的变化
    • 它无法监视对象属性的创建和删除
  2. Proxy

    Proxy 可以在 JavaScript 的许多对象操作中进行拦截,这使得它更加强大和灵活

    它可以用来创建对象的代理,这些代理可以自定义原对象的行为

    1
    2
    3
    4
    5
    6
    let obj = {};
    let p = new Proxy(obj, {
    get: function(target, property, receiver) {...},
    set: function(target, property, value, receiver) {...}
    // 更多的 handler
    });

    Proxy 相对 Object.defineProperty 的优点是:

    • Proxy 可以拦截并自定义更多的对象操作,不仅仅是属性访问

      例如,Proxy 可以监视属性查询、赋值、删除,函数调用,对象构造等等

    • Proxy 可以监视整个对象,而无需为每个属性单独设置

    • Proxy 可以监视数组操作

    然而,Proxy 的兼容性不如 Object.defineProperty,在不支持 Proxy 的环境中无法使用

最后,选择使用哪一个取决于你的具体需求和目标环境

如果需要更精细的控制或者操作更复杂的功能,Proxy 是一个更好的选择

但如果你只需要基本的属性监视,并且需要更好的兼容性,那么 Object.defineProperty 就足够了


Q35:ES6中的 Reflect 对象有什么用

难度:⭐⭐⭐

答案

ES6(ECMAScript 2015)引入了Reflect对象,它为某些通常由操作符完成的操作提供新的函数式API

Reflect对象提供了一系列静态方法,这些方法与Proxy对象的处理程序方法对应

这些方法的用途主要分为几个方面:

  1. 操作对象

    Reflect提供了用于执行JavaScript对象基本操作的方法,例如属性的获取、设置、删除,属性描述符的获取,以及对象的扩展等

    这些方法基本上与Object的相应方法功能相同,但用法略有不同

    例如,Reflect.getReflect.setReflect.deletePropertyReflect.getOwnPropertyDescriptor

  2. 改善与代理(Proxy)对象的交互

    Reflect对象的方法与Proxy对象的捕获器(trap)功能相对应

    代理对象可以拦截并重定义基本语言操作(如读取属性、赋值、函数调用等)

    Reflect的方法可以在捕获器内部被调用,以实现默认行为

    例如,当你在Proxyget捕获器中需要获取一个属性的值时,可以使用Reflect.get来做到这一点

  3. 提供更可靠的函数式API

    与直接使用操作符或全局函数不同,Reflect的方法总是期望对象作为它的第一个参数,这意味着它们能够在非对象值上抛出可预见的错误,而不是静默失败或者抛出非常不清晰的错误

    这对于编写更加清晰、容错的代码非常有帮助

  4. 动态地调用函数

    Reflect.construct方法允许你动态地调用一个构造函数,类似于new操作符的作用,但提供了更多的灵活性

    Reflect.apply方法则允许动态地调用函数,这类似于Function.prototype.apply的作用,但用法更为简洁

通过以上介绍,可以看出Reflect对象的引入主要是为了提供一种更规范、功能更强大且易于理解的方式来处理JavaScript中的基本操作,以及改善与Proxy对象的协同工作,使得元编程(metaprogramming)在JavaScript中变得更加容易和可控


Q36:详细的介绍一下ES6里面的迭代器

难度:⭐⭐⭐

答案

在 ES6 (ECMAScript 2015) 中,迭代器(Iterator)是一个新引入的概念,它为创建和工作 with 集合(如数组、对象、集合 或其他可迭代的数据类型)提供了一种更一致、更简洁的方式

迭代器是一个对象,它提供了一个next() 方法,可以返回序列中的下一个元素

这个next() 方法返回一个包含两个属性的对象:valuedonevalue 属性表示当前的值,而 done 属性是一个布尔值,当没有更多的数据可供迭代时,其值为 true

这是一个简单的迭代器例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let myIterator = {
data: ['apple', 'orange', 'banana'],
currentIndex: 0,
next() {
if (this.currentIndex < this.data.length) {
let result = { value: this.data[this.currentIndex++,], done: false };
return result;
} else {
return { done: true };
}
}
};

console.log(myIterator.next().value); // 'apple'
console.log(myIterator.next().value); // 'orange'
console.log(myIterator.next().value); // 'banana'
console.log(myIterator.next().done); // true

在 ES6 中,引入了一个叫做 Iterable 的协议

如果一个对象实现了 Iterable 接口(即该对象(或其原型链中的某个对象)包含一个名为 Symbol.iterator 的方法),那么它就可以被 for...of 循环遍历

Symbol.iterator` 方法应该返回一个迭代器对象

例如,数组就是一个内置的可迭代对象:

1
2
3
4
5
6
7
let arr = ['apple', 'orange', 'banana'];
let arrIterator = arr[Symbol.iterator]();

console.log(arrIterator.next().value); // 'apple'
console.log(arrIterator.next().value); // 'orange'
console.log(arrIterator.next().value); // 'banana'
console.log(arrIterator.next().done); // true

此外,以新的 MapSet 对象,以及新的字符串方法、Array.from() 和展开运算符等,这些新功能的引入,都依赖于 IterableIterator 概念

通过迭代器,JavaScript开发者可以创建自定义的迭代逻辑,或者工作 with 一些可能不支持直接迭代的数据结构


Q37:forEach能不能用await

难度:⭐⭐⭐

答案

forEach方法本身不能直接与await一起使用来实现数组中每个异步操作的顺序执行

这是因为forEach仅为数组中的每个元素执行提供的函数,但不会等待异步操作完成

如果在forEach的回调函数中使用await,它将不会按期望的方式工作,因为forEach不会等待异步操作完成,而是会立即继续执行下一个循环迭代

如果你需要按顺序等待每个异步操作完成,你可以使用for...of循环代替forEach

for...of循环中,你可以直接使用await等待每个异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const asyncFunction = async (value) => {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(value);
return value;
}

const values = [1, 2, 3, 4, 5];

const sequentialAsync = async () => {
for (let value of values) {
await asyncFunction(value);
}
}

sequentialAsync();

在上面的例子中,asyncFunction将会为数组中的每个元素依次被调用,并且每次调用都会等待前一个异步操作完成后才继续

这与直接在forEach中使用await达不到的效果是不同的


Q38:为什么部分请求中,参数需要使用encodeURlComponent 进行转码

难度:⭐⭐⭐

答案

在发送HTTP请求时,URL通常具有一定的格式和限制,这是由URI(统一资源标识符)标准规定的

如果请求的URL包含某些特殊字符,可能会影响URL的解析,导致请求无法正确处理,或者服务器解释请求参数的方式不正确

为了确保URL的特殊字符被正确地传输和接收,这些字符需要进行编码

encodeURIComponent 函数用于对统一资源标识符(URI)的组成部分进行编码,以下是一些关键点解释为什么在某些情况下需要使用这个函数:

  1. 特殊字符

    URL中只允许一小部分字符的直接使用,如字母、数字和一些符号(- _ . ~)。其他字符,如空格 (), 冒号 (:), 斜杠 (/), 问号 (?), 和号 (&), 等在URI中有特殊含义,所以它们必须被编码。

  2. 保留字符

    即使某些字符在URI中是允许的,它们也可能有特殊的含义,比如?#分别用于指示查询字符串开始和锚点的开始

    如果你的参数值中包含了这样的字符,并且它们的意图并不是作为URL的这些特定部分,那么必须对它们进行编码

  3. 不可见字符

    如空格在URL中不可见,或者在URL环境中无法直观理解,因此将它们转换为%20这样的编码很重要

  4. 国际化内容

    对于非ASCII字符(如中文、阿拉伯文、俄文等),以及一些符号,如货币符号,也需要进行编码以确保它们能够正确传输

  5. 服务器解析

    如果没有正确编码,服务器可能会错误地解析URL,或者甚至完全不接受这样的请求,因为它可能被视为不安全或无效的


Q39:Object.create跟new什么区别

难度:⭐⭐⭐

答案

Object.create()new 关键字在JavaScript中都用于创建新的对象,但是它们在如何创建对象以及这些对象的性质方面有本质的区别:

  1. 原型链:

    • Object.create(obj)

      会创建一个新的对象,其原型指向你传递给它的对象obj

      这意味着新创建的对象将继承obj中的所有属性和方法

    • new Constructor()

      会创建一个新的对象,并将这个对象的原型指向构造函数的prototype对象

      该对象接着通过构造函数初始化,这通常意味着它会具有构造函数中定义的属性和方法,不过原型链上的属性和方法也会被继承

  2. 属性初始化:

    • Object.create(obj)

      不会自动调用构造器

      如果有第二个参数,你可以为新对象定义额外的属性

    • new Constructor()

      会执行构造函数,构造函数中的代码通常会初始化对象的属性

  3. 用法区别:

    • Object.create(null)可以创建一个干净的对象,它没有原型链,连基本的Object方法如.toString()等都不会继承
    • new Object() 或者 new Constructor() 创建的对象,其原型至少会继承自Object.prototype,除非在构造函数中显式更改原型

举个例子来说明两者的使用差异:

假设我们有以下的构造函数:

1
2
3
4
5
6
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log("Hello, my name is " + this.name + ".");
};

使用new关键字创建的对象:

1
2
var person1 = new Person("Alice");
person1.greet(); // 输出: "Hello, my name is Alice."

这里,person1会继承Person的构造函数中的属性和Person.prototype中定义的方法。

使用Object.create()创建的对象:

1
2
3
var person2 = Object.create(Person.prototype);
Person.call(person2, "Bob");
person2.greet(); // 输出: "Hello, my name is Bob."

在这种情况下,person2直接继承自Person.prototype

注意,我们需要显式地调用Person构造函数来初始化person2对象的name属性


Q40:箭头函数跟普通函数之间的区别

难度:⭐⭐⭐

答案

箭头函数与传统的函数声明(function declaration)或函数表达式(function expression)在几个关键方面有所不同:

  1. this关键字的绑定

    • 箭头函数

      箭头函数不创建自己的this绑定。它们的this值继承自包裹它们的上下文

      这使得箭头函数特别适合非方法的函数和回调,比如setTimeout

    • 普通函数

      传统函数和构造函数会为其内部代码块创建一个新的this上下文,通常,如果一个函数被作为一个对象的方法来调用,this就会引用那个对象

      如果是在非严格模式下独立调用函数,this将会是全局对象(通常是window

      在严格模式下,this将是undefined

  2. 是否可以用作构造函数

    • 箭头函数

      不能被用作构造函数,调用new会抛出错误

    • 普通函数

      可以使用new关键字来调用,并创建新的对象实例

  3. arguments对象

    • 箭头函数

      没有自己的arguments对象,只能访问包含它的普通函数的arguments

    • 普通函数

      有自己的arguments对象,包含了被调用时传递的所有参数

  4. 语法简洁性

    • 箭头函数

      有更简洁的语法,没有function关键字,并且可以简化单个表达式的返回:param => param + 1

    • 普通函数

      需要function关键字,并且需要return语句来返回结果(除非函数体指定为{},在这种情况下,函数不返回任何内容)

  5. 函数名

    • 箭头函数

      通常是匿名的,除非它们被赋值给一个变量

    • 普通函数

      函数声明会创建一个具有其名称的函数,而函数表达式则是匿名的,除非它们被赋值给一个变量


Q41:Websocket中的心跳是为了解决什么问题

难度:⭐⭐⭐

答案

Websocket中的心跳机制主要用于解决以下几个问题:

  1. 维护连接状态

    在基于TCP的长连接如Websocket中,双方在建立连接后,如果一段时间内没有任何数据传输,那么网络设备(如路由器等)可能会认为连接已经不再使用,从而将其断开

    通过在连接上定期发送心跳消息,可以让网络设备知道这个连接仍然在使用中,从而避免被断开

  2. 检测网络故障

    如果一个连接在设定的时间间隔内没有收到心跳消息,可以认为连接可能已经不可用了

    可以启用一些恢复机制,如尝试重新连接,或者报告错误

  3. 检测对端是否存活

    如果Websocket的服务端或客户端崩溃或者处于非响应状态,对端是无法立即知道这个情况的

    通过心跳机制,如果在规定时间内没有收到心跳回应,对端可以判断出网络对端可能已经不可用

  4. 与服务器同步

    在某些场景下,也能利用心跳来进行时间同步或者执行定时任务

    比如,客户端可以根据每次收到服务器心跳的时间,来校准自己的计时器

心跳机制是网络编程中常见的技巧,它可以帮助软件更好地处理网络状态的变化,让软件运行得更加健壮


Q42:Axios的原理是什么

难度:⭐⭐⭐

答案

Axios 是一个基于Promise用于浏览器和node.js的HTTP客户端,它提供了一套简洁的API用于处理XmlHttpRequests(浏览器环境)和http请求(node.js环境)

一些核心原理和特性:

  1. 请求和响应拦截器(Interceptors)
    • Axios 允许在请求或响应被 then 或 catch 处理之前拦截它们
    • 实现方式是通过维护一个拦截器管理对象,管理用于处理请求和响应的拦截函数
  2. 适配器(Adapters)
    • Axios 使用适配器模式来定义对不同环境下发送HTTP请求的具体实现
    • 在浏览器中使用XMLHttpRequest对象发送请求,在Node.js中使用HTTP模块
  3. 请求取消(Cancellation)
    • Axios 提供了取消请求的功能,让你可以使用取消令牌来中断HTTP请求
    • 实现是基于AbortController 接口,这是一个 Web 标准,可以配合原生的 fetch 使用
  4. 错误处理(Error Handling)
    • Axios 在请求发生任何错误时,返回Promise的拒绝(reject)状态,并提供详细的错误信息
  5. 转换请求和响应数据(Transforming)
    • 在请求或响应被 then 或 catch 处理之前,可以通过转换函数修改请求或响应数据
  6. 自动转换JSON数据
    • 在发送请求时,如果数据类型是对象,Axios 会自动转换为JSON字符串
    • 在接收响应数据时,如果发现响应的Content-Typeapplication/json,Axios 会尝试将字符串转换成JSON对象

待补充手写实现简易版Axios


Q43:try…catch代码是否有问题

难度:⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这个代码有问题吗?如果有问题,应该怎么修改
try {
setTimeout(() => {
throw new Error('err')
}, 200);
} catch (err) {
console.log(err);
}

try {
Promise.resolve().then(() => {
throw new Error('err')
})
} catch (err) {
console.log(err);
}
答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setTimeout(() => {
try {
throw new Error('err')
} catch (err) {
console.log(err);
}
}, 200);

Promise.resolve().then(() => {
try {
throw new Error('err')
} catch (err) {
console.log(err);
}
})
引申

这段代码有问题。原因在于JavaScript的错误处理机制和异步行为之间的交互

在 JavaScript 中,try-catch 结构只能捕获同步代码中的异常,因此,既不能捕获setTimeout 也不能捕获 Promise 中的错误,这是因为它们都是异步的

换句话说,try-catch 块在事件队列中添加的函数(异步函数)执行时已经退出了

解决这个问题的方法是在异步代码自身内部处理错误

确认要捕获每个可能抛出错误的 Promise,并对 setTimeout 异步代码进行适当的错误处理


代码运行题

查看本题需要严格学习概念Q12、Q19、Q26

Q1:Promise(1)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})

Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(5)
}).then(() =>{
console.log(6)
})
解析

在浏览器环境中,Promise 的执行顺序是通过事件循环(Event Loop)和微任务队列来进行的。让我详细解释一下这段代码在浏览器中的执行过程:

  1. 第一个Promise链:

    • ```
      Promise.resolve().then(() => { console.log(0); return Promise.resolve(4); })

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11



      - `console.log(0)` 输出 0,然后创建一个新的Promise。
      - `return Promise.resolve(4);` 返回一个Promise,但它的微任务(Promise Resolution)被添加到微任务队列中。
      - `.then((res) => { console.log(res); })` 中的微任务被添加到微任务队列中。

      2. **第二个Promise链:**

      - ```
      Promise.resolve().then(() => { console.log(1); })

      • console.log(1) 输出 1,然后将微任务添加到微任务队列。
    • 后续的 .then 语句按顺序执行,每个 .then 中的 console.log 语句被添加到微任务队列中。

微任务队列是在当前事件循环结束后执行的,确保微任务按照它们被添加的顺序执行。所以,整体的执行顺序如下:

  1. 执行 console.log(0),输出 0。
  2. 将第一个 .then 中的 return Promise.resolve(4); 返回的Promise的微任务添加到微任务队列。
  3. 执行 console.log(1),输出 1。
  4. 执行 .then(() => { console.log(2); }) 的微任务,输出 2。
  5. 执行 .then(() => { console.log(3); }) 的微任务,输出 3。
  6. 执行 return Promise.resolve(4); 返回的Promise的微任务,输出 4。
  7. 执行 .then((res) => { console.log(res); }) 的微任务,输出 4。
  8. 执行 .then(() => { console.log(5); }) 的微任务,输出 5。
  9. 执行 .then(() => { console.log(6); }) 的微任务,输出 6。
答案
1
0123456


Q2:this指向(1)输出结果

难度:⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = '123';

var obj = {
name: '456',
print: function() {
function a() {
console.log(this.name);
}
a();
}
}

obj.print();
解析

obj.print 方法内部,有一个函数 a。在 JavaScript 中,函数内部的 this 默认指向全局对象(在浏览器中是 window)。因此,在函数 a 内部的 console.log(this.name) 中,this.name 实际上是访问了全局变量 name,其值为 '123'

如果你想在函数 a 中访问到 objname 属性,可以使用箭头函数,因为箭头函数的 this 不会被重新绑定,而是沿用外层的 this

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = '123';

var obj = {
name: '456',
print: function() {
const a = () => {
console.log(this.name);
};
a();
}
}

obj.print(); // 输出 '456'

在这个修改后的代码中,箭头函数 a 中的 this 将指向外部函数 printthis,因此能够正确地访问到 objname 属性

答案

这段代码会输出 '123'


Q3:类型判断(1)输出结果

难度:⭐

1
2
console.log(typeof typeof typeof null);
console.log(typeof console.log(1));
解析
  1. typeof typeof typeof null

    1
    console.log(typeof typeof typeof null);
    • typeof null 返回 'object',因为在 JavaScript 中 null 被认为是一个空的对象引用。
    • 然后,typeof 'object' 返回 'string',因为 'object' 是一个字符串。
    • 最后,typeof 'string' 返回 'string'

    所以,这个表达式的输出是 'string'

  2. typeof console.log(1)

    1
    console.log(typeof console.log(1));
    • console.log(1) 执行后,会先输出 1 到控制台,然后 console.log 返回 undefined
    • 接着,typeof undefined 返回 'undefined'

    所以,这个表达式的输出是 'undefined'

答案

两个表达式的输出分别是 'string''undefined'


Q4:变量提升(1)运行题

难度:⭐

1
2
3
4
5
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
答案

这段代码使用了自执行的匿名函数,并在函数内部声明了一个同名的函数 b。由于 JavaScript 具有变量提升,函数声明会被提升到作用域的顶部,因此这段代码的执行结果可能会令人困惑。

让我们来详细分析:

1
2
3
4
5
6
var b = 10;

(function b(){
b = 20;
console.log(b);
})();
  1. var b = 10; 定义了全局变量 b,其值为 10
  2. (function b(){ ... })(); 创建了一个自执行的匿名函数,并在函数内部声明了一个同名的函数 b。这个函数 b 的作用域仅限于自执行函数内部。
  3. 在函数内部,b = 20; 尝试给函数 b 赋值为 20。然而,由于函数声明提升,这行代码实际上在执行之前已经被解释器解释为 var b;,因此它尝试给函数 b 赋值,而不是全局变量 b
  4. console.log(b); 中,输出的是自执行函数内部的 b,即该函数本身。因为在自执行函数内部,函数声明 b 会覆盖外部的全局变量 b

结果是,自执行函数内部的函数 b 被调用,输出函数本身,而全局变量 b 的值仍然是 10。这样做可能会导致混淆和不易理解的代码,通常应该避免在函数内部声明与外部变量同名的函数


Q5:代码结果(7)运行题

难度:⭐

1
['1',2',3].map(parselnt) 
解析

map 方法在每次迭代时会将当前元素、当前索引和数组本身作为参数传递给传入的回调函数。parseInt 函数接受两个参数:要转换的值和进制数。但是,map 方法传递的第二个参数是当前元素的索引,而不是进制数。这导致了 parseInt 函数的行为不同于预期。

让我们来详细解释一下每次迭代的情况:

  1. 第一次迭代:parseInt('1', 0),将 '1' 转换为十进制,返回 1
  2. 第二次迭代:parseInt('2', 1),将 '2' 转换为一进制,但是一进制无法表示 '2',所以返回 NaN
  3. 第三次迭代:parseInt('3', 2),将 '3' 转换为二进制,但是二进制无法表示 '3',所以返回 NaN
答案

最终的返回值是 [1, NaN, NaN]

引申

返回期望的结果 [1, 2, 3]

1
2
3
const arr = ['1', '2', '3'];
const result = arr.map(item => parseInt(item));
console.log(result); // 输出: [1, 2, 3]


Q6:this指向(2)输出结果

难度:⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var name = 'window'
const obj = {
name: 'obj',
sayName:function() {
console.log(this.name)
},
}
obj.sayMyName = () => {
console.log(this.name)
}
const fn1 = obj.sayName
const fn2 = obj.sayMyName
fn1()
obj.sayName()
fn2()
obj.sayMyName()
解析

在这段代码中,你定义了一个全局变量 name 并赋值为 'window',然后创建了一个名为 obj 的对象,其中包含了两个方法 sayNamesayMyName。然后你将 sayName 方法赋值给了 fn1 变量,将 sayMyName 方法赋值给了 fn2 变量。

接下来,你调用了这些方法并打印出它们的执行结果。我们逐一分析:

  1. fn1() 这里 fn1 是通过将 obj.sayName 方法赋值给变量得到的。由于函数调用时 this 的指向取决于调用方式,而不是定义方式,因此在这种情况下,this 指向全局对象(浏览器环境下是 window),所以打印的结果是 'window'
  2. obj.sayName() 这是在 obj 上直接调用 sayName 方法,此时 this 指向的是 obj 对象,因此打印的结果是 'obj'
  3. fn2() 这里 fn2 是通过将 obj.sayMyName 方法赋值给变量得到的。而箭头函数的 this 始终指向定义时所在的作用域,即全局作用域,在这个作用域中,name 变量被赋值为 'window',因此打印的结果是 'window'
  4. obj.sayMyName() 这是在 obj 上直接调用 sayMyName 方法,但是由于箭头函数的 this 绑定在定义时的作用域,因此此时的 this 也是指向的全局作用域,因此打印的结果也是 'window'
答案
1
2
3
4
window
obj
window
window


Q7:变量提升(2)运行题

难度:⭐

1
2
3
4
5
6
7
8
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
解析

在这段代码中,虽然函数 foo() 被声明了两次,但是 JavaScript 中的函数声明会被提升到作用域的顶部,因此函数声明会优先于变量声明。这种行为被称为“函数提升”。

下面是代码的执行过程:

  1. JavaScript 引擎首先会进行变量和函数声明的提升,将它们移动到作用域的顶部。所以,函数声明 function foo() 会被提升到作用域的顶部,而变量声明 var foo 也会被提升,但由于存在函数声明,它将被忽略。
  2. 因此,实际上代码的执行顺序会变成这样:
1
2
3
4
5
6
7
8
function foo() {
console.log(1);
}
foo(); // 此时输出 1

foo = function() {
console.log(2);
}
  1. 接着,foo() 函数被调用,输出 1。
  2. 然后,foo 变量被赋值为一个新的函数,即匿名函数 function() { console.log(2); }

因此,最终的输出是 1,而不是 2。这是因为函数声明会在变量声明之前被处理,所以在执行时会使用函数声明定义的函数。

答案

输出 1


Q8:Promise(2)输出结果

难度:⭐

解析

在这段代码中,首先创建了一个 Promise 对象 promise1,然后输出了字符串 '1' 以及 promise1 的值。但是要注意,Promise 的构造函数中并没有调用 resolvereject,所以 promise1 处于 pending(待定)状态。因此,即使 promise1 已经被创建,但其状态仍然是 pending,直到调用 resolvereject 方法,Promise 的状态才会发生变化。

因此,虽然在创建 Promise 对象后立即输出 promise1,但它的状态仍然是 pending,因此输出的 promise1 可能并不包含实际的结果,而是一个 pending 状态的 Promise 对象。

答案
1
1 Promise {<pending>}


Q9 :Promise(3)输出结果

难度:⭐

1
2
3
4
5
6
7
8
const fn = () => (new Promise((resolve, reject) => {
console.log(1);
resolve('success')
}))
fn().then(res => {
console.log(res)
})
console.log('start')
解析

这段代码的执行过程如下:

  1. 首先,定义了一个箭头函数 fn,该箭头函数返回一个 Promise 对象。
  2. 在箭头函数内部,立即执行了一个 Promise 构造函数,这个构造函数内部打印了 1,然后通过 resolve 方法将 Promise 的状态设置为 resolved,同时传递了 'success' 作为成功的返回值。
  3. 调用 fn(),这会立即执行箭头函数,而箭头函数中的 Promise 构造函数也会立即执行,因此会在控制台打印出 1。此时,Promise 对象被成功解决,但 .then() 方法尚未执行。
  4. 继续执行下一行代码,即 console.log('start'),打印出 'start'
  5. 此时,整个脚本的主线程任务已经执行完毕,事件循环开始检查任务队列中是否有待执行的微任务。发现 Promise 对象的状态已经改变,并且有 .then() 方法注册了对应的处理函数,因此将这个微任务加入到微任务队列中。
  6. 事件循环开始处理微任务队列中的任务,执行 .then() 方法中的回调函数,即打印出 'success'
答案

这段代码的执行顺序是:

  1. 首先打印出 1
  2. 然后打印出 'start'
  3. 最后打印出 'success'


Q10:Promise(4)输出结果

难度:⭐⭐

1
2
3
4
5
6
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log(res))
解析

在给定的代码中,runAsync 函数会创建一个 Promise,在一秒后 resolve 该 Promise,并打印传入的参数 x,然后返回该 Promise。Promise.all 接受一个由 Promises 组成的数组作为参数,返回一个新的 Promise,当数组中所有的 Promise 都被解决时,该 Promise 也会被解决,解决值是一个数组,包含了每个 Promise 的解决值。

在这段代码中,Promise.all 会同时执行三个 runAsync 函数,每个函数都会在一秒后 resolve。然后,then 方法中的回调函数会在所有的 Promise 都被 resolve 后执行,并且传入的 res 参数是一个数组,包含了每个 Promise 的 resolve 值。

由于 runAsync 函数会在 resolve 时打印传入的参数 x,所以在输出结果中,我们会先看到 1、2、3 分别被打印出来,然后是整个数组 [1, 2, 3] 被打印出来,因为 Promise.all 返回的是一个包含所有 resolve 值的数组

答案
1
2
3
4
1
2
3
[1, 2, 3]


Q11:Promise(5)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
解析

在这段代码中,首先创建了一个 Promise,Promise 的执行器函数会立即执行。在执行器函数中,首先输出 1,然后调用了 resolve('success'),接着输出 2。注意,调用 resolve 并不会立即结束 Promise 的执行器函数,后面的代码仍然会执行。

接着,创建的 Promise 对象 promise 会立即进入 resolved(解决)状态,并且传递了 ‘success’ 给后续的 then 方法。

然后,下面的 then 方法注册了一个回调函数,在 Promise 对象 promise 被 resolved 时执行。由于 Promise 已经在创建时立即 resolved,所以这个回调函数会被添加到微任务队列中,等待当前的执行栈清空后执行。在这个回调函数中,输出了 3。

最后,输出了 4。因为 JavaScript 是单线程的,代码是同步执行的,所以在执行 console.log(4) 之前,Promise 相关的微任务已经被添加到微任务队列中,等待执行栈清空后执行。

答案
1
2
3
4
1
2
4
3


Q12:Promise(6)输出结果

难度:⭐

1
2
3
4
5
6
7
8
const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
解析

在这段代码中,虽然创建了一个 Promise,但在 Promise 的执行器函数中并没有调用 resolvereject,因此这个 Promise 不会进入 resolved 或 rejected 状态。因此,与该 Promise 关联的 then 方法中注册的回调函数也不会被执行

答案
1
2
3
1
2
4


Q13:代码结果(1)运行题

难度:⭐⭐

1
2
3
4
5
6
7
foo(typeof a);
function foo(p) {
console.log(this);
console.log(p);
console.log(typeof b);
let b = 0;
}
解析
  1. 函数声明提升
    • JavaScript 中的函数声明会被提升到所在作用域的顶部,因此在函数声明之前调用函数是合法的。
    • 因此,在执行 foo(typeof a) 之前,函数 foo() 已经被声明。
  2. 变量声明提升
    • 与函数声明不同,变量声明会提升至作用域顶部,但不会被初始化,直到执行到定义的语句才会被赋值。
    • 在函数 foo() 中,变量 b 的声明被提升到函数体的顶部,但在声明之前访问 typeof b 会得到 undefined

因此,当执行 foo(typeof a) 时,会按照以下步骤执行:

  1. 输出 this:由于 foo() 函数在全局作用域中调用,因此 this 指向全局对象(浏览器中是 window 对象)。
  2. 输出 pp 的值为 typeof a,即字符串 "undefined",因为在调用时变量 a 未定义,因此 typeof a 返回 "undefined"
  3. 输出 typeof b:在声明 let b = 0; 之前访问变量 b,此时变量 b 已经被声明但尚未初始化,因此输出 undefined
  4. 因为变量 b 是在声明之前使用,会导致 ReferenceError 错误。
答案
1
2
3
window
undefined
ReferenceError 错误


Q14:代码结果(2)运行题

难度:⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Foo(){
Foo.a = function(){
console.log(1);
}
this.a = function(){
console.log(2)
}
}

Foo.prototype.a = function(){
console.log(3);
}

Foo.a = function(){
console.log(4);
}

Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
解析
  1. Foo.a = function(){ console.log(4); }
    这一行在函数 Foo 自身定义了一个静态方法 a。当调用 Foo.a() 时,它会打印 4
  2. Foo.a();
    这行代码调用了刚刚定义的静态方法 a,所以它会打印 4
  3. let obj = new Foo();
    这行代码创建了 Foo 的一个新实例。当使用 new 调用 Foo 函数时,它创建了一个新对象。构造函数运行,为实例设置了属性 a,这个属性是一个函数,当被调用时会打印 2。但是,Foo 自身的静态属性 a 保持不变。
  4. obj.a();
    这行代码调用了新对象 obj 上的实例方法 a。由于我们在构造函数中定义了 this.a,它会打印 2
  5. Foo.a();
    这行代码再次调用 Foo 上的静态方法 a。由于自上次被调用以来它没有改变,它仍然会打印 4

在这个序列中没有使用 Foo.prototype.a = function(){ console.log(3); } 这个赋值,因为我们从未调用过没有自己的 a 属性的实例的 a 方法。如果我们在删除了 obj 自己的 a 属性(delete obj.a;)后调用 obj.a(),那么它会使用原型的 a 方法并打印 3

答案
1
2
3
4
2
4


Q15:代码结果(3)运行题

难度:⭐⭐

1
2
3
4
5
6
7
8
var a=3;
function c(){
alert(a);
}
(function(){
var a=4;
c();
})();
解析
  1. 首先,变量a被声明在全局作用域,并赋值为3
  2. 然后,函数c被声明在全局作用域。当调用c函数时,它会弹出当前作用域链中可访问的变量a的值。
  3. 紧接着,一个立即执行函数表达式(IIFE)被创建并执行。在这个函数内部,又声明了一个局部变量a,并赋值为4。但是这个变量只在立即执行函数的局部作用域内有效。
  4. 在IIFE内部,我们调用了函数c。因为函数c是在全局作用域中定义的,所以当它查找变量a时,会沿作用域链向上查找直到全局作用域,找到全局变量a的值,即3
答案
1
3


Q16:this指向(3)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
var sed = {
x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?

var fiv = {
x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?
解析

在 JavaScript 中,bind 方法创建了一个新的函数,该函数的 this 关键字被绑定到传递给 bind 方法的第一个参数,并返回一个新的函数

bind 方法还可以携带参数,这些参数将被应用到绑定函数的调用

在这个问题中,主要涉及到了 bind 方法的嵌套调用

需要注意的是,当你多次调用 bind 方法时,每次 bind 都会返回一个新的函数,并且绑定了第一次传递给 bindthis

后续的 bind 调用不会更改之前绑定的 this 值,因此后续传递的 this 值将被忽略

示例分析:

  • 在第一个示例中:
    • 首先,bar.bind(foo) 会创建一个新的函数,该函数的 this 值被绑定到 foo
    • 然后,再次调用 bar.bind(foo).bind(sed) 时,由于第一个 bind 已经绑定了 foo,第二个 bind 试图将 this 绑定到 sed 将被忽略,函数的 this 仍然保持绑定到 foo
    • func() 被调用时,函数中的 this.x 仍然是 foo.x,即 3
  • 在第二个示例中:
    • 首先,bar.bind(foo) 会创建一个新的函数,该函数的 this 值被绑定到 foo
    • 接着,再次调用 bar.bind(foo).bind(sed) 时,this 仍然绑定到 foo,而第二个 bindsed 作为 this 参数会被忽略
    • 再次调用 bar.bind(foo).bind(sed).bind(fiv) 时,this 仍然保持绑定到 foo,第三个 bindfiv 作为 this 参数也被忽略
    • 因此,当 func() 被调用时,函数中的 this.x 仍然是 foo.x,即 3

总结:

在这两个示例中,由于多次 bindthis 值已经被绑定到最初传递的 foo,后续 bindthis 参数将被忽略

因此,无论在 bar.bind(foo).bind(sed) 还是 bar.bind(foo).bind(sed).bind(fiv) 中调用 func(),函数中的 this 值都将保持绑定到 foo,并且 func() 调用的结果将始终是输出 3

答案
1
3  3


Q17:Promise(7)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
解析

JavaScript运行时包括一个包含消息队列的事件循环和一个微任务队列

async/awaitPromise相关的回调会进入微任务队列,而像setTimeout这样的函数设置的回调会进入消息队列

微任务队列在每次事件循环迭代的末尾执行,而消息队列中的事件则需要等到下一个迭代

根据这个信息,我们可以确定代码的输出顺序:

  1. 首先,async1函数被调用,打印 "async1 start"
  2. 然后,async1函数中调用await async2()async2函数被调用
  3. async2函数中,setTimeoutconsole.log('timer2')回调排入消息队列
  4. 然后,async2继续执行,打印 "async2"
  5. async2执行完毕,控制权返回到async1,但是async1await之后的代码需要等待微任务队列中的所有任务完成后才能执行
  6. async1函数暂停执行,事件循环继续,执行全局的setTimeout,将console.log('timer3')排入消息队列
  7. 然后,全局上下文中的最后一条语句console.log("start")被执行
  8. 此时,主线程代码执行完毕,事件循环开始处理微任务队列。async1await之后的代码console.log("async1 end")被执行
  9. async1中的setTimeoutconsole.log('timer1')排入消息队列
  10. 主线程代码和微任务队列都清空后,事件循环开始处理消息队列中的宏任务,按照它们被添加到队列的顺序执行。首先是timer2的回调,打印 "timer2"
  11. 接下来是timer3的回调,打印 "timer3"
  12. 最后是timer1的回调,打印 "timer1"

所以,输出的顺序将是:

1
2
3
4
5
6
7
async1 start
async2
start
async1 end
timer2
timer3
timer1

请注意,虽然setTimeout的延时被设置为0,它仍然会在当前执行堆栈清空后的下一个事件循环迭代中执行

这意味着async/await和其他同步代码总是先于setTimeout回调执行

答案
1
2
3
4
5
6
7
async1 start
async2
start
async1 end
timer2
timer3
timer1


Q18:Promise(8)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function runAsync(x) {
const p = new Promise(r =>
setTimeout(() => r(x, console.log(x)), 1000)
);
return p;
}
function runReject(x) {
const p = new Promise((res, rej) =>
setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
);
return p;
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log("result: ", res))
.catch(err => console.log(err));
解析

这段代码是一个 JavaScript 的 Promise 相关的示例,它创建了两个异步函数 runAsyncrunReject,然后使用 Promise.race() 方法来竞争它们的执行

逐步拆解这段代码,并解释其输出顺序:

  1. runAsync 函数:接受一个参数 x,返回一个 Promise 对象,在指定的时间后 resolve 这个 Promise,并打印参数 x 的值
  2. runReject 函数:接受一个参数 x,返回一个 Promise 对象,在指定的时间后 reject 这个 Promise,并打印一个错误信息以及参数 x 的值
  3. Promise.race() 方法:接受一个 Promise 数组,并返回一个新的 Promise 对象,该 Promise 对象会在数组中的任意一个 Promise 状态发生改变时立即改变

现在,让我们来解释输出顺序:

  • 首先,runReject(0) 会立即执行,并在 0 秒后 reject,但因为 setTimeout() 的延迟设定为 0,所以在这个阶段并不会立即输出任何内容
  • 然后,runAsync(1)runAsync(2)runAsync(3) 会同时执行,并在 1 秒、2 秒和 3 秒后分别 resolve,同时输出它们的参数值
  • 由于 Promise.race() 返回的 Promise 对象会在最先解决或拒绝的 Promise 状态发生改变时立即改变,因此最终的输出将会是第一个解决的 Promise 对象的结果
答案
1
2
3
4
5
6
7
0
Error 0

// 此时输出的值不是Promise.race返回的
1
2
3
引申

Promise.race() 是 JavaScript 中的一个静态方法,用于创建一个新的 Promise

该 Promise 在传入的可迭代对象中的任何一个 Promise 解决或拒绝时解决或拒绝,而不管其他 Promise 的状态如何

具体来说,Promise.race() 接受一个可迭代对象(通常是一个数组),并返回一个新的 Promise 对象

这个新的 Promise 对象将会在可迭代对象中的第一个 Promise 状态改变时采用该 Promise 的状态和值。如果可迭代对象为空,则返回的 Promise 将永远保持挂起状态

例如,在一个包含多个 Promise 的数组中,如果其中一个 Promise 解决或拒绝了,Promise.race() 返回的 Promise 将立即采用该解决或拒绝的状态和值,而不会等待其他 Promise 的解决或拒绝


Q19:Promise(9)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
10
11
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))
解析

这段代码使用了 Promise.all() 方法来等待所有的 Promise 都解决(resolve)或有一个被拒绝(reject),然后返回一个包含所有 Promise 解决值的数组

现在,让我们来拆解这段代码,并解释其输出顺序:

  1. runAsync 函数:接受一个参数 x,返回一个 Promise 对象,在指定的时间后 resolve 这个 Promise,并打印参数 x 的值
  2. runReject 函数:接受一个参数 x,返回一个 Promise 对象,在指定的时间后 reject 这个 Promise,并打印一个错误信息以及参数 x 的值
  3. Promise.all() 方法:接受一个 Promise 数组,并返回一个新的 Promise 对象,该 Promise 对象会在数组中所有的 Promise 都解决时 resolve,或者其中任意一个被拒绝时 reject

逐步详细地解释代码在浏览器中的执行过程:

  1. 开始执行代码
    • 调用 Promise.all(),传入一个包含了四个 Promise 的数组:[runAsync(1), runReject(4), runAsync(3), runReject(2)]
    • 每个 Promise 开始执行,并且计划在一定时间后解决或拒绝
  2. 1 秒后
    • runAsync(1) 解决并输出 1。
    • runReject(2) 被拒绝并输出错误信息 “Error: 2”
    • runAsync(3) 继续执行但尚未解决
  3. 2 秒后
    • runAsync(3) 解决并输出 3
    • runReject(4) 继续执行但尚未拒绝
  4. 4 秒后
    • runReject(4) 被拒绝并输出错误信息 “4”

因此,整个执行过程如下:

  • 1 秒后输出:1(来自 runAsync(1))、3(来自 runAsync(3)
  • 2 秒后输出:2(来自 runReject(2) 的错误信息)
  • 4 秒后输出:4(来自 runReject(4) 的错误信息)
答案
1
2
3
4
5
6
7
8
// 1s后输出
1
3
// 2s后输出
2
Error: 2
// 4s后输出
4
引申

Promise.all() 是 JavaScript 中的一个静态方法,用于创建一个新的 Promise,该 Promise 在传入的可迭代对象中的所有 Promise 都解决(resolve)时才会解决,如果任何一个 Promise 被拒绝(reject)了,则该 Promise 也会被拒绝

具体来说,Promise.all() 接受一个可迭代对象(通常是一个数组),并返回一个新的 Promise 对象

这个新的 Promise 对象将会在传入的所有 Promise 都解决时才会解决,并且它的解决值是一个包含了所有 Promise 解决值的数组。如果传入的任何一个 Promise 被拒绝,则返回的 Promise 将立即被拒绝,其拒绝原因是第一个被拒绝的 Promise 的拒绝原因

Promise.all() 在处理多个异步操作时非常有用,特别是当你需要等待多个异步操作都完成后才能继续执行下一步操作时

例如,你可以用它来等待多个网络请求都返回后再更新页面,或者等待多个文件的读取完成后再进行处理等


Q20:Promise(10)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
Promise.resolve()
.then(function success (res) {
throw new Error('error!!!')
}, function fail1 (err) {
console.log('fail1', err)
}).catch(function fail2 (err) {
console.log('fail2', err)
})
解析
  1. Promise.resolve() 创建一个立即解决的 Promise,并进入微任务队列
  2. 由于 Promise 是立即解决的,进入微任务队列中的任务会立即执行。因此,.then() 方法中的回调函数会被添加到微任务队列中,等待执行
  3. 微任务队列中的任务开始执行。由于 Promise 是成功状态,所以成功的回调函数 function success(res) 被调用,传入的参数 res 是 Promise 解决的值(在这个例子中是 undefined
  4. 在成功的回调函数中,throw new Error('error!!!') 抛出了一个错误
  5. 由于在成功的回调函数中抛出了错误,Promise 状态被改变为拒绝(rejected)。错误会被传递给下一个可用的拒绝回调函数
  6. 因为在 .then() 方法中提供了失败(reject)的回调函数 function fail1(err),所以这个失败的回调函数被调用,并接收到抛出的错误作为参数
  7. console.log('fail1', err) 打印了错误信息
  8. 由于没有错误处理链继续执行,这个错误会被传递到 .catch() 方法中
  9. .catch() 方法中的回调函数 function fail2(err) 被调用,并接收到抛出的错误作为参数
  10. console.log('fail2', err) 打印了错误信息
答案
1
fail2 Error: error!!!


Q21:Promise(11)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
解析
  1. new Promise(...) 的代码块立即执行,创建了一个 Promise 实例。传入的函数中包含了一个异步操作,即 setTimeout
  2. console.log(1) 打印了数字 1
  3. setTimeout 被调用,设置了一个定时器,该定时器会在 0 毫秒后执行回调函数
  4. console.log(2) 打印了数字 2
  5. 控制流程离开了 Promise 构造函数,继续执行下一行代码
  6. console.log(4) 打印了数字 4
  7. 这个时候,JavaScript 主线程中的同步代码执行完毕,事件循环开始检查是否有微任务需要执行
  8. 由于 Promise 的状态是异步确定的,它的回调函数不会立即执行,而是会被放入微任务队列中等待执行
  9. 在微任务队列中,promise.then(...) 中的回调函数被添加,等待执行
  10. 控制流程返回到微任务队列,开始执行微任务
  11. 执行 promise.then(...) 中的回调函数,传入的参数 res 是 Promise 解决的值(在这里是字符串 “success”)
  12. console.log(res) 打印了解决值 “success”
答案
1
2
3
4
5
6
1
2
4
timerStart
timerEnd
success


Q22:Promise(12)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
console.log('start')
setTimeout(() => {
console.log('time')
})
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')
解析
  1. console.log('start') 打印了字符串 'start'
  2. setTimeout(() => { console.log('time') }) 被调用,设置了一个定时器,该定时器会在默认时间后执行回调函数
  3. Promise.resolve() 创建了一个立即解决的 Promise,并进入微任务队列
  4. console.log('end') 打印了字符串 'end'
  5. 控制流程离开了当前同步代码块,开始执行微任务
  6. 微任务队列中的任务开始执行,.then() 方法中的回调函数被添加到微任务队列中
  7. 控制流程再次回到微任务队列,开始执行微任务
  8. .then() 方法中的回调函数被执行,打印了字符串 'resolve'
  9. 执行到这里,所有的同步代码执行完毕,事件循环开始检查是否有宏任务需要执行
  10. 定时器的回调函数被触发执行,打印了字符串 'time'
答案
1
2
3
4
start
end
resolve
time


Q23:Promise(13)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
const fn = () =>
new Promise((resolve, reject) => {
console.log(1);
resolve("success");
});
console.log("start");
fn().then(res => {
console.log(res);
});
解析
  1. 定义了一个名为 fn 的箭头函数,该函数返回一个 Promise 实例

    在 Promise 构造函数中,会立即执行传入的函数,这里会打印数字 1,然后解决(resolve)Promise 并传递字符串 “success”

  2. console.log("start") 打印了字符串 "start"

  3. 调用 fn(),执行函数体。这时候会执行 Promise 构造函数中的内容,打印了数字 1,并立即解决 Promise,并传递字符串 “success”

  4. fn() 返回的 Promise 实例进入微任务队列

  5. 控制流程离开了当前同步代码块,开始执行微任务

  6. 微任务队列中的任务开始执行,.then() 方法中的回调函数被添加到微任务队列中

  7. 控制流程再次回到微任务队列,开始执行微任务

  8. .then() 方法中的回调函数被执行,接收到解决值 "success",然后打印了 "success"

答案
1
2
3
start
1
success


Q24:Promise(14)输出结果

难度:⭐⭐

1
2
3
4
5
6
7
8
9
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
解析
  1. promise1 是通过 new Promise() 创建的,立即执行 Promise 构造函数中的函数体
    • console.log('promise1') 打印了字符串 'promise1'
  2. promise1 的执行过程中,resolve('resolve1') 被调用,将 Promise 状态设置为 resolved,并传递了解决值 'resolve1'
  3. promise2 是通过 promise1.then() 方法创建的,它绑定在 promise1 上。这时候,promise2 并不会立即执行,它会在 promise1 被解决后才会执行
  4. console.log('1', promise1) 打印了 '1'promise1。由于 promise1 已经被解决,所以输出时会显示 promise1 的状态和解决值(状态为 resolved,解决值为 'resolve1'
  5. console.log('2', promise2) 打印了 '2'promise2。此时,promise2 还未被解决,因为它是由 promise1.then() 创建的,而 promise1 的状态是 resolved,但 promise2 的回调函数还未执行,所以 promise2 的状态为 pending(待定)
  6. 在微任务队列中,promise1.then() 中的回调函数被添加,等待执行
  7. 控制流程离开了当前同步代码块,开始执行微任务
  8. 微任务队列中的任务开始执行,promise1.then() 中的回调函数被执行,打印了解决值 'resolve1'
答案
1
2
3
4
'promise1'
'1' Promise{<resolved>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'


Q25:代码结果(4)运行题

难度:⭐⭐

1
['10', '10', '10', '10', '10'].map(parseInt)
解析

在JavaScript中,map函数的回调函数可以接受三个参数:当前元素、当前元素的索引和整个数组

parseInt函数可以接受两个参数:一个是要解析的字符串,另一个是解析时使用的基数(进制)

当你执行['10', '10', '10', '10', '10'].map(parseInt)时,实际上你对每个元素调用了parseInt函数,并传入了两个参数:元素本身和它的索引

因此,实际调用是这样的:

  1. parseInt('10', 0) // 第一个元素,基数为0,返回10(0被当作10进制来解析)
  2. parseInt('10', 1) // 第二个元素,基数为1,但1不是有效基数,返回NaN
  3. parseInt('10', 2) // 第三个元素,基数为2,返回2(因为’10’在二进制表示为2)
  4. parseInt('10', 3) // 第四个元素,基数为3,返回3(因为’10’在三进制中表示为3)
  5. parseInt('10', 4) // 第五个元素,基数为4,返回4(因为’10’在四进制中表示为4)
答案
1
[10, NaN, 2, 3, 4]


Q26:代码结果(5)运行题

难度:⭐⭐

1
123['toString'].length +123
解析

在JavaScript中,当你看到 123["toString"] 这样的表达式时,实际上是在访问数字123的 toString 方法

数字字面量123被自动装箱成一个临时的 Number 对象,并访问其 toString 方法

123["toString"].length 这部分表达式求的是 toString 方法的 length 属性

在JavaScript中,函数的 length 属性表示该函数期望接收的参数个数

NumbertoString 方法可以接受一个可选参数,该参数指定要转换的基数,因此 toString 方法的 length 值为1

所以,123["toString"].length 的值为1

接下来,你将这个值1与123进行加法操作

在JavaScript中,加法操作符可以用于数字的算术加法或字符串的连接。当其中一个操作数是数字而另一个是数字时,JavaScript会执行算术加法

因此,123["toString"].length + 123 的计算过程是 1 + 123,结果为 124

答案
1
124


Q27:代码结果(6)运行题

难度:⭐⭐

1
2
3
4
5
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
解析

这段代码创建了一个for循环,用于设置5个setTimeout

每个setTimeout都有一个回调函数timer,在推迟0毫秒(实际会受到浏览器最小延时的限制)之后执行

由于setTimeout是异步执行的,因此所有的setTimeout回调都会在当前执行栈清空之后才会执行

关键点在于这里使用了var来声明变量i,而var声明的变量具有函数作用域,而不是块级作用域

这意味着在for循环结束时,变量i的值将是6(因为最后一次循环在执行i++后,i变为6且不满足i <= 5的条件而终止循环)

当事件循环执行这些setTimeout回调时,它们都会访问到同一个变量i,而此时i已经是6了

因为所有的setTimeout都在一个事件循环队列中,因此它们并不会彼此间有任何延迟

因此,输出不会按照1到5的顺序打印,而是会打印5次6

答案
1
2
3
4
5
6
6
6
6
6
引申

解决这个问题

  1. 闭包

    1
    2
    3
    4
    5
    6
    7
    for (var i = 1; i <= 5; i++) {
    (function(j) {
    setTimeout(function timer() {
    console.log(j);
    }, 0);
    })(i);
    }
  2. 使用let来声明i(ES6), let会在每次迭代的循环块中创建一个新的i绑定

    1
    2
    3
    4
    5
    for (let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
    console.log(i);
    }, 0);
    }


Q28:代码结果(8)运行题

难度:⭐⭐

1
[] == ![]的结果是什么
解析
  1. 取反操作

    ![] 首先计算右边的表达式

    ! 操作符将对 [](空数组)进行布尔取反操作

    在JavaScript中,一个空数组是一个真值(truthy)

    因此对它取反后,将得到布尔值 false

  2. 等于操作

    现在原始的比较已经变成了 [] == false

    在这个比较中,因为一边是对象(空数组)而另一边是布尔值

    根据抽象相等性比较(Abstract Equality Comparison)的规则,JavaScript将尝试将这两边转换成一个共同的类型

  3. 类型转换
    false 需要转换为数值型以便跟数组进行比较

    布尔值 false 转换为数字时值是 0

    这样,现在的比较变成了 [] == 0

  4. 数组到字符串的转换

    接下来,JavaScript会尝试将对象(本例中的空数组 [])转换为原始值

    数组对象转换为原始值时,会先转换为字符串

    由于空数组转换为字符串是一个空字符串(""),现在的比较就变成了 "" == 0

  5. 字符串到数字的转换

    因为另一边是数字,JavaScript会进一步将字符串转换为数字,空字符串转换为数字是 0

    最后,比较就变成了 0 == 0

  6. 比较结果

    0 等于 0,这个比较的结果为 true

因此,最终 [] == ![] 的结果就是 true,这是由于JavaScript内部类型转换规则的结果

答案
1
true


Q29:Promise(15)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
resolve('promise resolve')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => {
console.log(res)
})
new Promise(resolve => {
console.log('promise2')
setTimeout(() => {
console.log('timer')
})
})
解析
  1. 首先,执行全局代码,console.log('script start') 是第一个被执行的,所以首先输出 script start
  2. 然后,遇到了异步函数 async1()。这是一个异步调用,但在内部 console.log('async1 start') 会首先同步执行,所以第二个输出 async1 start
  3. 之后,遇到了 await 关键字,它会返回一个新的 Promise 对象。在这个 Promise 的执行器函数中,有一个 console.log(‘promise1’) 的同步操作,因此第三个输出 promise1
  4. Promise 对象在状态变更(这里是调用了 resolve 方法)前的代码属于微任务,在当前宏任务内执行。但由于 async/await 的特殊性,await 后面的代码 (console.log('async1 success')) 会被打包为一个微任务,等待当前的 Promise 状态变为 resolved 后进入微任务队列等待执行,至此当前的异步函数 async1 执行完毕
  5. 接着,继续执行全局代码,遇到另一处 Promise 构造,此时执行器函数内的 console.log(‘promise2’) 同步执行,所以第四个输出 promise2
  6. 再下来遇到setTimeout,这是一个宏任务,会被派发到宏任务事件队列中,由于Javascript是单线程执行,所以需要等待前面的任务处理完毕,此处就先不输出 ‘timer’
  7. 此时,全局的同步代码执行完毕,开始执行微任务队列中的任务。第一个待执行的微任务是 async1()await 后面的代码,所以第五个输出 async1 success
  8. async1() 代码执行完毕后,它的 .then() 方法被调用,返回的结果是 async1 end,所以第六个输出 async1 end
  9. 这时,微任务队列已经为空,开始执行宏任务队列中的任务,输出 timer
答案
1
2
3
4
5
6
7
"srcipt start"
"async1 start"
"promise1"
"promise2"
"async1 success"
"async1 end"
"timer"


Q30:Promise(16)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
解析
  1. 首先,console.log('script start') 这行代码在全局脚本中,它将首先执行,所以输出 script start

  2. 接着,遇到 async1() 函数调用

    由于 async1 是一个异步函数,它将被立即调用,但里面的代码可能不会立即完成

  3. 进入 async1() 函数内部,console.log('async1 start') 首先被同步执行,所以会输出 async1 start

  4. 然后,代码遇到 await 关键词和一个新的 Promise

    await 将暂停 async1 函数后面的代码执行,在这个例子中就是 console.log('async1 success'),直到 Promise 被解决(resolve)

    此时,Promise 内部调用了 console.log('promise1'),所以输出 promise1

    但是这个Promise从未被解决,因为它内部没有调用resolve函数,这将影响程序的后续行为,此部分的代码输出结束在此

  5. 跳出 async1() 函数,回到全局执行环境,紧接着执行 console.log('script end'),所以接下来输出 script end

  6. 此时,全局的同步代码已经执行完毕,但是 async1()await 后面的 console.log('async1 success') 依赖于之前的 Promise 解决

    由于该 Promise 没有解决(因为没有调用 resolve 或者 reject),它将永远挂起在那里,导致紧随其后的 console.log('async1 success').then(res => console.log(res)) 都不会执行

答案
1
2
3
4
"srcipt start"
"async1 start"
"promise1"
"srcipt end"


Q31:Promise(17)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
async function fn () {
// return await 1234
// 等同于
return 123
}
fn().then(res => console.log(res))
解析

一个用async关键字声明的异步函数fn,它的功能是返回一个数值

尽管函数内有两行被注释的代码,但我们只关注未被注释的那行

这段代码在执行时的行为如下:

  1. 函数fn是异步的,也就是说它返回的始终是一个Promise对象

    由于async函数允许你直接返回一个值(如这里的数字123),它会自动将这个值包裹在Promise.resolve中,即相当于返回了Promise.resolve(123)

  2. fn被调用时,由于其返回的是一个Promise,可以链式调用.then()方法处理当这个Promise被解决(resolved)时的值

  3. .then()中的回调函数将接收到fn函数返回的结果作为参数,这里是数字123

  4. .then()回调函数调用console.log(res)会将这个结果输出到控制台

因此,整个过程结束后,控制台上会输出123

这是因为fn()函数返回的Promise立即被解决了,并且有值123

答案
1
123


Q32:Promise(18)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
解析

代码使用了Promise.resolve来创建已经解决的Promise,并使用.then().finally()来处理这些Promise

以下是这段代码执行时的顺序和输出分析:

  1. Promise.resolve('1'):创建一个已经解析(resolved)的Promise对象,其解析值为字符串’1’
  2. .then(res => console.log(res)).then方法返回一个新的Promise对象,并接受resolved值为参数的调用函数。该调用函数(匿名函数res => console.log(res))会在Promise完成(resolved)时被调用,并打印出res的值,也就是打印’1’
  3. .finally(() => console.log('finally')):不论Promise是fulfilled还是rejected,都会执行控制台打印’finally’的操作。不过,需要注意的是,finally的回调函数是不接受任何参数的,不影响原有的Promise的值

接下来,关于第二个Promise的执行步骤如下:

  1. Promise.resolve('2'):创建一个已经解析(resolved)的Promise对象,其解析值为字符串’2’
  2. .finally(() => console.log('finally2')):同样的,不论Promise是fulfilled还是rejected,都会执行打印’finally2’的操作。并且返回了一个字符串,但这个返回值会被忽略,不会传递到下一个Promise
  3. .then(res => console.log('finally2后面的then函数', res)):这时,因为finally并没有改变值,所以这里的res会继承前一个Promise的解析值’2’,也就是打印出’finally2后面的then函数’后面输出的是’2’
答案
1
2
3
4
"1"
"finally2"
"finally"
"finally2后面的then函数" "2"


Q33:Promise(19)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promise = new Promise((resolve, reject) => {
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then1: ", res);
}).then(res => {
console.log("then2: ", res);
}).catch(err => {
console.log("catch: ", err);
}).then(res => {
console.log("then3: ", res);
})
解析
  1. 创建了一个新的Promise实例,其中传递的执行器(executor)函数是立即执行的。
    • 在执行器内部,首先调用reject("error"),这将导致Promise变为拒绝(rejected)状态,并将其理由设置为字符串"error"
    • 紧接着有一个resolve("success2")调用。但是,一旦Promise被拒绝或解决,它的状态就不能再改变了。因此,这行调用是没有任何效果的,所以resolve不会执行,Promise的拒绝理由保持为"error"
  2. 跟随这个Promise实例的是一个.then()方法的调用链,其中包含两个.then()和一个.catch()
    • 第一个.then(res => console.log("then1: ", res)):通常情况下,.then()会接收到Promise的解析值,但是由于Promise是被拒绝的,所以这个.then()是被跳过的
  3. .then()的跳过直接导致.catch(err => console.log("catch: ", err))的执行
    • .catch()方法是用来捕获前面Promise链上的错误的,由于之前的Promise已经被拒绝,并带有理由"error",所以.catch()会捕获到这个拒绝,并输出catch: error
  4. .catch()后面跟着另一个.then(res => console.log("then3: ", res))
    • .catch()可以处理错误,并且也返回一个新的Promise,这个Promise默认情况下是解决状态(resolved)的,除非你在.catch()里再抛出错误。因为在这个场景中.catch()里没有抛出新的错误,所以.catch()之后的.then()得到的Promise是解决状态的
    • 这个.then()没有被显式给定解析值,因为它是跟在.catch()后面的,所以它接收的解析值是undefined,因此输出then3: undefined
答案
1
2
"catch: " "error"
"then3: " undefined


Q34:Promise(20)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
解析
  1. Promise.resolve().then... 这是一个立即解决的 Promise,其 .then() 方法中的回调加入到微任务队列

  2. const timer1 = setTimeout(...) 定义了一个宏任务,将它放入宏任务队列中等待执行

  3. console.log('start'); 直接执行,打印 start

    执行结果到目前为止:start

  4. 此时,主线程的同步代码执行完成,事件循环首先检查微任务队列,遇到 Promise.resolve().then... 中的回调,执行它:

    • 打印 promise1
    • 定义并启动另一个宏任务 timer2
      执行结果累计:startpromise1
  5. 现在,微任务队列已为空,事件循环移向宏任务队列。第一个遇到的宏任务是 timer1 的回调:

    • 打印 timer1
    • 然后,Promise.resolve().then... 再次创建一个新的微任务,加入到微任务队列
      执行结果累计:startpromise1timer1
  6. timer1 执行结束后,再次检查微任务队列,发现刚刚添加的 Promise.resolve().then... 中的回调,执行它:

    • 打印 promise2
      执行结果累计:startpromise1timer1promise2
  7. 最后,执行 timer2 的回调(前面timer1timer2由于几乎同时设定,执行顺序在某些环境下可能会有细微差别,但基于大多数实现,timer1 因为在代码中先出现,通常会先执行):

    • 打印 timer2
      执行结果累计:startpromise1timer1promise2timer2
答案
1
2
3
4
5
"start"
"promise1"
"timer1"
"promise2"
"timer2"


Q35:Promise(21)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})

Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(5)
}).then(() =>{
console.log(6)
})
解析

首先,两个 Promise.resolve() 几乎同时执行

因为它们都是微任务,JavaScript 会在当前执行栈清空后,立即执行这些微任务

执行顺序如下:

  1. 第一个 .then() 回调执行,打印出 0
  2. 同时,第二个 Promise.resolve().then(() => { console.log(1) }) 开始执行其链中的第一个 .then(),打印出 1
  3. 第一个 .then() 回调返回了 Promise.resolve(4),这个返回值会被传递给链中的下一个 .then(),所以接下来的 .then((res) => { console.log(res) }) 会在微任务队列中等待执行,这里的 res 将是 4
  4. 回到第二个 Promise 链,它的第二个 .then() 执行,打印出 2
  5. 第二个 Promise 链的第三个 .then() 执行,打印出 3
  6. 现在回到第一个 Promise 的第二个 .then(),它打印出之前等待的 4
  7. 第二个 Promise 链的剩下的 .then() 依次执行,打印出 56
答案
1
2
3
4
5
6
7
0
1
2
3
4
5
6


Q36:this指向(4)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
const obj = {
fn1: () => console.log(this),
fn2: function() {console.log(this)}
}

obj.fn1();
obj.fn2();

const x = new obj.fn1();
const y = new obj.fn2();
解析

先了解箭头函数和普通函数(在此示例中用作方法)之间的区别,尤其是在它们如何处理this关键字方面

  • 箭头函数绑定this,它们会捕获其所在上下文的this值作为自己的this值,无论是否被new调用
  • 普通函数(使用function关键字定义的函数)的this指向调用它时的对象,或者在使用new构造函数时,指向新创建的对象

首先,定义一个对象obj1,该对象有两个函数作为属性,一个是以函数表达式方式定义,一个是以箭头函数方式定义:

1
2
3
4
const obj1 = {
fn1: () => console.log(this), // 这是一个箭头函数,它不会绑定自身的this
fn2: function() {console.log(this)} // 这是一个普通函数,它会绑定自身的this
}
  1. obj1.fn1();

    当你在控制台调用obj1.fn1()时,将会执行fn1

    由于fn1是一个箭头函数,它不绑定自身的thisthis的值取决于定义fn1时的上下文

    又因为fn1在全局作用域定义,所以在非严格模式下,this将指向全局对象,也就是浏览器环境下的window对象(严格模式下,this将是undefined

    所以第一个console.log将打印全局对象window

  2. obj1.fn2();

    然后你调用obj1.fn2(),将会执行fn2

    fn2是一个普通方法,当它作为对象的方法被调用时,this会指向调用它的对象,也就是obj1

    所以第二个console.log将打印出obj1

  3. const x = new obj1.fn1();

    接下来你尝试存储一个新的fn1实例到x变量

    这里会产生一个问题,因为箭头函数不能用作构造函数

    new关键字需要用在包含thisprototype的普通函数上,而箭头函数不具备这二者

    所以这里会抛出一个类型错误,表明obj1.fn1不是构造函数

答案
1
2
3
window
{fn1: ƒ, fn2: ƒ}
Uncaught TypeError: obj.fn1 is not a constructor


Q37:Promise(22)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
async function async1 () {  
try {
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
async1().then(res => console.log(res))
console.log('script start')
解析

在这段代码中,首先定义了一个异步函数async1,它包含一个错误处理的try...catch块和一些控制台日志输出

然后执行这个异步函数并在其返回的Promise上附加了一个.then处理程序

最后,代码执行一个console.log

现在,让我们详细了解代码的执行流程:

  1. 调用async1()函数
  2. 控制台输出'script start'
  3. async1中,异步等待(await)一个被拒绝(reject)的Promise
  4. Promise被拒绝,因此错误被抛出,接着catch块捕获这个错误
  5. 控制台输出捕获的错误'error!!!'
  6. catch块完成执行后,控制台输出'async1'
  7. async1返回一个解决(resolve)的Promise
  8. 最后,.then处理程序接收到返回的Promise中的值,控制台输出'async1 success'

这个顺序描述了异步操作的副作用和.then处理程序的调用顺序

由于async函数使得异步代码看起来像是同步的,它可以在await表达式上暂停执行,直到Promise解决或拒绝

这也是为什么即使有错误发生,async1之后的代码还是会执行的原因,因为错误被catch块捕获处理了

答案
1
2
3
4
script start
error!!!
async1
async1 success


Q38:Promise(23)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
async function async1 () {
await async2();
console.log('async1');
return 'async1 success'
}
async function async2 () {
return new Promise((resolve, reject) => {
console.log('async2')
reject('error')
})
}
async1().then(res => console.log(res))
解析

在这里有两个异步函数:async1async2

在函数async1中,有一个暂停点`await async2()``

`await关键字使得JavaScript运行时等待直到Promise settles,并返回其结果

现在,让我们理解一下这段代码:

  1. 调用async1()函数
  2. async1函数内部,执行到await async2(),调用async2()函数
  3. async2函数开始执行,输出 async2 ,然后返回一个Promise对象,并且这个Promise是以错误状态结束(rejected)
  4. await操作在碰到rejected状态的Promise时,会抛出一场错误,并立即结束当前函数的执行

在这段代码中,我们没有在async1函数中捕获可能发生的错误(async2函数返回的Promise被拒绝),所以当async2返回的Promise被拒绝时,它会在async1函数中抛出一个错误,导致async1函数立即结束,而且不会输出’async1’,也不会返回’async1 success’

答案
1
2
async2
Promise {<rejected>: 'error'}


Q39:Promise(24)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async function testSometing() {
console.log("执行testSometing");
return "testSometing";
}

async function testAsync() {
console.log("执行testAsync");
return Promise.resolve("hello async");
}

async function test() {
console.log("test start...");
const v1 = await testSometing();
console.log(v1);
const v2 = await testAsync();
console.log(v2);
console.log(v1, v2);
}

test();

var promise = new Promise(resolve => {
console.log("promise start...");
resolve("promise");
});
promise.then(val => console.log(val));

console.log("test end...");
解析
  1. 首先执行 test() 函数,在控制台上打印 "test start..."

  2. 然后进入 testSomething() 函数,在控制台上打印 "执行testSometing"

    这个函数返回一个字符串 "testSometing",但因为是异步函数,这个返回值会被包装在一个解析的 Promise 对象中

  3. 然后执行 Promise 构造函数的回调,打印 "promise start..."

    这个 Promise 立即被解析为 "promise",此时 Promise 的解析在任务队列中等待执行

  4. 在所有同步代码执行完成后(此时包括在 test() 函数外部立即执行的 console.log("test end...")),打印 "test end..."

  5. 之后,异步函数 test() 再次回到执行的控制下,testSomething() 函数返回的结果,也就是 "testSometing" 已经准备好,打印出来

  6. 接着,testAsync() 函数被调用,在控制台上打印 "执行testAsync"

    它返回 "hello async",这个值封装在一个Promise中,由于已经是resolve的状态,会在后面的事件循环中输出

  7. 由于我们先前的 Promise 已经处于解析状态,在 testAsync() 完成之前,任务队列中的 Promise 解析输出 "promise" 实际先于 hello async 执行

  8. 紧接着,testAsync() 函数返回的 "hello async" 输出到控制台

  9. 最后,打印出包含 v1(testSometing) 和 v2(hello async) 的结果

答案
1
2
3
4
5
6
7
8
9
test start...
执行testSometing
promise start...
test end...
testSometing
执行testAsync
promise
hello async
testSometing hello async


Q40:Promise(25)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
setTimeout(() => {
console.log('timer')
}, 0)
console.log("async2");
}
async1();
console.log("start")
解析
  1. 调用async1(),这个异步函数开始执行
  2. "async1 start"被打印到控制台
  3. async1()内部,await async2()暂停了async1()的进一步执行,直到async2()完成。接下来,async2()被调用
  4. async2()执行并立即将一个setTimeout计划为0毫秒后执行。这会将回调函数(打印'timer')加入到宏任务队列中,但实际的调用会在当前执行栈清空以及当前宏任务完成之后才发生
  5. "async2"被打印到控制台,这是async2()内直接执行的代码
  6. async2()执行完成,执行权返回到async1(),因为async1()在等待async2()
  7. 现在回到全局上下文,执行最后的console.log("start"),打印 "start" 到控制台
  8. 此时,当前的调用栈和宏任务队列(当前宏任务是调用async1()与之后的全局上下文中的代码)都已清空。JS引擎现在会从宏任务队列中取出下一个任务,也就是setTimeout的回调函数
  9. setTimeout的回调函数执行并在控制台打印出'timer'
  10. "async1 end"是在async2()之后在async1()中打印的,但由于async1()函数内部是由await async2()暂停的,所以会在"start"'timer'之间打印
答案
1
2
3
4
5
async1 start
async2
start
async1 end
timer


Q41:Promise(26)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})
解析

在这段代码中,你正在创建一个被拒绝的Promise,然后使用 then 方法来处理它的结果

这个 then 方法有两个参数:一个成功回调和一个失败回调

因为你的Promise是被拒绝的,所以失败的回调会被触发并接收到你传递的值 ‘err!!!’

注意,catch 并不会被执行

因为当你提供了 then 的失败回调后,这个回调就接管了错误处理,所以 catch 不会再被调用

答案
1
error err!!!


Q42:Promise(27)输出结果

难度:⭐⭐⭐

1
2
3
4
const promise = Promise.resolve().then(() => {
return promise;
})
promise.catch(console.err)
解析

这段代码中,首先,你创建了一个已解决的 Promise 对象,然后你在 then 函数中返回了相同的 Promise 对象

这会导致 Promise 陷入一个循环状态,因为它在等待自己完成

因此,这样的写法会抛出一个类型错误,表示 Promise 不能返回自身

答案
1
TypeError: Chaining cycle detected for promise #


Q43:Promise(28)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
Promise.resolve().then(() => {
return new Error('error!!!')
}).then(res => {
console.log("then: ", res)
}).catch(err => {
console.log("catch: ", err)
})
解析

这段代码中,你创建了一个已解决的 Promise 对象,在它的 then 方法中返回了一个新的错误对象 new Error('error!!!')

但这样并不会导致 promise 被拒绝

如你所见,then 代码块中返回一个错误对象,它并不会自动地把 promise 状态变为 rejected,这就是为什么错误信息会被传递给下一个 then 而非 catch

这意味着即使你创建了一个 Error 对象,并把它作为结果返回,Promise 也仍然会保持其解析状态(Resolved,也就是说状态是完成的而不是被拒绝的)

然后代码进入下一个 then,打印出结果 “then: Error: error!!!”

如果你想抛出错误并让其被 catch 捕捉,有两种方法:

  1. 你可以抛出一个错误 throw new Error('error!!!'),而不是返回它
  2. 或者你可以返回一个被拒绝(rejected)的 Promise,比如 return Promise.reject(new Error('error!!!'))

在这两种情况下,错误都会被 catch 块捕获并处理

答案
1
then:  Error: error!!!


Q44:Promise(29)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('timer')
resolve('success')
}, 1000)
})
const start = Date.now();
promise.then(res => {
console.log(res, Date.now() - start)
})
promise.then(res => {
console.log(res, Date.now() - start)
})
解析

在这段代码中,首先创建了一个新的 Promise 对象 promise

在 Promise 的执行器函数中,通过 setTimeout() 方法设置了一个定时器,1秒(1000毫秒)后,打印 'timer' 到控制台,并且将 Promise 的状态改为已解决(resolved),解决值为字符串 'success'

随后,有两个 then 方法被链式调用在 promise

每个 then 方法都注册了一个回调函数,用于处理 Promise 解决时的情形

在这个回调函数中,你打印出了 Promise 的解决值 'success' 以及从变量 start 至当前的时间差

由于 start 是在 Promise 被创建之前就记录的,这个时间差大致上代表了从 Promise 创建到它解决时经过的时间

因为设置了一个1秒的延迟,打印出的时间应该大约是1000毫秒(可能会稍微多一点,取决于执行环境和其他延迟)

两个 then 方法几乎会同时接收到解决值 'success',所以它们打印出的时间差应该也会非常接近

答案
1
2
3
timer
success 1004
success 1004


Q45:Promise(30)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
Promise.reject(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
console.log(err);
return 3
})
.then(res => {
console.log(res);
});
解析

这段代码展示了 Promise 的处理流程,包括了拒绝(reject),捕获错误(catch),以及成功解析(resolve)的处理

  1. Promise.reject(1) 创建了一个被拒绝的 Promise,其拒绝理由是数字 1
  2. 随后,.then(res => { console.log(res); return 2; }) 是一个处理解析(resolve)的 then 方法。但由于 Promise 已经被拒绝,这个 then 方法中的回调函数将不会被调用
  3. 接着,.catch(err => { console.log(err); return 3 }) 是用于捕获错误的 catch 方法。由于前面的 Promise 被拒绝且没有被任何 then 的第二个回调参数(用于处理拒绝)捕获,所以这个 catch 方法将会被调用,打印出拒绝理由 1,并返回一个新的值 3
  4. 最后,.then(res => { console.log(res); }) 会处理上一个 .catch 返回的结果。由于 .catch 返回了 3,这个 then 方法将被调用,结果 3 会被打印出来
答案
1
2
1
3


Q46:Promise(31)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
解析

这段代码呈现了一个典型的 Promise 链式调用流程

  1. 首先是通过 Promise.resolve(1) 创建了一个立即解决的 Promise,其解决值为数字 1
  2. .then(res => { console.log(res); return 2; }) 链接到了这个 Promise,并提供了一个回调函数。这个回调被调用时,会打印出解决值 1,然后返回数字 2
  3. .catch(err => { return 3; }) 是一个错误处理函数,但由于前面的 Promise 被成功解决,而不是拒绝,这个回调不会被调用
  4. 最后是另一个 .then(res => { console.log(res); }),它处理了前一个 .then 方法返回的 2。它的回调会打印出 2
答案
1
2
1
2


Q47:Promise(32)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
const promise = new Promise((resolve, reject) => {
resolve("success1");
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then: ", res);
}).catch(err => {
console.log("catch: ", err);
})
解析

在这段代码中,promise 被初始化为一个新的 Promise 对象

在其执行器函数中,依次调用了 resolvereject 方法,最后又尝试调用了 resolve

然而,Promise 的状态一旦改变(从“pending”变为“fulfilled”或“rejected”)就会固定下来,后续的 resolvereject 调用将不会有任何效果

因此,第一次调用 resolve("success1") 会将 promise 的状态从“pending”更改为“fulfilled”,并设置其结果值为 "success1"

紧接着的 reject("error") 和再次的 resolve("success2") 调用将被忽略,因为 Promise 的状态已经确定,且为“fulfilled”

接下来的 .then(res => { console.log("then: ", res); }) 用于处理 Promise 解决时的情形

因为 Promise 已经成功解决,故此回调将被调用,并打印出 then: success1

.catch(err => { console.log("catch: ", err); }) 用于捕获任何可能的拒绝情况,但由于 promise 成功解决而无需调用

答案
1
then:  success1


Q48:Promise(33)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
console.log("timer1");
}, 1000);
console.log("promise1里的内容");
});
const promise2 = promise1.then(() => {
throw new Error("error!!!");
});
console.log("promise1", promise1);
console.log("promise2", promise2);
setTimeout(() => {
console.log("timer2");
console.log("promise1", promise1);
console.log("promise2", promise2);
}, 2000);
解析
  1. 首先,promise1 被初始化为一个新的 Promise 对象,其内部包含一个异步操作(setTimeout),该操作在 1000 毫秒(1秒)后执行,将 promise1 的状态从“pending”更改为“fulfilled”,且解决值为 "success"

    在这之前,会立即打印出 "promise1里的内容"

  2. 然后,几乎同时(由于事件循环,这些都是在初始化阶段同步完成的),由控制台输出 "promise1", promise1"promise2", promise2

    由于JS的异步特性,这两个 Promise 对象此时都处于“pending”状态

  3. 接下来,第一个 setTimeout 的回调函数(延迟了 1 秒)执行,打印 "timer1" 并解决 promise1

    因为 promise2 是通过 promise1.then() 产生的,并在其回调中抛出了一个错误,所以 promise2 将会被拒绝

  4. 最后,第二个 setTimeout(延迟了 2 秒)的回调函数执行,打印出 "timer2" 和此时 promise1promise2 的状态

    此时,promise1 已经解决(fulfilled)且其值为 "success",而 promise2 由于之前的错误抛出已经被拒绝(rejected)

根据这个流程,控制台的输出顺序和内容应该如下:

  • "promise1里的内容":同步打印,表示 Promise 初始化
  • "promise1", Promise {<pending>}:初始化时 promise1 的状态
  • "promise2", Promise {<pending>}:初始化时 promise2 的状态
  • "timer1":1秒后的异步打印
  • "timer2":2秒后的异步打印
  • "promise1", Promise {<fulfilled>: "success"}:2秒时 promise1 的状态和解决值
  • "promise2", Promise {<rejected>: Error: error!!!}:2秒时 promise2 的状态和拒绝原因
答案
1
2
3
4
5
6
7
8
promise1里的内容
promise1 Promise {<pending>}
promise2 Promise {<pending>}
timer1
timer2
promise1 Promise {<fulfilled>: 'success'}
promise2 Promise {<rejected>: Error: error!!!
at <anonymous>:9:9}


Q49:Promise(34)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
解析
  1. 首先,创建了一个名为promise1的新 Promise

    这个 Promise 在1秒后解决(‘success’)

  2. 然后,通过调用promise1.then()创建了另一个名为 promise2 的 Promise

    promise2 的回调函数在 promise1 解决时立即抛出一个错误,导致 promise2 被立即拒绝

  3. 同时,在创建 Promise 后,立即打印 promise1promise2

    由于此时两个 Promise 都处于 pending 状态,所以看到的是两个 pending 的 Promise:Promise {<pending>}

  4. 在2秒后,通过 setTimeout 打印了 promise1promise2 的状态

    此时,promise1 应该已经完成并解决 (‘success’),而 promise2 由于在其 then 方法中抛出了错误,应该已经被拒绝

答案
1
2
3
4
5
6
7
promise1 Promise {<pending>}
promise2 Promise {<pending>}
// 1秒后
// 无输出,但是已经抛出错误
// 2秒后
promise1 Promise {<fulfilled>: "success"}
promise2 Promise {<rejected>: Error: error!!!}


Q50:Promise(35)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log('timer1');
setTimeout(() => {
console.log('timer3')
}, 0)
}, 0)
setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
解析
  1. 首先,两个 setTimeout 函数作为异步操作被添加到任务队列中

    它们都设定为延迟为0毫秒,但其实并不意味着它们会立即执行,相反地,它们会等到当前的同步代码全部执行完毕后,才会执行

  2. 然后,console.log('start');作为同步代码首先被执行并打印出start

  3. 同步代码执行完毕后,开始执行队列中的异步任务,首先执行的是第一个添加到队列的 setTimeout,它会打印出timer1,并且在其回调函数中添加了另一个 setTimeout

  4. 然后,继续执行下一个 setTimeout,打印出timer2

  5. 最后,执行最后添加到队列中的 setTimeout,打印出timer3

答案
1
2
3
4
'start'
'timer1'
'timer2'
'timer3'


Q51:Promise(36)输出结果

难度:⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise')
})
}, 0)
setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
解析
  1. 两个setTimeout调用被排定为宏任务,它们将在当前执行栈清空后的下一个事件循环迭代中执行

  2. console.log('start')是同步代码,会立刻执行,打印start到控制台

  3. 当当前的执行栈清空时,即同步代码执行完毕后,事件循环将检查微任务队列

    此时,微任务队列是空的,因此事件循环进入下一步,开始执行宏任务队列中的任务

  4. 第一个setTimeout回调执行,打印timer1

    它内部的Promise.resolve().then()会创建一个微任务,该微任务会在当前宏任务完成后、下个宏任务开始前执行

  5. 第一个宏任务的微任务(console.log('promise'))现在执行,打印promise

  6. 第一个setTimeout的宏任务及其微任务执行完毕后,事件循环继续执行下一个宏任务,即第二个setTimeout回调,打印timer2

答案
1
2
3
4
'start'
'timer1'
'promise'
'timer2'


Q52:Promise(37)输出结果

难度:⭐⭐⭐

1
2
3
4
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
解析

这里 .then(2) 并不会改变 Promise 链中的值,因为 2 不是一个函数

.then()` 接受一个函数作为参数,如果你传递一个非函数的值,它将会被忽略,Promise 链中的值将不会变

然后,.then(Promise.resolve(3)) 也同样不会改变 Promise 链中的值

这是因为 .then 期望一个函数作为参数,即使你给出的是一个已决的 Promise,它也不会改变当前 Promise 链中的值

你只能通过一个返回 Promise 或返回一个新值的函数来改变 Promise 链中的值

再接下来的 .then(console.log) 则会在控制台打印出来Promise链中的值,也就是 1

这段代码的执行过程可以描述为:

  1. Promise.resolve(1) 创建并立即解析了一个含有值 1 的 Promise

    这个 Promise 中的值现在可以在其后的链中的 .then() 调用中被访问到

  2. .then(2) 试图将链中的值改变为 2,但由于 2 不是一个函数,所以这一步并未改变链中的值,链中的值仍为 1

  3. .then(Promise.resolve(3)) 试图将链中的值改变为从 Promise.resolve(3) 得到的 Promise 的解析值,即 3

    但同样因为 Promise.resolve(3) 不是一个函数,所以这一步也并未改变链中的值,链中的值仍然为 1

  4. .then(console.log) 打印出当前链中的值,即 1

答案
1
1


Q53:Promise(38)输出结果

难度:⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve('resolve3');
console.log('timer1')
}, 0)
resolve('resovle1');
resolve('resolve2');
}).then(res => {
console.log(res)
setTimeout(() => {
console.log(p1)
}, 1000)
}).finally(res => {
console.log('finally', res)
})
解析
  1. p1是一个Promise实例,其构造函数中包含一个异步操作(setTimeout)和两次立即的resolve调用

  2. Promise的执行器(executor)函数中,首先通过setTimeout设置了一个宏任务,延迟0ms后执行。然后立即执行了两次resolve

  3. 根据Promise的特性,一个Promise对象的状态只能从pending变为fulfilledrejected,且状态变化后不会再改变

    因此,第一次调用resolve('resovle1')将会决定p1的状态和结果,后续的resolve('resolve2')setTimeout中的resolve('resolve3')不会改变p1的状态或结果

  4. 第一个then注册的回调函数取得的res值是'resovle1'(由于上面提到的Promise的状态变化特性),所以控制台会先打印出”resovle1”

  5. 在这个then的回调函数中,又设置了一个setTimeout,延迟1000ms后执行

    因为setTimeout会创建一个宏任务,所以这里的console.log(p1)会在延迟1000ms之后执行,打印出p1的当前状态

    此时p1已经完成,所以会显示Promise完成态的相关信息

  6. Promise链的最后有一个finally调用

    finally注册的回调函数是在Promise完成(不管是fulfilled还是rejected)后调用的,finally`不接受任何参数,所以这里打印出的是”finally undefined”

  7. setTimeout中设置的打印'timer1'会在所有同步代码执行完毕、事件循环到达对应的宏任务队列时执行,因此'timer1'会在'resovle1'之后、'finally undefined'之后打印

答案
1
2
3
4
'resolve1'
'finally' undefined
'timer1'
Promise{<resolved>: undefined}


Q54:Promise(39)输出结果

难度:⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
解析
  1. "script start"首先被同步输出

  2. 调用async1函数,该函数首先同步输出"async1"

  3. 接着设置一个定时器(timer1),延迟2000ms后打印"timer1",此定时器会被放入宏任务队列中等待执行

  4. 接下来,async1里执行到await new Promise(...),在这个Promise中"promise1"被同步输出

    然而,这个Promise从未被resolve,导致await后面的代码(包括"async1 end"及函数返回值"async1 success")不会立即执行,async1函数会在这里暂停执行

  5. 主线程继续执行,同步输出"script end"

  6. 然后执行Promise.resolve(1)

    但是.then(2)是错误的用法,因为.then里应该传入函数。由于不是函数,这个.then不会对结果产生影响,直接传递给下一个.then

    Promise.resolve(3)也是一个Promise,但由于它不是在.then的回调函数中返回,因此它也不会对链上的流程产生影响,1直接传递给最后一个.then的回调函数,导致输出1

  7. 设置第二个定时器(timer2),延迟1000ms执行,也被加入宏任务队列

需要注意的是,由于async1中的Promise没有resolve"async1 end""async1 success"不会被输出,因为async1函数在遇到await操作符时暂停了执行,等待Promise解决,但这个Promise实际上永远不会被解决

答案
1
2
3
4
5
6
7
'script start'
'async1'
'promise1'
'script end'
1
'timer2'
'timer1'


Q55:Promise(40)输出结果

难度:⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p)
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
解析
  1. first 函数被调用:
    • 首先,在 first 函数的 Promise 中打印 3
    • 然后,创建 p Promise,在 p 的执行器函数中首先打印 7
    • p 的执行器中,setTimeout 被设置,预计在下一轮事件循环的宏任务中打印 5 并且解决该 Promise
    • p 的 Promise 立即被解决,其解决值为 1
    • first 函数中的 Promise 被解决,解决值为 2
    • 接着,将 p.then 放置在微任务队列中,预计在当前执行栈清空后打印已解决的 p 的值 1
  2. 然后,同步打印 4

到这里,同步任务已经完成。接下来,事件循环检查微任务队列:

  1. 它找到 p.then,并且打印 1
  2. 然后找到 first().then 并且打印 2

这样,微任务队列清空后,事件循环继续检查宏任务队列:

  1. 第一个宏任务是 setTimeout 中的回调,它打印 5

  2. setTimeout的回调执行时,控制台打印出 p 的状态

    由于此时 p 的状态已经解决了,它应该是一个带有解决值 1Promise {<fulfilled>: 1}

答案
1
2
3
4
5
6
7
3
7
4
1
2
5
Promise{<fulfilled>: 1}


Q56:Promise(41)输出结果

难度:⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}

async function async2() {
console.log("async2");
}

console.log("script start");

setTimeout(function() {
console.log("setTimeout");
}, 0);

async1();

new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log('script end')
解析
  1. 首先,打印”script start”
  2. 然后,设置一个0ms后执行的setTimeout回调,这是一个宏任务被加入到宏任务队列中,等待后续的事件循环执行
  3. 调用async1()函数
    • “async1 start”被打印
    • 执行async1()中的await async2()
    • 调用async2(),”async2”被打印
    • 然后,await操作会将async1函数后续的操作包装成一个微任务(这个微任务包含打印”async1 end”的操作),并将其加入微任务队列
  4. 创建一个新的Promise,执行器中打印”promise1”
  5. 使用.then方法注册Promise的成功回调,在Promise状态变为fulfilled时打印”promise2”。这个回调被加入到微任务队列中
  6. 打印”script end”

到这里,程序的同步部分已经完成,开始进行事件循环,处理任务队列中的微任务和宏任务

  1. 由于微任务队列优先于宏任务队列执行,因此会先执行两个微任务:
    • 首先是async1函数中的微任务,打印”async1 end”
    • 然后执行Promise的成功回调,打印”promise2”
  2. 所有微任务执行完后,执行宏任务队列中的setTimeout回调,打印”setTimeout”
答案
1
2
3
4
5
6
7
8
'script start'
'async1 start'
'async2'
'promise1'
'script end'
'async1 end'
'promise2'
'setTimeout'


Q57:Promise(42)输出结果

难度:⭐⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function promise1 () {
let p = new Promise((resolve) => {
console.log('promise1');
resolve('1')
})
return p;
}
function promise2 () {
return new Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally1'))

promise2()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally2'))
解析

第一个Promise链 —— promise1():

  1. promise1 函数被调用,创建并返回一个新的Promise对象p
  2. p的executor函数内部,”promise1”被打印到控制台
  3. 紧接着,resolve('1')调用使得Promisep的状态变为fulfilled
  4. then 方法被调用,在其中会打印出来自promise1的结果"1"
  5. 由于then已经处理了Promise的fulfilled状态,catch块将被跳过
  6. finally 块始终会执行,打印”finally1”

第二个Promise链 —— promise2():

  1. promise2 函数被调用,创建并返回一个新的Promise对象
  2. 这个新的Promise立即走向reject状态,并带有值”error”
  3. 由于没有设置then方法的第二个参数(为rejected状态的处理函数),所以Promise的rejected状态会被后续的catch块捕获
  4. catch块执行并打印出”error”
  5. finally块始终会执行,打印”finally2”

以上两个Promise链是独立的,它们的输出也会独立地显示到控制台

由于JavaScript事件循环和浏览器的微任务队列的执行顺序,这些Promises的回调(then, catch, finally)会在同步代码执行完毕后,按照它们被添加到队列中的顺序执行

答案
1
2
3
4
5
'promise1'
'1'
'error'
'finally1'
'finally2'