# 前端面试题及答案汇总(二) 11-20

原文引用

# 第 11 题:将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组

function spreadArr(arr) {
  let flatArr = arr.toString().split(",");
  let newArr = [];
  let hash = {};
  for (let i = flatArr.length - 1; i >= 0; i--) {
    if (!hash[flatArr[i]]) {
      hash[flatArr[i]] = true;
      newArr.push(Number(flatArr[i]));
    }
  }
  console.log(newArr.sort((a, b) => a - b));
  return newArr.sort((a, b) => a - b);
}
spreadArr([
  [1, 2, 2],
  [3, 4, 5, 5],
  [6, 7, 8, 9, [11, 12, [12, 13, [14]]]],
  10
]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 第 12 题:(滴滴、挖财、微医、海康)JS 异步解决方案的发展历程以及优缺点。

  1. 回调函数(callback)
setTimeout(() => {
  // callback 函数体
}, 1000);
1
2
3

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

回调地狱的根本问题在于:

  • 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
  • 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
  • 嵌套函数过多的多话,很难处理错误
ajax("XXX1", () => {
  // callback 函数体
  ajax("XXX2", () => {
    // callback 函数体
    ajax("XXX3", () => {
      // callback 函数体
    });
  });
});
1
2
3
4
5
6
7
8
9

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)

  1. Promise

Promise 就是为了解决 callback 的问题而产生的。

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题

ajax("XXX1")
  .then(res => {
    // 操作逻辑
    return ajax("XXX2");
  })
  .then(res => {
    // 操作逻辑
    return ajax("XXX3");
  })
  .then(res => {
    // 操作逻辑
  });
1
2
3
4
5
6
7
8
9
10
11
12

缺点:无法取消 Promise ,错误需要通过回调函数来捕获

  1. Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

function* fetch() {
  yield ajax("XXX1", () => {});
  yield ajax("XXX2", () => {});
  yield ajax("XXX3", () => {});
}
let it = fetch();
let result1 = it.next();
let result2 = it.next();
let result3 = it.next();
1
2
3
4
5
6
7
8
9
  1. Async/await

async、await 是异步的终极解决方案

优点是:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

async function test() {
  // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
  // 如果有依赖性的话,其实就是解决回调地狱的例子了
  await fetch("XXX1");
  await fetch("XXX2");
  await fetch("XXX3");
}
1
2
3
4
5
6
7

# 第 13 题:(微医)Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

# 第 14 题:(兑吧)情人节福利题,如何实现一个 new

function _new(func) {
  let res = {};
  if (func.prototype !== null) {
    res.__proto__ = func.prototype;
  }
  let ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
  if (
    (typeof ret === "function" || typeof ret === "object") &&
    typeof ret !== null
  ) {
    return ret;
  }
  return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 第 15 题:(网易)简单讲解一下 http2 的多路复用

# 第 16 题:谈谈你对 TCP 三次握手和四次挥手的理解

# 第 17 题:A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态

# 第 18 题:(微医)React 中 setState 什么时候是同步的,什么时候是异步的?

# 第 19 题:React setState 笔试题,下面的代码输出什么?

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 });
    console.log(this.state.val); // 第 1 次 log

    this.setState({ val: this.state.val + 1 });
    console.log(this.state.val); // 第 2 次 log

    setTimeout(() => {
      this.setState({ val: this.state.val + 1 });
      console.log(this.state.val); // 第 3 次 log

      this.setState({ val: this.state.val + 1 });
      console.log(this.state.val); // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  1. 第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。

  2. 两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。

  3. setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。

输出: 0 0 2 3

# 第 20 题:介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

# npm 模块安装机制:

发出 npm install 命令 查询 node_modules 目录之中是否已经存在指定模块 若存在,不再重新安装 若不存在 npm 向 registry 查询模块压缩包的网址 下载压缩包,存放在根目录下的.npm 目录里 解压压缩包到当前项目的 node_modules 目录

# npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall 当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

  2. 确定首层依赖模块 首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  1. 获取模块 获取模块是一个递归的过程,分为以下几步:

获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。 查找该模块依赖,如果有依赖则回到第 1 步,如果没有则停止。

  1. 模块扁平化(dedupe) 上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

举个例子,假设一个依赖树原本是这样:

node_modules -- foo ---- lodash@version1

-- bar ---- lodash@version2

假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:

node_modules -- foo

-- bar

-- lodash(保留的版本为兼容版本)

假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

node_modules -- foo -- lodash@version1

-- bar ---- lodash@version2

  1. 安装模块 这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

  2. 执行工程自身生命周期 当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

最后一步是生成或更新版本描述文件,npm install 过程完成。