自问自答系列(持续更新!2-26)

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

自答:

要明白key的作用,首先我们要知道列表组件带key与不带key会有什么区别。

前提我们需要知道vue如何更新列表组件的。

在vue的diff函数中。经过交叉对比后,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。

不带key:

  • 就地复用组件:当 Vue 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。比如绑定的数据项发生了改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。以上是vue官网的说法。当不带key时,在比较新旧两个组件是否是同一个组件的过程中会判断成新旧两个组件是同一个组件,因为 old.key 和 new.key 都是 undefined。所以不会重新创建组件和删除组件,只会在组件的属性层面上进行比较和更新。所以可能在某种程度上(创建和删除组件方面)会有渲染性能上的提升。

  • 无法维持组件的状态:因为时就地复用组件,可能在维持组件状态方面会导致不可预知的错误,比如无法维持改组件的动画效果、开关等状态,在现实业务场景中会有更多不可预知的问题。

  • 也有可能带来性能下降:因为是直接就地复用节点,如果修改的组件,需要复用的很多节点,顺序又和原来的完全不同的话,那么创建和删除的节点数量就会比带 key 的时候增加很多,(为什么会很多),性能就会有所下降;

带key:

  • 维持组件的状态:保证组件的复用:因为有 key 唯一标识了组件,在进行新旧节点比较的时候,会在接下来的节点中找到 key 相同的节点去比较,能找到相同的 key 的话就复用节点,不能找到的话就增加或者删除节点。

  • 查找性能上的提升:有 key 的时候,会生成 hash(即对应关系),这样在查找的时候就是 hash 查找了,基本上就是 O(1) 的复杂度。

  • 节点复用带来的性能提升:因为有 key 唯一标识了组件,所以会尽可能多的对组件进行复用(尽管组件顺序不同),那么创建和删除节点数量就会变少,这方面的消耗就会下降,带来性能的提升。

衍生问题:为什么不推荐使用数组的index作为列表组件的key?

官网推荐推荐的使用key,应该理解为“使用唯一id作为key”。因为index作为key,和不带key的效果是一样的。index作为key时,每个列表项的index在变更前后也是一样的,都是直接判断为sameVnode然后复用。

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

自答:

像此类的问题只需要逐个分析函数的参数即可,map函数第一个参数是时候一个callback,而这里相当于parseInt这个函数,callback有三个参数,第一个参数元素本身,第二个参数是下标,第三个参数为执行数组。parseInt第一个参数是需要取整的字符串,第二个参数radix是解析时的基数,是一个介于2-36之间的整数,默认是10。

那么执行’1’的过程就是,parseInt('1',0) === 1 为啥为1呢?因为radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1

那么执行’2’的过程就是,parseInt('2',1) === NaN radix是一个介于2-36之间的整数

那么执行’3’的过程就是,parseInt('3',2) === NaN 没有数的二进制能用3来表示。

返回的结果为[1,NaN,NaN]

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

自答:

防抖:

指定时间内,方法只能执行一次。而这个时间的计算,是从最后一次触发监听事件开始算起。

自己的理解:无数次请求执行方法,如果最后一次请求在规定时间内没有再发生请求,那么以最后一次为准

一般表现为,在一段连续触发的事件中,最终会转化为一次方法执行,就像防止抖动一样,你做一个事,防止你手抖不小心重复干了

场景:如在一个输入框内输入文字,你想在输入停止一段时间过后再去获取数据(如过滤),而不是每输入一个文字就去请求一次,那么这时候你就可以利用防抖,指定keyup事件不断触发的过程中不要重复发请求,到最后一次停止输入再去请求。

