问题汇总

4 阅读7分钟

苦难的开始

javascript == 和 === 的区别

1== ‘1’ 是把‘1’做了隐饰转换为1, === 是值和类型都相同才会相等
隐饰转换都有哪些?

  1. 对象与原始类型:对象会通过 valueOf() 或 toString() 方法转换为原始值。

    • [] == '' → true (空数组转为空字符串)
    • [1] == '1' → true (数组转为字符串 '1')
  2. 字符串与数字:字符串会被转换为数字。

    • '5' == 5 → true (字符串 '5' 转为数字 5)
    • '' == 0 → true (空字符串转为数字 0)
  3. 布尔值与其他类型:布尔值会被转换为数字 (true → 1, false → 0)。

    • true == 1 → true
    • false == 0 → true
    • '0' == false → true (字符串 '0' 转为数字 0,false 也转为 0)
  4. null 与 undefined:它们之间互相比较为 true,但与任何其他值比较都为 false

    • null == undefined → true
    • null == 0 → false

空间复杂度的理解

O(1) - 常数空间

类似于数组已经固定了,只是在其内部进行进行位置调整,删除等操作。

O(n) - 线性空间

function duplicateArray(arr) {
2  let copy = []; // O(n) 空间
3  for (let i = 0; i < arr.length; i++) {
4    copy.push(arr[i]);
5  }
6  return copy;
7}

这个函数创建了一个和原数组一样大的新数组 copy,所以额外空间随 n 线性增长,空间复杂度为 O(n)

O(n²) - 二次空间

算法需要的额外空间与输入规模 n 的平方成正比。这在创建二维数组或矩阵时很常见。

示例:创建一个 n x n 的矩阵

javascript

编辑

1function createMatrix(n) {
2  // 创建一个 n 行 n 列的二维数组
3  let matrix = new Array(n);
4  for (let i = 0; i < n; i++) {
5    matrix[i] = new Array(n).fill(0);
6  }
7  return matrix;
8}

这个函数开辟了 n * n 的空间,因此空间复杂度为 O(n²)

javascript里的对象是存储在堆还是栈内存

  • 基础数据类型放在栈中,引用类型会在栈中放地址,真实数据放在堆中,
  • 堆内存 (Heap):  存放对象本身(包括数组、函数等复杂数据类型)。这里的内存分配是动态的,空间大但不规则。
  • 栈内存 (Stack):  存放变量标识符(变量名)和原始值(如 Number, String, Boolean 等)。对于对象来说,栈里存的是一个引用地址(指针) ,指向堆内存中实际的对象。

复杂请求 在前端跨域资源共享(CORS)的语境下,“复杂请求”是一个有明确定义的技术术语。它特指那些不能直接发送,而需要浏览器先进行一次“预检”(Preflight)请求的跨域请求。

🤔 什么是复杂请求?

浏览器会将请求分为“简单请求”和“复杂请求”。只有当一个请求同时满足以下所有条件时,它才是一个简单请求。只要不满足其中任何一个条件,它就是复杂请求。

简单请求的条件

  1. 请求方法:只能是 GETHEAD 或 POST

  2. 请求头:只能包含浏览器自动添加的“安全”字段,如 AcceptAccept-LanguageContent-Language 等,不能包含任何自定义请求头(如 X-Auth-Token)。

  3. Content-Type:值仅限于以下三种:

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

复杂请求的常见场景

因此,以下情况都会触发复杂请求:

  • 使用了 PUTDELETEPATCH 等 HTTP 方法。
  • 请求头中包含了自定义字段,例如用于身份验证的 Authorization 或 X-User-ID
  • Content-Type 的值为 application/json,这在现代前端开发中极为常见。

🔄 复杂请求的处理流程:预检机制

复杂请求的核心处理机制就是“预检”。这个过程由浏览器自动完成,无需开发者手动干预。整个流程分为两步:

第一步:发送预检请求 (OPTIONS)

在实际请求发出之前,浏览器会首先自动发送一个 OPTIONS 方法的 HTTP 请求,这个请求就是“预检请求”。它的目的是“询问”服务器:我接下来想发送一个这样的请求,你允许吗?

