高级前端进阶(id:FrontendGaoji)
作者:木易杨,资深前端工程师,前网易工程师,13K star Daily-Interview-Question 作者
引言
半月刊第三期到来,这段时间 Daily-Interview-Question 新增了 15 道高频面试题,今天就把最近半月汇总的面试题和部分答案发给大家,帮助大家查漏补缺。
更多更全的面试题和答案在下面的项目地址中,点击查看。
项目地址:https://github.com/Advanced-Frontend/Daily-Interview-Question
如果你还没看过半月刊1、2期,点击下面链接查看,直接点击即可。
第 25 题:说说浏览器和 Node 事件循环的区别
浏览器
关于微任务和宏任务在浏览器的执行顺序是这样的:
执行一个task(宏任务)
执行完micro-task队列 (微任务)
如此循环往复下去
浏览器的 task(宏任务)执行顺序在 html#event-loops 里面有讲就不翻译了
常见的 task(宏任务) 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。
Node
Node的事件循环是libuv实现的,引用一张官网的图:
大体的task(宏任务)执行顺序是这样的:
timers定时器:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。
pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
idle, prepare:仅系统内部使用。
poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
check 检测:setImmediate() 回调函数在这里执行。
close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on('close', …)。
微任务和宏任务在Node的执行顺序
Node 10以前:
执行完一个阶段的所有任务
执行完nextTick队列里面的内容
然后执行完微任务队列的内容
Node 11以后:
和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/26
第 26 题:介绍模块化发展历程
可从IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module">
这几个角度考虑。
解答:
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
(function(){
return {
data:[]
}
})()
AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好。
define('./index.js',function(code){
// code 就是index.js 返回的内容
})
CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
CommonJS: nodejs 中自带的模块化。
var fs = require('fs');
UMD:兼容AMD,CommonJS 模块化语法。
webpack(require.ensure):webpack 2.x 版本中的代码分割。
ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。
import a from 'a';
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/28
第 27 题:全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?。
在ES5中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。
var a = 12;
function f(){};
console.log(window.a); // 12
console.log(window.f); // f(){}
但ES6规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
let aa = 1;
const bb = 2;
console.log(window.aa); // undefined
console.log(window.bb); // undefined
在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:
通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中
怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。
let aa = 1;
const bb = 2;
console.log(aa); // 1
console.log(bb); // 2
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/30
第 28 题:cookie 和 token 都存放在 header 中,为什么不会劫持 token?
1、首先token不是防止XSS的,而是为了防止CSRF的;
2、CSRF攻击的原因是浏览器会自动带上cookie,而浏览器不会自动带上token
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/31
第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的
VM 主要做了两件微小的事情:
从 M 到 V 的映射(Data Binding),这样可以大量节省你人肉来 update View 的代码
从 V 到 M 的事件监听(DOM Listeners),这样你的 Model 会随着 View 触发事件而改变
1、M 到 V 实现
做到这件事的第一步是形成类似于:
// template
var tpl = '<p>{{ text }}</p>';
// data
var data = {
text: 'This is some text'
};
// magic process
template(tpl, data); // '<p>This is some text</p>'
中间的 magic process 是模板引擎所做的事情,已经有非常多种模板引擎可供选择
JavaScript templates
当然你比较喜欢造轮子的话也可以自己实现一个
JavaScript template engine in just 20 lines
一个 JavaScript 模板引擎的实现
无论是 Angular 的 $scope
,React 的 state 还是 Vue 的 data 都提供了一个较为核心的 model 对象用来保存模型的状态;它们的模板引擎稍有差别,不过大体思路相似;拿到渲染后的 string 接下来做什么不言而喻了(中间还有很多处理,例如利用 model 的 diff 来最小量更新 view )。
但是仅仅是这样并不够,我们需要知道什么时候来更新 view( 即 render ),一般来说主要的 VM 做了以下几种选择:
VM 实例初始化时
model 动态修改时
其中初始化拿到 model 对象然后 render 没什么好讲的;model 被修改的时候如何监听属性的改变是一个问题,目前有以下几种思路:
借助于 Object 的 observe 方法
自己在 set,以及数组的常用操作里触发 change 事件
手动 setState(),然后在里面触发 change 事件
知道了触发 render 的时机以及如何 render,一个简单的 M 到 V 映射就实现了。
2、V 到 M 实现
从 V 到 M 主要由两类( 虽然本质上都是监听 DOM )构成,一类是用户自定义的 listener, 一类是 VM 自动处理的含有 value 属性元素的 listener
第一类类似于你在 Vue 里用 v-on 时绑定的那样,VM 在实例化得时候可以将所有用户自定义的 listener 一次性代理到根元素上,这些 listener 可以访问到你的 model 对象,这样你就可以在 listener 中改变 model
第二类类似于对含有 v-model 与 value 元素的自动处理,我们期望的是例如在一个输入框内
<input type="text" v-model="message" />
输入值,那么我与之对应的 model 属性 message 也会随之改变,相当于 VM 做了一个默认的 listener,它会监听这些元素的改变然后自动改变 model,具体如何实现相信你也明白了
参考地址:mvvm 中 viewmodel该如何设计?
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/34
第 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']。
解法:
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
})
未完待续,点击查看更多细节: https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/39
第 31 题:改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
解法1:
for (var i = 0; i< 10; i++){
setTimeout((i) => {
console.log(i);
}, 1000,i)
}
解法2:
for (var i = 0; i< 10; i++){
((i) => {
setTimeout(() => {
console.log(i);
}, 1000)
})(i)
}
解法3:
for (let i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/43
第 32 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。
采用尤大大的回答:
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. 性能比较也要看场合
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。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。
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/47
第 33 题:下面的代码打印什么内容,为什么?
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
解答:
函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
对于一个常量进行赋值,在 strict 模式下会报错,非 strict 模式下静默失败。
IIFE中的函数是函数表达式,而不是函数声明。
实际上,有点类似于以下代码,但不完全相同,因为使用const不管在什么模式下,都会TypeError类型的错误
const foo = function () {
foo = 10;
console.log(foo)
}
(foo)() // Uncaught TypeError: Assignment to constant variable.
b 函数是一个相当于用const定义的常量,内部无法进行重新赋值,如果在严格模式下,会报错"Uncaught TypeError: Assignment to constant variable."
例如下面的:
var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/48
第 34 题:简单改造下面的代码,使之分别打印 10 和 20。
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
解法:
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/51
第 35 题:浏览器缓存读取规则
可以分成 Service Worker、Memory Cache、Disk Cache 和 Push Cache,那请求的时候 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache中?
解答:
总的来说:
如果开启了Service Worker首先会从Service Worker中拿
如果新开一个以前打开过的页面缓存会从Disk Cache中拿(前提是命中强缓存)
刷新当前页面时浏览器会根据当前运行环境内存来决定是从 Memory Cache 还是 从Disk Cache中拿(可以看到下图最后几个文件有时候是从 Memory Cache中拿有时候是从Disk Cache中拿)
注意:以上回答全部基于chrome浏览器
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/53
第 36 题:使用迭代的方式实现 flatten 函数。
迭代的实现:
let arr = [1, 2, [3, 4, 5, [6, 7], 8], 9, 10, [11, [12, 13]]]
const flatten = function (arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr
}
console.log(flatten(arr))
递归的实现(ES6简写):
const flatten = array => array.reduce((acc, cur) => (Array.isArray(cur) ? [...acc, ...flatten(cur)] : [...acc, cur]), [])
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/54
第 37 题:为什么 Vuex 的 mutation 和 Redux 的 reducer 中不能做异步操作?
Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/65
第 38 题:下面代码中 a 在什么情况下会打印 1?
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
解法1:利用 toString
let a = {
i: 1,
toString () {
return a.i++
}
}
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
解法2:利用 valueOf
let a = {
i: 1,
valueOf () {
return a.i++
}
}
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
解法3:数组这个就有点妖了
var a = [1,2,3];
a.join = a.shift;
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
解法4:ES6的symbol
let a = {
[Symbol.toPrimitive]: (i => () => ++i) (0)
};
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
解法5:Object.defineProperty
Object.defineProperty(window, 'a', {
get: function() {
return this.value = this.value ? (this.value += 1) : 1;
}
});
if(a == 1 && a == 2 && a == 3) {
console.log(1);
}
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/57