前言
不多BB,直接开卷
面试题:
算法:大数相加,你会如何写函数
在前端面试中,如果面试官让你写一个大数相加,可能你会想到用big int去加,但是一般他是想考察你将大数转成string再每位相加。
代码:
/**
* 大数相加
* @param {string} num1 - 第一个大数(字符串形式)
* @param {string} num2 - 第二个大数(字符串形式)
* @returns {string} - 两个大数的和(字符串形式)
*/
function addLargeNumbers(num1, num2) {
let result = ''; // 存储结果
let carry = 0; // 进位
let i = num1.length - 1; // num1 的索引
let j = num2.length - 1; // num2 的索引
// 从后向前逐位相加
while (i >= 0 || j >= 0 || carry > 0) {
const digit1 = i >= 0 ? parseInt(num1[i]) : 0; // 获取 num1 当前位数字
const digit2 = j >= 0 ? parseInt(num2[j]) : 0; // 获取 num2 当前位数字
// 计算当前位的和
const sum = digit1 + digit2 + carry;
carry = Math.floor(sum / 10); // 计算进位
result = (sum % 10) + result; // 当前位的结果
i--; // 移动到 num1 的前一位
j--; // 移动到 num2 的前一位
}
return result; // 返回最终结果
}
// 使用示例
const num1 = "123456789012345678901234567890";
const num2 = "987654321098765432109876543210";
const sum = addLargeNumbers(num1, num2);
console.log(sum); // 输出: "1111111110111111111011111111100"
易错点:
- 首先:我们要得到,两端字符串的长度,可能会出现一长一短的情况,所以要记得在短的那边补零。
- 其次:在做加法的时候,要记得用变量保存进位。
- 最后:正确拼接答案,注意要将新计算出的数放在前面 result = (sum % 10) + result;
三次握手 四次挥手
考你对网络的理解
三次握手(TCP连接建立)
- 第一次握手
- 客户端发送
SYN报文(序列号x),进入SYN_SENT状态。 - 意义:告知服务端“我要建立连接”。
- 第二次握手
- 服务端回复
SYN-ACK报文(确认号x+1,序列号y),进入SYN_RECEIVED状态。 - 意义:服务端确认收到客户端的请求,并同意建立连接。
- 第三次握手
- 客户端发送
ACK报文(确认号y+1),进入ESTABLISHED状态。 - 服务端收到后也进入
ESTABLISHED状态。 - 意义:双方确认彼此的收发能力正常。
为什么是三次?
- 如果只有两次握手,客户端可能发送过期的
SYN报文(如因网络延迟),导致服务端误认为这是一个新连接,浪费资源。 - 三次握手确保服务端的
SYN-ACK报文必须被客户端响应,排除历史连接的干扰。
四次挥手(TCP连接断开)
- 第一次挥手
- 客户端发送
FIN报文,进入FIN_WAIT_1状态。 - 意义:告知服务端“我不再发送数据”。
- 第二次挥手
- 服务端回复
ACK报文,进入CLOSE_WAIT状态。 - 客户端收到后进入
FIN_WAIT_2状态。 - 意义:服务端确认收到关闭请求,但可能仍有数据要发送。
- 第三次挥手
- 服务端发送
FIN报文,进入LAST_ACK状态。 - 意义:服务端通知客户端“我也不会再发送数据”。
- 第四次挥手
- 客户端回复
ACK报文,进入TIME_WAIT状态,等待2MSL(最大报文段生存时间)后关闭。 - 服务端收到
ACK后立即关闭。 - 意义:客户端确保服务端收到最后的
ACK,防止数据丢失。
为什么是四次?
TCP 是全双工协议,双方均可独立收发数据。关闭连接时,主动关闭方(如客户端)发送 FIN 后,被动方(如服务端)可能仍有数据要发送,需单独发送自己的 FIN。
TIME_WAIT 的作用
- 确保最后一个
ACK抵达服务端,避免因网络延迟导致服务端重传FIN。 - 等待
2MSL可让网络中残留的旧数据包消失,防止新连接收到历史数据。
https 更安全的http
如果使用http有可能有以下问题:
- 明文传输:HTTP 数据以明文形式传输,易被中间人窃听(如公共 Wi-Fi 下的密码泄露)。
- 无身份验证:无法验证服务器真实性,可能遭遇钓鱼网站(如伪造银行网站)。
- 数据篡改:攻击者可篡改传输内容(如修改交易金额),且客户端无法察觉。
基础知识点:
-
对称加密:使用 AES 等算法加密实际数据,速度快,加密和解密使用同一个密钥。
- 加密和解密使用同一密钥,必须通过安全通道预先共享密钥。
- 无法验证通信双方身份,可能被伪造身份的攻击者欺骗(如中间人冒充服务器)。
-
非对称加密:在 TLS 握手阶段,通过 RSA/ECDHE 算法交换对称密钥,解决密钥传输的安全问题。
- 生成密钥对:服务端生成公钥(Public Key)和私钥(Private Key)。
- 分发公钥:服务端将公钥发送给客户端(可能被中间人篡改)。
- 加密数据:客户端用服务端公钥加密数据,发送给服务端。
- 解密数据:服务端用私钥解密数据。
- 非对称加密算法(如 RSA)计算复杂度高,加密/解密速度远低于对称加密。
-
混合加密机制:结合两者优点,既保证效率,又确保密钥交换安全。 -问题: 攻击者截获公钥并替换为自己的公钥,客户端误用攻击者公钥加密数据,攻击者用私钥解密后窃取信息。
-
身份认证: 服务端证书中的公钥用于密钥交换,浏览器通过 CA 签名验证服务器身份,防止中间人攻击
总结:HTTPS = TCP 握手 + TLS 握手(非对称加密交换密钥 + 证书验证) + 对称加密传输数据。
通过非对称加密解决密钥分发和身份认证问题,再通过对称加密高效传输数据,兼顾安全与性能。
关于promsie的手写题
如何用promise,每隔1秒输出1 2 3 呢?
当时想的是在promsie里面使用定时器
const x = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('1', new Date());
resolve();
}, 1000);
});
x.then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log('2', new Date());
resolve();
}, 1000);
});
}).then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log('3', new Date());
resolve();
}, 1000);
});
}).then(() => {
console.log("所有步骤完成");
});
1 2025-05-08T12:40:21.351Z
2 2025-05-08T12:40:22.364Z
3 2025-05-08T12:40:23.377Z
所有步骤完成
让我们看看吸引面试官的写法:
const a = [1, 2, 3];
a.reduce((p, x) => {
return p.then(() => {
return new Promise(r => {
setTimeout(() => r(console.log(x, new Date())), 1000);
});
});
}, Promise.resolve());
初始化
a = [1, 2, 3]:定义了一个包含三个数字的数组。
Promise.resolve():作为 reduce 方法的第二个参数传入,它是一个已经 resolved 状态的 Promise,作为累积器(累加器)的初始值。
第一次迭代 (x = 1)
-
累积器
p初始为Promise.resolve()。 -
调用
p.then(...),因为p已经处于 resolved 状态,.then(...)注册的回调函数会立即进入事件队列等待执行。 -
在
.then(...)回调中,创建一个新的 Promise:return new Promise(r => { setTimeout(() => r(console.log(x, new Date())), 1000); });这个新的 Promise 包含一个
setTimeout,在 1000ms(1秒)后触发,并且在其回调中打印当前元素x和当前时间戳。然后调用r()来 resolve 这个 Promise。
关键点解释
定时器与事件循环
- 当前 JavaScript 引擎继续处理同步任务,而定时器开始计时。
- 1秒后,
setTimeout的回调被放入任务队列等待执行。 - 一旦当前调用栈为空(即所有同步任务完成),JavaScript 引擎开始从任务队列中取出并执行回调函数。
- 回调函数首先执行
console.log(x, new Date()),这将输出1和当时的时间。 - 然后调用
r(),这意味着这个新的 Promise 被标记为 resolved,允许任何后续.then(...)回调被执行。
后续迭代 (x = 2 和 x = 3)
对于数组中的每个后续元素(x = 2 和 x = 3),上述过程重复:
- 累积器
p变成上一步返回的新 Promise。 - 每次
.then(...)回调都会创建一个新的 Promise,该 Promise 会在 1秒后通过setTimeout触发,并在触发时打印当前元素和时间戳,随后 resolve 自身。 - 这种方式确保了每次输出之间有固定的延迟(1秒),并且按照数组顺序依次输出。
使用Promise实现红绿灯交替重复亮
当时的想法如果按第一题那样写,再加个while(true)不就好了 错误写法:
function x() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('red', new Date());
resolve();
}, 1000);
});
}
while(true){
x().then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log('green', new Date());
resolve();
}, 2000);
});
}).then(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log('yellow', new Date());
resolve();
}, 3000);
});
}).then(() => {
console.log("所有步骤完成");
});
}
解释:
因为 while 循环不会等待 Promise 链式调用完成就会立即开始下一次循环,这会导致大量 Promise 实例同时创建和执行,可能使内存占用过高。可以使用 async/await 来确保每次循环都在前一次循环的 Promise 链式调用完成后再开始。
所以下面的版本出来了:
function sleep(time){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, time)
})
}
async function traficLight() {
while (true) {
console.log('red', new Date())
await sleep(3000)
console.log('yellow', new Date())
await sleep(1000)
console.log('green', new Date())
await sleep(2000)
}
}
结语:
面试就是总结,面试再总结,不断优化的过程啊!