前端每天10问 ------ 1-10


前端每天10问 —— 1-10

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

更详细的参考资料请移步:

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

2.解析vue2.0的diff算法

A:不带key时节点能够就地复用,省去了销毁/创建组件的开销,同时只需要修改DOM文本内容而不是移除/添加节点,这就是文档中所说的“刻意依赖默认行为以获取性能上的提升”。但是key的作用是什么呢?
(1)更准确

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

(2) 更快

利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。

(3) 保证组件状态正确

使用唯一id作为key

当新旧节点的头头、尾尾、头尾、尾头对比都没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。

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
// vue项目  src/core/vdom/patch.js  -448行
// 以下是为了阅读性进行格式化后的代码

// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
// map 方式获取
idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
// 遍历方式获取
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}
//创建map
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
//遍历寻找,sameVnode 是对比新旧节点是否相同的函数
function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}

2.Q:[‘1’, ‘2’, ‘3’].map(parseInt) what & why ?

A:[1, NaN, NaN],map函数的第一个参数callback,这个callback一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。parseInt则是用来解析字符串的,使字符串成为指定基数的整数。parseInt(string, radix)接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。

解析:

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

3.Q:什么是防抖和节流?有什么区别?如何实现?

A:
  1. 防抖

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

1
2
3
4
5
6
7
8
9
10
11
function debounce(fn,delay){
let timer=null,context,args;
return function(){
context=this;
args=[...arguments];
if(timer) clearTimeout(timer);
timer=setTimeout(function(){
fn.apply(context,args)
},delay);
}
}

​ 2.节流

隔一段时间,执行一次,所以节流会稀释函数的执行频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn,delay){
let timer=null,context,args;
return function(){
context=this;
args=[...arguments];
if(!timer){
timer=setTimeout(function(){
timer=null;
fn.apply(context,args);
},delay);
}
}
}

4.Q:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

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

5.Q:介绍下深度优先遍历和广度优先遍历,如何实现?

A:

深度优先遍历:对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。

广度优先遍历:又叫层次遍历,从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有结点可以访问为止。

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
//HTML
<div class="parent">
<div class="child-1">
<div class="child-1-1">
<div class="child-1-1-1">
a
</div>
</div>
<div class="child-1-2">
<div class="child-1-2-1">
b
</div>
</div>
<div class="child-1-3">
C
</div>
</div>
<div class="child-2">
<div class="child-2-1">
d
</div>
<div class="child-2-2">
e
</div>
</div>
<div class="child-3">
<div class="child-3-1">
f
</div>
</div>
</div>
//DFS -----> 一般借助栈完成
const deepTraversal=(node) => {
let [nodeList, stack] = [[],[]];
let parent, children;
if (node) {
stack.push(node);
while (stack.length !== 0) {
parent = stack.pop();
nodeList.push(parent);
children = parent.children;
for (let i = children.length - 1; i >= 0; i--) { //先压入右孩子再压入左孩子
stack.push(children[i]);
}
}
}
return nodeList;
}
//BFS ---->一般借助队列完成
const wideTraversal = (node) => {
let [nodeList, queue] = [
[],
[]
];
let parent, children;
if (node) {
queue.push(node);
while (queue.length !== 0) {
parent = queue.shift();
nodeList.push(parent);
children = parent.children;
for (let i = 0; i < children.length; i++) {
queue.push(children[i]);
}
}
}
return nodeList;
}

6.Q:ES5/ES6 的继承除了写法以外还有什么区别?

A:Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的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
27
28
29
//ES6
class Super {}
class Sub extends Super {}

const sub = new Sub();
Sub.__proto__ === Super;//子类可以直接通过__proto__ 寻址到父类。
Sub.prototype.__proto__ === Super.prototype;
//ES6继承原理
class A {}
class B extends A {}
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);//--->B.prototype.__proto__= A.prototype
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);//--->B.__proto__=A

//ES5
function Super() {}
function Sub() {}

Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

var sub = new Sub();
Sub.__proto__ === Function.prototype;
Sub.prototype.__proto__ === Super.prototype

7.Q: 请写出下面代码的运行结果(setTimeout、Promise、Async/Await 的区别)

更详细的资料请移步:

从一道题浅说 JavaScript 的事件循环

重点:await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

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
76
77
78
79
80
81
82
//原式
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');

//变式一
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');

//变式二
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');
A:
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
//原式
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

//变式一
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout

//变式二
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1

8. Q:

已知如下数组:

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

A:
1
2
3
4
5
6
7
function handleArr(arr){
let newArr = Array.from(new Set(arr.flat(Infinity)));
newArr.sort(function(a,b){
return a-b;
});
return newArr;
}

9.Q:JS 异步解决方案的发展历程以及优缺点。

A:

参考:JS 异步解决方案的发展历程以及优缺点。


10.Q:情人节福利题,如何实现一个 new

A:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function New(){
//创建一个新对象
let obj={};
let constructor=[...arguments].shift();
//链接到原型
obj.__proto=constructor.prototype;
//执行构造函数,传递参数,改变this指向
constructor.apply(obj,[...arguments].slice(1));
//返回对象
return obj;
}
//更精简版
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}