函数实现

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
/**
* 防抖函数
* @param {Function} fn - 实际要执行的函数
* @param {Number} wait - 规定在什么时间内执行一次函数,单位是秒
* @param {Boolean} immediate - 是否立即执行,true为立即执行,立即执行指触发监听事件是先执行
* @return {Function} 经过防抖处理后的要执行的函数
*/
function debounce(fn, wait, immediate) {
let timerId = null; // 记录定时器id
wait = +wait || 0; // 如果wait没有传,那么初始化0值
if (typeof fn !== 'function') {
throw new Error('debounce的第一个参数请传入函数');
return;
}
// 防抖后的执行函数
function debounced() {
timerId && clearTimeout(timerId);
// 如果是立即执行
if (immediate) {
// 如果已经过了规定时间,则执行函数 或 第一次触发监听事件
!timerId && fn.apply(this, arguments);
// 规定时间后情况定时器id,表明到达了规定时间
timerId = setTimeout(() => {
timerId = null;
}, wait);
} else { // 延后执行
// 只有到达了规定时间后才会执行fn函数
timerId = setTimeout(() => {
fn.apply(this, arguments);
timerId = null;
}, wait);
}
}
// 手动取消该次设定的防抖时间,取消后当成是“第一次触发”一样
function cancel() {
clearTimeout(timerId);
timerId = null;
}
debounced.cancel = cancel;
return debounced;
}

节流:

指定时间内,方法只能执行一次。而这个时间的计算,是从上次执行方法开始算起。

自己的理解:立马执行第一请求,在一次执行方法请求后的规定时间内,不能再次发出请求,规定时间结束后可再次请求,如果规定时间内再次发出,则重置规定时间。

一般表现为,在一段连续触发的事件中,根据你设定的时间间隔,降低触发频率,重复执行。

场景:如你需要做无限加载,监听到滚动条到达底部就加载更多数据,这时候其实你不必要时时刻刻都执行scroll事件绑定的函数,这样没必要,只要把执行频率降低点同样可以达到效果,节约资源。这就是利用节流。

函数实现

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
/**
* 节流函数
* @param {Function} fn - 实际要执行的函数,对其进行节流处理
* @param {Number} wait - 规定的执行时间间隔
* @param {Object} option - 用于设置节流的函数的触发时机,
* - 默认是{leading: true, trailing: true},表示第一次触发监听事件马上执行,停止后最后也执行一次
* - leading为false时,表示第一次触发不马上执行
* - trailing为false时,表示最后停止触发后不执行
* @return {Function} 返回经过节流处理后的函数
*/
function throttle(fn, wait, option) {
let timerId = null; // 用于记录定时器的id
let lastTime = 0; // 上次触发fn的时间戳
wait = +wait || 0; // 如果wait没有传,那么初始化0值
option = option || {}; // 如果option没有传,那么初始化{}值
if (typeof fn !== 'function') {
throw new Error('throttle的第一个参数请传入函数');
return;
}
if (option.leading === false && option.trailing === false) {
throw new Error('option的leading 和 trailing不能同时为false');
return;
}
// 节流后的执行函数
function throttled() {
let now = +new Date(); // 获取当前时间
// 如果没有上次触发执行时间(即第一次运行),以及leading设置为false
!lastTime && option.leading === false && (lastTime = now);
// 距离到达规定的wait时间剩余时间
let remainingTime = wait - (now - lastTime);
// 条件①:如果到达了规定的间隔时间或用户自己设定了系统时间导致的不合理时间差,则立刻执行一次触发函数
if (remainingTime <= 0 || remainingTime > wait) {
fn.apply(this, arguments);
lastTime = now;
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
// 条件②:如果未达到规定时间,以及要求停止后延迟执行(trailing=false)
} else if(!timerId && option.trailing !== false) {
timerId = setTimeout(() => {
timerId = null;
fn.apply(this, arguments);
lastTime = option.leading === false ? 0 : +new Date();
}, remainingTime);
}
}
// 手动提前终止节流时间,恢复初始状态
function cancel() {
clearTimeout(timerId);
timerId = null;
lastTime = 0;
}
throttled.cancel = cancel;
return throttled;
}

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

自答:

