Skip to content

1. new操作符的实现原理 ​

当我们使用 new 调用一个构造函数时,底层大致会按照以下步骤执行:

  1. 创建一个全新的对象
    引擎首先创建一个空对象,这个对象将成为新实例。
  2. 设置原型链
    将这个新对象的内部 [[Prototype]](即 __proto__)指向构造函数的 prototype 属性。这样,新对象就可以继承构造函数原型上的所有属性和方法。
js
newObj.__proto__ = Constructor.prototype;
  1. 绑定 this 并执行构造函数
    将构造函数内部的 this 绑定到新创建的对象上,并传入相应的参数执行构造函数:
js
let result = Constructor.apply(newObj, args);
  1. 确定返回值
    如果构造函数显式返回了一个对象(即非 null 且类型为对象或函数),则返回这个对象;否则返回新创建的对象。这样可以保证构造函数可以根据需要选择返回自定义对象。
js
return (result !== null && (typeof result === 'object' || typeof result === 'function')) ? result : newObj;

模拟实现 new 操作符 ​

下面是一个简单的模拟实现,用于理解 new 的工作原理:

js
function myNew(Constructor, ...args) {
  // 1. 创建一个新对象,新对象的原型指向 Constructor.prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 将 Constructor 的 this 绑定到 obj 上,并执行构造函数
  const result = Constructor.apply(obj, args);

  // 3. 如果构造函数返回一个对象,则返回该对象,否则返回 obj
  return (result !== null && (typeof result === 'object' || typeof result === 'function')) ? result : obj;
}

示例 ​

js
function Person(name, age) {
  this.name = name;
  this.age = age;
  // 如果显式返回一个对象,则 new 的返回值为该对象
  // return { msg: 'This is an object returned explicitly' };
}

// 给 Person 的原型添加方法
Person.prototype.getInfo = function() {
  return `${this.name} - ${this.age}`;
};

// 使用 myNew 模拟 new 操作符创建实例
const person1 = myNew(Person, 'Alice', 30);
console.log(person1.getInfo()); // 输出:Alice - 30

// 使用原生 new 操作符创建实例
const person2 = new Person('Bob', 25);
console.log(person2.getInfo()); // 输出:Bob - 25

在上述代码中,myNew 函数通过四个步骤实现了与 new 操作符类似的行为:

  1. 使用 Object.create(Constructor.prototype) 创建了一个新对象,并建立了正确的原型链。
  2. 通过 Constructor.apply(obj, args) 执行构造函数,使得构造函数内部的 this 指向新创建的对象。
  3. 判断构造函数是否返回了一个对象。如果返回了对象,则使用它,否则返回新对象。

2. Map和Object的区别 ​

MapObject
意外的键Map默认情况不包含任何键,只包含显式插入的键。Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。
键的类型Map的键可以是任意值,包括函数、对象或任意基本类型。键只能是字符串或 Symbols(ES6 引入),对象的属性名会被自动转换为字符串类型。
键的顺序Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。对象的属性没有固定的顺序,属性在内部存储时是无序的。
SizeMap 的键值对个数可以轻易地通过size 属性获取,大小和性能通常比对象更加可预测,因为 Map 是专为存储键值对而设计的数据结构。在大多数情况下,对象的属性数量没有明确的限制。
迭代Map 是 iterable 的,所以可以直接被迭代。也提供了一些专门用于遍历和操作的方法,如 Map.prototype.keys(), Map.prototype.values(), Map.prototype.entries() 等。迭代Object需要通过 Object.keys(), Object.values(), Object.entries() 等方法来遍历对象的属性。
性能在频繁增删键值对的场景下表现更好。在频繁添加和删除键值对的场景下未作出优化。

3. map和weakMap的区别 ​

1. 键的类型限制 ​

  • Map:
    • 键可以是任意类型,包括基本数据类型(字符串、数字、布尔值等) 和 对象。
  • WeakMap:
    • 键必须是对象(包括函数、数组、对象等)。如果尝试使用非对象类型作为键,会抛出错误。

2. 引用的强弱和垃圾回收 ​

  • Map:
    • 对于存储在 Map 中的键和值,Map 会保持强引用,这意味着只要键存在于 Map 中,即使外部没有其他引用,该键也不会被垃圾回收。
  • WeakMap:
    • WeakMap 对键保持弱引用。如果某个对象仅作为 WeakMap 的键存在(外部没有其他引用),该对象可以被垃圾回收,且对应的键值对会自动从 WeakMap 中移除。这种特性使 WeakMap 在缓存或关联私有数据等场景下非常有用,因为它可以防止内存泄漏。

3. 可枚举性和迭代器 ​

  • Map:
    • Map 是可迭代的,支持诸如 for...of 循环、Map.prototype.forEach 方法,以及 keys()、values()、entries() 等方法,允许遍历所有的键值对。
    • 它还提供了 size 属性,可以直接获取 Map 中元素的个数。
  • WeakMap:
    • WeakMap 不可枚举。它不提供遍历方法(例如没有 forEach、keys、values、entries 方法),也没有 size 属性。原因在于键是弱引用,随时可能因为垃圾回收而发生变化,所以无法确定 WeakMap 中有多少键存在。

4. 使用场景 ​

  • Map:

    • 适用于需要存储各种类型键值对并且需要遍历、计数等操作的场景。
    • 当需要一个通用的键值存储结构时,Map 是理想选择。
  • WeakMap:

    • 适用于需要将数据与对象关联,但又不希望这种关联阻止垃圾回收的场景。例如,常用于存储 DOM 元素相关的元数据,或者为对象“私有”数据的存储提供支持。
    • 因为 WeakMap 中的键不会阻止对象被垃圾回收,所以可以有效防止内存泄漏。

总结 ​

  • 键的类型:Map 的键可以是任意类型,而 WeakMap 的键必须是对象。
  • 内存管理:Map 使用强引用,不会因垃圾回收而自动删除键;WeakMap 使用弱引用,当键对象没有其他引用时,会被垃圾回收并自动移除。
  • 可枚举性:Map 是可枚举的,并支持遍历和计数;WeakMap 不可枚举,无法直接获取键值对的数量。
  • 使用场景:Map 适合需要全面管理键值对的情况;WeakMap 则适用于需要关联数据但又不想阻碍垃圾回收的场景(如缓存和私有数据)。

4. JavaScript有哪些内置对象 ​

  1. Object:这是所有对象的基类,其他所有对象都继承自它。常用方法有 Object.keys(), Object.values(), Object.entries()等。
  2. Array:用于存储有序集合,并提供了一系列操作方法,如 push(), pop(), forEach(), map(), filter() 等。
  3. String:用于处理文本字符串。例如 length, indexOf(), slice(), toUpperCase(), toLowerCase() 等。
  4. Number:用于表示和处理数值的对象,包括浮点数和整数。常见方法有 parseInt(), parseFloat(), toFixed(), toString()等。
  5. Boolean:用于表示 true 和 false 值,非常简单但常用。
  6. Function:这是 JavaScript 的一等公民,每个函数其实是 Function 对象的实例。常见方法有 call(), apply(), bind()等。
  7. Date:用于处理日期和时间的对象,可以精确到毫秒。常见方法有 getDate(), getDay(), getFullYear(), getHours(), getMinutes()等。
  8. RegExp:用于处理正则表达式的对象,强大且灵活。常见方法有 exec(), test(), match(), replace()等。
  9. Math:提供数学计算的常用工具,如 Math.random(), Math.floor(), Math.ceil(), Math.max(), Math.min()等。
  10. JSON:用于解析和格式化 JSON 格式的数据。方法包括 JSON.parse() 和 JSON.stringify()。
  11. Symbol:一个独特且不可变的基本类型,常用于对象属性的唯一标识符。
  12. Map:用于存储键/值对,且可以记住键值对的插入顺序。常见方法有 set(), get(), has(), delete() 等。
  13. Set:用于存储独一无二的值,不管值是原始值还是对象引用。常见方法包括 add(), has(), delete(), clear()等。
  14. WeakMap:与 Map 类似,但其键必须是对象,且该对象引用的键是弱引用,因此并不会阻止垃圾回收。
  15. WeakSet:与 Set 类似,但其值必须是对象,且这些对象优点是弱引用的特点。
  16. Promise:用于处理异步操作的对象,极大简化了异步编程。常见方法有 then(), catch(), finally(),还有静态的 Promise.resolve(), Promise.reject(), Promise.all(), 和 Promise.race() 等。

5. 常用的正则表达式有哪些? ​

正则表达式(Regular Expression)是一种用于匹配、查找、替换字符串中字符组合的强大工具。在实际开发中,常常会用到各种正则表达式来验证输入、提取数据等。下面整理了一些常见的正则表达式及其用途:


