前言
当你老了,回顾一生,就会发觉:什么时候出国读书,什么时候决定做第一份职业,何时选定对象而恋爱,什么时候结婚,其实都是命运的巨变。只是当时站在三岔路口,眼见风云千樯,你做出选择的那一日,在日记上,相当沉闷和平凡,当时还以为是生命中普通的一天。
一个改变面试的项目 -- 面试图谱。
金九银十的秋招季近在眼前,想必大家也都心痒难耐,准备挑战更好的工作机会。那么,面试肯定是最大的挑战。
对于面试来说,平时的积累肯定是必须的,但是在面试前的准备也是至关重要的。
在几月前我个人组建了一个小团队,花了将近半年的时间寻找大厂的面试题,筛选出了近百个知识点然后成文,并全部翻译为英文。今天,终于开源出了第一个版本,目前总字数已高达 10 余万字。团队的每个成员都是来自一线的工程师,其中还有一个来自谷歌的大佬。
可以这样说,我们有自信,把这个项目打造成关于面试相关中的 NO 1. 因为所有内容都没有照搬任何的书籍,每一个知识点都有详细的查阅过资料才成文。后期更会加入更多的内容,尽可能地覆盖大部分知识点。
我们认为,一味的背面试题是没多大作用的。只有熟悉了各个知识点并融会贯通,才能在面试中披荆斩棘。本图谱目前包含了近百个高频知识点,无论是面试前的准备还是平时学习中的查漏补缺,我们相信肯定能帮助到大家。目前内容包含了 JS、网络、浏览器相关、性能优化、安全、框架、Git、数据结构、算法等内容,无论是基础还是进阶,亦或是源码解读,你都能在本图谱中得到满意的答案,希望这个面试图谱能够帮助到大家更好的准备面试。
该仓库内容会持续更新,后期将会包含更多的内容,比如:系统设计、区块链、运维、后端等等,当然这些不是我的强项,我会邀请这方面有不错经验的朋友来书写内容。
大纲
求职
最近本人在寻找工作机会,如果有杭州的不错岗位的话,欢迎联系我 zx597813039@gmail.com。
部分内容预览
MVVM
MVVM 由以下三个内容组成
- View:界面
- Model:数据模型
- ViewModel:作为桥梁负责沟通 View 和 Model
在 JQuery 时期,如果需要刷新 UI 时,需要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。
在 MVVM 中,UI 是通过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 如果改变,也会改变对应的数据。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。
在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。
脏数据检测
当触发了指定事件后会进入脏数据检测,这时会调用 $digest
循环遍历所有的数据观察者,判断当前值是否和先前的值有区别,如果检测到变化的话,会调用 $watch
函数,然后再次调用 $digest
循环直到发现没有变化。循环至少为二次 ,至多为十次。
脏数据检测虽然存在低效的问题,但是不关心数据是通过什么方式改变的,都可以完成任务,但是这在 Vue 中的双向绑定是存在问题的。并且脏数据检测可以实现批量检测出更新的值,再去统一更新 UI,大大减少了操作 DOM 的次数。所以低效也是相对的,这就仁者见仁智者见智了。
数据劫持
Vue 内部使用了 Obeject.defineProperty()
来实现双向绑定,通过这个函数可以监听到 set
和 get
的事件。
var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
}
})
}
以上代码简单的实现了如何监听数据的 set
和 get
的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅
<div>
{{name}}
</div>
在解析如上模板代码时,遇到 {{name}}
就会给属性 name
添加发布订阅。
// 通过 Dep 解耦
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
// sub 是 Watcher 实例
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null
function update(value) {
document.querySelector('div').innerText = value
}
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.cb(this.value)
}
}
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'
接下来,对 defineReactive
函数进行改造
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
// 执行 watcher 的 update 方法
dp.notify()
}
})
}
以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加。
Proxy 与 Obeject.defineProperty 对比
Obeject.defineProperty
虽然已经能够实现双向绑定了,但是他还是有缺陷的。
- 只能对属性进行数据劫持,所以需要深度遍历整个对象
- 对于数组不能监听到数据的变化
虽然 Vue 中确实能检测到数组数据的变化,但是其实是使用了 hack 的办法,并且也是有缺陷的。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 以下几个函数
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 获得原生函数
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
// 调用原生函数
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 触发更新
ob.dep.notify()
return result
})
})
反观 Proxy 就没以上的问题,原生支持监听数组变化,并且可以直接对整个对象进行拦截,所以 Vue 也将在下个大版本中使用 Proxy 替换 Obeject.defineProperty
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
路由原理
前端路由实现起来其实很简单,本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新。目前单页面使用的路由就只有两种实现方式
- hash 模式
- history 模式
www.test.com/#/
就是 Hash URL,当 #
后面的哈希值发生变化时,不会向服务器请求数据,可以通过 hashchange
事件来监听到 URL 的变化,从而进行跳转页面。
History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美观
内置类型
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。
基本类型有六种: null
,undefined
,boolean
,number
,string
,symbol
。
其中 JS 的数字类型是浮点类型的,没有整型。并且浮点类型基于 IEEE 754标准实现,在使用中会遇到某些 Bug。NaN
也属于 number
类型,并且 NaN
不等于自身。
对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型
let a = 111 // 这只是字面量,不是 number 类型
a.toString() // 使用时候才会转换为对象类型
对象(Object)是引用类型,在使用过程中会遇到浅拷贝和深拷贝的问题。
let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF
Typeof
typeof
对于基本类型,除了 null
都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined
typeof
对于对象,除了函数都会显示 object
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
对于 null
来说,虽然它是基本类型,但是会显示 object
,这是一个存在很久了的 Bug
typeof null // 'object'
PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000
开头代表是对象,然而 null
表示为全零,所以将它错误的判断为 object
。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
如果我们想获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)
。这样我们就可以获得类似 [Object Type]
的字符串。
let a
// 我们也可以这样判断 undefined
a === undefined
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值
let undefined = 1
// 这样判断就会出错
// 所以可以用下面的方式来判断,并且代码量更少
// 因为 void 后面随便跟上一个组成表达式
// 返回就是 undefined
a === void 0
类型转换
转Boolean
在条件判断时,除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都转为 true
,包括所有对象。
对象转基本类型
对象在转换基本类型时,首先会调用 valueOf
然后调用 toString
。并且这两个方法你是可以重写的。
let a = {
valueOf() {
return 0
}
}
四则运算符
只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。
1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
对于加号需要注意这个表达式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"
// 因为 + 'b' -> NaN
// 你也许在一些代码中看到过 + '1' -> 1
==
操作符
上图中的 toPrimitive
就是对象转基本类型。
一般推荐使用 ===
判断两个值,但是你如果想知道一个值是不是 null
,你可以通过 xx == null
来比较。
这里来解析一道题目 [] == ![] // -> true
,下面是这个表达式为何为 true
的步骤
// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
比较运算符
- 如果是对象,就通过
toPrimitive
转换对象 - 如果是字符串,就通过
unicode
字符索引来比较
公众号
Todo
- 完成 CSS 内容
- 完成 Webapck 内容
- 完成小程序相关内容
- 完善关于框架的内容
以上内容预计将于 9 月份更新完毕,欢迎你一起参与本图谱的建设。
面试图谱 项目地址,如果你觉得项目还不错,你可以点个小星星支持我们一下,你的支持是我们更新的源动力。