苦难的开始
javascript == 和 === 的区别
1== ‘1’ 是把‘1’做了隐饰转换为1, === 是值和类型都相同才会相等
隐饰转换都有哪些?
-
对象与原始类型:对象会通过
valueOf()或toString()方法转换为原始值。[] == ''→true(空数组转为空字符串)[1] == '1'→true(数组转为字符串 '1')
-
字符串与数字:字符串会被转换为数字。
'5' == 5→true(字符串 '5' 转为数字 5)'' == 0→true(空字符串转为数字 0)
-
布尔值与其他类型:布尔值会被转换为数字 (
true→ 1,false→ 0)。true == 1→truefalse == 0→true'0' == false→true(字符串 '0' 转为数字 0,false也转为 0)
-
null与undefined:它们之间互相比较为true,但与任何其他值比较都为false。null == undefined→truenull == 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)请求的跨域请求。
🤔 什么是复杂请求?
浏览器会将请求分为“简单请求”和“复杂请求”。只有当一个请求同时满足以下所有条件时,它才是一个简单请求。只要不满足其中任何一个条件,它就是复杂请求。
简单请求的条件
-
请求方法:只能是
GET、HEAD或POST。 -
请求头:只能包含浏览器自动添加的“安全”字段,如
Accept、Accept-Language、Content-Language等,不能包含任何自定义请求头(如X-Auth-Token)。 -
Content-Type:值仅限于以下三种:
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
复杂请求的常见场景
因此,以下情况都会触发复杂请求:
- 使用了
PUT、DELETE、PATCH等 HTTP 方法。 - 请求头中包含了自定义字段,例如用于身份验证的
Authorization或X-User-ID。 Content-Type的值为application/json,这在现代前端开发中极为常见。
🔄 复杂请求的处理流程:预检机制
复杂请求的核心处理机制就是“预检”。这个过程由浏览器自动完成,无需开发者手动干预。整个流程分为两步:
第一步:发送预检请求 (OPTIONS)
在实际请求发出之前,浏览器会首先自动发送一个 OPTIONS 方法的 HTTP 请求,这个请求就是“预检请求”。它的目的是“询问”服务器:我接下来想发送一个这样的请求,你允许吗?
这个预检请求会包含几个关键的请求头:
Origin: 标明请求的来源(协议+域名+端口)。Access-Control-Request-Method: 告知服务器,实际请求将使用哪种 HTTP 方法(如PUT)。Access-Control-Request-Headers: 告知服务器,实际请求将包含哪些自定义请求头(如Content-Type,X-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 的底层想象成一个长长的储物柜数组,每个储物柜都有一个编号(索引)。
- 哈希函数 (Hash Function)
当你调用map.get(key)时,Map内部会使用一个“哈希函数”来处理你的key。这个函数就像一个智能计算器,它能将任意类型的key(比如字符串、对象)快速转换成一个固定的数字,即哈希值(Hash Code) 。 - 计算索引 (Index Calculation)
得到哈希值后,Map会通过一个简单的运算(通常是哈希值 % 数组长度或更高效的位运算),将这个哈希值映射到储物柜数组的一个具体索引上。 - 直接定位 (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) 的高效性能。