Set(集合)

Set 是一种叫做集合的数据结构,成员是唯一且无序的,没有重复的值。
Set 本身是一种构造函数,用来生成 Set 数据结构。,Set 对象允许你储存任何类型的唯一值,Set 内部判断两个值是否不同的判断类似于精确相等运算符(===),主要的区别是在Set内NaN和NaN是相同的,但是NaN === NaN => false

Set的实例方法:add(value),delete(value),has(value),clear(),keys(),values(),entries(),forEach(callbackFn, thisArg),map(),filter()

WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中。

WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
  • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的引用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素。

WeakSet实例方法:add(value),has(value),delete(value)

1
2
3
4
5
6
7
8
9
10
11
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}

上面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用 WeakSet 的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。

Map(字典)

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值(Map里的key如果是引用类型,看上去像”重复”了一样,实际上不重复是指内存地址的不重复)
  • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(即[[key,value],[key,value]])都可以当作Map构造函数的参数,例如:

Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等(===),Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串'true'则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键(这个点和Set是一样的)。

实例方法:set(key, value),get(key),has(key),delete(key),clear(),keys(),values(),entries(),forEach(callback(value,key,map),thisArg)

WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意。注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

实例方法:has(key),get(key),set(key),delete(key)

总结:

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

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

自答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
parent= {
child_1: {
child_1_1: {
child_1_1_1
},
child_1_2: {
child_1_2_1
},
child_1_3
},
child_2: {
child_2_1,
chhild_2_2,
},
child_3
}

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

遍历结果为

1
2
3
4
5
6
7
8
9
10
11
12
[
child_1,
child_1_1,
child_1_1_1,
child_1_2,
child_1_2_1,
child_1_3,
child_2,
child_2_1,
child_2_2,
child_3
]

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

遍历结果为

1
2
3
4
5
6
7
8
9
10
11
12
13
[
child_1,
child_2,
child_3,
child_1_1,
child_1_2,
child_1_3,
child_2,
child_2_1,
child_2_2,
child_1_1_1,
child_1_2_1
]

深度优先与广度优先经常用在二叉树的搜索上。

二叉树:一种每个节点不能多于有两个儿子的非线性的数据结构。

6、请分别用深度优先思想和广度优先思想实现一个拷贝函数。

7、setTimeout、Promise、Async/Await 的区别

事件循环中分为宏任务队列和微任务队列。参考文章

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

宏任务主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)。

微任务主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

其中settimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;

浏览器和Node 事件循环的区别,主要区别在node10:

Node 10以前:node会先执行完一个阶段的所有任务,将同源的宏任务队列执行完毕后再去清空微任务队列。

Node 11以后:,node与浏览器统一,执行完一个宏任务就会去清空微任务队列

promise本身是同步的立即执行函数,promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行;

async函数表示函数里面可能会有异步方法,await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是微任务。

1
2
3
4
5
6
7
8
9
10
11
12
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
/*等价于*/
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 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
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');

// 运行顺序
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
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
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')

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

8、JS 异步解决方案的发展历程以及优缺点。

回调函数(callback)

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

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

Promise

Promise就是为了解决callback的问题而产生的,本质上感觉还是个回调函数,只是代码顺序让人看着舒服。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

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

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

Generator 生成器函数

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

缺点:不够普及,需要自行调用next方法继续执行。

Async/await

优点:代码清晰,以同步的方式解决了异步请求,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

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

9、Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

Promise 构造函数是同步的,then方法也是同步的,只是then里的回电函数callback是异步的。

10、使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果

[102, 15, 22, 29, 3, 8],根据MDN上对Array.sort()的解释,默认的排序方法会将数组元素转换为字符串,然后比较字符串中字符的UTF-16编码顺序来进行排序。所以’102’ 会排在 ‘15’ 前面

11、new关键词的作用,如何实现一个new

  • 不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
  • 不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
  • 不用 return 临时对象,因为 new 会帮你做;
  • 不要给原型想名字了,因为 new 指定名字为 prototype。