这个预检请求会包含几个关键的请求头:

  • Origin: 标明请求的来源(协议+域名+端口)。
  • Access-Control-Request-Method: 告知服务器,实际请求将使用哪种 HTTP 方法(如 PUT)。
  • Access-Control-Request-Headers: 告知服务器,实际请求将包含哪些自定义请求头(如 Content-TypeX-Auth-Token)。

第二步:服务器响应与决策

服务器收到预检请求后,会根据自身的 CORS 配置进行判断,并返回一个响应。

  • 如果服务器允许该请求,它会在响应中包含以下关键响应头:

    • Access-Control-Allow-Origin: 允许的请求源。
    • Access-Control-Allow-Methods: 允许的 HTTP 方法(如 PUT, POST)。
    • Access-Control-Allow-Headers: 允许的自定义请求头。
    • Access-Control-Max-Age (可选): 预检结果的缓存时间,单位是秒,用于优化性能,避免重复预检。

    当浏览器收到这个“通行证”后,才会正式发出第二步的实际请求

  • 如果服务器不允许(例如,响应头中不包含请求的方法或头),浏览器就会拦截实际请求,并在控制台抛出跨域错误,前端代码将无法获取到响应数据。

map 获取数据为什么时间复杂度为1

核心原理:哈希表如何工作

你可以把 Map 的底层想象成一个长长的储物柜数组,每个储物柜都有一个编号(索引)。

  1. 哈希函数 (Hash Function)
    当你调用 map.get(key) 时,Map 内部会使用一个“哈希函数”来处理你的 key。这个函数就像一个智能计算器,它能将任意类型的 key(比如字符串、对象)快速转换成一个固定的数字,即哈希值(Hash Code)
  2. 计算索引 (Index Calculation)
    得到哈希值后,Map 会通过一个简单的运算(通常是 哈希值 % 数组长度 或更高效的位运算),将这个哈希值映射到储物柜数组的一个具体索引上。
  3. 直接定位 (Direct Access)
    有了这个索引,Map 就可以像访问普通数组一样,直接“走到”那个储物柜,取出里面的数据。数组的随机访问特性是 O(1) 的,因此整个查找过程的绝大部分时间都花在了计算哈希值上,而这一步也是非常快的。

这个过程跳过了在数组或链表中逐个遍历比较元素的步骤,实现了“一步到位”的定位。

⚠️ 为什么是“平均”O(1)?哈希冲突怎么办?

理想情况下,每个 key 都能对应一个独一无二的储物柜。但现实中,不同的 key 经过哈希函数计算后,可能会得到相同的索引,这种情况被称为哈希冲突(Hash Collision)

Map 通过巧妙的机制来处理冲突,这也是理解其时间复杂度的关键。以 Java 的 HashMap 为例,它主要采用链地址法(Separate Chaining)

  • 链表:当发生冲突时,Map 不会覆盖旧数据,而是将新数据以节点的形式“挂”在同一个索引位置的链表上。查找时,先定位到索引,再在小范围的链表里进行遍历。
  • 红黑树:这是一个重要的性能优化。当哈希冲突非常严重,导致某个索引位置的链表过长时(例如,在 Java 8 中,链表长度超过 8),Map 会自动将这个链表转换为一棵红黑树

红黑树是一种自平衡的二叉搜索树,它的查找、插入和删除操作的时间复杂度都是 O(log n) 。这保证了即使在最坏的情况下,Map 的性能也不会退化到 O(n) 的线性查找。

📊 时间复杂度总结

因此,Map 获取数据的时间复杂度可以总结为:

表格

场景时间复杂度说明
平均情况O(1)哈希函数分布均匀,冲突很少,链表很短,查找几乎是一步完成。
最坏情况O(log n)发生大量哈希冲突,链表过长已转换为红黑树,查找效率变为对数级别。

🔑 关键保障:负载因子与扩容

为了保证 Map 能持续高效运行,它内部还有一个负载因子(Load Factor) 的概念(默认通常是 0.75)。

  • 负载因子:它衡量了 Map 的填充程度(元素数量 / 数组容量)。
  • 扩容(Rehashing) :当 Map 中的元素数量超过 容量 × 负载因子 时,Map 会自动创建一个更大的数组,并将所有旧元素重新计算索引后“搬迁”过去。

这个扩容机制确保了数组不会过于拥挤,从而将哈希冲突的概率控制在一个较低的水平,维持了平均 O(1) 的高效性能。