复盘一次失败的面试过程

匆匆准备了一周时间,看些了些算法的理论部分,在半理解的情况下跟着敲了一遍代码(未运行验证),手写代码部分没有任何行动,场面一度紧张,手心直冒汗,结果自然是一败涂地。现将本次面试过程中的教训一一列出如下:

技术层面
  1. 面试前一定要认真且至少复习一遍技术面试题,理解面试和业务的区别,不盲目冲动参加面试;
  2. 平时学习和工作中要真正地理解某个技术的原理,增加技术深度学习,不要停留在会用的程度;
  3. 算法及模拟实现的抽象代码,多练习,明确输入输出,边界和输出条件,异常逻辑,运行示例;
  4. 知识薄弱点:Promise 异步使用、算法(排序)、数据结构(数组+链表)、网络协议(https);
技巧层面
  1. 做过的核心项目(2-3 个)准备 5 页 PPT,列出技术栈以及其中遇到的技术难点,解决的思路和方案;
  2. 视频面试带上耳机,认真听懂面试官提出的每一个问题,思考 3-5s 后,理清逻辑再回答,不慌张;
  3. 真正理解的标准:能够给别人讲明白&过一段时间后仍能够熟练记得核心逻辑和原理&能够写出代码验证;

代码题:

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 并不是一样长。还需要考虑其中一个提前到头的情况:

    1. 提前遍历完的是 nums2nums1 剩余,由于容器本身是 nums1 , 此时不必做任何额外操作;
    1. 提前遍历完的是 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. 应用场景:股票实时行情、实时新闻、多人在线游戏或聊天、火车票剩余票数更新

3. react hooks

学习 React 钩子函数,以 useEffect 为例