1. 数字相关 ​

  • 匹配一个或多个数字
regex
/\d+/

说明:\d 表示任意一个数字,\d+ 匹配连续出现的一个或多个数字。

  • 匹配整数(包括正负数)
regex
/^-?\d+$/

说明:^-? 表示可选的负号,\d+ 表示至少一个数字,^ 和 $ 确保整个字符串都是数字。

  • 匹配浮点数(包括整数和小数)
regex
/^-?\d+(\.\d+)?$/

说明:(\.\d+)? 为可选的小数部分。


2. 字母和单词 ​

  • 匹配英文字母(大小写均可)
regex
  /^[A-Za-z]+$/

说明:匹配仅由字母组成的字符串。

  • 匹配单词字符(字母、数字和下划线)=
regex
/^\w+$/

说明:\w 等价于 [A-Za-z0-9_]。


3. 空白字符 ​

  • 匹配空白字符(空格、制表符、换行符等)
regex
/\s+/

说明:\s 表示任意空白字符,常用于分隔符匹配或去除多余空白。


4. Email 地址验证 ​

  • 常见 Email 正则表达式
regex
/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/

说明:

  • ^[A-Za-z0-9._%+-]+:匹配邮箱用户名部分(字母、数字及部分特殊字符)。
    • @:必须包含 @ 符号。
    • [A-Za-z0-9.-]+:匹配域名部分。
  • \.[A-Za-z]{2,}$:匹配顶级域名,至少两个字母。

5. URL(统一资源定位符) ​

  • 基本的 URL 匹配
regex
/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w .-]*)*\/?$/

说明:

  • (https?:\/\/)?:可选的协议部分(http 或 https)。
  • ([\da-z.-]+):匹配域名前半部分。
  • \.([a-z.]{2,6}):匹配域名后缀(如 .com、.net 等)。
  • ([\/\w .-]*)*:匹配路径部分。

注意:URL 的正则表达式可能需要根据实际需求进行调整,上面的只是一个常见示例。


6. 手机号码(以中国为例) ​

  • 匹配中国大陆的手机号码
regex
/^1[3-9]\d{9}$/

说明:

  • 1 开头,接着第二位是 3 到 9 中的任意数字,后面跟着 9 个数字,共 11 位。

7. IP 地址(IPv4) ​

  • 匹配 IPv4 地址
regex
/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/

说明:

  • 第一部分匹配 0-255 中的一个数字,后面部分重复 3 次,中间用句点分隔。
  • 通过各个部分的细致匹配来确保每个段的数字合法。

8. 日期格式 ​

  • 匹配日期格式(YYYY-MM-DD)
regex
/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/

说明:

  • \d{4}:年份部分,必须是 4 位数字。
  • (0[1-9]|1[0-2]):月份部分,01 至 12。
  • (0[1-9]|[12]\d|3[01]):日期部分,01 至 31。
  • 这种写法只做了基础范围限制,复杂情况(如闰年、月份天数)需要更复杂的正则或额外逻辑验证。

9. 中文字符匹配 ​

  • 匹配中文字符
regex
/[\u4e00-\u9fa5]+/

说明:

  • Unicode 范围 \u4e00 至 \u9fa5 覆盖大部分常用汉字。

10. 邮政编码(以中国为例) ​

  • 匹配中国邮政编码
regex
/^[1-9]\d{5}(?!\d)$/

说明:

  • 中国邮政编码由 6 位数字组成,第一位不能为 0,并且确保只有 6 位。

总结 ​

以上列举的正则表达式只是常用示例,根据实际业务需求,正则表达式可能需要做相应调整和扩展。掌握正则表达式的基本语法和常见模式,可以帮助你更高效地处理字符串匹配、数据验证和文本处理任务。

在使用正则表达式时,也建议注意以下几点:

  • 可读性:尽量保持正则表达式的可读性,必要时添加注释。
  • 性能问题:复杂或低效的正则表达式可能导致性能瓶颈,特别是在大文本处理时。
  • 测试验证:编写正则表达式后,使用各种测试用例验证其正确性和健壮性。

希望这些常见正则表达式对你有所帮助!

6. 对JSON的理解 ​

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,具有易于读写和解析的特点。它源自 JavaScript,但现在已成为与语言无关的数据格式,被广泛应用于前后端数据传输、配置文件存储、日志记录等场景。下面对 JSON 的几个关键点进行说明:


1. 基本概念 ​

  • 数据格式:
    JSON 采用纯文本格式表示数据,主要由两种结构构成:
    • 对象(Object):由大括号 {} 包围,内部是由键值对组成的无序集合。键必须是字符串,值可以是字符串、数字、布尔值、数组、对象或 null。
    • 数组(Array):由中括号 [] 包围,内部是一个有序的值列表,每个值可以是任意类型(包括对象和数组)。
  • 数据类型:
    JSON 支持的数据类型主要有:
    • 字符串(String):必须使用双引号包围,如 "hello"。
    • 数字(Number):可以是整数或浮点数,如 123、3.14。
    • 布尔值(Boolean):true 或 false。
    • 空值(Null):null。
    • 对象(Object):如 { "name": "Alice", "age": 30 }。
    • 数组(Array):如 [1, 2, 3, 4]。

2. 语法示例 ​

一个典型的 JSON 数据示例如下:

json
{
  "name": "Alice",
  "age": 30,
  "isStudent": false,
  "hobbies": ["reading", "coding", "traveling"],
  "address": {
    "city": "Shanghai",
    "zip": "200000"
  }
}

在这个示例中:

  • 整个数据是一个对象,包含多个键值对。
  • "hobbies" 对应的是一个数组,而 "address" 对应的又是一个嵌套对象。

3. 优势与特点 ​

  • 轻量级和易读性:
    JSON 的语法简单、格式紧凑,既方便机器解析,也便于人类阅读和编写。
  • 跨语言支持:
    虽然 JSON 源自 JavaScript,但如今几乎所有主流编程语言(如 Java、Python、C#、Go、Ruby 等)都内置或提供第三方库来解析和生成 JSON 数据。
  • 数据交换的标准:
    在 Web 开发中,JSON 已成为前后端数据通信的标准格式,相较于 XML,其语法更简洁,数据量更小,解析速度更快。
  • 无注释限制:
    JSON 规范中不允许包含注释,这有助于保持数据格式的一致性,但也意味着在需要添加解释信息时需依赖其他方式(如文档)。

4. 应用场景 ​

  • 前后端通信:
    Web API 通常使用 JSON 作为数据传输格式,通过 HTTP 请求在服务器与客户端之间传递数据。

  • 配置文件:
    许多现代应用程序使用 JSON 文件来存储配置数据,因为其格式清晰且易于编辑和解析。

  • 数据存储:
    某些数据库(如 MongoDB)本质上以 JSON 或类似格式存储数据,使得前端开发者在处理数据时更为自然。

  • 日志与数据交换:
    在日志记录、消息队列等场景中,JSON 格式的数据也被广泛采用,方便跨平台和跨语言的处理。

5.应用 ​

在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,

  • JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
  • JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

7. JavaScript脚本延迟加载的方式有哪些? ​

在前端开发中,为了提升页面加载速度和用户体验,经常会采用延迟加载(Lazy Loading)JavaScript 脚本的方式,避免初始加载时阻塞页面渲染。常见的延迟加载脚本方式主要有以下几种:


1. HTML 标签属性:async 和 defer ​

async ​

  • 使用方式:在 <script> 标签上添加 async 属性,例如:
html
<script src="script.js" async></script>
  • 特点:
    • 脚本会异步下载,不会阻塞 HTML 渲染。
    • 下载完成后立即执行,执行顺序与在文档中的顺序可能不一致。
    • 适用于独立的、不依赖其他脚本或 DOM 结构的脚本。

defer ​

  • 使用方式:在 <script> 标签上添加 defer 属性,例如:
html
<script src="script.js" defer></script>
  • 特点:
    • 脚本同样会异步下载,但会在 HTML 解析完成后才执行。
    • 保证多个 defer 脚本按照在文档中出现的顺序执行。
    • 适用于依赖 DOM 或需要按顺序执行的脚本。

2. 动态创建和插入 <script> 标签 ​

通过 JavaScript 动态创建 <script> 标签并插入到页面中,可以实现脚本的按需加载和延迟加载。例如:

js
function loadScript(url, callback) {
  const script = document.createElement('script');
  script.src = url;
  script.onload = function() {
    if (typeof callback === 'function') {
      callback();
    }
  };
  document.body.appendChild(script);
}

// 在需要的时候调用
loadScript('script.js', function() {
  console.log('脚本加载完成并执行了回调');
});

特点:

  • 可以精确控制加载时机,例如在用户操作或特定事件后再加载脚本。
  • 加载完成后通过 onload 回调通知,可以进行后续处理。

3. 动态 import()(ES Module 动态加载) ​

在支持 ES6 模块的环境中,可以使用动态 import() 函数按需加载模块。示例如下:

js
// 当需要时再加载模块
import('./module.js')
  .then(module => {
    // 使用加载的模块
    module.someFunction();
  })
  .catch(error => {
    console.error('加载模块出错:', error);
  });

特点:

  • import() 返回一个 Promise,便于处理异步加载。
  • 模块代码只在调用 import() 时加载,并且不会阻塞初始页面渲染。
  • 适用于代码拆分和按需加载模块化代码。

4. 第三方模块加载器 ​

如 RequireJS 等模块加载器也提供了延迟加载功能,特别是在采用 AMD(Asynchronous Module Definition)模式的项目中。使用方式示例如下:

js
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
  // 模块 moduleA 和 moduleB 加载完成后执行
});

