# 前端面试题及答案汇总(一) 1-10

原文引用

# 第 1 题:写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

公司:滴滴、饿了么

解析:

key 是给每一个 vnode 的唯一 id,可以依靠 key,更准确, 更快的拿到 oldVnode 中对应的 vnode 节点。

  1. 更准确 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

  2. 更快 利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map 会比遍历更快。)

vue 和 react 都是采用 diff 算法来对比新旧虚拟节点,从而更新节点。在 vue 的 diff 函数中(建议先了解一下 diff 算法过程)。 在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点(这里对应的是一个 key => index 的 map 映射)。如果没找到就认为是一个新增节点。而如果没有 key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个 map 映射,另一种是遍历查找。相比而言。map 映射的速度更快。

# 第 2 题:['1', '2', '3'].map(parseInt) what & why ?

解析:

第一眼看到这个题目的时候,脑海跳出的答案是 [1, 2, 3],但是真正的答案是[1, NaN, NaN]。

  • 首先让我们回顾一下,map 函数的第一个参数 callback: var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]) 这个 callback 一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。

  • 而 parseInt 则是用来解析字符串的,使字符串成为指定基数的整数。 parseInt(string, radix) 接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。

  • 了解这两个函数后,我们可以模拟一下运行情况

  1. parseInt('1', 0) //radix 为 0 时,且 string 参数不以“0x”和“0”开头时,按照 10 为基数处理。这个时候返回 1
  2. parseInt('2', 1) //基数为 1(1 进制)表示的数中,最大值小于 2,所以无法解析,返回 NaN
  3. parseInt('3', 2) //基数为 2(2 进制)表示的数中,最大值小于 3,所以无法解析,返回 NaN

# 第 3 题:什么是防抖和节流?有什么区别?如何实现?

公司:挖财

解析:

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

  1. 防抖 触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间

思路: 每次触发事件时都取消之前的延时调用方法

function debounce(fn) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function() {
    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
    timeout = setTimeout(() => {
      // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
      fn.apply(this, arguments);
    }, 500);
  };
}
function sayHi() {
  console.log("防抖成功");
}

var inp = document.getElementById("inp");
inp.addEventListener("input", debounce(sayHi)); // 防抖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

可以参考JavaScript 专题之跟着 underscore 学防抖

  1. 节流 高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率

思路: 每次触发事件时都判断当前是否有等待执行的延时函数

