匆匆准备了一周时间,看些了些算法的理论部分,在半理解的情况下跟着敲了一遍代码(未运行验证),手写代码部分没有任何行动,场面一度紧张,手心直冒汗,结果自然是一败涂地。现将本次面试过程中的教训一一列出如下:
技术层面
- 面试前一定要认真且至少复习一遍技术面试题,理解面试和业务的区别,不盲目冲动参加面试;
- 平时学习和工作中要真正地理解某个技术的原理,增加技术深度学习,不要停留在会用的程度;
- 算法及模拟实现的抽象代码,多练习,明确输入输出,边界和输出条件,异常逻辑,运行示例;
- 知识薄弱点:Promise 异步使用、算法(排序)、数据结构(数组+链表)、网络协议(https);
技巧层面
- 做过的核心项目(2-3 个)准备 5 页 PPT,列出技术栈以及其中遇到的技术难点,解决的思路和方案;
- 视频面试带上耳机,认真听懂面试官提出的每一个问题,思考 3-5s 后,理清逻辑再回答,不慌张;
- 真正理解的标准:能够给别人讲明白&过一段时间后仍能够熟练记得核心逻辑和原理&能够写出代码验证;
代码题:
1. 实现有序数组 O(n)
两个有序整数数组 nums1 和 nums2, 请将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明: num1 和 nums2 元素数量分别为 m 和 n。可以假设 nums1 有足够的空间(空间足够大,大于或等于 m+n)来保存 nums 2 中的元素,且不考虑元素重复问题。
输入:
nums1 = [1,2,3,0,0,0],m=3 ; nums2 = [2,5,6],n=3
输出:
[1,2,3,4,5,6]
思路分析: 标准解法就是双指针法。分别定义两个指针,各指向两个数组生效部分的尾部。
↓
1 2 3 0 0 0
2 5 6
↑
↓
1 2 3 0 0 6
2 5
↑
每次只对指针所指的元素进行比较。 取其中较大的元素,把它从 nums1
的末尾往前面填补。为什么是从后往前填补?是因为要把所有的值合并到 nums1
中,可以把它看做是一个”容器”。但这个容器不是空的,前面几个坑是有内容的。如果从前往后补,就会覆盖前面坑位的值。从后往前补,填的都是没有内容的坑位。
由于 nums1
的有效部分与 nums2
并不是一样长。还需要考虑其中一个提前到头的情况:
- 提前遍历完的是
nums2
,nums1
剩余,由于容器本身是nums1
, 此时不必做任何额外操作;
- 提前遍历完的是
- 提前遍历完的是
nums1
的有效部分,nums2
剩余,意味着nums1
的头部空出,直接把nums2
整个补到nums1
前面即可;
- 提前遍历完的是
编码实现:
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {number[]} return modified nums1
*/
const nums1 = [1, 2, 3, 0, 0, 0],
m = 3;
const nums2 = [2, 5, 6],
n = 3;
const merge = (nums1, m, nums2, n) => {
// 初始化两个指针且分别指向末尾元素的索引
let i = m - 1;
let j = n - 1;
// 初始化最终 nums1 尾部索引 k
let k = m + n - 1;
//当两个数组都没有遍历完时,指针同步移动
while (j >= 0) {
if (i < 0) {
nums1[k--] = nums2[j--];
continue;
}
nums1[k--] = nums1[i] >= nums2[j] ? nums1[i--] : nums2[j--];
}
return nums1;
};
console.log(merge(nums1, m, nums2, n));
两个有序整数数组 nums1 和 nums2, 请将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
nums1 = [1,2,3] nums2 = [2,5,6]
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]} return modified nums1
*/
const nums1 = [1, 2, 3],
nums2 = [2, 5, 6];
const merge = (nums1, nums2) => {
const { length: len1 } = nums1;
const { length: len2 } = nums2;
let i = len1 - 1;
let j = len2 - 1;
let k = len1 + len2 - 1;
while (j >= 0) {
// nums1.length < nums2.length
if (i < 0) {
nums1[k--] = nums2[j--];
continue;
}
// nums1.length >= nums2.length
nums1[k--] = nums1[i] >= nums2[j] ? nums1[i--] : nums2[j--];
}
return nums1;
};
//console.log(merge(nums1, nums2))
2. 实现 promiseAll
思路:
- (1). 输入参数:接收一个 Promise 实例的数组或 iterator 接口的对象;
- (2). 输出结果:返回一个新的 promise 对象
- (3). 对入参的处理:遍历传入的参数,用 Promise.resolve()将参数包一层,使其变为一个 promise 对象
- (4). 参数所有回调成功才是成功,返回值数组顺序与参数顺序一致
- (5). 参数数组中有一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息做为 Promise.all 的错误信息
代码:
const promiseAll = (promises) => {
// 输入参数
if (!Array.isArray(promises)) {
alert("arguments must be arr");
return;
}
// 输出参数
return new Promise(
(resolve, reject) => {
let resolveCounter = 0;
let resolveNum = promises.length;
let resolveRes = [];
// 对输入参数处理
for (let i = 0; i < resolveNo; i++) {
Promise.resolve(promises[i]).then((value) => {
resolveCounter++;
resolveNum[i] = value;
if (resolveCounter === resolveNo) {
return resolve(resolveNum);
}
});
}
},
(err) => reject(err)
);
};
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 2000);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 2000);
});
promiseAll([p3, p2, p1]).then((res) => {
console.log(res);
});
3. promisfy;
作用:将原本需要通过传入回调参数来实现回调执行(同步执行)改为利用 promise.then 方式来调用,从而实现逻辑上的同步操作。
代码实现:
// 入参:fn 需要执行的函数
const promisfy = (fn) => {
// 返回值: 自定义匿名函数
return function (...args) {
return new Promise((resolve, reject) => {
// fn 调用 callback
let callback = function (...args) {
// callback 转变为 resolve 的调用,接下来就可以在 then 函数中执行
resolve(args);
};
// 执行 fn 函数
fn.apply(null, [...args, callback]);
});
};
};
// 使用示例
function fn(str1, str2, callback) {
setTimeout(() => {
console.log("setTimeout");
// callback 函数是通过最后一个参数的位置来识别的,名字可以任意取
callback(str1, str2);
}, 1000);
}
let agent = promisfy(fn);
agent("hello", "world").then((res) => {
console.log(res);
});
// setTimeout
// ["hello", "world"]
4. 深浅拷贝
// obj => Array or Object => 浅拷贝只复制一层,当对象只有一层时为深拷贝
const shallowClone = (obj) => {
// 只拷贝对象
if (typeof obj !== "object") {
return;
}
// 初始化 newObj 判断
let newObj = obj instanceof Array ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
};
const obj = {
a: 1,
b: 2,
c: 3,
};
const arr = ["a", "b", "c"];
const copyObj = shallowClone(obj);
const copyArr = shallowClone(arr);
console.log(copyObj);
console.log("copyObj === obj", copyObj === obj);
console.log(copyArr);
console.log("copyArr === arr", copyArr === arr);
const deepClone = (obj) => {
// 非对象不拷贝
if (typeof obj !== "object") {
return;
}
// 初始化拷贝的对象 => Array or Object
let newObj = obj instanceof Array ? [] : {};
for (let key in obj) {
// 遍历 obj,判断 obj[key] 是否为对象,对象 => 递归;非对象 直接赋值
newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
}
return newObj;
};
const obj = {
a: {
b: 1,
},
c: {
d: 2,
},
};
const arr = {
a: ["1"],
c: ["2"],
};
const copyObj = deepClone(obj);
const copyArr = deepClone(arr);
console.table(copyObj);
console.log("copyObj == obj", copyObj === obj);
console.table(copyArr);
console.log("copyArr == arr", copyArr === arr);
5. redcue 实现 map
/**
* @param { function } condition map 方法里的回调函数
* @param { array } thisArr this 默认为空数组
* @return { array } 返回修改后的数组
*/
Array.prototype.myMap = function (condition, thisArr) {
let ans = [];
thisArr = thisArr || [];
console.log(thisArr);
this.reduce((pre, cur, curIndex) => {
ans.push(condition.call(thisArr, cur, curIndex));
}, []);
return ans;
};
let arr = [1, 2, 4];
let test = arr.myMap((item) => {
return item * 2;
});
console.log(test); // [2,4,8]
6. 版本号对比
比较版本 v1= 2.2.3 和 v2= 2.1.15 大小, 大则返回 1,小则返回 -1,相等则返回 0
// 这是微信小程序官方给出的比较方法
const compareVersion = (v1, v2) => {
v1 = v1.split(".");
v2 = v2.split(".");
// 取其中较长的数组长度
const len = Math.max(v1.length, v2.length);
// v1 末尾补齐
while (v1.length < len) {
v1.push("0");
}
// v2 末尾补齐
while (v2.length < len) {
v2.push("0");
}
// 循环比较 v1 和 v2
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i]);
const num2 = parseInt(v2[i]);
if (num1 > num2) {
return 1;
} else if (num1 < num2) {
return -1;
}
}
return 0;
};
let res = compareVersion("2.2.3", "2.1.15");
console.log(res); // 1
问答题:
1. https 加密握手的过程
https 介绍: http 协议都是明文传输数据,https 是安全版的 http 协议,传输的是加密后的数据;https 采用了对加密和非对称加密来确保客户端和服务端的通信安全;对称加密算法加密数据 + 非对称加密算法交换密钥 + 数字证书 = 安全
https 组成:http + ssl/tls, 在 http 上加了一层处理加密信息的模块,客户端和服务端通过 tls 进行加密
https 加密握手过程:
a. 客户端发起握手请求,以明文传输请求信息(输入 https 的网址,请求端口为 443);(一次握手)
b. 服务端配置证书公钥(锁)和证书私钥(钥匙),可自己制作或向组织申请;
c. 服务端返回证书公钥(锁)及其他信息;(一次挥手)
d. 客户端验证证书的合法性和可信性,是否吊销、过期,域名是否合法,并用公钥对称加密一个随机值;
e. 客户端将对称加密后的密钥(随机值)发送给服务端,以供服务端后期解密;(二次握手)
f. 服务端用私钥解密,拿到对称加密后的密钥(随机值),将内容通过随机值进行加密;
g. 服务端将(私钥)加密后的内容发送给客户端;(一次挥手)
h. 客户端解密信息,客户端用之前生产的私钥解密服务端传输过来的内容;
对称加密和非对称加密:
对称加密:收发双方规定密钥,比如字母偏移 5 位加密;对称-加密的人也能解密;问题-密钥需要传输,传输的过程中,可能被窃听或篡改。
非对称加密: 发送人留着钥匙,把带锁的盒子传送过去,加密的人锁上,但是解不开,这就是非对称;问题-可能被窃听更换掉盒子; 认证机构来个盒子做签名,也就是我们 https 需要的网站证书。
区别:对称加密需要一把密钥,非对称加密需要一对密钥(公钥和私钥);对称加密算法简单,加密解密容易,效率高,只有一把钥匙,密文和密钥都被拦截,信息很容被破译,安全性能就降低了;非对称加密算法较复杂,效率较低,即使密文和公钥被劫持,没有私钥,也无法破译密文,安全性能高。
总结: 非对称加密可靠但性能低,对称加密性能高但不可靠;要配合使用,非对称加密对身份验证和密钥交换,对称加密对数据进行加密。
2. websocket 的原理和使用
a. 为什么要用 websocket
http 是半双工协议,在同一时刻流量只能单向流动,客户端向服务器发送请求(单向),然后服务端响应请求(单向),服务器不能主动推送数据给客户端(浏览器);半双工的缺点是,效率低下(股票实时行情&火车票剩余票数就 over 了);
http 的轮询: 浏览器定时向 web 服务器发送 http 的 get 请求,服务器收到请求后,把最新的数据发送给客户端,客户端得到数据后,将其显示出来,周而复始的重复该过程;轮询的缺点是,某段时间服务端没有更新数据,但浏览器依旧定时发送 get 请求过来轮询,即浪费带宽,又浪费 cpu 的利用率;
http 的长轮询: 客户端在设定的时间段内,始终保持请求服务端,直到客户端有可用的信息,或者指定的时间用完;长轮询会延长 http 响应的完成,直到服务器有需要发送给客户端的内容,通常叫 “挂起 GET” 或 “搁置 POST”;缺点是,信息量大的时候,客户端需要频繁地重连服务端读取新信息,造成网络表现异常。
b. websocket 原理
websocket 是一种全双工,双向的连接,http 请求变成 websocket 的单一请求,而且能够重用客户端到服务端以及服务端到客户端的同一连接;一旦建立 websocket 连接,服务器可以在有消息时,向客户端发送信息,与轮询不同的是:websocket 只发出一个请求,服务器不需要等待来自客户端的请求,单一请求大大减少了延迟。
c. websocket 使用 api
// 注意:URL字符串必须以 "ws" 或 "wss"(加密通信)开头
const websocket = new WebSocket("wss://echo.websocket.org");
// 客户端发送数据 send
websocket.send(data);
// 客户端接收服务端传过来的数据
websocket.onmessage = function (e) {
let data = e.data;
};
// 客户端监听 socket 的打开事件
websocket.onopen = function (e) {
//开始通信时的处理
};
// 客户端监听 socket 的关闭事件
websocket.onclose = function (e) {
//结束通信时的处理即关闭 websocket
};
websocket.readyState = {
CONNECTING: 0, // 正在连接
OPEN: 1, // 已建立连接
CLOSING: 2, // 正在关闭连接
CLOSED: 3, // 已关闭连接
};
在 HTML5 中,除了可以使用 websocket 发送文本数据以及对象外,还可以发送 ArrayBuffer(发送二进制对象) 与 Blob 对象。
const websocket = new WebSocket("wss://echo.websocket.org");
websocket.onopen = function (e) {
const buffer = new ArrayBuffer(128);
websocket.send(buffer);
const blob = new Blob([buffer]);
websocket.send(blob);
};
websocket.onmessage = function (e) {
console.log(e.data);
websocket.onclose();
};
websocket.onclose = function (e) {
console.log("connection closed");
};
d. 应用场景:股票实时行情、实时新闻、多人在线游戏或聊天、火车票剩余票数更新