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

在react和vue中,数据因用户行为发生改变时,会生成新的虚拟DOM,然后会比较新旧的虚拟DOM,通过diff算法找到区别,key的作用就是通过该唯一的标识,快速定位锁定变化,避免不必要的遍历查找,浪费性能。

但是在vue中对于key的效率又有了分支,因为有时候在没有的key的情况下,diff算法是速度甚至会更快。在没有绑定key值的情况下,而且在遍历一些简单的模板的情况下,由于会发生节点的复用(就地复用),diff算法速度更快,在vue中被称为默认模式。而key存在时,就不存在节点复用了,而是增删节点,相对于简单的模板而言是比较费时的。

但是这种没有key的默认模式存在副作用,例如:在通过表单输入值时,会发生状态错位(示例链接:https://www.jianshu.com/p/4bd5e745ce95

总结:

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

1. 更准确

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

2. 更快

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

 

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

其实际执行的代码是:

['1', '2', '3'].map((item, index) => {
	return parseInt(item, index)
})

返回的值是:

parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN, 3 不是二进制

原因:

parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。

const intValue = parseInt(string[, radix]);

string: 要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。

radix: 一个介于2和36之间的整数(数学系统的基础),表示上述字符串的基数。默认为10。

返回值:返回一个整数或NaN

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

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

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

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

 

节流:高频事件触发,但在 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;
}, 1000);
};
}
function sayHi() {
console.log('节流成功');
}
button.addEventListener('click', throttle(sayHi));

 

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

四者都是ES6提供的新的数据结构类型。

Set:类似于数组,它的特点是成员唯一没有重复值。set本身是一个构造函数,用来生成数据结构。它可以接受一个数组作为参数(或者具有 iterable 接口的其他数据结构),用来初始化。

const s=new Set([1,2,3,4,5,5,5]);
console.log(s)//Set(5) {1, 2, 3, 4, 5}
const s1=new Set("hello");
console.log(s1);//Set(4) {"h", "e", "l", "o"}

WeakSet:和set类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机

制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

Map:JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值

对”的数据结构,Map 比 Object 更合适。

const m=new Map([["name","lily"],["age","18"]]);
console.log(m)//Map(2) {"name" => "lily", "age" => "18"}

let a=new Map([[{d:'3',f:'6'},{e:'5'}]]);
a.set({a:"1",b:"2"},{c:"4"});
console.log(a)
//0: {Object => Object} key: {d: "3", f: "6"} value: {e: "5"}
//1: {Object => Object} key: {a: "1", b: "2"} value: {c: "4"}

WeakMap:它和map的区别类似于set和WeakSet的区别。WeakMap的键名只接受对象,不接受其他类型。键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;

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

答案完全引用自:https://www.cnblogs.com/luoxiaoyi/p/10073154.html

深度优先遍历:

假设给定图G的初态是所有顶点均未曾访问过。在G中任选一顶点v为初始出发点(源点),则深度优先遍历可定义如下:首先访问出发点v,并将其标记为已访问过;然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点(亦称为从源点可达的顶点)均已被访问为止。若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。

图的深度优先遍历类似于树的前序遍历。采用的搜索方法的特点是尽可能先对纵深方向进行搜索。这种搜索方法称为深度优先搜索(Depth-First Search)。相应地,用此方法遍历图就很自然地称之为图的深度优先遍历。

说白了深度优先遍历就是一种不撞南墙不会头的算法,他会把一条路走完之后再回溯到有分叉的节点继续遍历。

如图:

  1. 首先标记点0,然后按字典序寻找未标记的相邻点进行遍历(点1)。
  2. 标记点1,按字典序寻找未标记的相邻点继续遍历(点4)。
  3. 同步骤2,直到遍历到点3,因为与他相邻的点(点0,点6)都被标记过,所以回溯到点6,继续寻找点6的未标记的相邻点继续遍历(点7)。
  4. 标记点7,同步骤3,回溯点6。
  5. 这时点6的所有相邻点都被标记,回溯点4。
  6. 同步骤5,继续回溯到点1。
  7. 按字典序寻找点1的未标记的相邻点继续遍历(点2)。
  8. 同步骤2,遍历点5。
  9. 同步骤5,回溯到点0,此时整个图的点都被遍历过,结束。

 