function throttle(fn) {
  let canRun = true; // 通过闭包保存一个标记
  return function() {
    if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
    canRun = false; // 立即设置为false
    setTimeout(() => {
      // 将外部传入的函数的执行放在setTimeout中
      fn.apply(this, arguments);
      // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
      canRun = true;
    }, 500);
  };
}
function sayHi(e) {
  console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener("resize", throttle(sayHi));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

可以参考JavaScript 专题之跟着 underscore 学节流

# 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

参考阮大大

  • Set
    1. 成员唯一、无序且不重复
    2. [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    3. 可以遍历,方法有:add、delete、has
  • WeakSet
    1. 成员都是对象
    2. 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏
    3. 不能遍历,方法有 add、delete、has
  • Map
    1. 本质上是键值对的集合,类似集合
    2. 可以遍历,方法很多可以跟各种数据格式转换
  • WeakMap
    1. 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
    2. 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    3. 不能遍历,方法有 get、set、has、delete

# 第 5 题:介绍下深度优先遍历和广度优先遍历,如何实现?

参考更多

将用深度优先遍历和广度优先遍历对这个 dom 树进行查找

dom树

  • 深度优先遍历

深度优先遍历 DFS 与树的先序遍历比较类似。 假设初始状态是图中所有顶点均未被访问,则从某个顶点 v 出发,首先访问该顶点然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和 v 有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

/*深度优先遍历三种方式*/
let deepTraversal1 = (node, nodeList = []) => {
  if (node !== null) {
    nodeList.push(node);
    let children = node.children;
    for (let i = 0; i < children.length; i++) {
      deepTraversal1(children[i], nodeList);
    }
  }
  return nodeList;
};
let deepTraversal2 = node => {
  let nodes = [];
  if (node !== null) {
    nodes.push(node);
    let children = node.children;
    for (let i = 0; i < children.length; i++) {
      nodes = nodes.concat(deepTraversal2(children[i]));
    }
  }
  return nodes;
};
// 非递归
let deepTraversal3 = node => {
  let stack = [];
  let nodes = [];
  if (node) {
    // 推入当前处理的node
    stack.push(node);
    while (stack.length) {
      let item = stack.pop();
      let children = item.children;
      nodes.push(item);
      // node = [] stack = [parent]
      // node = [parent] stack = [child3,child2,child1]
      // node = [parent, child1] stack = [child3,child2,child1-2,child1-1]
      // node = [parent, child1-1] stack = [child3,child2,child1-2]
      for (let i = children.length - 1; i >= 0; i--) {
        stack.push(children[i]);
      }
    }
  }
  return nodes;
};
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

输出结果

  • 广度优先遍历

广度优先遍历 BFS 从图中某顶点 v 出发,在访问了 v 之后依次访问 v 的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。 如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

let widthTraversal2 = node => {
  let nodes = [];
  let stack = [];
  if (node) {
    stack.push(node);
    while (stack.length) {
      let item = stack.shift();
      let children = item.children;
      nodes.push(item);
      // 队列,先进先出
      // nodes = [] stack = [parent]
      // nodes = [parent] stack = [child1,child2,child3]
      // nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2]
      // nodes = [parent,child1,child2]
      for (let i = 0; i < children.length; i++) {
        stack.push(children[i]);
      }
    }
  }
  return nodes;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

输出结果

# 第 6 题:请分别用深度优先思想和广度优先思想实现一个拷贝函数?

深度优先:

let DFSdeepClone = (obj, visitedArr = []) => {
  let _obj = {};
  if (isTypeOf(obj, "array") || isTypeOf(obj, "object")) {
    let index = visitedArr.indexOf(obj);
    _obj = isTypeOf(obj, "array") ? [] : {};
    if (~index) {
      // 判断环状数据
      _obj = visitedArr[index];
    } else {
      visitedArr.push(obj);
      for (let item in obj) {
        _obj[item] = DFSdeepClone(obj[item], visitedArr);
      }
    }
  } else if (isTypeOf(obj, "function")) {
    _obj = eval("(" + obj.toString() + ")");
  } else {
    _obj = obj;
  }
  return _obj;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

广度优先:

let BFSdeepClone = obj => {
  let origin = [obj],
    copyObj = {},
    copy = [copyObj];
  // 去除环状数据
  let visitedQueue = [],
    visitedCopyQueue = [];
  while (origin.length > 0) {
    let items = origin.shift(),
      _obj = copy.shift();
    visitedQueue.push(items);
    if (isTypeOf(items, "object") || isTypeOf(items, "array")) {
      for (let item in items) {
        let val = items[item];
        if (isTypeOf(val, "object")) {
          let index = visitedQueue.indexOf(val);
          if (!~index) {
            _obj[item] = {};
            //下次while循环使用给空对象提供数据
            origin.push(val);
            // 推入引用对象
            copy.push(_obj[item]);
          } else {
            _obj[item] = visitedCopyQueue[index];
            visitedQueue.push(_obj);
          }
        } else if (isTypeOf(val, "array")) {
          // 数组类型在这里创建了一个空数组
          _obj[item] = [];
          origin.push(val);
          copy.push(_obj[item]);
        } else if (isTypeOf(val, "function")) {
          _obj[item] = eval("(" + val.toString() + ")");
        } else {
          _obj[item] = val;
        }
      }
      // 将已经处理过的对象数据推入数组 给环状数据使用
      visitedCopyQueue.push(_obj);
    } else if (isTypeOf(items, "function")) {
      copyObj = eval("(" + items.toString() + ")");
    } else {
      copyObj = obj;
    }
  }
  return copyObj;
};
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

测试:

/**测试数据 */
// 输入 字符串String
// 预期输出String
let str = "String";
var strCopy = DFSdeepClone(str);
var strCopy1 = BFSdeepClone(str);
console.log(strCopy, strCopy1); // String String 测试通过
// 输入 数字 -1980
// 预期输出数字 -1980
let num = -1980;
var numCopy = DFSdeepClone(num);
var numCopy1 = BFSdeepClone(num);
console.log(numCopy, numCopy1); // -1980 -1980 测试通过
// 输入bool类型
// 预期输出bool类型
let bool = false;
var boolCopy = DFSdeepClone(bool);
var boolCopy1 = BFSdeepClone(bool);
console.log(boolCopy, boolCopy1); //false false 测试通过
// 输入 null
// 预期输出 null
let nul = null;
var nulCopy = DFSdeepClone(nul);
var nulCopy1 = BFSdeepClone(nul);
console.log(nulCopy, nulCopy1); //null null 测试通过

// 输入undefined
// 预期输出undefined
let und = undefined;
var undCopy = DFSdeepClone(und);
var undCopy1 = BFSdeepClone(und);
console.log(undCopy, undCopy1); //undefined undefined 测试通过
//输入引用类型obj
let obj = {
  a: 1,
  b: () => console.log(1),
  c: {
    d: 3,
    e: 4
  },
  f: [1, 2],
  und: undefined,
  nul: null
};
var objCopy = DFSdeepClone(obj);
var objCopy1 = BFSdeepClone(obj);
console.log(objCopy === objCopy1); // 对象类型判断 false 测试通过
console.log(obj.c === objCopy.c); // 对象类型判断 false 测试通过
console.log(obj.c === objCopy1.c); // 对象类型判断 false 测试通过
console.log(obj.b === objCopy1.b); // 函数类型判断 false 测试通过
console.log(obj.b === objCopy.b); // 函数类型判断 false 测试通过
console.log(obj.f === objCopy.f); // 数组类型判断 false 测试通过
console.log(obj.f === objCopy1.f); // 数组类型判断 false 测试通过
console.log(obj.nul, obj.und); // 输出null,undefined 测试通过

// 输入环状数据
// 预期不爆栈且深度复制
let circleObj = {
  foo: {
    name: function() {
      console.log(1);
    },
    bar: {
      name: "bar",
      baz: {
        name: "baz",
        aChild: null //待会让它指向obj.foo
      }
    }
  }
};
circleObj.foo.bar.baz.aChild = circleObj.foo;
var circleObjCopy = DFSdeepClone(circleObj);
var circleObjCopy1 = BFSdeepClone(circleObj);
console.log(circleObjCopy, circleObjCopy1); // 测试通过?
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# 第 7 题:ES5/ES6 的继承除了写法以外还有什么区别?

  1. class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
const bar = new Bar(); // it's ok
function Bar() {
  this.bar = 42;
}

const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
  constructor() {
    this.foo = 42;
  }
}
1
2
3
4
5
6
7
8
9
10
11
  1. class 声明内部会启用严格模式。
// 引用一个未声明的变量
function Bar() {
  baz = 42; // it's ok
}
const bar = new Bar();

class Foo {
  constructor() {
    fol = 42; // ReferenceError: fol is not defined
  }
}
const foo = new Foo();
1
2
3
4
5
6
7
8
9
10
11
12
  1. class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
// 引用一个未声明的变量
function Bar() {
  this.bar = 42;
}
Bar.answer = function() {
  return 42;
};
Bar.prototype.print = function() {
  console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']

class Foo {
  constructor() {
    this.foo = 42;
  }
  static answer() {
    return 42;
  }
  print() {
    console.log(this.foo);
  }
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []
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
  1. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
function Bar() {
  this.bar = 42;
}
Bar.prototype.print = function() {
  console.log(this.bar);
};

const bar = new Bar();
const barPrint = new bar.print(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
  print() {
    console.log(this.foo);
  }
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. 必须使用 new 调用 class。
function Bar() {
  this.bar = 42;
}
const bar = Bar(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
1
2
3
4
5
6
7
8
9
10
11
  1. class 内部无法重写类名。
function Bar() {
  Bar = "Baz"; // it's ok
  this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}

class Foo {
  constructor() {
    this.foo = 42;
    Foo = "Fol"; // TypeError: Assignment to constant variable
  }
}
const foo = new Foo();
Foo = "Fol"; // it's ok
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 第 8 题:setTimeout、Promise、Async/Await 的区别

# 第 9 题:(头条、微医)Async/Await 如何通过同步的方式实现异步

async/await 是一个语法糖,是由 Generator(生成器) 和 自动执行器组成的

async/await -> 自动执行器 + Generator(生成器) -> Iterator(迭代器) ([Symbol.iterator]) -> 类似单向链表

一个简单的 Iterator(迭代器)

const myIterator = function(arr) {
  let current = 0;
  return {
    next: function() {
      const done = current >= arr.length;
      const value = !done ? arr[current++] : undefined;
      return {
        value,
        done
      };
    }
  };
};
let obj = {};
const arr = [1, 2, 3, 4];
obj[Symbol.iterator] = function() {
  return myIterator(arr);
};
for (key of obj) {
  console.log(key);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

简单的 Generator(生成器)

# (第 10 题:(头条)异步笔试题)[https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7]

请写出下面代码的运行结果

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
console.log("script start");
setTimeout(function() {
  console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
  console.log("promise1");
  resolve();
}).then(function() {
  console.log("promise2");
});
console.log("script end");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

script start async1 start async2 promise1 script end async1 end promise2 setTimeout

关键点:宏任务微任务

# 变式一

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  //async2做出如下更改:
  new Promise(function(resolve) {
    console.log("promise1");
    resolve();
  }).then(function() {
    console.log("promise2");
  });
}
console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);
async1();

new Promise(function(resolve) {
  console.log("promise3");
  resolve();
}).then(function() {
  console.log("promise4");
});

console.log("script end");
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
29

script start async1 start promise1 promise3 script end promise2 async1 end promise4 setTimeout

# 变式二

async function async1() {
  console.log("async1 start");
  await async2();
  //更改如下:
  setTimeout(function() {
    console.log("setTimeout1");
  }, 0);
}
async function async2() {
  //更改如下:
  setTimeout(function() {
    console.log("setTimeout2");
  }, 0);
}
console.log("script start");

setTimeout(function() {
  console.log("setTimeout3");
}, 0);
async1();

new Promise(function(resolve) {
  console.log("promise1");
  resolve();
}).then(function() {
  console.log("promise2");
});
console.log("script end");
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

script start async1 start promise1 script end promise2 setTimeout3 setTimeout2 setTimeout1

# 变式三

async function a1() {
  console.log("a1 start");
  await a2();
  console.log("a1 end");
}
async function a2() {
  console.log("a2");
}

console.log("script start");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("promise1");
});

a1();

let promise2 = new Promise(resolve => {
  resolve("promise2.then");
  console.log("promise2");
});

promise2.then(res => {
  console.log(res);
  Promise.resolve().then(() => {
    console.log("promise3");
  });
});
console.log("script end");
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
29
30
31
32
33

script start a1 start a2 promise2 script end promise1 a1 end promise2.then promise3 setTimeout

更新时间: 11/8/2019, 4:51:43 PM