1
2
3
4
5
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}

12、介绍下重绘和回流(Repaint & Reflow),以及如何进行优化

重绘:由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如outline, visibility, color、background-color等,重绘的代价是高昂的,因为浏览器必须验证DOM树上其他节点元素的可见性。

回流:回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。

回流必定会发生重绘,重绘不一定会引发回流。

13、改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。

1
2
3
4
5
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
  • 利用 setTimeout 函数的第三个参数,会作为回调函数的第一个参数传入
1
2
3
4
5
6
7
8
9
10
// 代码1
for (var i = 0; i < 10; i++) {
setTimeout(i => {
console.log(i);
}, 1000, i)
}
// 代码2
for (var i = 0; i < 10; i++) {
setTimeout(console.log, 1000, i)
}
  • 利用 bind 函数部分执行的特性
1
2
3
for (var i = 0; i < 10; i++) {
setTimeout(console.log.bind(Object.create(null), i), 1000)
}
  • 利用 let 变量的特性 — 在每一次 for 循环的过程中,let 声明的变量会在当前的块级作用域里面(for 循环的 body 体,也即两个花括号之间的内容区域)创建一个文法环境(Lexical Environment),该环境里面包括了当前 for 循环过程中的 i
1
2
3
4
5
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000)
}
  • 利用函数自执行的方式,把当前 for 循环过程中的 i 传递进去,构建出块级作用域。IIFE (自运行函数)其实并不属于闭包的范畴。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 代码1
for (var i = 0; i < 10; i++) {
(i => {
setTimeout(() => {
console.log(i);
}, 1000)
})(i)
}
// 代码2
for (var i = 0; i < 10; i++) {
try {
throw new Error(i);
} catch ({
message: i
}) {
setTimeout(() => {
console.log(i);
}, 1000)
}
}
  • 很多其它的方案只是把 console.log(i) 放到一个函数里面,因为 setTimeout 函数的第一个参数只接受函数以及字符串,如果是 js 语句的话,js 引擎应该会自动在该语句外面包裹一层函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1
for (var i = 0; i < 10; i++) {
setTimeout(console.log(i), 1000)
}
// 2
for (var i = 0; i < 10; i++) {
setTimeout((() => {
console.log(i);
})(), 1000)
}
// 3
for (var i = 0; i < 10; i++) {
setTimeout((i => {
console.log(i);
})(i), 1000)
}
// 4
for (var i = 0; i < 10; i++) {
setTimeout((i => {
console.log(i);
}).call(Object.create(null), i), 1000)
}
  • 利用 eval 或者 new Function 执行字符串
1
2
3
4
5
6
7
8
// 1
for (var i = 0; i < 10; i++) {
setTimeout(eval('console.log(i)'), 1000)
}
// 2
for (var i = 0; i < 10; i++) {
setTimeout(new Function('i', 'console.log(i)')(i), 1000)
}

14、下面代码中 a 在什么情况下会打印 1?

1
2
3
4
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}

引用类型在比较运算符时候,隐式转换会调用本类型toString或valueOf方法,所以要将a设置为引用类型,并且重写toString或者valueOf方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = {
i: 1,
toString() {
return a.i++;
}
}

if( a == 1 && a == 2 && a == 3 ) {
console.log(1);
}
// 或者
var a = {num:0};
a.valueOf = function(){
return ++a.num
}
if(a == 1 && a == 2 && a == 3){
console.log(1);
}

15、介绍下 BFC 及其应用。

BFC全场Block Formatting context(格式化上下文) 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。

只要元素满足下面任一条件即可触发 BFC 特性:

  • body 根元素
  • 浮动元素:float 除 none 以外的值
  • 绝对定位元素:position (absolute、fixed)
  • display 为 inline-block、table-cells、flex
  • overflow 除了 visible 以外的值 (hidden、auto、scroll)