特点:

  • 通过配置模块依赖关系,实现模块间的异步加载和延迟执行。
  • 适用于较早期的项目或需要兼容旧代码的场景。

5. 基于事件或条件的延迟加载 ​

在某些场景下,可以根据用户行为或页面状态来决定何时加载脚本:

  • 滚动加载:利用滚动事件(或 Intersection Observer)在用户滚动到页面特定区域时加载脚本。
  • 点击加载:在用户点击按钮或其他交互操作后,再加载相应的脚本。
  • 定时加载:使用 setTimeout 延迟一定时间后加载脚本。

例如,使用 Intersection Observer 延迟加载某个脚本:

js
const target = document.querySelector('#loadScriptTrigger');
const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadScript('script.js', () => {
        console.log('脚本在视口中时加载');
      });
      observer.unobserve(entry.target);
    }
  });
});
observer.observe(target);

特点:

  • 根据实际需求加载脚本,避免不必要的网络请求和资源浪费。
  • 有助于提升首屏渲染速度和整体用户体验。

8.类数组对象? ​

在 JavaScript 中,类数组对象(array-like object)指的是具有类似数组结构但不是真正的数组的对象。它们通常具备以下特征:

  1. 具有 length 属性:
    类数组对象都有一个 length 属性,用于表示其中元素的数量(通常为非负整数)。
  2. 具有数值索引:
    类数组对象的元素通过数字索引(例如 0, 1, 2, …)来访问,这和数组的访问方式类似。
  3. 不具有数组的方法:
    虽然结构上类似数组,但类数组对象并不是 Array 的实例,因此它们通常不继承 Array.prototype 上的方法,如 push、pop、forEach 等。

常见的类数组对象示例 ​

  • arguments 对象:
    在函数内部,arguments 对象包含了传入该函数的所有参数,但它不是一个真正的数组。

  • DOM 节点集合:
    例如,document.querySelectorAll 返回的 NodeList 对象,以及 document.getElementsByTagName、document.getElementsByClassName 返回的 HTMLCollection,都是类数组对象。

  • 自定义对象:
    开发者也可以创建具有数值索引和 length 属性的对象,使其表现得像数组。例如:

    js
    const arrayLike = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3
    };
    console.log(arrayLike[1]); // 输出 'b'
    console.log(arrayLike.length); // 输出 3

转换为真正的数组 ​

由于类数组对象没有数组原生方法,通常我们需要将它们转换为真正的数组以便调用数组的方法。常用的方法包括:

  1. 使用 Array.prototype.slice.call:
    js

function example() { const args = Array.prototype.slice.call(arguments); // 现在 args 就是一个真正的数组 console.log(args); } example(1, 2, 3); ```

  1. 使用 ES6 的 Array.from: Array.from() 静态方法从可迭代或类数组对象创建一个新的浅拷贝的数组实例。
js
function example() {
    const args = Array.from(arguments);
    console.log(args);
}
example(1, 2, 3);
  1. 使用扩展运算符(spread operator):
js
function example() {
    const args = [...arguments];
    console.log(args);
}
example(1, 2, 3);

9. 数组有哪些原生方法? ​

JavaScript 中的数组对象提供了许多原生方法,方便开发者进行数据处理、遍历、修改、搜索等操作。下面按照用途分类,介绍常用的原生方法:


一、遍历与转换 ​

  • forEach(callback, thisArg)
    对数组的每个元素执行指定的回调函数,不返回新数组。
js
[1, 2, 3].forEach(item => console.log(item));
  • map(callback, thisArg)
    对数组的每个元素执行回调函数,并返回由结果组成的新数组。
js
const doubled = [1, 2, 3].map(item => item * 2);
  • filter(callback, thisArg)
    根据回调函数的测试条件,返回满足条件的元素构成的新数组。
js
const evens = [1, 2, 3, 4].filter(item => item % 2 === 0);
  • reduce(callback, initialValue)
    对数组的每个元素执行累加(或累积)操作,将数组归纳为单一值。
js
const sum = [1, 2, 3, 4].reduce((acc, cur) => acc + cur, 0);
  • reduceRight(callback, initialValue)
    与 reduce 类似,不过是从数组的最后一项开始向前累积。

    js
    const sum = [1, 2, 3, 4].reduceRight((acc, cur) => acc + cur, 0);
  • every(callback, thisArg)
    检查数组中所有元素是否都满足指定条件,返回布尔值。

js
const allPositive = [1, 2, 3].every(item => item > 0);
  • some(callback, thisArg)
    检查数组中是否至少有一个元素满足指定条件,返回布尔值。

    js
    const hasNegative = [1, -2, 3].some(item => item < 0);
  • find(callback, thisArg)
    返回数组中第一个满足测试条件的元素,如果没有则返回 undefined。

js
const found = [1, 2, 3, 4].find(item => item > 2);
  • findIndex(callback, thisArg)
    返回数组中第一个满足条件的元素的索引,如果没有则返回 -1。

    js
    const index = [1, 2, 3, 4].findIndex(item => item > 2);
  • flat([depth]) 将嵌套的数组“扁平化”,默认为一层,可指定扁平化深度。

js
const flattened = [1, [2, [3, 4]]].flat(2);
  • flatMap(callback, thisArg)
    先对每个元素进行映射,然后将结果扁平化一层。
js
const result = [1, 2, 3].flatMap(item => [item, item * 2]);

二、搜索与查找 ​

  • indexOf(searchElement, fromIndex)
    返回指定元素在数组中第一次出现的索引,若不存在则返回 -1。

    js
    const idx = [1, 2, 3, 2].indexOf(2); // 返回 1
  • lastIndexOf(searchElement, fromIndex)
    返回指定元素在数组中最后一次出现的索引,若不存在则返回 -1。

    js
    const idx = [1, 2, 3, 2].lastIndexOf(2); // 返回 3
  • includes(searchElement, fromIndex)
    判断数组中是否存在指定元素,返回 true 或 false。

    js
    const exists = [1, 2, 3].includes(2); // true

三、修改原数组的方法 ​

  • push(...items)
    在数组末尾添加一个或多个元素,并返回新数组的长度。
js
const arr = [1, 2];
arr.push(3); // arr 变为 [1, 2, 3]
  • pop()
    删除数组的最后一个元素,并返回该元素。
js
const last = arr.pop(); // 返回 3,arr 变为 [1, 2]
  • shift()
    删除数组的第一个元素,并返回该元素。
js
const first = arr.shift(); // 返回 1,arr 变为 [2]
  • unshift(...items)
    在数组的开头添加一个或多个元素,并返回新数组的长度。
js
arr.unshift(0); // arr 变为 [0, 2]
  • splice(start, deleteCount, ...items)
    从指定位置开始删除若干元素,同时可插入新元素,直接修改原数组。
js
const removed = arr.splice(1, 1, 3, 4); // 删除 arr[1],插入 3 和 4
// 例如 [0, 2] 变为 [0, 3, 4]
  • sort(compareFunction)
    对数组元素进行排序,默认按照 Unicode 码点进行排序。
js
const sorted = [3, 1, 2].sort((a, b) => a - b); // [1, 2, 3]
  • reverse()
    将数组中元素的顺序颠倒,直接修改原数组。
js
const reversed = [1, 2, 3].reverse(); // [3, 2, 1]
  • fill(value, start, end)
    使用一个固定值填充数组的指定部分,直接修改原数组。
js
const arr = [1, 2, 3, 4];
arr.fill(0, 1, 3); // arr 变为 [1, 0, 0, 4]
  • copyWithin(target, start, end)
    将数组中指定位置的元素复制到目标位置,直接修改原数组。
js
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3); // 从索引 3 开始复制到索引 0,结果为 [4, 5, 3, 4, 5]

