2万5千字大厂面经

以下面试题来自腾讯、阿里、网易、饿了么、美团、拼多多、百度等等大厂综合起来常考的题目。

如何写一个漂亮的简历

简历不是一份记流水账的东西,而是让用人方了解你的亮点的。

平时有在做一些修改简历的收费服务,也算看过蛮多简历了。很多简历都有如下特征

  • 喜欢说自己的特长、优点,用人方真的不关注你的性格是否阳光等等
  • 个人技能能够占半页的篇幅,而且长得也都差不多
  • 项目经验流水账,比如我会用这个 API 实现了某某功能
  • 简历页数过多,真心看不下去

以上类似简历可以说用人方也看了无数份,完全抓不到你的亮点。除非你呆过大厂或者教育背景不错或者技术栈符合人家要求了,否则基本就是看运气约面试了。

以下是我经常给别人修改简历的意见:

简历页数控制在 2 页以下

  • 技术名词注意大小写
  • 突出个人亮点,扩充内容。比如在项目中如何找到 Bug,解决 Bug 的过程;比如如何发现的性能问题,如何解决性能问题,最终提升了多少性能;比如为何如此选型,目的是什么,较其他有什么优点等等。总体思路就是不写流水账,突出你在项目中具有不错的解决问题的能力和独立思考的能力。
  • 斟酌熟悉、精通等字眼,不要给自己挖坑
  • 确保每一个写上去的技术点自己都能说出点什么,杜绝面试官问你一个技术点,你只能答出会用 API 这种减分的情况

做到以上内容,然后在投递简历的过程中加上一份求职信,对你的求职之路相信能帮上很多忙。

Vuex 源码深度解析

该文章内容节选自团队的开源项目 InterviewMap。项目目前内容包含了 JS、网络、浏览器相关、小程序、性能优化、安全、框架、Git、数据结构、算法等内容,无论是基础还是进阶,亦或是源码解读,你都能在本图谱中得到满意的答案,希望这个面试图谱能够帮助到大家更好的准备面试。

Vuex 思想

在解读源码之前,先来简单了解下 Vuex 的思想。

Vuex 全局维护着一个对象,使用到了单例设计模式。在这个全局对象中,所有属性都是响应式的,任意属性进行了改变,都会造成使用到该属性的组件进行更新。并且只能通过 commit 的方式改变状态,实现了单向数据流模式。

Vuex 解析

Vuex 安装

在看接下来的内容前,推荐本地 clone 一份 Vuex 源码对照着看,便于理解。

在使用 Vuex 之前,我们都需要调用 Vue.use(Vuex) 。在调用 use 的过程中,Vue 会调用到 Vuex 的 install 函数

install 函数作用很简单

  • 确保 Vuex 只安装一次
  • 混入 beforeCreate 钩子函数,可以在组件中使用 this.$store
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export function install (_Vue) {
// 确保 Vuex 只安装一次
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}

// applyMixin
export default function (Vue) {
// 获得 Vue 版本号
const version = Number(Vue.version.split('.')[0])
// Vue 2.0 以上会混入 beforeCreate 函数
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// ...
}
// 作用很简单,就是能让我们在组件中
// 使用到 this.$store
function vuexInit () {
const options = this.$options
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}

深度解析 Vue 响应式原理

Vue 初始化

在 Vue 的初始化中,会先对 props 和 data 进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
Vue.prototype._init = function(options?: Object) {
// ...
// 初始化 props 和 data
initState(vm)
initProvide(vm)
callHook(vm, 'created')

if (vm.$options.el) {
// 挂载组件
vm.$mount(vm.$options.el)
}
}

接下来看下如何初始化 props 和 data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
export function initState (vm: Component) {
// 初始化 props
if (opts.props) initProps(vm, opts.props)
if (opts.data) {
// 初始化 data
initData(vm)
}
}
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// 缓存 key
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// 非根组件的 props 不需要观测
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 验证 prop
const value = validateProp(key, propsOptions, propsData, vm)
// 通过 defineProperty 函数实现双向绑定
defineReactive(props, key, value)
// 可以让 vm._props.x 通过 vm.x 访问
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}

