前端面试总结
写React/Vue项目时为什么要在列表组件中写Key,其作用是什么?
key是给每个vnode的唯一ID,依靠key可以更准确,更快的拿到oldVnode中对应的vnode节点。
-
更准确
因为带key就不是就地复用了,在sameNode函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
-
更快
利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。
-
原答案:vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中(建议先了解一下diff算法过程)。
在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
[‘1’,‘2’,‘3’].map(parseInt) what & why?
本题主要是讲JS的映射与解析
早在2013年,加里·伯恩哈德就在微博上发布了以下代码段:
['10','10','10','10','10'].map(parseInt);
// [10, NaN, 2, 3, 4]
parseInt
parseInt()函数解析一个字符串参数,并返回一个指定基数的整数。
const intValue = parseInt(string[, radix]);
string要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用ToString抽象操作)。字符串开头的空白符将会被忽略。
radix一个介于2和36之间的整数,表示上述字符串的基数。默认为10.
返回值返回一个整数或者NaN
parseInt(100); // 100
parseInt(100, 10); // 100
parseInt(100, 2); // 4 -> converts 100 in base 2 to base 10
注意:
在radix为undefined,或者radix为0或者没有指定的情况下,JavaScript作如下处理:
- 如果字符串string以’0x’或者’0X’开头,则基数是16(16进制)
- 如果字符串string以"0"开头,基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript5规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
- 如果字符串string以其他任何值开头,则基数是10(十进制)
map
map()方法创建一个新数组,其结果是该数组中每个元素都调用一个提供的函数后返回的结果。
var new_array = arr.map(function callback(currentValue[,index[, array]]) {
// Return element for new_array
}[, thisArg])
可以看到callback回调函数需要三个参数,我们通常只使用第一个参数(其他两个参数是可选的)。
currentValue是callback数组中正在处理的当前元素。
index可选,是callback数组中正在处理的当前元素的索引。
array可选,是callback map方法被调用的数组。
另外还有thisArg可选,执行callback函数时使用的this值。
const arr = [1,2,3]
arr.map((num) => num + 1) //[2,3,4]
回到题目
['1', '2', '3'].map(parseInt)
对于每个迭代map,parseInt()传递两个参数:字符串和基数。
所以实际执行的代码是:
['1', '2', '3'].map((item, index) => {
return parseInt(item, index)
})
即返回的值分别为:
parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN, 3 不是二进制
所以:
['1', '2', '3'].map(parseInt)
// 1, NaN, NaN
由此,加里·伯恩哈德例子也就很好解释了,这里不再赘述
['10','10','10','10','10'].map(parseInt);
// [10, NaN, 2, 3, 4]
如何实现
如果您实际上想要循环访问字符串数组, 该怎么办? map()然后把它换成数字?使用编号
['10','10','10','10','10'].map(Number);
// [10, 10, 10, 10, 10]
什么是防抖和节流?有什么区别?如何实现?
- 防抖
触发高频事件后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));//防抖
- 节流
高频事件触发,但在N秒内只会执行一次,所以节流会稀释函数的执行频率
- 思路:
每次触发事件都判断当前是否有等待执行的延时函数
function throttle(fn){
let canRun = true; //通过闭包保存一个标记
return function(){
if(!canRun) return; //在函数开头判断标记是否为true,不为true则return
canRun = false; //立即设置为false
setTimeout(() => {
fn.apply(this,argument); //这里使用apply是为了保证sayHi函数里的this指向是input对象(否则会指向window)
//最后再setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了,当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true;
},500);
}
}
function sayHi(e){
console.log(e.target.innerWidth,e.target.innerHeight);
}
window.addEventListener('resize',throttle(sayHi));
介绍下Set、Map、WeakSet和WeakMap的区别?
1.集合(Set)
Set和Map主要的应用场景在于数组重组和数据存储
Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构
ES6新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。
Set本身是一种构造函数,用来生成Set数据结构
new Set([iterable])
举个例子:
const s = new Set();
[1,2,3,4,3,2,1].forEach(x = > s.add(x));
for (let i of s){
console.log(i); //1 2 3 4
}
//去重数组的重复对象
let arr = [1,2,3,4,2,1,2,3];
[...new Set(arr)]; //1 2 3 4
set对象允许你存储任何类型的唯一值,无论是院士值或者是对象引用
向Set加入值的时候,不会发生类型转换,所以’5’和5是两个不同的值。Set内部判断两个值是否不同,使用的算法叫做’Same-value-zero equality’,它类似于精确相等运算符(===),主要的区别是:NAN等于自身,而精确相等运算符认为NAN不等于自身
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set //Set {NaN}
let set1 = new Set();
set1.add(1);
set1.add('1');
console.log([...set1]); //[5,'5']
- Set实例对象
- constructor:构造函数
- size:元素数量
let set = new Set([1,2,3,2,1]);
console.log(set.length); //undefined
console.log(set.size); //3
- Set实例方法
- 操作方法
- add(value):新增,相当于array里的push
- delete(value):存在即删除集合中的value
- has(value):判断集合中是否存在value
- clear():清空集合
let set = new Set();
set.add(1).add(2).add(1);
set.has(1); //true
set.has(3); //false
set.delete(1);
set.has(1); //false
//Array.from方法可以将Set结构转为数组
const items = new Set([1,2,3,2]);
const array = Array.from(items);
console.log(array); //[1,2,3]
//或者
const arr = [...items]
console.log(arr); //[1,2,3]
- 遍历方法(遍历顺序为插入顺序)
- keys():返回一个包含集合中所有键的迭代器
- values():返回一个包含集合中所有值的迭代器
- entries():返回一个包含Set对象中所有元素的键值对迭代器
- forEach(callbackFn,thisArg):用于对集合成员执行callbackFn操作,如果提供了this.Arg参数,回调中的this会是这个参数,没有返回值
let set = new Set([1, 2, 3])
console.log(set.keys()) // SetIterator {1, 2, 3}
console.log(set.values()) // SetIterator {1, 2, 3}
console.log(set.entries()) // SetIterator {1, 2, 3}
for (let item of set.keys()) {
console.log(item);
} // 1 2 3
for (let item of set.entries()) {
console.log(item);
} // [1, 1] [2, 2] [3, 3]
set.forEach((value, key) => {
console.log(key + ' : ' + value)
}) // 1 : 1 2 : 2 3 : 3
console.log([...set]) // [1, 2, 3]
Set可默认遍历,默认迭代器生成函数是values()方法
Set.proptotype[Symbol.iterator] === Set.proptotype.values //true
所以,Set可以使用map、filter方法
let set = new Set([1, 2, 3])
set = new Set([...set].map(item => item * 2))
console.log([...set]) // [2, 4, 6]
set = new Set([...set].filter(item => (item >= 4)))
console.log([...set]) //[4, 6]
因此,Set很容易实现交集(Intersect)、并集(Union)、差集(Difference)
let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])
let intersect = new Set([...set1].filter(value => set2.has(value)))
let union = new Set([...set1, ...set2])
let difference = new Set([...set1].filter(value => !set2.has(value)))
console.log(intersect) // Set {2, 3}
console.log(union) // Set {1, 2, 3, 4}
console.lo![在这里插入图片描述](https://user-images.githubusercontent.com/19721451/54000884-27290900-4184-11e9-92f0-4d19ac6d080b.png)g(difference) // Set {1}
2.WeakSet
WeakSet对象允许你讲弱引用对象储存在一个集合中
WeakSet与Set的区别:
- WeakSet只能储存对象引用,不能存放值,而Set对象都可以
- WeakSet对象中存储的对象值都是被弱引用的,即垃圾回收机制不考虑WeakSet对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于WeakSet中),所以,WeakSet对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet对象是无法被遍历的(ES6规定WeakSet不可遍历),也没有办法拿到它包含的所有元素
属性:
- constructor:构造函数,任何一个具有Iterable接口的对象,都可以作参数
const arr = [[1,2],[3,4]];
const weakset = new WeakSet(arr);
console.log(weakset);
方法:
- add(value):在WeakSet对象中添加一个元素value
- has(value):判断WeakSet对象中是否包含value
- delete(value):删除元素value
- clear():清空所有元素,注意该方法已废弃
var ws = new WeakSet();
var obj = {};
var foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); //true
ws.has(foo); //false
ws.delete(window); //true
ws.has(window); //false
3.字典(Map)
集合与字典的区别:
- 共同点:集合、字典都可以储存不重复的值
- 不同点:集合是以[value,value]的形式储存元素,字典是以[key,value]的形式储存
const m = new Map();
const o = {p:'haha'};
m.set(o,'content');
m.get(o); //content
m.has(o); //true
m.delete(o); //true
m.has(o)l //false
任何具有Iterator接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数,例如:
const set = new Set([
['foo',1],
['bar',2]
]);
const m1 = new Map(set);
m1.get('foo'); //1
const m2 = new Map([['baz',3]]);
const m3 = new Map(m2);
m3.get('baz'); //3
如果读取一个未知的键,则返回Undefined。
new Map().get('a'); //undefined
注意,只有对同一个对象的引用,Map结构才将其视为同一个键。这点要小心。
const map = new Map();
map.set(['a'],555);
map.get(['a']); //undefined
上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get无法读取该键,返回undefined。
由上可知,Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象为键名,就不用担心自己的属性与原作者的属性同名。
如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map将其视为一个键,比如0和-0就是同一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但Map将其视为同一个键。
let map = new Map();
map.set(-0,123);
map.get(+0); //123
map.set(true,1);
map.set('ture',2);
map.get(true); //1
map.set(undefined,3);
map.set(null,4);
map.get(undefined);
map.set(NaN,123);
map.get(NaN); //123
Map的属性及方法
属性:
- constructor:构造函数
- size:返回字典中所包含的元素个数
const map = new Map([
['name', 'An'],
['des', 'JS']
]);
map.size // 2
操作方法:
- set(key, value):向字典中添加新元素
- get(key):通过键查找特定的数值并返回
- has(key):判断字典中是否存在键key、
- delete(key):通过键 key 从字典中移除对应的数据
- clear():将这个字典中的所有元素删除
遍历方法:
- keys():将字典中包含的所有键名以迭代器形式返回
- values():将字典中包含的所有数值以迭代器形式返回
- entries():返回所有成员的迭代器
- forEach():遍历字典的所有成员
const map = new Map([
['name','An'],
['des','JS']
]);
console.log(map.entries()); //MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()); //MapIterator {"name", "des"}
Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
map[Symbol.iterator] === map.entries
// true
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(…)。
对于 forEach ,看一个例子
const reporter = {
report:function(key,value){
console.log("Key: %s, Value: %s", key, value);
}
};
let map = new Map([
['name', 'An'],
['des', 'JS']
]);
map.forEach(function(value,key,map){
this.report(key,value);
},reporter)
// Key: name, Value: An
// Key: des, Value: JS
在这个例子中,forEach方法的回调函数的this,就指向reporter
与其他数据结构相互转换
- Map转Array
const map = new Map([1,1],[2,2],[3,3]);
console.log([...map]); // [[1, 1], [2, 2], [3, 3]]
- Array转Map
const map = new Map([[1, 1], [2, 2], [3, 3]]);
console.log(map); //Map {1 => 1, 2 => 2, 3 => 3}
- Map转Object
因为Object的键名都为字符串,而Map的键名为对象,所以转换的时候回把非字符串键名转换为字符串键名。
function mapToObj(map){
let obj = Object.create(null);
for(let [key,value] of map){
obj[key] = value;
}
return obj;
}
const map = new Map().set('name','An').set('des','JS');
mapToObj(map); //{name:'An',des:'JS'}
- Object转Map
function objToMap(obj){
let map = new Map();
for(let key of Object.keys(obj)){
map.set(key,obj[key]);
}
return map;
}
objToMap({'name': 'An', 'des': 'JS'}); Map {"name" => "An", "des" => "JS"}
- Map转JSON
function mapToJson(map) {
return JSON.stringify([...map])
}
let map = new Map().set('name', 'An').set('des', 'JS')
mapToJson(map) // [["name","An"],["des","JS"]]
- JSON转Map
function jsonToStrMap(jsonStr) {
return objToMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}
4.WeakMap
WeakMap对象是一组键值对的集合,其中的键是弱引用的对象,而值可以是任意
注意,WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用
WeakMap中,每个键对自己所引用的对象都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap的key是不可枚举的。
属性:
方法:
- has(key):判断是否有 key 关联对象
- get(key):返回key关联对象(没有则则返回 undefined)
- set(key):设置一组key关联对象
- delete(key):移除 key 的关联对象
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakMap.set(myElement,{timeClicked:0});
myElement.addEventListener('click',function(){
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
},false)
总结
-
Set
- 成员唯一、无序且不重复
- [value,value],键值与键名是一致的(或者说只有键值,没有键名)
- 可以遍历,方法有:add、delete、has
-
WeakSet
- 成员都是对象
- 成员都是弱引用,可以被垃圾回收机制挥手,可以用来保存DOM节点,不容易造成内存泄露
- 不能遍历,方法有:add、delete、has
-
Map
- 本质上是键值对的集合,类似集合
- 可以遍历,方法很多可以跟各种数据格式转换
-
Weakmap
- 只接受对象作为键名(null除外),不接受其他类型的值作为键名
- 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
- 不能遍历,方法有get、set、has、delete
扩展
//Object
const properties1 = {
'width':1,
'height':1
}
console.log(properties1['width'] ? true : false); //true
//Set
const properties2 = new Set();
properties2.add('width');
properties2.add('height');
console.log(properties2.has('width')); //true
- Object和Map
JS中的对象(object),本质上是键值对的集合(hash结构)
const data = {};
const element = document.getElementByClassName('App');
data[element] = 'metadata';
console.log(data['[object HTMLCollection]']); //'metadata'
但当以一个DOM节点作为对象data的键,对象会被自动转换为字符串[Object HTMLCollection],所以说,Object结构提供了字符串-键对象,Map则提供了值-值的对应。
介绍下深度优先遍历和广度优先遍历,如何实现?
深度优先:
找到一个节点后,把它所有的子节点全部查找出来,常用递归法。
深度优先遍历DFS 与树的先序遍历比较类似。
假设初始状态是DOM树中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点然后依次从它的各个未被访问的邻节点出发深度优先搜索遍历图,直至DOM树中所有和v有路径想通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另一个未被访问的顶点作起始点,重复上述过程,直至途中所有顶点都被访问到为止。
/*深度优先遍历三种实现方式*/
//1
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;
}
//2
let deepTraversall2 = (node) => {
let nodes = [];
if(node !== null){
nodes.push(node);
let children = node.children;
for(let i = 0;i < children.length;i++){
nodes = nodes.concat(deepTraversall2(children[i]));
}
}
return nodes;
}
//3(非递归)
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;
}
广度优先:
找到一个节点后,把它所有的同级兄弟节点都查找出来放在前边,把子节点放在后边。
广度优先遍历 BFS
从DOM树某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得‘先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至DOM树中所有已被访问的顶点的邻接点都被访问到。如果此时DOM树中尚有未被访问的顶点,则需要另一个未曾被访问过的顶点作为新的起始点,重复上述的过程,直至图中所有顶点都被访问到为止。
let widthTraversal = (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;
}
请分别用深度优先思想和广度优先思想实现一个拷贝函数
//工具函数
let _toString = Object.prototype.toString;
let map = {
array: 'Array',
object: 'Object',
function: 'Function',
string: 'String',
null: 'Null',
undefined: 'Undefined',
boolean: 'Boolean',
number: 'Number'
}
let getType = (item) => {
return _toString.call(item).slice(8,-1);
}
let isTypeOf = (item,type) => {
return map[type] && map[type] === getType(item);
}
let getEmpty = (o) => {
if(Object.prototype.toString.call(o) === '[object Object]'){
return {};
}
if(Object.prototype.toString.call(o) === '[object Array]'){
return [];
}
return o;
}
深度优先遍历
function deepCopyDFS(origin){
let stack = [];
let map = new Map(); // 记录出现过的对象,用于处理环
let target = getEmpty(origin);
if(target !== origin){
stack.push([origin, target]);
map.set(origin, target);
}
while(stack.length){
let [ori, tar] = stack.pop();
for(let key in ori){
// 处理环状
if(map.get(ori[key])){
tar[key] = map.get(ori[key]);
continue;
}
tar[key] = getEmpty(ori[key]);
if(tar[key] !== ori[key]){
stack.push([ori[key], tar[key]]);
map.set(ori[key], tar[key]);
}
}
}
return target;
}
广度优先遍历
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
}
function deepCopyBFS(origin){
let queue = [];
let map = new Map(); // 记录出现过的对象,用于处理环
let target = getEmpty(origin);
if(target !== origin){
queue.push([origin, target]);
map.set(origin, target);
}
while(queue.length){
let [ori, tar] = queue.shift();
for(let key in ori){
// 处理环状
if(map.get(ori[key])){
tar[key] = map.get(ori[key]);
continue;
}
tar[key] = getEmpty(ori[key]);
if(tar[key] !== ori[key]){
queue.push([ori[key], tar[key]]);
map.set(ori[key], tar[key]);
}
}
}
return target;
}
ES5/ES6的继承除了写法以为还有什么区别?
- 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;
}
}
- 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();
- 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); // []
- 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
- 必须使用 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'
- 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
setTimeout、Promise、Async/Await 的区别
- setTimiout
console.log('script start');
setTimeout(function(){
console.log('settimeout');
});
console.log('script end');
//输出顺序:script start->script end->settimeout
- Promise
Promise本身是同步的立即执行函数,当在executor中执行resolve或者reject的时候,此时是异步操作,会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印P的时候,是打印的返回结果,一个Promise实例。
console.log('script start');
let promise1 = new Promise(function (resolve){
console.log('promise1');
resolve();
console.log('promise1 end');
}).then(function(){
console.log('primise2');
})
setTimeout(function(){
console.log('settimeout');
})
console.log('script end');
//输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
当JS主线程执行到Promise对象时,
1. promise1.then()的回调就是一个task
2. promise1是resolved或rejected:那这个task就会放入当前事件循环回合的microtask queue
3. promise1是pending:这个task就会放入事件循环的未来某个(可能下一个)回合的microtask queue中
4. setTimeout的回调也是个task,它会被放入macrotask queue即使是0ms的情况
- async await
async function async1(){
console.log('async start');
await async2();
console.log('async1 end');
}
async function async2(){
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
//输出顺序:script start->async1 start->async2->script end->async1 end
async函数返回一个Promise对象,当函数执行的时候,一旦遇到await就会先返回,等触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了async函数体。
举个例子:
async function func1(){
return 1
}
console.log(func1());
很显然,func1的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。
func1().then(res => {
console.log(res); //30
})
await的含义为等待,也就是async函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。
常见异步笔试题
原题
//请写出输出内容
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
*/
这道题主要考察的是事件循环中函数执行顺序的问题,其中包括async,await,setTimeout,Promise函数。下面来说一下本体中涉及到的知识点。
任务队列
首先我们需要明白以下几件事情:
- JS分为同步和异步任务
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件
- 一旦执行栈中所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行
根据规范,事件循环是通过任务队列的机制来进行协调的。一个Event loop中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的task必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise等API便是任务源,而进入任务队列的是它们指定的具体执行任务。
宏任务
macrotask(宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得JS内部macrotask与DOM任务能够有序的执行,会在一个macrotask执行结束后,在下一个macrotask执行开始前,对页面进行重新渲染,流程如下:
macrotask --> 渲染 --> macrotask --> ...
macrotask主要包括:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js环境)
微任务
microtask(微任务),**可以理解是在当前task执行结束后立即执行的任务。**也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
microtask主要包括:Promise.then、MutaionObserver、process.nextTick(Node.js环境)
运行机制
在事件循环中,每进行一次循环操作称为tick,每一次tick的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
流程图如下:
Promise和async中的立即执行
我们知道Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
await做了什么
从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。所以对于本题中的
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');
})
}
回到本题
以上就本道题涉及到的所有相关知识点了,下面我们再回到这道题来一步一步看看怎么回事儿。
-
首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:
-
然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中:
-
script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。
遇到了await时,会将await后面的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log(‘async1 end’)加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。
-
script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。
-
script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。
根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 end和promise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
-
第二轮循环开始,这个时候就会跳回async1函数中执行后面的代码,然后遇到了同步任务 console 语句,直接输出 async1 end。这样第二轮的循环就结束了。(也可以理解为被加入到script任务队列中,所以会先与setTimeout队列执行)
-
第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。
下面改变一下代码:
变式一
在第一个变式中将async2的函数也变成了Promise函数,代码如下:
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');
结果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
在第一次macrotask执行完之后,也就是输出script end之后,会去清理所有microtask。所以会相继输出promise2, async1 end ,promise4,其余不再多说。
变式二
在第二个变式中,我将async1中await后面的代码和async2的代码都改为异步的,代码如下:
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');
结果:
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
在输出为promise2之后,接下来会按照加入setTimeout队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1,所以会按3,2,1的顺序来输出。
变式三
变式三是我在一篇面经中看到的原题,整体来说大同小异,代码如下:
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
(携程)算法手写题
将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})
(滴滴、挖财、微医、海康)JS 异步解决方案的发展历程以及优缺点。
- 回调函数(callback)
setTimeout(() => {
// callback 函数体
}, 1000)
缺点:回调地狱,不能用try catch捕获错误,不能return
回调地狱的根本问题:
- 缺乏顺序性:回调地狱导致的调试困难,和大脑的思维方式不符
- 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
- 嵌套函数过多的话,很难处理错误
ajax('XXX1', () => {
// callback 函数体
ajax('XXX2', () => {
// callback 函数体
ajax('XXX3', () => {
// callback 函数体
})
})
})
优点:解决了同步问题(只要有一个任务耗时长,后面的任务都必须排队等着,会拖延整个程序的执行。)
2. 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 => {
// 操作逻辑
})
缺点:无法取消Promise,错误需要通过回调函数来捕获
3.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()
- Async/await
async/await是异步的终极解决方案
优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用await会导致性能上的降低
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}
下面来看一个await例子:
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
对于以上代码你可能会有疑惑,让我来解释原因:
- 首先函数b先执行,在执行到await10之前变量a还是0,因为await内部实现了generator会保留堆栈中东西,所以这时候a = 0被保存下来
- 因为await是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
- 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。
Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
执行结果是:1243
promise构造函数是同步执行的,then方法是异步执行的
如何实现一个 new
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}
简单讲解一下http2的多路复用
HTTP2采用二进制格式传输,取代了HTTP1.x的文本格式,二进制格式解析更高效。
多路复用代替了HTTP1.x的序列和阻塞机制,所有的相同域名请求都通过同一个TCP连接并发完成。在HTTP1.x中,并发多个请求需要多个TCP连接,浏览器为了控制资源会有6-8个TCP连接都限制。
HTTP2中
- 同域名下所有通信都在单个连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗。
- 单个连接上可以并行交错的请求和响应,之间互不干扰
谈谈你对TCP三次握手四次挥手的理解
A、B 机器正常连接后,B 机器突然重启,问 A 此时处于 TCP 什么状态
A侧的TCP链路状态在未发送任何数据的情况下与等待的时间相关,如果在多个超时值范围以内那么状态为;如果触发了某一个超时的情况那么视情况的不同会有不同的改变。
一般情况下不管是KeepAlive超时还是内核超时,只要出现超时,那么必然会抛出异常,只是这个异常截获的时机会因编码方式的差异而有所不同。(同步异步IO,以及有无使用select、poll、epoll等IO多路复用机制)
React 中 setState 什么时候是同步的,什么时候是异步的?
在React 中,**如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。**所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。
原因:在React的setState函数实现中,会根据一个变量isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state
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、第一次和第二次都是在 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
介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?
1. npm模块安装机制:
发出npm install
查询node_modules目录之中是否已经存在指定模块
* 若存在,不再重新安装
* 若不存在
* npm 向 registry 查询模块压缩包的网址
* 下载压缩包,存放在根目录下的.npm目录里
* 解压压缩包到当前项目的node_modules目录
2. npm实现原理
输入npm install命令并敲下回车后,会经理如下几个阶段(以npm 5.5.1为例):
-
执行工程自身preinstall
当前 npm 工程如果定义了 preinstall 钩子此时会被执行。
-
确定首层依赖模块
首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。
工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。
-
获取模块
获取模块是一个递归的过程,分为以下几步:
- 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
- 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
查找该模块依赖,如果有依赖则回到第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,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。
-
执行工程自身生命周期
当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。
最后一步是生成或更新版本描述文件,npm install 过程完成。