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 不可枚举。它不提供遍历方法(例如没有 forEachkeysvaluesentries 方法),也没有 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):可以是整数或浮点数,如 1233.14
    • 布尔值(Boolean):truefalse
    • 空值(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 标签属性:asyncdefer

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 上的方法,如 pushpopforEach 等。

常见的类数组对象示例

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

  • DOM 节点集合
    例如,document.querySelectorAll 返回的 NodeList 对象,以及 document.getElementsByTagNamedocument.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.fromArray.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)
    判断数组中是否存在指定元素,返回 truefalse

    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)
    返回数组的一个浅拷贝,从 startend(不包含)位置,不修改原数组。
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 开始依次递增的数字,还有calleelength等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。

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

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

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.innerHTMLelement.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.innerHTMLelement.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)修改元素内容innerHTMLtextContent 的区别在于,前者可以解析 HTML 标签,而后者则只会识别并处理纯文本,这是需要注意的细节。

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

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...ofArray.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