function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (props && hasOwn(props, key)) {
} else if (!isReserved(key)) {
// 可以让 vm._data.x 通过 vm.x 访问
proxy(vm, `_data`, key)
}
}
// 监听 data
observe(data, true /* asRootData */)
}
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果 value 不是对象或者使 VNode 类型就返回
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 使用缓存的对象
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建一个监听者
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 通过 defineProperty 为对象添加 __ob__ 属性,并且配置为不可枚举
// 这样做的意义是对象遍历时不会遍历到 __ob__ 属性
def(value, '__ob__', this)
// 判断类型,不同的类型不同处理
if (Array.isArray(value)) {
// 判断数组是否有原型
// 在该处重写数组的一些方法,因为 Object.defineProperty 函数
// 对于数组的数据变化支持的不好,这部分内容会在下面讲到
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
// 遍历对象,通过 defineProperty 函数实现双向绑定
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 遍历数组,对每一个元素进行观测
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

VueRouter 源码深度解析

路由原理

在解析源码前,先来了解下前端路由的实现原理。
前端路由实现起来其实很简单,本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新。目前单页面使用的路由就只有两种实现方式

  • hash 模式
  • history 模式

www.test.com/#/ 就是 Hash URL,当 # 后面的哈希值发生变化时,不会向服务器请求数据,可以通过 hashchange 事件来监听到 URL 的变化,从而进行跳转页面。

History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美观

深入框架本源系列 —— Virtual Dom

该系列会逐步更新,完整的讲解目前主流框架中底层相通的技术,接下来的代码内容都会更新在 这里

为什么需要 Virtual Dom

众所周知,操作 DOM 是很耗费性能的一件事情,既然如此,我们可以考虑通过 JS 对象来模拟 DOM 对象,毕竟操作 JS 对象比操作 DOM 省时的多。

举个例子

1
2
3
4
// 假设这里模拟一个 ul,其中包含了 5 个 li
[1, 2, 3, 4, 5]
// 这里替换上面的 li
[1, 2, 5, 4]

从上述例子中,我们一眼就可以看出先前的 ul 中的第三个 li 被移除了,四五替换了位置。

如果以上操作对应到 DOM 中,那么就是以下代码

1
2
3
4
5
6
7
8
9
// 删除第三个 li
ul.childNodes[2].remove()
// 将第四个 li 和第五个交换位置
let fromNode = ul.childNodes[4]
let toNode = node.childNodes[3]
let cloneFromNode = fromNode.cloneNode(true)
let cloenToNode = toNode.cloneNode(true)
ul.replaceChild(cloneFromNode, toNode)
ul.replaceChild(cloenToNode, fromNode)

当然在实际操作中,我们还需要给每个节点一个标识,作为判断是同一个节点的依据。所以这也是 Vue 和 React 中官方推荐列表里的节点使用唯一的 key 来保证性能。

那么既然 DOM 对象可以通过 JS 对象来模拟,反之也可以通过 JS 对象来渲染出对应的 DOM

以下是一个 JS 对象模拟 DOM 对象的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
export default class Element {
/**
* @param {String} tag 'div'
* @param {Object} props { class: 'item' }
* @param {Array} children [ Element1, 'text']
* @param {String} key option
*/
constructor(tag, props, children, key) {
this.tag = tag
this.props = props
if (Array.isArray(children)) {
this.children = children
} else if (isString(children)) {
this.key = children
this.children = null
}
if (key) this.key = key
}
// 渲染
render() {
let root = this._createElement(
this.tag,
this.props,
this.children,
this.key
)
document.body.appendChild(root)
return root
}
create() {
return this._createElement(this.tag, this.props, this.children, this.key)
}
// 创建节点
_createElement(tag, props, child, key) {
// 通过 tag 创建节点
let el = document.createElement(tag)
// 设置节点属性
for (const key in props) {
if (props.hasOwnProperty(key)) {
const value = props[key]
el.setAttribute(key, value)
}
}
if (key) {
el.setAttribute('key', key)
}
// 递归添加子节点
if (child) {
child.forEach(element => {
let child
if (element instanceof Element) {
child = this._createElement(
element.tag,
element.props,
element.children,
element.key
)
} else {
child = document.createTextNode(element)
}
el.appendChild(child)
})
}
return el
}
}

如何正确的使用你的时间

你是否时常会焦虑时间过的很快,没时间学习,本文将会分享一些个人的见解。

花时间补基础,读文档

在工作中我们时常会花很多时间去 debug,但是你是否发现很多问题最终只是你基础不扎实或者文档没有仔细看。

基础是你技术的基石,一定要花时间打好基础,而不是追各种新的技术。一旦你的基础扎实,学习各种新的技术也肯定不在话下,因为新的技术,究其根本都是相通的。

文档同样也是一门技术的基础。一个优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

学会搜索

如果你还在使用百度搜索编程问题,请尽快抛弃这个垃圾搜索引擎。同样一个关键字,使用百度和谷歌,谷歌基本完胜的。即使你使用中文在谷歌中搜索,得到的结果也往往是谷歌占优,所以如果你想迅速的通过搜索引擎来解决问题,那一定是谷歌。

几道高级前端面试题解析

为什么 0.1 + 0.2 != 0.3,请详述理由

因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

1
2
// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)

那么如何得到这个二进制的呢,我们可以来演算下

小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。

所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

下面说一下原生解决办法,如下代码所示

1
parseFloat((0.1 + 0.2).toFixed(10))

深度解析原型中的各个难点

本文不会过多介绍基础知识,而是把重点放在原型的各个难点上。

大家可以先仔细分析下该图,然后让我们进入主题

prototype

首先来介绍下 prototype 属性。这是一个显式原型属性,只有函数才拥有该属性。基本上所有函数都有这个属性,但是也有一个例外

1
let fun = Function.prototype.bind()

如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype 属性的。

prototype 如何产生的

当我们声明一个函数时,这个属性就被自动创建了。

1
function Foo() {}

并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor

constructor 对应着构造函数,也就是 Foo

Redux 源码深度解析

前言

同步更新在 我的Github

在进入正题前,我们首先来看一下在项目中是如何使用 Redux 的,根据使用步骤来讲解源码。以 我开源的 React 项目 为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 首先把多个 reducer 通过 combineReducers 组合在一起
const appReducer = combineReducers({
user: UserReducer,
goods: GoodsReducer,
order: OrdersReducer,
chat: ChatReducer
});
// 然后将 appReducer 传入 createStore,并且通过 applyMiddleware 使用了中间件 thunkMiddleware
// replaceReducer 实现热更新替换
// 然后在需要的地方发起 dispatch(action) 引起 state 改变
export default function configureStore() {
const store = createStore(
rootReducer,
compose(
applyMiddleware(thunkMiddleware),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
);

if (module.hot) {
module.hot.accept("../reducers", () => {
const nextRootReducer = require("../reducers/index");
store.replaceReducer(nextRootReducer);
});
}

return store;
}

介绍完了使用步骤,接下来进入正题。

明白 JS 模块化

执行环境(Execution context)

var 和 let 的正确解释

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。

接下来让我们看一个老生常谈的例子,var

1
2
3
4
5
6
7
8
b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
console.log('call b')
}

想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

1
2
3
4
5
6
7
8
9
b() // call b second

function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'

var 会产生很多错误,所以在 ES6中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用。