16、HTTPS为什么比HTTP安全

HTTP传输为明文传输,相当于在网络世界裸奔,容易被hacker劫持并且串改传输内容,比如一些微信网页会被当地电信商劫持并且插入广告。

而加密传输就是将传输内容经过一种可逆的数学计算后发送给接受方,接受方通过逆计算获得原数据。

把操作数A作为明文,操作数B作为密钥,结果C作为密文。可以看到加密解密运用同一个密钥B,把这种加解密都用同一个密钥的方式叫做对称加密(AES)。但是对称加密有一个问题,这个对称加密用到的密钥怎么互相告知呢?如果在传输真正的数据之前,先把密钥传过去,那Hacker还是能嗅探到,那之后就了无秘密了。

所以有一种更安全的非对称加密(RSA)方式:任何人都可以通过拿到A公开的公钥对内容进行加密,然后只A自己私有的钥匙才能解密还原出原来内容。但是非对称加密由于要经过更多的计算,所以会被认为性能更低,因此我们用它来先协商对称加密的密钥即可,后续真正通信的内容还是用对称加密的手段,提高整体的性能。

上边虽然解决了密钥配送的问题,但是中间人还是可以欺骗双方,只要在Alice像Bob要公钥的时候,Hacker把自己公钥给了Alice,而Alice是不知道这个事情的,以为一直都是Bob跟她在通信。

为了解决这个问题,我们需要证明这个公钥就是Bob给的,所以我们要看Bob的“身份证”。这个身份证就是一个数字证书。数字证书是一个权威组织CA颁发给Bob的,前边说到用公钥进行加密,只有拥有私钥的人才能解密。数字证书有点反过来:用私钥进行加密,用公钥进行解密。CA用自己的私钥对Bob的信息(包含Bob公钥)进行加密,由于Alice无条件信任CA,所以已经提前知道CA的公钥,当她收到Bob证书的时候,只要用CA的公钥对Bob证书内容进行解密,发现能否成功解开(还需要校验完整性),此时说明Bob就是Bob,那之后用证书里边的Bob公钥来走之前的流程,就解决了中间人欺骗这个问题了。

最后还要解决传输的完整性,单向Hash函数可以把输入变成一个定长的输出串,其特点就是无法从这个输出还原回输入内容,并且不同的输入几乎不可能产生相同的输出,即便你要特意去找也非常难找到这样的输入(抗碰撞性),因此Alice只要将明文内容做一个Hash运算得到一个Hash值,并一起加密传递过去给Bob。Hacker即便篡改了内容,Bob解密之后发现拿到的内容以及对应计算出来的Hash值与传递过来的不一致,说明这个包的完整性被破坏了。

一次安全可靠的通信包括以下几点:

  1. 对称加密以及非对称加密来解决:保密性
  2. 数字签名:认证、不可抵赖
  3. 单向Hash算法:完整性

参考文章

所以HTTPS握手过程为:

1、客户端通过https的url访问服务端要求建立ssh连接

2、服务端收到消息后,把自己的公钥和网站证书(证书里一般有数字签名)打包发给客户端。

3、客户端使收到后,会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个会话秘钥。

4、客户端用服务端发过来的的公钥对这个随机产生的会话密钥进行加密发给服务端,服务端再用自己的密钥对其解密获得客户端发过来的会话密钥。(这一步即为非对称加密传输过程)

5、接下来客户端与服务端就可以通过这个会话密钥进行加密传输了(这一步即为对称加密传输过程)。

HTTPS 握手过程中,客户端如何验证证书的合法性?

浏览器都会有内置的根证书,根据证书上写的CA签发机构,找到对应的证书公钥,用此公钥解开数字签名,得到摘要(digest,证书内容的hash值),据此验证证书的合法性。

17、为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?

