大厂面试题第二部分
https://github.com/Advanced-Frontend/Daily-Interview-Question/blob/master/datum/summary.md
第 31 - 40 题
第 30 题:两个数组合并成一个数组
请把两个数组 [‘A1’, ‘A2’, ‘B1’, ‘B2’, ‘C1’, ‘C2’, ‘D1’, ‘D2’] 和 [‘A’, ‘B’, ‘C’, ‘D’],合并为 [‘A1’, ‘A2’, ‘A’, ‘B1’, ‘B2’, ‘B’, ‘C1’, ‘C2’, ‘C’, ‘D1’, ‘D2’, ‘D’]。
自己解法:如果两个数组已经排序,那么比较当前第一项,然后依次加入到新数组即可。如果没有排序,那么直接 concat ,然后排序一下。是否更改原始数组,还是返回一个新的数组。
解析: 第 30 题
let a1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
let a2 = ['A', 'B', 'C', 'D'].map((item) => {
return item + 3
})
let a3 = [...a1, ...a2].sort().map((item) => {
if(item.includes('3')){
return item.split('')[0]
}
return item
})
第 31 题:改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
自己想法:这里考察同步和异步。循环结束后,i已经变成9,所以这里输出的都是9。解决方法:直接把异步去掉,原生输出0-9。如果必须使用异步实现,那么可以写一个异步函数 async await 确保执行过程是异步的。(没有注意到题目需要每个1000实现)
for (var i = 0; i< 10; i++){
console.log(i);
}
async function fn() {
for (let i = 0; i < 10; i++) {
await console.log(i);
}
}
fn();
或者单独写一个函数
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
async function fn() {
for (var i = 0; i < 10; i++){
await sleep(1000);
console.log(i);
}
}
fn();
官方 https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/43
利用 setTimeout 函数的第三个参数,会作为回调函数的第一个参数传入
for (var i = 0; i < 10; i++) {
setTimeout(i => {
console.log(i);
}, 1000, i)
}
或者使用 let
for (let i = 0; i < 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
第 32 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。
自己:虚拟DOM应该没有原生DOM直接操作快。例如React,也需要通过原生的DOM方法操作节点。相对于手动操作DOM,React主要实现了DIFF算法,通过比较DOM树中节点的前后变化,部分渲染节点,所以更便捷。
解析:第 32 题
1. 原生 DOM 操作 vs. 通过框架封装操作。
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。
2. 对 React 的 Virtual DOM 的误解。
React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。
我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:
- innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
- Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去写你的应用。
3. MVVM vs. Virtual DOM
相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任何变动都有固定的 O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依赖收集,在 js 和 DOM 层面都是 O(change):
- 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
- 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要昂贵很多。这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,那么就不需要做无用功。
Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,你也可以直接 track by $index 来进行 “原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话,后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)
顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上和 track-by 是一回事。
4. 性能比较也要看场合
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。
- 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
- 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
- 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化
不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。
5. 总结
以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架的内置实现自己搞一个。
附上尤大的回答链接 链接:https://www.zhihu.com/question/31809713/answer/53544875
第 33 题:下面的代码打印什么内容,为什么?
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
自己:闭包函数自执行。默认函数内部打印B时,首先在内部寻找是否存在B,已经找到,那么就打印20。但是这里 b = 20,并不是 let b = 20。
1打印结果内容如下:
ƒ b() {
b = 20;
console.log(b)
}
2原因: 作用域:执行上下文中包含作用于链: 在理解作用域链之前,先介绍一下作用域,作用域可以理解为执行上下文中申明的变量和作用的范围;包括块级作用域/函数作用域; 特性:声明提前:一个声明在函数体内都是可见的,函数声明优先于变量声明; 在非匿名自执行函数中,函数变量为只读状态无法修改;
这个没看懂,为什么会把这个函数打印出来。
第 34 题:简单改造下面的代码,使之分别打印 10 和 20。
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
解析:第 34 题
自己:打印20很简单
// 20
var b = 10;
(function b(){
var b = 20;
console.log(b);
})();
// 打印10
var b = 10;
function fn(){
console.log(b);
}
fn();
官方:
打印10
var b = 10; (function b(b) { window.b = 20; console.log(b) })(b)
或者
var b = 10; (function b(b) { b.b = 20; console.log(b) })(b)
2)打印20
var b = 10; (function b(b) { b = 20; console.log(b) })(b)
或
var b = 10; (function b() { var b = 20; console.log(b) })()
第 35 题:浏览器缓存读取规则
可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache中?
深入理解浏览器的缓存机制 https://www.jianshu.com/p/54cc04190252
解析:第 35 题
第 36 题:使用迭代的方式实现 flatten 函数。
解析:第 36 题
递归和迭代是都相同?
迭代是将输出做为输入,再次进行处理。比如将摄像头对着显示器;比如镜子对着镜子;比如KTV中将麦克对着音箱;比如机关枪扣动扳机发射子弹后,利用后座力继续扣动扳机。 用程序表述就是:for (int i=0; i < 100; i++) n = f(n);
递归,简讲就是自己调用自己,自己包含自己。 用程序表述就是:void f(int n){f(n - 1);}不要在意这是死循环代码,只需知道这个函数中,又调用了函数自身,属于自己调用自己。
https://www.jianshu.com/p/32bcc45efd32
自己:可以使用递归实现,迭代试试吧
// 递归:循环一个数组,如果内部元素还是数组,那么递归实现flat功能。
function flat(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result.push(...flat(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
官方
const flatten = function (arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr
}
const flatten = array => array.reduce((acc, cur) => (Array.isArray(cur) ? [...acc, ...flatten(cur)] : [...acc, cur]), [])
第 37 题:为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?
解析:第 37 题
vue用的不是很多,所以不是很清楚mutation里面为什么不能有异步操作,下面解释一下为什么Redux的reducer里不能有异步操作。
- 先从Redux的设计层面来解释为什么Reducer必须是纯函数
如果你经常用React+Redux开发,那么就应该了解Redux的设计初衷。Redux的设计参考了Flux的模式,作者希望以此来实现时间旅行,保存应用的历史状态,实现应用状态的可预测。所以整个Redux都是函数式编程的范式,要求reducer是纯函数也是自然而然的事情,使用纯函数才能保证相同的输入得到相同的输入,保证状态的可预测。所以Redux有三大原则:
- 单一数据源,也就是state
- state 是只读,Redux并没有暴露出直接修改state的接口,必须通过action来触发修改
-
使用纯函数来修改state,reducer必须是纯函数
-
下面在从代码层面来解释为什么reducer必须是纯函数
那么reducer到底干了件什么事,在Redux的源码中只用了一行来表示:
currentState = currentReducer(currentState, action)
这一行简单粗暴的在代码层面解释了为什么currentReducer必须是纯函数。currentReducer就是我们在createStore中传入的reducer(至于为什么会加个current有兴趣的可以自己去看源码),reducer是用来计算state的,所以它的返回值必须是state,也就是我们整个应用的状态,而不能是promise之类的。
要在reducer中加入异步的操作,如果你只是单纯想执行异步操作,不会等待异步的返回,那么在reducer中执行的意义是什么。如果想把异步操作的结果反应在state中,首先整个应用的状态将变的不可预测,违背Redux的设计原则,其次,此时的currentState将会是promise之类而不是我们想要的应用状态,根本是行不通的。
其实这个问题应该是Redux中为什么不能有副作用的操作更合适。
第 38 题:(京东)下面代码中 a 在什么情况下会打印 1?
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
解析:第 38 题
自己:这里需要满足,A经过类型变化后,可以等于123三个数字。1比较好实现。23怎么处理?没有想出来。
答案解析 因为==会进行隐式类型转换 所以我们重写toString方法就可以了
var a = {
i: 1,
toString() {
return a.i++;
}
}
if( a == 1 && a == 2 && a == 3 ) {
console.log(1);
}
let a = [1,2,3];
a.toString = a.shift;
if( a == 1 && a == 2 && a == 3 ) {
console.log(1);
}
var a = [1,2,3];
a.join = a.shift;
if(a == 1 && a == 2 && a == 3) {
console.log('1');
}
第 39 题:介绍下 BFC 及其应用。
解析:第 39 题
BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:
- html 根元素
- float 浮动
- 绝对定位
- overflow 不为 visiable
- display 为表格布局或者弹性布局
BFC 主要的作用是:
- 清除浮动
- 防止同一 BFC 容器中的相邻元素间的外边距重叠问题
BFC特性:
- 内部box会在垂直方向,一个接一个地放置。
- Box垂直方向的距离由margin决定,在一个BFC中,两个相邻的块级盒子的垂直外边距会产生折叠。
- 在BFC中,每一个盒子的左外边缘(margin-left)会触碰到容器的左边缘(border-left)(对于从右到左的格式来说,则触碰到右边缘)
- 形成了BFC的区域不会与float box重叠
- 计算BFC高度时,浮动元素也参与计算
生成BFC除了 @webproblem 童鞋所说的还有:行内块元素、网格布局、contain值为layout、content或 strict的元素等。 更多生成BFC的方法:传送门
BFC作用:
- 利用特性4可实现左图右文之类的效果:
<img src='image.png'>
<p>我是超长的文字<p>
img {
float:left
}
p {
overflow:hidden
}
- 利用特性5可以解决浮动元素造成的父元素高度塌陷问题:
<div class='parent'>
<div class='float'>浮动元素</div>
</div>
.parent {
overflow:hidden;
}
.float {
float:left;
}
第 40 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop
如果修改了,Vue 是如何监控到属性的修改并给出警告的。
解析:第 40 题
- 子组件为何不可以修改父组件传递的 Prop 单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。
- 如果修改了,Vue 是如何监控到属性的修改并给出警告的。
if (process.env.NODE_ENV !== 'production') {
var hyphenatedKey = hyphenate(key);
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
vm
);
}
defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
"value. Prop being mutated: \"" + key + "\"",
vm
);
}
});
}
在initProps的时候,在defineReactive时通过判断是否在开发环境,如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改,如果不是,说明此修改来自子组件,触发warning提示。
需要特别注意的是,当你从子组件修改的prop属于基础类型时会触发提示。 这种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。你直接将另一个非基础类型(Object, array)赋值到此key时也会触发提示(但实际上不会影响父组件的数据源), 当你修改object的属性时不会触发提示,并且会修改父组件数据源的数据。
第 41 - 50 题
第 41 题:下面代码输出什么
var a = 10;
(function () {
console.log(a)
a = 5
console.log(window.a)
var a = 20;
console.log(a)
})()
依次输出:undefined -> 10 -> 20
解析:
在立即执行函数中,var a = 20;
语句定义了一个局部变量 a
,由于js的变量声明提升机制,局部变量a
的声明会被提升至立即执行函数的函数体最上方,且由于这样的提升并不包括赋值,因此第一条打印语句会打印undefined
,最后一条语句会打印20
。
由于变量声明提升,a = 5;
这条语句执行时,局部的变量a
已经声明,因此它产生的效果是对局部的变量a
赋值,此时window.a
依旧是最开始赋值的10
,
执行解析步骤: var a = undefined; a = 10; (function () { // 变量提升(预解析) var a = undefined; console.log(a); // 输出undefined a = 5; console.log(window.a); // 找window(全局)对象的a, 输出10 a = 20; console.log(a); // 输出20 })()
解析:第 41题
第 42 题:实现一个 sleep 函数
比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现
const sleep = (time) => {
return new Promise(resolve => setTimeout(resolve, time))
}
sleep(1000).then(() => {
// 这里写你的骚操作
})
//Promise
const sleep = time => {
return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
console.log(1)
})
//Generator
function* sleepGenerator(time) {
yield new Promise(function(resolve,reject){
setTimeout(resolve,time);
})
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})
//async
function sleep(time) {
return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
let out = await sleep(1000);
console.log(1);
return out;
}
output();
//ES5
function sleep(callback,time) {
if(typeof callback === 'function')
setTimeout(callback,time)
}
function output(){
console.log(1);
}
sleep(output,1000);
参考:
解析:第 42 题
第 43 题:使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果
解析:第 43 题
我的答案:
[102, 15, 22, 29, 3, 8]
解析:
根据MDN上对Array.sort()
的解释,默认的排序方法会将数组元素转换为字符串,然后比较字符串中字符的UTF-16编码顺序来进行排序。所以'102'
会排在 '15'
前面。以下是MDN中的解释原文:
The sort() method sorts the elements of an array in place and returns the array. The default sort order is built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.
第 44 题:介绍 HTTPS 握手过程
解析:第 44 题
- 客户端使用https的url访问web服务器,要求与服务器建立ssl连接
- web服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端
- 客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥
- 客户端利用公钥将会话秘钥加密, 并传送给服务端, 服务端利用自己的私钥解密出会话秘钥
- 之后服务器与客户端使用秘钥加密传输
第 45 题:HTTPS 握手过程中,客户端如何验证证书的合法性
解析:第 45 题
1、首先什么是HTTP协议? http协议是超文本传输协议,位于tcp/ip四层模型中的应用层;通过请求/响应的方式在客户端和服务器之间进行通信;但是缺少安全性,http协议信息传输是通过明文的方式传输,不做任何加密,相当于在网络上裸奔;容易被中间人恶意篡改,这种行为叫做中间人攻击; 2、加密通信: 为了安全性,双方可以使用对称加密的方式key进行信息交流,但是这种方式对称加密秘钥也会被拦截,也不够安全,进而还是存在被中间人攻击风险; 于是人们又想出来另外一种方式,使用非对称加密的方式;使用公钥/私钥加解密;通信方A发起通信并携带自己的公钥,接收方B通过公钥来加密对称秘钥;然后发送给发起方A;A通过私钥解密;双发接下来通过对称秘钥来进行加密通信;但是这种方式还是会存在一种安全性;中间人虽然不知道发起方A的私钥,但是可以做到偷天换日,将拦截发起方的公钥key;并将自己生成的一对公/私钥的公钥发送给B;接收方B并不知道公钥已经被偷偷换过;按照之前的流程,B通过公钥加密自己生成的对称加密秘钥key2;发送给A; 这次通信再次被中间人拦截,尽管后面的通信,两者还是用key2通信,但是中间人已经掌握了Key2;可以进行轻松的加解密;还是存在被中间人攻击风险;
3、解决困境:权威的证书颁发机构CA来解决; 3.1制作证书:作为服务端的A,首先把自己的公钥key1发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套自己的公私钥,CA通过自己的私钥来加密key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样使用机构的私钥进行加密;制作完成后,机构将证书发给A; 3.2校验证书真伪:当B向服务端A发起请求通信的时候,A不再直接返回自己的公钥,而是返回一个证书; 说明:各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B只需要知道是哪个权威机构发的证书,使用对应的机构公钥,就可以解密出证书签名;接下来,B使用同样的规则,生成自己的证书签名,如果两个签名是一致的,说明证书是有效的; 签名验证成功后,B就可以再次利用机构的公钥,解密出A的公钥key1;接下来的操作,就是和之前一样的流程了; 3.3:中间人是否会拦截发送假证书到B呢? 因为证书的签名是由服务器端网址等信息生成的,并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;
4、https主要的思想是在http基础上增加了ssl安全层,即以上认证过程;:
第 46 题:输出以下代码执行的结果并解释为什么
var obj = {
'2': 3,
'3': 4,
'length': 2,
'splice': Array.prototype.splice,
'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/76
以下为个人猜想没有确切的理论依据:
push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
根据MDN的说法理解,push
方法应该是根据数组的length
来根据参数给数组创建一个下标为length
的属性,我们可以做以下测试:
根据这个测试我们发现,push
方法影响了数组的length
属性和对应下标的值。 然后,正如楼上所说:
在对象中加入splice属性方法,和length属性后。这个对象变成一个类数组。
这个时候控制台输出的是一个数组,但是实际上它是一个伪数组,并没有数组的其他属性和方法,我们可以通过这种方法验证:
所以我认为题目的解释应该是:
- 使用第一次push,obj对象的push方法设置
obj[2]=1;obj.length+=1
2.使用第二次push,obj对象的push方法设置obj[3]=2;obj.length+=1
3.使用console.log输出的时候,因为obj具有 length 属性和 splice 方法,故将其作为数组进行打印 4.打印时因为数组未设置下标为 0 1 处的值,故打印为empty,主动 obj[0] 获取为 undefined
第一第二步还可以具体解释为:因为每次push只传入了一个参数,所以 obj.length 的长度只增加了 1。push方法本身还可以增加更多参数,详见 MDN
第 47 题:双向绑定和 vuex 是否冲突
解析:第 47 题
官网说的比较详细 https://vuex.vuejs.org/zh/guide/forms.html
在严格模式中使用Vuex,当用户输入时,v-model会试图直接修改属性值,但这个修改不是在mutation中修改的,所以会抛出一个错误。当需要在组件中使用vuex中的state时,有2种解决方案: 1、在input中绑定value(vuex中的state),然后监听input的change或者input事件,在事件回调中调用mutation修改state的值 2、使用带有setter的双向绑定计算属性。见以下例子(来自官方文档):
<input v-model="message">`
`computed: { message: { get () { return this.$store.state.obj.message }, set (value) { this.$store.commit('updateMessage', value) } } }
第 48 题:call 和 apply 的区别是什么,哪个性能更好一些
解析:第 48 题
- Function.prototype.apply和Function.prototype.call 的作用是一样的,区别在于传入参数的不同;
- 第一个参数都是,指定函数体内this的指向;
- 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
- call比apply的性能要好,平常可以多用call, call传入参数的格式正是内部所需要的格式,参考call和apply的性能对比
第 49 题:为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
why?
- 没有跨域问题,一般这种上报数据,代码要写通用的;(排除ajax)
- 不会阻塞页面加载,影响用户的体验,只要new Image对象就好了;(排除JS/CSS文件资源方式上报)
- 在所有图片中,体积最小;(比较PNG/JPG)
补充楼上~以下为参考了网上资料后的一些个人整理:
- 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
- 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
- 跨域友好
- 执行过程无阻塞
- 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
- GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
参考资料: SegmentFault 上的回答 Web beacon Using a Beacon Image for GitHub, Website and Email Analytics 为什么前端监控要用 GIF 打点
第 50 题:(百度)实现 (5).add(3).minus(2) 功能。
例: 5 + 3 - 2,结果为 6
解析:第 50 题
第一个方法处理一般情况,第二个方法处理大数字的加法
Number.prototype.add = function(n) {
return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
return this.valueOf() - n;
};
Number.MAX_SAFE_DIGITS = Number.MAX_SAFE_INTEGER.toString().length-2
Number.prototype.digits = function(){
let result = (this.valueOf().toString().split('.')[1] || '').length
return result > Number.MAX_SAFE_DIGITS ? Number.MAX_SAFE_DIGITS : result
}
Number.prototype.add = function(i=0){
if (typeof i !== 'number') {
throw new Error('请输入正确的数字');
}
const v = this.valueOf();
const thisDigits = this.digits();
const iDigits = i.digits();
const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
const result = (v * baseNum + i * baseNum) / baseNum;
if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}
Number.prototype.minus = function(i=0){
if (typeof i !== 'number') {
throw new Error('请输入正确的数字');
}
const v = this.valueOf();
const thisDigits = this.digits();
const iDigits = i.digits();
const baseNum = Math.pow(10, Math.max(thisDigits, iDigits));
const result = (v * baseNum - i * baseNum) / baseNum;
if(result>0){ return result > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : result }
else{ return result < Number.MIN_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : result }
}
第 51 - 60 题
第 51 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
- Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
-
Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
Object.defineProperty本身有一定的监控到数组下标变化的能力: Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。具体我们可以参考 《记一次思否问答的问题思考:Vue为什么不能检测数组变动》这篇文章,文章底部配图中有尤大大的严肃回复截图; 下方的讨论区也很值得大家下去看一看,有对于 for / forEach / for .. in .. 几个循环方式的讨论。
关于 Vue 3.0 的其他信息我们可以参考 尤大大发布的 Vue 3.0 新特性预览PPT
另外补充一些其他资料给大家:
第 52 题:怎么让一个 div 水平垂直居中
<div class="parent">
<div class="child"></div>
</div>
div.parent {
display: flex;
justify-content: center;
align-items: center;
}
div.parent {
position: relative;
}
div.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 或者 */
div.child {
width: 50px;
height: 10px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -25px;
margin-top: -5px;
}
/* 或 */
div.child {
width: 50px;
height: 10px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
div.parent {
display: grid;
}
div.child {
justify-self: center;
align-self: center;
}
div.parent {
font-size: 0;
text-align: center;
&::before {
content: "";
display: inline-block;
width: 0;
height: 100%;
vertical-align: middle;
}
}
div.child{
display: inline-block;
vertical-align: middle;
}
第 53 题:输出以下代码的执行结果并解释为什么
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x)
console.log(b.x)
结果: undefined {n:2}
首先,a和b同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行a.x,相当于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。之后执行a.x = {n:2}的时候,并不会重新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。 后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。
上面是之前写的解释,最近看周爱民老师的文章的时候,发觉这部分解释有不少地方没说到本质上,有的还是错误的,所以我重新结合老师的文章研究了一下,修改如下: 以这段代码为例:
var a = {n:1};
a.x = a ={n:2};
console.log(a.x);
代码 | 注释 | 补充 |
---|---|---|
a | 计算单值表达式 a,得到 a 的引用 | 这里的 a 是初始 a |
a.x | 将 x 这个标识符作为. 运算符的右操作数,计算表达式 a.x,得到结果值(Result),它是一个 a.x 的“引用” | 这个“引用”当作一个数据结构,通常有 base、name、strict 三个成员。无论x 属性是否存在(这里暂时不存在),a.x 都会被表达为 {"base": a, "name": "x", ...}。而这里的 a 仍然指向旧对象。 |
a | 计算单值表达式 a,得到 a 的引用 | 这里的 a 是初始 a |
a = {n:2} | 赋值操作使得左操作数 a 作为一个引用被覆盖,同时操作完成后返回右操作数 {n:2} | 这里的这个 a 的的确确被覆盖了,这意味着往后通过 a 访问到的只能是新对象。但是,有一个 a 是不会变的,那就是被 a.x 的 Result 保存下来的引用 a,它作为一个当时既存的、不会再改变的结果,仍然指向旧对象。 |
a.x = {n:2} | 指向旧对象的 a 新建了 x 属性,这个属性关联对象 {n:2} | 注意,这里对 a.x 进行了写操作(赋值),直到这次赋值发生的那一刻,才有了为旧对象动态创建 x 属性这个过程。 |
所以,旧对象(丧失了引用的最初对象)和新对象(往后通过 a
可以访问到的那个对象)分别变成:
// 旧对象
a:{
n:1,
x:{n:2}
}
// 新对象
a:{
n:2
}
现在,执行 console.log(a.x)
,这里 a.x
被作为 rhs(右手端) 读取,引擎会开始检索是否真的有 a["x"]
这个东西,因为此时通过 a
能访问到的只能是新对象,它自然是没有 x
属性的,所以打印 undefined
。而且 —— **直到**这次读取发生的那一刻,才有了为新对象**动态创建 x
属性**这个过程。
Note:也就是说,在引擎从左到右计算表达式的过程中,尽管可能遇见类似 a.x
这样本不存在的属性,但无论如何,都会存在 {"base": a, "name": "x", ...}
这样的数据结构,而在后续真正对 x
进行 读写 的时候,这个 x
才会得到创建。
这个代码块所做的事情,实际上是**向旧有对象添加一个指向新对象的属性**,并且如果我们想要在后续仍然持有对旧对象的访问,可以在赋值覆盖之前新建一个指向旧对象的变量。
第 54 题:冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?
function bubbleSort(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
console.log(arr);
}
// 改进冒泡排序
function bubbleSort1(arr) {
let i = arr.length - 1;
while (i > 0) {
let pos = 0;
for (let j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
pos = j;
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
i = pos;
}
console.log(arr);
}
第 55 题:某公司 1 到 12 月份的销售额存在一个对象里面
如下:{1:222, 2:123, 5:888},请把数据处理为如下结构:[222, 123, null, null, 888, null, null, null, null, null, null, null]。
let obj = {1:222, 2:123, 5:888};
const result = Array.from({ length: 12 }).map((_, index) => obj[index + 1] || null);
console.log(result)
解析:第 55 题
第 56 题:要求设计 LazyMan 类,实现以下功能。
LazyMan('Tony');
// Hi I am Tony
LazyMan('Tony').sleep(10).eat('lunch');
// Hi I am Tony
// 等待了10秒...
// I am eating lunch
LazyMan('Tony').eat('lunch').sleep(10).eat('dinner');
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food');
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
class LazyManClass {
constructor(name) {
this.taskList = [];
this.name = name;
console.log(`Hi I am ${this.name}`);
setTimeout(() => {
this.next();
}, 0);
}
eat (name) {
var that = this;
var fn = (function (n) {
return function () {
console.log(`I am eating ${n}`)
that.next();
}
})(name);
this.taskList.push(fn);
return this;
}
sleepFirst (time) {
var that = this;
var fn = (function (t) {
return function () {
setTimeout(() => {
console.log(`等待了${t}秒...`)
that.next();
}, t * 1000);
}
})(time);
this.taskList.unshift(fn);
return this;
}
sleep (time) {
var that = this
var fn = (function (t) {
return function () {
setTimeout(() => {
console.log(`等待了${t}秒...`)
that.next();
}, t * 1000);
}
})(time);
this.taskList.push(fn);
return this;
}
next () {
var fn = this.taskList.shift();
fn && fn();
}
}
function LazyMan(name) {
return new LazyManClass(name);
}
LazyMan('Tony').eat('lunch').eat('dinner').sleepFirst(5).sleep(4).eat('junk food');
第 57 题:分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景。
解析:第 57 题
- display: none (不占空间,不能点击)(场景,显示出原来这里不存在的结构)
- visibility: hidden(占据空间,不能点击)(场景:显示不会导致页面结构发生变动,不会撑开)
- opacity: 0(占据空间,可以点击)(场景:可以跟transition搭配)
补充:株连性 如果祖先元素遭遇某祸害,则其子孙孙无一例外也要遭殃,比如: opacity:0和display:none,若父节点元素应用了opacity:0和display:none,无论其子孙元素如何挣扎都不会再出现在大众视野; 而若父节点元素应用visibility:hidden,子孙元素应用visibility:visible,那么其就会毫无意外的显现出来。
第 58 题:箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:
1、函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
2、不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
3、不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
4、不可以使用 new 命令,因为:
- 没有自己的 this,无法调用 call,apply。
- 没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 proto
new 过程大致是这样的:
function newFunc(father, ...rest) {
var result = {};
result.__proto__ = father.prototype;
var result2 = father.apply(result, rest);
if (
(typeof result2 === 'object' || typeof result2 === 'function') &&
result2 !== null
) {
return result2;
}
return result;
}
第 59 题:给定两个数组,写一个方法来计算它们的交集。
例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]。
解析:第 59 题
这道题不是工程题,是道算法题。求的是两个数组的最长公共子序列 (子序列要求顺序,交集不需要)。所以上面用一个filter一个includes或者indexOf的都是错的。
反例很简单。
var nums1 = [1]
var nums2 = [1,1]
或者
var nums1 = [1,1]
var nums2 = [1]
交集应该是[1]
跑一下你们的方法就能知道错了。
这道题两种思路,空间换时间,或者不用额外空间就提升时间复杂度。
空间换时间的思路是用个Hash
表来存数组1的元素以及出现的个数(此处需要遍历n次,并存一个n级别的空间)。 遍历数组2,发现数组2里有Hash
表里的值就存到Result
数组里,并把Hash
表内该值次数减一(为0之后就Delete)。如果不存在Hash
表里,就跳过。这样时间复杂度就是(m+n)
不用额外空间,就用遍历n的时候,判断值在不在m里,如果在,把m里的该值push到Result
数组里,并将该值从m数组里删掉(用splice)。这样就是不用额外空间,但是提高了时间复杂度。
const intersect = (nums1, nums2) => {
const map = {}
const res = []
for (let n of nums1) {
if (map[n]) {
map[n]++
} else {
map[n] = 1
}
}
for (let n of nums2) {
if (map[n] > 0) {
res.push(n)
map[n]--
}
}
return res
}
第 60 题:已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。
第 61 题:介绍下如何实现 token 加密
jwt举例
- 需要一个secret(随机数)
- 后端利用secret和加密算法(如:HMAC-SHA256)对payload(如账号密码)生成一个字符串(token),返回前端
- 前端每次request在header中带上token
- 后端用同样的算法解密
第 62 题:redux 为什么要把 reducer 设计成纯函数
首先命题应当改一下,中文有歧义,可能改为 “redux中的reducer为什么必须(最好)是纯函数“,我想表达的意思是,redux没有强制你reducer是个纯函数,事实上,没有人能通过框架限制判断一个函数是否是纯函数,所以题目中的'设计成'这个短语貌似在说redux已经把reducer强制规定是纯函数了。这回让你怀疑你对redux的认知。
正文如下
然后说一下为什么reducer最好是纯函数,首先你得看看文档怎么说reducer的作用的,‘接收旧的 state 和 action,返回新的 state’,您可得瞧好咯,他就是起一个对数据做简单处理后返回state的作用,为什么只起这个作用,这时用设计这个词回答这个问题才恰当,因为redux把reducer设计成只负责这个作用。很白痴的问答对吧,所以题目的答案也就简单了,reducer的职责不允许有副作用,副作用简单来说就是不确定性,如果reducer有副作用,那么返回的state就不确定,举个例子,你的reducer就做了一个value = value + 1这个逻辑,然后返回state为{value},ok,这个过程太jr纯了,然后你可能觉得要加个请求来取得value后再加1,那么你的逻辑就是value = getValue() + 1, getValue是个请求函数,返回一个值,这种情况,退一万步讲,如果你的网络请求这次出错,那么getValue就返回的不是一个数值,value就不确定了,所以return的state你也不确定了,前端UI拿到的数据也不确定了,所以就是这个环节引入了副作用,他娘的redux设计好的规范就被你破坏了,redux就没卵用了。到此为止这个问题回答完了,我没有说什么上面几个jr说的教科书的理论,甚至还加了些脏话。请原谅,这只是戏剧需要。
最后我回答下如何解决这个副作用,实际上也很白痴的问题,这里的请求可以放在reducer之前,你先请求,该做出错处理的就做出错处理,等拿到实际数据后在发送action来调用reducer。这样通过前移副作用的方式,使reducer变得纯洁。
第 63 题:如何设计实现无缝轮播
这里说一个不需要clone的方案:
<div class="slide">
<ul>
<li>图片1</li>
<li>图片2</li>
<li>图片3</li>
</ul>
</div>
1、最外层div.slide
定宽、相对定位relative
2、ul
足够宽,最起码li数*li宽度
,这里有个技巧,直接 width: 9999em
,目的是让里面的所有li
一字排开
3、滚动效果通过控制ul
的left
或者transform
来进行滚动效果
4、到了最后一个li
,往后看第一个li
的时:
4.1、准备继续滚动,把最后一个的li
设置为相对定位relative
,left
值为此时此刻相对ul的位置(设置的时候不要带缓动效果),目的是让最后一个li
不动。
4.2、然后把ul的left
或者transform
设为0(这步没有缓动效果),
4.3、然后再正常的开始一样出现第一个li的滚动效果(这步有缓动效果)
4.4、最后等无缝的第一个li
效果完成后,把最后一个li
的left
值复原为0
5、到第一个li
,往前看最后一个li
时,也是和上面同理
具体效果可以看下 汽车之家首页的轮播图效果
第 64 题:模拟实现一个 Promise.finally
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
第 65 题: a.b.c.d 和 a['b']['c']['d'],哪个性能更高?
应该是 a.b.c.d
比 a['b']['c']['d']
性能高点,后者还要考虑 [ ]
中是变量的情况,再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点。 下图是两者的 AST 对比:
第 66 题:ES6 代码转成 ES5 代码的实现思路是什么
解析:第 66 题
将ES6的代码转换为AST语法树,然后再将ES6 AST转为ES5 AST,再将AST转为代码
把 ES6 代码转成 ES5 代码的实现思路可以分成三步:
- 打开冰箱
- 把大象装进去
- 关上冰箱
喔,不对,原谅我开了个玩笑,嗯,有点冷……
回到正题上来,说到 ES6 代码转成 ES5 代码,我们肯定会想到 Babel。所以,我们可以参考 Babel 的实现方式。
那么 Babel 是如何把 ES6 转成 ES5 呢,其大致分为三步:
- 将代码字符串解析成抽象语法树,即所谓的 AST
- 对 AST 进行处理,在这个阶段可以对 ES6 代码进行相应转换,即转成 ES5 代码
- 根据处理后的 AST 再生成代码字符串
基于此,其实我们自己就可以实现一个简单的“编译器”,用于把 ES6 代码转成 ES5。
比如,可以使用 @babel/parser
的 parse
方法,将代码字符串解析成 AST;使用 @babel/core
的 transformFromAstSync
方法,对 AST 进行处理,将其转成 ES5 并生成相应的代码字符串;过程中,可能还需要使用 @babel/traverse
来获取依赖文件等。对此感兴趣的可以看看这个。
如果有误,希望可以帮忙指出来,多谢。
第 67 题:数组编程题
随机生成一个长度为 10 的整数类型的数组,例如 [2, 10, 3, 4, 5, 11, 10, 11, 20],将其排列成一个新数组,要求新数组形式如下,例如 [[2, 3, 4, 5], [10, 11], [20]]。
解析:第 67 题
区间分类
// 得到一个两数之间的随机整数,包括两个数在内
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
}
// 随机生成10个整数数组, 排序, 去重
let initArr = Array.from({ length: 10 }, (v) => { return getRandomIntInclusive(0, 99) });
initArr.sort((a,b) => { return a - b });
initArr = [...(new Set(initArr))];
// 放入hash表
let obj = {};
initArr.map((i) => {
const intNum = Math.floor(i/10);
if (!obj[intNum]) obj[intNum] = [];
obj[intNum].push(i);
})
// 输出结果
const resArr = [];
for(let i in obj) {
resArr.push(obj[i]);
}
console.log(resArr);
第 68 题: 如何解决移动端 Retina 屏 1px 像素问题
解析:第 68 题
1 伪元素 + transform scaleY(.5) 2 border-image 3 background-image 4 box-shadow
问题是 现在已经没有1px的问题了啊。17以前的BUG了吧?
第 69 题: 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC’ 变成 ‘aBc’ 。
解析:第 69 题
function processString (s) {
var arr = s.split('');
var new_arr = arr.map((item) => {
return item === item.toUpperCase() ? item.toLowerCase() : item.toUpperCase();
});
return new_arr.join('');
}
console.log(processString('AbC'));
'AbcDefGh'.replace(/[a-zA-Z]/g,function(a){ return /[a-z]/.test(a)?a.toUpperCase():a.toLowerCase(); });
第 70 题: 介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的
1.当修改了一个或多个文件; 2.文件系统接收更改并通知webpack; 3.webpack重新编译构建一个或多个模块,并通知HMR服务器进行更新; 4.HMR Server 使用webSocket通知HMR runtime 需要更新,HMR运行时通过HTTP请求更新jsonp; 5.HMR运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。