四、数组创建与转换 ​

  • concat(...items)
    合并两个或多个数组或值,返回一个新数组,不修改原数组。
js
const newArr = [1, 2].concat([3, 4], 5); // [1, 2, 3, 4, 5]
  • join(separator)
    将数组所有元素连接成一个字符串,可指定分隔符。
js
const str = [1, 2, 3].join('-'); // "1-2-3"
  • slice(start, end)
    返回数组的一个浅拷贝,从 start 到 end(不包含)位置,不修改原数组。
js
const subArr = [1, 2, 3, 4].slice(1, 3); // [2, 3]
  • toString()
    将数组转换为字符串,元素间以逗号分隔。
js
const str = [1, 2, 3].toString(); // "1,2,3"
  • valueOf()
    返回数组对象本身,通常与 toString() 一起使用以便隐式转换。

  • Array.from(arrayLike, mapFn, thisArg)
    从类数组或可迭代对象创建一个新的数组实例。

js
const arr = Array.from('hello'); // ["h", "e", "l", "l", "o"]
  • Array.of(...elements)
    根据一组参数创建一个新数组,不受传入数字的影响(与 new Array(7) 不同)。
js
const arr = Array.of(7); // [7]

五、其他方法 ​

  • entries()、keys()、values()
    返回一个遍历器对象,用于遍历数组的 [index, value] 键值对、键或值。
js
for (const [index, value] of [1, 2, 3].entries()) {
  console.log(index, value);
}
  • Symbol.iterator
    数组实现了迭代器协议,可以直接用于 for...of 循环遍历。

10. Unicode、UTF-8、UTF-16、UTF-32的区别? ​

1. Unicode ​

  • 概念:
    Unicode 是一个字符集标准,旨在为世界上所有书写系统中的每个字符分配一个唯一的“码点”(code point)。

    • 例如,字母 A 对应的码点为 U+0041,汉字 中 对应 U+4E2D。
  • 特点:

    • 定义了字符和符号的集合以及每个字符的唯一编号。
    • 并不规定如何在内存中存储这些码点,也不涉及具体的字节序列表示。
    • 仅仅是一种抽象的字符集合和编码标准。

2. UTF-8 ​

  • 概念:
    UTF-8 是一种针对 Unicode 码点的可变长度字符编码方式,将每个 Unicode 字符编码为 1 至 4 个字节的序列。

  • 特点:

    • 可变长度:
      • 对于 U+0000 至 U+007F(基本拉丁字符,包括 ASCII 字符)只用 1 个字节表示。
      • 对于其他字符可能使用 2、3 或 4 个字节。
    • 向后兼容 ASCII:
      • 前 128 个 Unicode 字符与 ASCII 编码完全一致。
    • 节省空间:
      • 对于英文、数字等常见字符,UTF-8 编码非常紧凑;但对于某些亚洲文字则可能占用更多字节。
    • 广泛应用:
      • 是 Web、文件存储、网络传输等场景中最常用的编码格式。

3. UTF-16 ​

  • 概念:
    UTF-16 也是一种针对 Unicode 码点的编码方式,使用 16 位(2 字节)为基本单元,但采用可变长度编码表示全部字符。

  • 特点:

    • 基本多文种平面(BMP):
      • 对于大多数常用字符(码点在 U+0000 至 U+FFFF 之间),UTF-16 使用 2 个字节表示。
    • 代理对(Surrogate Pairs):
      • 对于超出 BMP 范围(码点大于 U+FFFF)的字符,UTF-16 使用一对 16 位的值(共 4 字节)来表示。
    • 字节序问题:
      • 由于每个单位是 2 字节,可能存在大端(Big Endian)和小端(Little Endian)之分,通常通过 BOM(字节顺序标记)来指明。
    • 应用:
      • 在 Windows 内部、JavaScript(早期部分场景)和 Java 中都有应用。

4. UTF-32 ​

  • 概念:
    UTF-32 是一种固定长度的编码方式,每个 Unicode 码点都用固定 4 个字节表示。

  • 特点:

    • 固定长度:
      • 无论哪个字符,始终使用 4 个字节,无需代理对或额外标记。
    • 简单直接:
      • 解码和编码都非常简单,因为每个字符的边界都固定。
    • 空间占用大:
      • 对于存储和传输来说,UTF-32 会浪费较多空间,特别是对于主要使用低码点字符的文本。
    • 使用场景较少:
      • 主要用于内存中需要直接索引字符的应用或内部处理,不常用于文件存储或网络传输。

总结 ​

  • Unicode:
    • 定义了字符和对应码点的标准,但并不涉及实际存储格式。
  • UTF-8:
    • 可变长度编码,1~4 字节表示一个字符,兼容 ASCII,广泛用于互联网和文件存储。
  • UTF-16:
    • 主要用 2 字节表示常用字符,对于超出范围的字符用 4 字节表示,涉及字节序问题,应用于一些系统和编程语言中。
  • UTF-32:
    • 固定使用 4 字节表示每个字符,解码简单但空间浪费大,使用场景较窄。

11. 常见的位运算符有哪些?其计算规则是什么? ​

现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。

常见的位运算有以下几种:

