mqyqingfeng/frontend-interview-question-and-answer

[汇总]阿里前端攻城狮们写了一份前端面试题答案,请查收(一)

mqyqingfeng opened this issue · 6 comments

前言

话说一周前,我发了这样一条沸点:

于是我真的就建群收集了题目,和团队的同事一起写答案,我们也不图什么,就是想做一件有意义的事情,现在我整理了下我们的回答,有的不一定就是非常具体的回答,但也提供了思路和参考资料,大家看看是否还有什么补充的?

[高德一面]一个 tcp 连接能发几个 http 请求?

如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。

而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限;
同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送

[腾讯一面]Virtual Dom 的优势在哪里?

「Virtual Dom 的优势」其实这道题目面试官更想听到的答案不是上来就说「直接操作/频繁操作 DOM 的性能差」,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天。所以面试官更想听到 VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差。

首先我们需要知道:

DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程)
JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化,
引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。

其次是 VDOM 和真实 DOM 的区别和优化:

  1. 虚拟 DOM 不会立马进行排版与重绘操作
  2. 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
  3. 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部

[字节跳动] common.js 和 es6 中模块引入的区别?

CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。在使用上的差别主要有:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJs 是单个值导出,ES6 Module可以导出多个
  4. CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
  5. CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined

[未知] cookie token 和 session 的区别

这道题绝对不是你回答的点越多就越好。这道题考察的是你对浏览器缓存知识的理解程度,所以你应该回答的是 Cookie、 Session、Token 的产生背景、原理、有什么问题,在回答这个的基础上把差别讲出来。把这些东西答出本质,再加点装逼的东西,再故意拓展讲到你准备的其他内容才是答好这道题的关键,而要理解好这些东西,其实一两天就够了。关于 Cookie,最近还发生了 Chrome80 屏蔽第三方 Cookie 的事件,如果真的问到这个问题,讲到这件事情妥妥的加分项,前提是你对这件事情也有比较深入的了解。关于 Cookie 和这件事情 我写了篇文章 mqyqingfeng/Blog#157 可以看一下。

[头条]如何选择图片格式,例如 png, webp

图片格式 压缩方式 透明度 动画 浏览器兼容 适应场景
JPEG 有损压缩 不支持 不支持 所有 复杂颜色及形状、尤其是照片
GIF 无损压缩 支持 支持 所有 简单颜色,动画
PNG 无损压缩 支持 不支持 所有 需要透明时
APNG 无损压缩 支持 支持 FirefoxSafariiOS Safari 需要半透明效果的动画
WebP 有损压缩 支持 支持 ChromeOperaAndroid ChromeAndroid Browser 复杂颜色及形状浏览器平台可预知
SVG 无损压缩 支持 支持 所有(IE8以上) 简单图形,需要良好的放缩体验需要动态控制图片特效

[未知]首屏和白屏时间如何计算?

首屏时间的计算,可以由 Native WebView 提供的类似 onload 的方法实现,在 ios 下对应的是 webViewDidFinishLoad,在 android 下对应的是onPageFinished事件。

白屏的定义有多种。可以认为“没有任何内容”是白屏,可以认为“网络或服务异常”是白屏,可以认为“数据加载中”是白屏,可以认为“图片加载不出来”是白屏。场景不同,白屏的计算方式就不相同。

方法1:当页面的元素数小于x时,则认为页面白屏。比如“没有任何内容”,可以获取页面的DOM节点数,判断DOM节点数少于某个阈值X,则认为白屏。
方法2:当页面出现业务定义的错误码时,则认为是白屏。比如“网络或服务异常”。
方法3:当页面出现业务定义的特征值时,则认为是白屏。比如“数据加载中”。

[未知]小程序和 H5 有什么区别?

  1. 渲染方式与 H5 不同,小程序一般是通过 Native 原生渲染的,但是小程序同时也支持 web 渲染,如果使用 web 渲染的方式,我们需要初始化一个WebView 组件,然后在 WebView 中加载 H5 页面;

所以当我们开发一个小程序时,通常会使用 hybrid 的方式,即会根据具体情况选择部分功能用小程序原生的代码来开发,部分功能通过 WebView 加载 H5 页面来实现。Native 与 Web 渲染混合使用,以实现项目的最优解;

这里值得注意的是,小程序下,native 方式通常情况下性能要优于 web 方式。

  1. 小程序特有的双线程设计。 H5 下我们所有资源通常都会打到一个 bundle.js 文件里(不考虑分包加载),而小程序编译后的结果会有两个bundle,index.js封装的是小程序项目的 view 层,以及 index.worker.js 封装的是项目的业务逻辑,在运行时,会有两条线程来分别处理这两个bundle,一个是主渲染线程,它负责加载并渲染 index.js 里的内容,另外一个是 Service Worker线 程,它负责执行 index.worker.js 里封装的业务逻辑,这里面会有很多对底层api调用。

[未知]如何判断 0.1 + 0.2 与 0.3 相等?

作为一道面试题,我觉得重要的是要讲出一点其他人一般不会答出来的深度。像这道题,可以从原理和解决方案两个地方作为答题点,最好在编一个案例。大致讲自己遇到过这个问题,于是很好奇深入研究了一下,发现是浮点数精度导致……原理怎样怎样……然后又看了业界的库的源码,然后怎样怎样解决。