够完成整个 HTTP 请求+响应(尽管不需要响应内容),请求图片资源不会造成跨域的问题,gif相比其他图片资源又是最小的,一般埋点操作后单不需要做响应(响应码为204,即接受到请求但不做响应),相比XMLHttpRequest 对象发送 GET 请求,性能上更好,因为不会对dom产生影响,所以不会阻塞页面加载,影响用户的体验,只要new Image对象就好了,一般情况下也不需要append到DOM中,通过它的onerror和onload事件来检测发送状态;

1
2
3
4
5
6
7
<script type="text/javascript">
var thisPage = location.href;
var referringPage = (document.referrer) ? document.referrer : "none";
var beacon = new Image();
beacon.src = "http://www.example.com/logger/beacon.gif?page=" + encodeURI(thisPage)
+ "&ref=" + encodeURI(referringPage);
</script>

18、模拟实现一个 Promise.finally

1
2
3
4
5
6
7
Promise.protptype.finally = function(callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(()=>value),
reason => P.reject(callback()).then(()=>{ throw.reason} )
)
}

in运算符会检查到原型链上,hasOwnProperty方法只会检查到实例上

19、用已有的promise实现 Prmoise.all方法

首先需要将输入数组中的所有 Promise 对象均运行起来; 2. 在有 Promise 对象 resolve 后,判断是否所有对象均已 resolve,当所有 Promise 均被 resolve 后进行整体的 resolve;此外,当任何一个 Promise 对象出现 reject 后,直接 reject。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function promiseAll(promises) {
return new Promise(function(resolve, reject) {
if (!isArray(promises)) {
return reject(new TypeError('arguments must be an array'));
}
var resolvedCounter = 0;
var promiseNum = promises.length;
var resolvedValues = new Array(promiseNum);
for (var i = 0; i < promiseNum; i++) {
(function(i) {
Promise.resolve(promises[i]).then(function(value) {
resolvedCounter++
resolvedValues[i] = value
if (resolvedCounter == promiseNum) {
return resolve(resolvedValues)
}
}, function(reason) {
return reject(reason)
})
})(i)
}
})
}

20、用已有的promise实现 Prmoise.race方法

上面的修改一下,不用resolvedCounter == promiseNum判断。

21、闭包的理解

你不知道的JS摘抄:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。(无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包)。本质上无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!

22、精度问题

如何判断0.1+0.2和0.3是否相等?最常见的方法是设置一个误差范围值,通常称为“机器精度”,对于JavaScript来说,这个值通常是2^-52。在ES6开始,该值定义在Number.EPSILON中,ES6之前的版本可以写polyfill:Math.pow(2, -52)。可是使用Number.EPSILON来比较俩个数字是否相等。

1
2
3
4
5
6
function numbersCloseEnoughToEqual(n1,n2) {
return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b );

23、原型链