广度优先遍历:

  1. 从图中某个顶点V0出发,并访问此顶点;
  2. 从V0出发,访问V0的各个未曾访问的邻接点W1,W2,…,Wk;然后,依次从W1,W2,…,Wk出发访问各自未被访问的邻接点;
  3. 重复步骤2,直到全部顶点都被访问为止。

这是一种层层递进的算法,与树的层序遍历类似。

在广度优先搜索时,会从起点开始“一层一层”扩展的方法来遍历,扩展时每发现一个点就将这个点加入到队列,直到整张图都被遍历过位置。

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

(暂无答案)

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

ES5:通过把被继承构造函数的实例赋值给要继承的构造函数的原型对象,是通过原型或构造函数机制来实现的,其实质上是先创建子类的实例对象,然后再将父类的方法添加到this上。

ES6:通过class类来替代了构造函数的写法,其包含有构造方法,类和类之间通过extends来实现继承。()ES6继承机制不同于ES5,它是通过先创建父类的实例对象this,然后再在子类中修改this,所以,子类必须在constructor方法中调用super方法,因为子类没有自己的this对象,而是继承了父类的this对象。

总结:

ES5和ES6继承最大的区别就是在于:

1.ES5先创建子类,在实例化父类并添加到子类this中

2.ES6先创建父类,在实例化子集中通过调用super方法访问父级后,在通过修改this实现继承

 

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

js主线程会不断的循环往复的从任务列表中读取任务,执行任务,这种运行机制称为事件循环(event loop)。

执行异步任务有两种类型,分别是microtasks和macrotasks。microtasks的优先级要高于macrotasks。

每一个event loop都有一个microtasks queue;每一个event loop都有一个或多个macrotasks queue(也称task queue)。

每一次event loop,会先执行microtasks queue,执行完毕之后,会提取macrostaks queue中的一个任务加入到microtasks queue中,继续执行microtasks queue,依次执行下去直到结束。

microtasks 包含以下api:process.nextTick、promise、Object.observe (废弃)、MutationObserver。

macrostaks包含以下api:setTimeout、setImmerdiate、setInterval、I/O、UI 渲染。

由此可见在执行顺序上promise的优先级要高于setTimeout。promise的性能要优于setTimeout,后者会发生两次UI重渲染。

promise本身是同步的立即执行函数,当执行到resolve和reject的时候,此时是异步的操作,会放入到任务队列中。(如下)

console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

async/await是建立在promise之上的,其返回的也是一个promise对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。(如下:)

async function async1(){
console.log('async1 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/await相对于promise的优点如下:

1. 使用async/await可以使代码简洁优雅,不需要书写.then(),不需要新建一个匿名函数处理响应,也不需要再把数据赋值给一个我们其实并不需要的变量。

2.在错误处理上,错误会出现在promise内部,由此需要多嵌套一个try/catch。

3.业务如果需要先获取一个数据,根据该数据来决定是否获取更多的数据。如果使用promise,只能通过嵌套来完成,如果这种业务迭代很深,那么嵌套也会很深(闻到了回调地狱的味道)。

const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
}
else {
console.log(data)
return data
}
})
}

如果用async/await就简单很多,只需要在获取条件数据的方法前面使用await即可。

const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
}
else {
console.log(data)
return data
}
}

 

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

(该题欢迎指正)

async本质也是返回了一个promise对象,而promise对象本身是一个同步立即执行的函数,当执行到.then()时,才会放入到异步队列中,而async当执行到await时,会执行其后面的方法,然后跳出当前async函数体,等待下次被提取。

第 10 题:(头条)异步笔试题

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

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

 

posted on 2019-07-24 15:42  一仓库代码  阅读(439)  评论(1编辑  收藏  举报