关于原理,我专门写了一篇文章 mqyqingfeng/Blog#155 来解释,实际回答的时候,我觉得答出来

  1. 非是 ECMAScript 独有
  2. IEEE754 标准中 64 位的储存格式,比如 11 位存偏移值
  3. 其中涉及的三次精度丢失

就已经 OK 了。

再讲解决方案,这个可以直接搜索到,各种方案都了解一下,比较一下优劣,还可以参考业界的一些库的实现,比如 math.js,不过相关的我并没有看过……

如果还有精力的话,可以从加法再拓展讲讲超出安全值的数字的计算问题。

所以我觉得一能回答出底层实现,二能回答出多种解决方案的优劣,三能拓展讲讲 bignum 的处理,就是一个非常不错的回答了。

[腾讯二面]了解v8引擎吗,一段js代码如何执行的

在执行一段代码时,JS 引擎会首先创建一个执行栈

然后JS引擎会创建一个全局执行上下文,并push到执行栈中, 这个过程JS引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,JS引擎会进入执行阶段,这个过程JS引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值)。

如果这段代码中存在function的声明和调用,那么JS引擎会创建一个函数执行上下文,并push到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,JS引擎会在父函数执行的过程中,将子函数的全局执行上下文push到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。

还有一种特殊情况是,在子函数执行的过程中,父函数已经return了,这种情况下,JS引擎会将父函数的上下文从执行栈中移除,与此同时,JS引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,JS引擎才会将子函数的上下文及闭包一并从执行栈中移除。

最后,JS引擎是单线程的,那么它是如何处理高并发的呢?即当代码中存在异步调用时JS是如何执行的。比如setTimeout或fetch请求都是non-blocking的,当异步调用代码触发时,JS引擎会将需要异步执行的代码移出调用栈,直到等待到返回结果,JS引擎会立即将与之对应的回调函数push进任务队列中等待被调用,当调用(执行)栈中已经没有需要被执行的代码时,JS引擎会立刻将任务队列中的回调函数逐个push进调用栈并执行。这个过程我们也称之为事件循环。

附言:需要更深入的了解JS引擎,必须理解几个概念,执行上下文,闭包,作用域,作用域链,以及事件循环。建议去网上多看看相关文章,这里推荐一篇非常精彩的博客,对于JS引擎的执行做了图形化的说明,更加便于理解。

https://tylermcginnis.com/ultimate-guide-to-execution-contexts-hoisting-scopes-and-closures-in-javascript/?spm=ata.13261165.0.0.2d8e16798YR8lw

不错

var module = new Module();
var exports = module.exports;

太棒啦

最后提到了事件循环但是没有讲到这也做点共贡献:
这是自己总结的文章浅谈Event Loop 欢迎大家一起学习,有写错的地方话,欢迎来我的博客讨论✨。

js是一门单线程的语言,它是通过事件循环机制实现(Event Loop)异步任务和多线程的。事件循环有三部分组成:

  • 调用栈(call Stack)
  • 消息队列(Message Queue)
  • 微任务队列(Microtack Queue)

1、普通函数执行时先放入调用栈中按顺序执行并立即释放。

function foo1(){
	console.log("22")
}
function foo() {
    console.log("11");
	foo1()
    console.log("33")
}
foo()

// 打印顺序是:11 22 33

2、异步函数(setTimeout,setInteval,xhr...)执行时放入消息队列中,执行完调用栈中的任务后执行。

console.log("00");
function foo() {
    console.log("11");
    setTimeout(() => {
        console.log("22")
    }, 0);
    console.log("33")
}				
foo();
console.log("44")
// 打印顺序是:00 11 33 44 22	

3、promiseasyncawait创建的函数先放入到微任务队列中,调用栈清空后立即被执行

综合上面所讲的分析下这些代码。先不看答案自己尝试做一下。

第一阶段,执行调用栈new 一个promise的时候里面的代码立即会执行,所以先输出{1},接着运行函数foo2 ,将setTimeout放到消息队列中,执行函数foo并输出{2},往后执行输出{3},之后将 p.then 放进微任务队列中继续执行函数,输出{4},结束函数foo2执行并销毁。

第二阶段,微任务队列的消息放入调用栈执行,里面第一个任务是第一个 then 所以输出{5},之后执行第二个then 输出{6},这时微任务队列清空。

第三阶段,消息队列的内容放入调用栈执行,输出{7} 并清空带哦用栈。

var p = new Promise(resolve => {
    console.log("我是promise,new的时候执行哦");		// {1}
    resolve(555);
});

function foo() {
    console.log(222);							 // {2}
}

function foo2() {
    setTimeout(() => {
        console.log('我是定时器哦')				// {7}
    }, 0);
    foo();										
    console.log(444)							// {3}
    p.then(resolve=>{
        console.log(resolve)					// {5}
    }).then(()=>{
        console.log(666)						// {6}
    });
    console.log("foo2运行结束")					// {4}
}
foo2();

// 输出👇
// 我是promise,new的时候执行哦
// 222
// foo2运行结束
// 555
// 666
// 我是定时器哦
1135 commented

HTTP 2.0 多路复用