运算符描述运算规则
&与两个位都为1时,结果才为1
|或两个位都为0时,结果才为0
^异或两个位相同为0,相异为1
~取反0变1,1变0
<<左移各二进制位全部左移若干位,高位丢弃,低位补0
>>右移各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃
`

12. 为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组? ​

关于为什么 arguments 是类数组而不是数组: ​

  1. 历史原因: arguments 对象是在 JavaScript 语言早期就被引入的。那时候,JavaScript 还没有真正的数组对象,所以 arguments 被设计成了一个类数组对象。
  2. 性能考虑: 将 arguments 实现为真正的数组可能会带来一些性能开销。类数组对象可以更高效地实现某些操作。 arguments 是一个对象,它的属性是从 0 开始依次递增的数字,还有 callee 和 length 等属性,与数组相似;但是它却没有数组常见的方法属性,如 forEach, reduce 等,所以叫它们类数组。 arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。

要遍历类数组,有三个方法: ​

(1)将数组的方法应用到类数组上,这时候就可以使用call和apply方法,如:

js
function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}

(2)使用Array.from方法将类数组转化成数组:‌

js
function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}

(3)使用展开运算符将类数组转化成数组

js
function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}

13. 什么是 DOM 和 BOM? ​

DOM( 全拼是:Document Object Model)文档对象模型BOM(全拼是:Browser Object Model)浏览器对象模型。

DOM ​

下面具体看一下: DOM 是一个编程接口,它将 HTML 或 XML 文档表示为树结构。 简单来说,它就是把网页内容转换成 JavaScript 可以操作的对象。

  1. DOM 的主要特点:
    • 它将文档解析为一个由节点和对象组成的结构集合。
    • 它定义了文档的结构,以及如何访问和操作文档。
  2. DOM 的作用:
    • 允许程序和脚本动态地访问和更新文档的内容、结构和样式。
    • 提供了一种标准化的方式来操作网页内容。
  3. 常见的 DOM 操作:
    • 获取元素:document.getElementById()、document.querySelector() 等
    • 修改元素内容:element.innerHTML、element.textContent 等
    • 修改元素样式:element.style.property
    • 添加或删除元素:document.createElement()、element.appendChild() 等

BOM ​

BOM 提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。 简单来说,它就是浏览器提供的用于操作浏览器的接口。

  1. BOM 的主要组成部分:
    • window 对象:JavaScript 层级中的顶层对象,表示浏览器窗口。
    • navigator 对象:包含有关浏览器的信息。
    • location 对象:包含当前 URL 的信息。
    • history 对象:包含浏览器的历史记录。
    • screen 对象:包含有关用户屏幕的信息。
  2. BOM 的作用:
    • 提供了与浏览器交互的方法和接口。
    • 允许 JavaScript 与浏览器对话。
  3. 常见的 BOM 操作:
    • 打开新窗口:window.open()
    • 移动、调整窗口大小:window.moveTo()、window.resizeTo()
    • 导航到其他 URL:window.location.href = "[http://example.com](http://example.com/)"
    • 获取浏览器信息:navigator.userAgent
    • 操作浏览历史:history.back()、history.forward()

DOM 和 BOM 的主要区别:

  1. DOM 主要处理网页内容,而 BOM 处理浏览器窗口和功能。
  2. DOM 是 W3C 标准,而 BOM 没有相关标准。
  3. DOM 可以在任何支持 XML 的环境中使用,而 BOM 只能在浏览器环境中使用。

14.常见的 DOM 操作有哪些? ​

从事前端开发,我们常常需要与 DOM(Document Object Model)进行互动。常见的 DOM 操作有如下几种:

  1. 获取元素:使用 document.getElementById(), document.getElementsByClassName(), 以及 document.querySelector() 等方法。
  2. 创建元素:使用 document.createElement() 方法。
  3. 修改元素属性:用 element.setAttribute() 和 element.removeAttribute()。
  4. 修改元素内容:用 element.innerHTML 或 element.textContent 修改节点内的文本或 HTML。
  5. 添加/删除元素:用 parentNode.appendChild(), parentNode.removeChild()。
  6. 事件监听:使用 element.addEventListener() 给元素绑定事件。

扩展知识 ​

1)获取元素:除了上述提到的方法,还可以使用 document.getElementsByTagName() 来获取所有特定标签的元素集合。要注意的是,getElementById() 返回单个元素,而其他几种方法返回的是元素集合。

2)创建元素:创建的元素只是存在于内存中,还需要用 appendChild() 或 insertBefore() 之类的方法把新创建的元素插入到 DOM 树中。

3)修改元素属性:setAttribute 是一个通用设置属性的方法,但针对某些特定属性(例如,class 或 id),也可以直接通过点操作符来设置,像 element.className = 'new-class'。

4)修改元素内容:innerHTML 和 textContent 的区别在于,前者可以解析 HTML 标签,而后者则只会识别并处理纯文本,这是需要注意的细节。

5)添加/删除元素:这些方法实际上是操作节点,即包含元素、文本节点和注释节点等。除了 appendChild 和 removeChild,还有 insertBefore 和 replaceChild 可以使用。

6)事件监听:addEventListener() 方法是现代浏览器推荐的绑定事件的方式,相对于老旧的 onclick 这种方式更加灵活。特别是,它可以绑定多个事件到同一个元素上,而 onclick 每次会覆盖之前的事件。

一些进阶操作: ​

1)克隆节点:使用 element.cloneNode() 方法可以复制节点。 2)查找父节点/子节点:通过 element.parentNode, element.childNodes, element.firstChild, element.lastChild 等可以遍历不同层次的节点。 3)获取或设置样式:可以通过 element.style 对象来操作内联样式,或者通过 window.getComputedStyle() 获取元素的计算样式。

15.escape、encodeURI、encodeURIComponent 的区别是什么? ​

escape、encodeURI 和 encodeURIComponent 这三个函数都是处理 URL 编码的问题的,下面我们具体看一下区别

escape () 废弃 ​

首先,escape() 函数已经被废弃了,我们不应该再在新的代码中使用它。但是我们可能在维护旧代码时遇到它。所以需要了解 escape() 主要用于对字符串进行编码。它会将所有非字母数字字符转换为它们的十六进制转义序列,除了 @*_+-./ 这些字符。 例如:

js
console.log(escape("Hello World! @*_+-./"));
// 输出: Hello%20World%21%20@*_+-./

encodeURI(URI) ​

encodeURI()用于编码完整的 URI。它会编码所有对 URI 有特殊含义的字符,除了 ,/?:@&=+$# 这些在 URI 中有特定含义的字符。

js
console.log(encodeURI("https://example.com/path?name=张三&age=18"));
// 输出: https://example.com/path?name=%E5%BC%A0%E4%B8%89&age=18

encodeURIComponent () ​

encodeURIComponent() 用于编码 URI 的组成部分。它会编码所有对 URI 有特殊含义的字符,包括 ,/?:@&=+$# 这些字符。

js
console.log(encodeURIComponent("https://example.com/path?name=张三&age=18"));
// 输出: https%3A%2F%2Fexample.com%2Fpath%3Fname%3D%E5%BC%A0%E4%B8%89%26age%3D18

主要区别:

  1. escape() 主要用于字符串编码,而 encodeURI() 和 encodeURIComponent() 用于 URL 编码。
  2. encodeURI() 不会编码 URI 中的功能字符(如 /:?&=),而 encodeURIComponent() 会编码这些字符。
  3. encodeURIComponent() 编码的字符最多,通常用于编码 URL 的参数。
  4. escape() 不能正确处理非 ASCII 字符,而 encodeURI() 和 encodeURIComponent() 可以。

拓展 ​

在实际开发中,我通常会这样使用它们:

  • 当我需要编码整个 URL 时,我会使用 encodeURI()。
  • 当我需要编码 URL 参数时,我会使用 encodeURIComponent()。
  • 我会避免使用 escape(),因为它已经被废弃了。

16.什么是 AJAX?如何实现一个 AJAX 请求? ​

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建AJAX请求的步骤:

  1. 创建一个 XMLHttpRequest 对象。
  2. 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  3. 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5个状态,当它的状态变化时会触发 onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  4. 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
js
function makeAjaxRequest(url, method, data, callback) {
  //1.创建XMLHttpRequest对象
  const xhr = new XMLHttpRequest();
  
  //2.添加信息和监听函数
  xhr.onreadystatechange = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        callback(null, xhr.responseText);
      } else {
        callback(new Error('请求失败: ' + xhr.status));
      }
    }
  };

  //3.使用open方法创建http请求
  xhr.open(method, url, true);

  //4.发起请求
  if (method.toUpperCase() === 'POST') {
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify(data));
  } else {
    xhr.send();
  }
}

// 使用示例
makeAjaxRequest('https://api.example.com/data', 'GET', null, function(error, response) {
  if (error) {
    console.error('出错了:', error);
  } else {
    console.log('收到响应:', response);
  }
});

使用Fetch API实现 AJAX 请求(更现代的方法):

js
function makeAjaxRequest(url, method, data) {
  const options = {
    method: method,
    headers: {
      'Content-Type': 'application/json'
    }
  };

  if (method.toUpperCase() !== 'GET' && data) {
    options.body = JSON.stringify(data);
  }

  return fetch(url, options)
    .then(response => {
      if (!response.ok) {
        throw new Error('请求失败: ' + response.status);
      }
      return response.json();
    });
}

// 使用示例
makeAjaxRequest('https://api.example.com/data', 'GET')
  .then(data => console.log('收到数据:', data))
  .catch(error => console.error('出错了:', error));

17.use strict 是什么意思?使用它有什么区别? ​

"use strict" 是 JavaScript 中的一个指令,用于启用严格模式。让我详细解释一下它的作用和使用它带来的区别:

"use strict" 是什么 ​

"use strict" 是一个字符串声明,放在脚本或函数的开头,用来指定代码应该在严格模式下执行。

js
"use strict";
// 后面的代码会在严格模式下执行

严格模式的主要作用 ​

严格模式主要是为了捕获一些常见的编程错误,并防止使用一些可能在未来版本中定义的语法。

使用 "use strict" 的主要区别 ​

1)变量必须声明后再使用

js
"use strict";
x = 3.14; // 错误:x 未定义

2)禁止使用 with 语句

js
"use strict";
with (Math){x = cos(2)}; // 语法错误

3) 创建 eval 作用域 在严格模式下,eval() 中的代码会在自己的作用域中执行,而不是在当前作用域中。

4)禁止 this 关键字指向全局对象

js
"use strict";
function f(){
    return this;
}
f(); // 返回 undefined,而不是全局对象

5)函数参数不能有重名

js
"use strict";
function sum(a, a, c){ // 语法错误
    return a + a + c;
}

6)禁止八进制数字语法

js
"use strict";
var sum = 015 + 197 + 142; // 语法错误

7)禁止对只读属性赋值

js
"use strict";
var obj = {};
Object.defineProperty(obj, "x", { value: 0, writable: false });
obj.x = 3.14; // 抛出错误

8)禁止删除不可删除的属性

js
"use strict";
delete Object.prototype; // 抛出错误

使用 "use strict" 的好处 ​

  • 消除 JavaScript 语法的一些不合理、不严谨之处,减少一些怪异行为。
  • 消除代码运行的一些不安全之处,保证代码运行的安全。
  • 提高编译器效率,增加运行速度。
  • 为未来新版本的 JavaScript 做好铺垫。

拓展 ​

在类和模块中,严格模式会自动启用。使用 "use strict" 可以帮助我们写出更加规范、安全的代码,并且可以在开发阶段就发现一些潜在的问题。在现代 JavaScript 开发中,特别是使用 ES6 模块或类时,严格模式已经成为默认行为。

18.JavaScript 如何判断一个对象是否属于某个类? ​

在 JavaScript 中,有五种方法可以判断一个对象是否属于某个类。

instanceof 运算符 ​

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

js
class Animal {}
class Dog extends Animal {}

const dog = new Dog();

console.log(dog instanceof Dog);     // true
console.log(dog instanceof Animal);  // true
console.log(dog instanceof Object);  // true

constructor 属性 ​

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

js
console.log(dog.constructor === Dog);  // true

Object.prototype.isPrototypeOf() 方法 ​

这个方法用于测试一个对象是否存在于另一个对象的原型链上。

js
console.log(Dog.prototype.isPrototypeOf(dog));    // true
console.log(Animal.prototype.isPrototypeOf(dog)); // true

Object.getPrototypeOf() 方法 ​

这个方法返回指定对象的原型。

js
console.log(Object.getPrototypeOf(dog) === Dog.prototype);  // true

自定义类型检查函数 ​

有时候,我们可能需要更精确的类型检查。这时可以自定义一个函数:

js
function isDog(obj) {
  return obj && typeof obj === 'object' && obj.constructor === Dog;
}

console.log(isDog(dog));  // true

Symbol.hasInstance ​

ES6 引入了 Symbol.hasInstance 方法,允许类自定义 instanceof 的行为。

js
class MyClass {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof MyClass);  // true

需要注意的是,这些方法各有优缺点:

  • instanceof 和 isPrototypeOf() 可以检查整个原型链,但可能会受到原型链被修改的影响。
  • constructor 属性可以被重写,因此不总是可靠。
  • Object.getPrototypeOf() 更可靠,但只检查直接原型。
  • 自定义函数可以提供最精确的检查,但需要为每个类型单独实现。

在实际开发中,选择哪种方法取决于具体的需求和上下文。通常,instanceof 是最常用的方法,因为它简单直观,并且能够处理继承关系。

19.ajax、axios、fetch 的区别是什么? ​

Ajax、axios 和 fetch 是用于进行 HTTP 请求的三种常见方式。它们各有优缺点和适用场景

Ajax (Asynchronous JavaScript and XML) ​

Ajax 不是一种单一的技术,而是一种使用现有技术集合的方法。它最常见的实现是使用 XMLHttpRequest (XHR) 对象。 特点:

  • 是最早的异步请求解决方案
  • 可以与服务器交换数据并更新部分网页内容,而无需重新加载整个页面
  • 使用回调函数处理响应
  • 不支持 Promise

示例:

js
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

Fetch ​

Fetch 是较新的 API,旨在替代 XMLHttpRequest。它是基于 Promise 的。 特点:

  • 语法更简洁,使用起来更加直观
  • 基于 Promise,支持 async/await
  • 原生支持,不需要额外的库
  • 不会自动拒绝 HTTP 错误状态

示例:

js
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

Axios ​

Axios 是一个基于 Promise 的 HTTP 客户端,可以用于浏览器和 Node.js。 特点:

  • 支持浏览器和 Node.js
  • 自动转换 JSON 数据
  • 可以拦截请求和响应
  • 可以取消请求
  • 客户端支持防御 XSRF

示例:

js
axios.get('https://api.example.com/data')
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

主要区别: ​

语法和使用方式: ​

  • Ajax 使用回调函数
  • Fetch 使用 Promise
  • Axios 也使用 Promise,但提供了更简洁的 API

错误处理: ​

  • Fetch 不会自动抛出错误,即使响应状态为 404 或 500
  • Axios 会在响应状态不在 2xx 范围内时自动抛出错误

请求取消: ​

  • Fetch 需要使用 AbortController
  • Axios 提供了取消请求的方法

浏览器支持: ​

  • Ajax (XMLHttpRequest) 支持所有现代浏览器
  • Fetch 不支持一些旧版浏览器(如 IE11)
  • Axios 通过适当的 polyfill 可以支持更广泛的浏览器

功能丰富度: ​

  • Axios 提供了更多的功能,如拦截器、自动转换 JSON 等
  • 对于简单的请求,Fetch 可能就足够了。如果需要更多功能和更好的错误处理,Axios 是一个很好的选择。

20.JavaScript 数组的遍历方法有哪些? ​

for 循环 ​

js
const arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

for循环是最传统的方法,我们可以通过索引来访问数组的每个元素,而且可以提前终止循环

forEach 循环 ​

js
arr.forEach((item, index) => {
  console.log(item, index);
});

这是一个更现代、更简洁的方法。它为每个数组元素执行一次回调函数 forEach 方法没有返回值,它只是对数组的每个元素执行一次对应的函数。

ES6 引入的新语法 for...of ​

js
arr.forEach((item, index) => {
  console.log(item, index);
});

for...of 遍历具有 Iterator 迭代器的对象的属性,可以直接遍历数组的值,不能遍历普通的 obj 对象,将异步循环变成同步循环

map ​

map 方法会返回一个新数组,这个新数组由原数组中的每个元素经过指定函数处理后的值组成。

js
const newArr = arr.map(item => item * 2);

这个方法会创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后的返回值,可链式调用

filter ​

js
const evenNumbers = arr.filter(item => item % 2 === 0);

过滤数组,返回包含符合条件的元素的数组,可链式调用

不常用的方法 ​

reduce 和 reduceRight ​

js
const sum = arr.reduce((acc, cur) => acc + cur, 0);
const sum = arr.reduceRight((acc, cur) => acc + cur, 0);

some 和 every ​

js
const hasEven = arr.some(item => item % 2 === 0);
const allPositive = arr.every(item => item > 0);
  1. some() 测试数组中是不是至少有一个元素通过了指定函数的测试
  2. every() 测试一个数组内的所有元素是否都能通过某个指定函数的测试

21.mouseover 和 mouseenter 事件的区别是什么? ​

>[!tip] 一句话概括: mouseover 会冒泡,mouseenter 不会。

下面具体看一下 mouseover 和 mouseenter 都是鼠标事件,它们的区别在于:

触发条件不同 ​

  • mouseover 在鼠标指针进入元素或元素的子元素时会触发,即当鼠标指针从元素外部移入元素边界时触发;
  • mouseenter 只在鼠标指针进入元素时触发,不会在进入元素的子元素时触发。

事件冒泡不同 ​

  • mouseover 会冒泡,即当鼠标指针从子元素移出到父元素时也会触发;
  • mouseenter 不会冒泡,即只在进入元素时触发,不影响其它元素的事件。

使用场景 ​

  • 如果希望在鼠标进入元素及其子元素时都触发事件,可以使用 mouseover。
  • 如果只想在鼠标进入元素本身时触发事件,可以使用 mouseenter。

最后,我们看一下示例代码:

js
const parent = document.getElementById('parent');
const child = document.getElementById('child');

parent.addEventListener('mouseover', () => {
  console.log('Parent mouseover');
});

parent.addEventListener('mouseenter', () => {
  console.log('Parent mouseenter');
});

child.addEventListener('mouseover', () => {
  console.log('Child mouseover');
});

child.addEventListener('mouseenter', () => {
  console.log('Child mouseenter');
});

当鼠标从父元素移动到子元素时:

  • mouseover 会在子元素和父元素上都触发
  • mouseenter 只会在最初进入父元素时触发一次

22.JavaScript 中 substring 和 substr 函数的区别是什么? ​

参数含义 -最主要的区别 ​

substring(startIndex, endIndex) 方法:

  • startIndex:开始提取字符的位置。
  • endIndex:结束提取字符的位置(不包括该位置的字符)。

substr(startIndex, length) 方法:

  • startIndex:开始提取字符的位置。
  • length:要提取的字符数。
js
let str = "Hello, World!";

console.log(str.substring(0, 5)); // 输出: "Hello"
console.log(str.substr(0, 5));    // 输出: "Hello"

console.log(str.substring(7, 12)); // 输出: "World"
console.log(str.substr(7, 5));     // 输出: "World"

负值参数的处理和参数顺序 ​

1)负值参数的处理 substring 方法会将负值参数都转换为 0。 substr 方法允许第一个参数为负值,它会从字符串的末尾开始计数

js
let str = "Hello, World!";

console.log(str.substring(-3)); // 输出: "Hello, World!"
console.log(str.substr(-3));    // 输出: "ld!"

2)参数顺序 substring 方法会自动调整参数的顺序,使得 startIndex 总是小于等于 endIndex。而 substr 不会这样做。

js
let str = "Hello, World!";

console.log(str.substring(5, 2)); // 输出: "llo"
console.log(str.substr(5, 2));    // 输出: ", "

浏览器兼容性和未来发展 ​

substring 方法在所有现代浏览器中都得到了很好的支持。而 substr 方法虽然目前仍被广泛支持,但已经被 MDN 标记为废弃(deprecated)。这意味着在未来的 JavaScript 版本中,substr 可能会被移除。

拓展 ​

1)可以更现代的 slice 方法来平替 substring 方法

23.JavaScript 中如何合并对象? ​

可以使用 Object.assign() 或扩展运算符 ... 来浅度合并 js 对象。 如果需要深度合并,可以使用 递归来实现

使用展开运算符(Spread Operator) ​

js
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = { ...obj1, ...obj2 };

console.log(mergedObj); // { a: 1, b: 3, c: 4 }

这种方法进行的是浅拷贝,如果有重复的属性,后面对象的属性会覆盖前面的

使用Object.assign() ​

js
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = Object.assign({}, obj1, obj2);

console.log(mergedObj); // { a: 1, b: 3, c: 4 }

这个方法也是浅拷贝,同样后面的对象属性会覆盖前面的

使用递归进行深度合并 ​

js
function deepMerge(target, source) {
  for (let key in source) {
    if (source[key] instanceof Object && target[key] instanceof Object) {
      target[key] = deepMerge(Object.assign({}, target[key]), source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const obj1 = { a: { x: 1, y: 2 }, b: 2 };
const obj2 = { a: { y: 3, z: 4 }, c: 3 };
const mergedObj = deepMerge({}, obj1, obj2);

console.log(mergedObj); // { a: { x: 1, y: 3, z: 4 }, b: 2, c: 3 }

这种方法可以处理嵌套对象,但要注意可能会有性能问题,特别是对于大型复杂对象

拓展 ​

1)在实际开发中,会使用像 Lodash 这样的工具库,它提供了更强大和灵活的对象合并功能:

js
const _ = require('lodash');

const obj1 = { a: { x: 1, y: 2 }, b: 2 };
const obj2 = { a: { y: 3, z: 4 }, c: 3 };
const mergedObj = _.merge({}, obj1, obj2);

console.log(mergedObj); // { a: { x: 1, y: 3, z: 4 }, b: 2, c: 3 }

Lodash 的 merge 方法可以进行深度合并,而且处理了很多边界情况

24.JavaScript 如何判断一个对象是不是空对象? ​

可以使用以下几种判断对象是否为空的方法。

使用 Object.keys() ​

但是在使用这个方法之前,我们需要对传入的对象进行 判空,

js
function isEmptyObject(obj) {
  return obj && typeof obj === 'object' && Object.keys(obj).length === 0;
}

let emptyObj = {};
console.log(isEmptyObject(emptyObj)); // true

let nonEmptyObj = { key: 'value' };
console.log(isEmptyObject(nonEmptyObj)); // false

Object.keys() 返回一个包含对象所有可枚举属性的数组。如果这个数组的长度为 0,那么对象就是空的

使用 for...in 循环 ​

如果你需要兼容较老的浏览器,可以使用 for...in 循环:

js
function isEmptyObject(obj) {
  for (let prop in obj) {
    if (Object.hasOwn(obj, prop)) {
      return false;
    }
  }
  return true;
}

这个方法会遍历对象的所有可枚举属性。如果找到任何一个属性,就说明对象不为空

使用 JSON.stringify() ​

js
function isEmptyObject(obj) {
  return JSON.stringify(obj) === '{}';
}

这个方法将对象转换为 JSON 字符串,然后比较是否等于 '{}'。不过要注意,这个方法可能会有一些边界情况,比如对象中包含 undefined、函数等值时可能会出现问题

使用 Object.getOwnPropertyNames() ​

js
function isEmptyObject(obj) {
  return Object.getOwnPropertyNames(obj).length === 0;
}

这个方法比 Object.keys() 更严格,因为它会检查所有属性,而不仅仅是可枚举属性 注意:这些方法都不能检测对象的原型链。如果你需要考虑原型链上的属性,可能需要自己实现一个递归函数。

25.JavaScript 的 splice 和 slice 函数会改变原数组吗? ​

splice() 会改变原数组,可以用来添加或删除元素。 slice() 不改变原数组,而是返回一个新的数组,包含原数组的一部分浅拷贝。

splice 方法(剪接) ​

splice() 方法可以在数组中添加、删除或替换元素,并返回被删除的元素

js
const arr = [1, 2, 3, 4, 5];
const removed = arr.splice(2, 2); // 从索引2开始删除2个元素
console.log(arr); // 输出: [1, 2, 5]
console.log(removed); // 输出: [3, 4]
const arr1 = [1, 2, 3, 4, 5];
arr1.splice(2, 2, 'a', 'b');
console.log(arr1); // 输出: [1, 2, 'a', 'b', 5]
const arr2 = [1, 2, 3, 4, 5];
arr2.splice(2, 0, 'a', 'b'); // 插入元素
console.log(arr2); // 输出: [1, 2, 'a', 'b', 3, 4, 5]

slice 方法 (切片) ​

slice() 方法是从原数组中返回指定开始和结束位置的元素组成的新数组,包含原数组的一部分浅拷贝。

js
const arr = [1, 2, 3, 4, 5];
const sliced = arr.slice(1, 3); // 从索引1开始到索引3结束(不包括索引3)
console.log(arr); // 输出: [1, 2, 3, 4, 5]
console.log(sliced); // 输出: [2, 3]

26.JavaScript 中怎么删除数组最后一个元素? ​

在 JavaScript 中,我在 JavaScript 中,我们有几种方法可以实现这个功能,

1.使用 pop() 方法 ​

最直接、最常用的方法就是使用数组的 pop() 方法。这个方法不仅会删除数组的最后一个元素,还会返回被删除的元素。

js
let fruits = ['apple', 'banana', 'orange'];
let lastFruit = fruits.pop();
console.log(lastFruit); // 输出: 'orange'
console.log(fruits); // 输出: ['apple', 'banana']

pop() 方法的优点是简单直观,而且它会直接修改原数组,日常工作我最常用它。 pop() 方法处理空数组时要小心,因为它返回 undefined

1.使用 splice() ​

方法 splice() 方法也可以用来删除数组的最后一个元素。它的用法稍微复杂一些,但更加灵活:

js
let fruits = ['apple', 'banana', 'orange'];
let removedFruits = fruits.splice(-1, 1);
console.log(removedFruits); // 输出: ['orange']
console.log(fruits); // 输出: ['apple', 'banana']

splice() 方法的第一个参数 -1 表示从数组末尾开始,第二个参数 1 表示删除一个元素。日常开发,如果需要更加灵活的数组操作,我会使用它

3.使用 slice() 方法 ​

如果不想修改原数组,可以使用 slice() 方法创建一个新数组:

js
let fruits = ['apple', 'banana', 'orange'];
let newFruits = fruits.slice(0, -1);
console.log(newFruits); // 输出: ['apple', 'banana']
console.log(fruits); // 输出: ['apple', 'banana', 'orange']

这在日常开发中,如果不能修改原数组的情况下,非常好用

4.使用数组长度 ​

我们还可以通过修改数组的 length 属性来删除最后一个元素,这种简单高效,但是代码阅读性稍微差了点:

js
let fruits = ['apple', 'banana', 'orange'];
fruits.length = fruits.length - 1;
console.log(fruits); // 输出: ['apple', 'banana']

拓展 ​

  1. pop() 方法的时间复杂度为O(1),即删除操作的性能是非常高效的。
  2. slice() 方法返回新数组,适合需要保留原数组的场景,但会有额外的内存开销。
  3. 实际面试,可能会顺便问一下,如如何删除数组第一个元素,如何对数据进行深拷贝,什么是深拷贝,浅拷贝等等

27.如何判断网页元素是否到达可视区域? ​

判断网页元素是否到达可视区域是一个常见的需求,尤其在实现懒加载、无限滚动或者触发动画等场景中非常有用。有以下几种几种:

1.使用 Intersection Observer API ​

js
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('元素进入可视区域');
      // 在这里执行你的逻辑
    }
  });
});

const target = document.querySelector('#your-element');
observer.observe(target);

这种方法的优点是性能好,不会阻塞主线程,而且使用起来相对简单

2.使用 getBoundingClientRect() 方法 ​

如果需要更精确的控制或者需要兼容旧版浏览器,我们可以使用 getBoundingClientRect() 方法。这个方法返回元素的大小及其相对于视口的位置。

js
function isElementInViewport(el) {
  const rect = el.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

// 使用
const element = document.querySelector('#your-element');
if (isElementInViewport(element)) {
  console.log('元素在可视区域内');
}

这种方法的优点是可以精确控制元素的可见范围,但需要注意的是,如果在滚动事件中频繁调用,可能会影响性能

3.使用 Element.checkVisibility() 方法 ​

这是一个相对较新的 API,可以直接检查元素是否可见:

js
const element = document.querySelector('#your-element');
if (element.checkVisibility()) {
  console.log('元素可见');
}

这个方法还可以接受一个选项对象,用于进行更细粒度的检查

js
element.checkVisibility({
  checkOpacity: true,  // 检查 opacity 是否为 0
  checkVisibilityCSS: true  // 检查 visibility CSS 属性
});

虽然这个方法使用起来很简单,但需要注意浏览器兼容性问题 实际开发中,如果是现代浏览器环境,我会优先选择 Intersection Observer API,因为它既高效又易用。如果需要更精确的控制或者需要兼容旧版浏览器,我会使用 getBoundingClientRect() 方法

拓展 ​

在使用 getBoundingClientRect() 方法时,可以使用节流(throttle)或防抖(debounce)技术来限制函数的调用频率,以避免过度消耗性能。

28.JavaScript 操作数组元素的方法有哪些? ​

JavaScript 提供了丰富的方法来操作数组元素,可以按照添加、修改、删除、查找、遍历、筛选和变换数组元素来看数组的方法

添加 ​

push(): 在数组末尾添加一个或多个元素。 unshift(): 在数组开头添加一个或多个元素。 splice(): 在指定位置添加元素。

js
const arr = [1, 2, 3];
arr.push(4); // [1, 2, 3, 4]
arr.unshift(0); // [0, 1, 2, 3, 4]
arr.splice(2, 0, 1.5); // [0, 1, 1.5, 2, 3, 4]

修改 ​

fill(): 用一个固定值填充数组中从起始索引到终止索引的全部元素。
copyWithin(): 将数组中的一部分复制到同一数组中的另一个位置,并返回它,而不改变数组的长度。

js
let arr = [1, 2, 3, 4, 5];
console.log(JSON.parse(JSON.stringify(arr)).fill(0, 2, 4)); // 输出: [1, 2, 0, 0, 5]
arr.copyWithin(0, 3, 5);
console.log(arr); // 输出: [4, 5, 3, 4, 5]

删除 ​

pop(): 删除数组最后一个元素。 shift(): 删除数组第一个元素。 splice(): 删除指定位置的元素。

js
const arr = [1, 2, 3, 4];
arr.pop(); // [1, 2, 3]
arr.shift(); // [2, 3]
arr.splice(1, 1); // [2] 删除索引1处的一个元素

查找 ​

indexOf(): 返回指定元素在数组中的第一个索引,如果不存在则返回 -1。 lastIndexOf(): 返回指定元素在数组中的最后一个索引,如果不存在则返回 -1。 includes(): 判断数组是否包含指定的元素,返回布尔值。 find(): 返回符合条件的第一个元素。 findIndex(): 返回符合条件的第一个元素的索引。

js
const arr = [1, 2, 3, 4];
arr.indexOf(3); // 2
arr.lastIndexOf(3); // 2
arr.includes(3); // true
arr.find(num => num > 2); // 3
arr.findIndex(num => num > 2); // 2

遍历 ​

forEach(): 对数组中的每个元素执行一次提供的函数。 map(): 创建一个新数组,其结果是对原数组中的每个元素执行一次提供的函数后的返回值。写 React 最常用的数组方法

js
const arr = [1, 2, 3];
arr.forEach(num => console.log(num)); // 1 2 3
const newArr = arr.map(num => num * 2); // [2, 4, 6]

筛选 ​

filter(): 创建一个新数组,其包含通过所提供函数实现的测试的所有元素。 every(): 测试数组的所有元素是否都通过了指定函数的测试。如果是,返回 true,否则返回 false。 some(): 测试数组中的某些元素是否至少有一个通过了指定函数的测试。如果是,返回 true,否则返回 false。

js
const arr = [1, 2, 3, 4];
const evenNumbers = arr.filter(num => num % 2 === 0); // [2, 4]
let arr1 = [1, 2, 3, 2, 5];
console.log(arr.every(element => element < 10)); // 输出: true
console.log(arr.some(element => element % 2 === 0)); // 输出: true

变换数组 ​

reduce(): 对数组中的每个元素执行一个 reducer 函数,将其结果汇总为单个值。 sort(): 对数组元素进行排序。 reverse(): 反转数组中元素的顺序。

js
const arr = [1, 2, 3, 4];
const sum = arr.reduce((acc, num) => acc + num, 0); // 10
arr.sort((a, b) => b - a); // [4, 3, 2, 1]
arr.reverse(); // [1, 2, 3, 4]

复制和合并 ​

slice(): 返回一个新数组,是原数组的浅拷贝。 concat(): 合并两个或多个数组,返回一个新数组。 spread operator (...): 扩展运算符,用于复制数组或合并数组。

js
const arr = [1, 2, 3];
const copy = arr.slice(); // [1, 2, 3]
const merged = arr.concat([4, 5]); // [1, 2, 3, 4, 5]
const spreadMerged = [...arr, 4, 5]; // [1, 2, 3, 4, 5]

转换数组 ​

toString(): 将数组转换为一个字符串,其中每个数组元素用逗号分隔。 join(): 将数组的所有元素连接成一个字符串,并可指定一个分隔符。 toLocaleString(): 将数组的所有元素转换为本地字符串表示。 Array.from(): 从类数组对象或可迭代对象创建一个新的数组实例。 valueOf(): 返回的是数组本身,

js
let arr = [1, 2, 3, 4, 5];
console.log(arr.toString()); // 输出: "1,2,3,4,5"
console.log(arr.join('-')); // 输出: "1-2-3-4-5"
let arr1 = [1, 'a', new Date()];
console.log(arr1.toLocaleString()); // 输出: "1,a,6/25/2024, 12:00:00 AM"(日期格式会根据本地设置有所不同)
let str = 'hello';
let arr = Array.from(str);
console.log(arr); // 输出: ["h", "e", "l", "l", "o"]
let colors = ["red", "blue", "green"];  
console.log(colors.valueOf())  // ["red", "blue", "green"]

拓展 ​

  1. 数组方法可以链式调用,这在对数组多个操作时候非常有用,结合使用多个数组方法可以实现复杂的数据处理逻辑,如先过滤再映射。
js
const arr = [1, 2, 3, 4, 5];
const result = arr.filter(num => num % 2 === 0).map(num => num * 2); // [4, 8]
  1. 数组快速去重实现:[...new Set([1,2,3,2])]

29.JavaScript 中 for...in 和 for...of 的区别是什么? ​

for...in 和 for...of 的区别主要有 2 点,最关键的区别在于它们遍历的对象和返回的值不同:

  1. for...in 循环 (遍历Key)
  • 用于遍历对象的可枚举属性,包括其原型链上的可枚举属性
  • 返回的是属性名(键名)
  • 可以遍历普通对象,也可以遍历数组(但不推荐)。
  • ES3 语法
  1. for...of 循环( 遍历Value)
  • 用于遍历可迭代对象(如数组、字符串、Map、Set等)。
  • 返回的是每次迭代的值。
  • 不能直接用于遍历普通对象。
  • ES6 语法

看一个示例代码:

js
// for...in 示例
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key); // 输出: "a", "b", "c"
}

// for...of 示例
const arr = [1, 2, 3];
for (const value of arr) {
  console.log(value); // 输出: 1, 2, 3
}

在使用这两种循环时,我还注意到一些其他的重要区别:

  1. 遍历顺序:for...in 不保证遍历顺序,而 for...of 会按照迭代器定义的顺序进行遍历。
  2. 性能:通常来说,for...of 的性能比 for...in 更好,特别是在遍历数组时。所以遍历优先选 for...of
  3. 继承属性:for...in 会遍历对象的原型链,而 for...of 不会。
  4. 使用场景:for...in 更适合用于遍历对象的属性,而 for...of 更适合用于遍历数组或其他可迭代对象的值。

拓展 ​

  1. for...in 可以用于遍历数组,但它可能会遍历到数组的非数字属性,导致意外的结果,例如:
js
// for...in 示例
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key); // 输出: "a", "b", "c"
}

// for...of 示例
const arr = [1, 2, 3];
for (const value of arr) {
  console.log(value); // 输出: 1, 2, 3
}
  1. 如果需要同时获取数组的索引和值,可以结合使用for...of 和 Array.entries():
js
const arr = ['a', 'b', 'c'];
for (const [index, value] of arr.entries()) {
  console.log(index, value); // 输出: 0 "a", 1 "b", 2 "c"
}

遍历对象也可以通过 Object.entries() + for ... of 实现

评论区

欢迎留言、补充或勘误。

xiaoba.blog