[[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他对象,通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。

24、Promise

1
2
3
4
5
6
7
// Promise.resolve // 返回一个Promise,状态为fulfilled
// Promise.reject // 返回一个Promise,状态为rekected
// Promise.all // 所有都成功=>fulfilled 有一个失败=>rejected
// Promise.race // 有一个率先改变,就跟着它的状态
// Promise.any // 有一个成功=>fulfilled 全部失败=>rejected
// Promise.allSettled // 无论成功失败,等所有都改变了状态,返回为一个数组包含了每个Promise的值和状态
// Promise.try // 如果是同步代码执行同步的,异步的就返回Promise,和async 函数相似

手写实现一个简单版的Promise

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
let pending = 'pending';
let reject = 'reject';
let resolve = 'resolve';
class Promise {
constructor(fn) {
this.status = pending;
this.value = '';
this.reason = '';
this.resolveCbs = [];
this.rejectCbs = [];
function resolve(value) {
if (this.status !== pending) return;
this.status = resolve;
this.value = value;
this.resolveCbs.map(fn => fn(this.value));
}
function reject(reason) {
if (this.status !== pending) return;
this.status = reject;
this.reason = reason;
this.rejectCbs.map(fn => fn(this.reason));
}

fn(resolve.bind(this), reject.bind(this))
}
then(onResolve, onReject) {
if (this.status === resolve) {
onResolve(this.value)
} else if (this.status === reject) {
onReject(this.reason)
} else if (this.status === pending) {
this.resolveCbs.push(onResolve);
this.rejectCbs.push(onReject);
}
}
}

25、async await

async函数的返回值是 Promise 对象,async函数内部return语句返回的值,会成为then方法回调函数的参数。async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try…catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

26、http2

二进制分帧: HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流

多路复用:多路复用,代替原来的序列和阻塞机制。所有就是请求的都是通过一个 TCP连接并发完成。 HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制,如下图,红色圈出来的请求就因域名链接数已超过限制,而被挂起等待了一段时间。

服务器推送:服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。服务端可以主动推送,客户端也有权利选择是否接收。

头部压缩:HTTP 1.1请求的大小变得越来越大,有时甚至会大于TCP窗口的初始大小,因为它们需要等待带着ACK的响应回来以后才能继续被发送。HTTP/2对消息头采用HPACK(专为http/2头部设计的压缩格式)进行压缩传输,能够节省消息头占用的网络的流量。

27、JS继承的方式

  1. 原型链继承
1
2
3
4
function Cat(){}
Cat.prototype = new Animal()
Cat.prototype = 'cat'
var cat = new Cat();
  1. 构造继承
1
2
3
4
5
function Cat(name){
Animal.call(this);
this.name = name || 'cat';
}
var cat = new Cat();
  1. 实例继承
1
2
3
4
5
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
  1. 拷贝继承
1
2
3
4
5
6
7
function Cat(name) {
var animal = new Animal();
for(var p in animal){
Cat.prototype[p]=animal[p];
}
Cat.prototype.name = name || 'Tom';
}
  1. 组合继承
1
2
3
4
5
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
  1. 寄生组合继承
1
2
3
4
5
6
7
8
9
function Cat(name) {
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
var Super = function(){};
Super.prototype = Animal.prototype;
Cat.prototype = new Super();
})()

28、三次握手,四次挥手

TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接,采用四次挥手关闭一个连接。

三次握手的作用就是双方都能明确自己和对方的收、发能力是正常的。需要三次握手才能确认双方的接收与发送能力是否正常

第一次握手:客户端发送网络包。服务端收到,服务端得出结论:客户端的发送能力、服务端的接受能力是正常的。

第二次握手:服务端发包,客户端收到了,这样客户端得出结论:服务端的接收、发送能留,客户端的接收、发送能力是正常的。

第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力,服务端的发送、接收能力是正常的。

经历了上面三次握手过程,客户端和服务端都确认了自己的接收、发送能力是正常的,之后就可以正常通信了。

四次挥手,TCP连接是双向传输的对等模式,就是说双方都可以同时向对方发送或接收数据,当有一方要关闭连接时,会发送指令告知对方。为什么是四次,是由于TCP的半关闭照成的,半关闭就是TCP提供了连接的一端在结束它的发送后还能接受来自另一段数据的能力。

为什么挥手需要四次:当服务端收到客服端SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,”你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

29、快速排序

先找到一个基准点(一般指数组的中部),然后数组被该基准点分为两部分,依次与该基准点数据比较,如果比它小,放左边;反之,放右边。左右分别用一个空数组去存储比较后的数据,最后递归执行上述操作,直到数组长度 <= 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function quickSort(list) {
if (list.length <= 1) {
return list;
}
let minInd = Math.floor(list.length / 2);
let left = [];
let right = [];
let minVal = list.splice(minInd,1)[0];
for (let i =0; i < list.length; i++) {
if (list[i] < minVal) {
left.push(list[i])
} else {
right.push(list[i])
}
}
return [...quickSort(left), minVal, ...quickSort(right)]
}
我想吃鸡腿!