面试总结
一、CSS
(1)CSS盒模型
页面渲染时,dom 元素所采用的 布局模型。可通过box-sizing进行设置。根据计算宽高的区域可分为:
- content-box (W3C 标准盒模型)
- border-box (IE 盒模型)
- padding-box
- margin-box
(2)BFC
块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。
规则:
- 属于同一个 BFC 的两个相邻 Box 垂直排列
- 属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
- BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box的左边相接触 (子元素 absolute
除外) - BFC 的区域不会与 float 的元素区域重叠
- 计算 BFC 的高度时,浮动子元素也参与计算
- 文字层不会被浮动层覆盖,环绕于周围
应用:
- 阻止margin重叠
- 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)
- 自适应两栏布局
- 可以阻止元素被浮动元素覆盖
(3)居中布局
水平居中
行内元素: text-align: center
块级元素: margin: 0 auto
absolute + transform
flex + justify-content: center
垂直居中
line-height: height
absolute + transform
flex + align-items: center
table
水平垂直居中
absolute + transform
flex + justify-content + align-items
(4)选择器优先级
- !important > 行内样式 > #id > .class > tag > * > 继承 > 默认
- 选择器 从右往左 解析
(5)去除浮动影响,防止父级高度塌陷
- 通过增加尾元素清除浮动 :after /
: clear: both - 创建父级 BFC
- 父级设置高度
(6)link 与 @import 的区别
- link功能较多,可以定义 RSS,定义 Rel 等作用,而@import只能用于加载 css
- 当解析到link时,页面会同步加载所引的 css,而@import所引用的 css 会等到页面加载完才被加载
- @import需要 IE5 以上才能使用
- link可以使用 js 动态引入,@import不行
(7)CSS预处理器(Sass/Less/Postcss)
CSS预处理器的原理: 是将类 CSS 语言通过 Webpack 编译 转成浏览器可读的真正 CSS。在这层编译之上,便可以赋予 CSS 更多更强大的功能,常用功能:
- 嵌套
- 变量
- 循环语句
- 条件语句
- 自动前缀
- 单位转换
- mixin复用
(8)CSS动画
transition: 过渡动画
- transition-property: 属性
- transition-duration: 间隔
- transition-timing-function: 曲线
- transition-delay: 延迟
- 常用钩子: transitionend
animation / keyframes
- animation-name: 动画名称,对应@keyframes
- animation-duration: 间隔
- animation-timing-function: 曲线
- animation-delay: 延迟
- animation-iteration-count: 次数
- infinite: 循环动画
- animation-direction: 方向
- alternate: 反向播放
- animation-fill-mode: 静止模式
- forwards: 停止时,保留最后一帧
- backwards: 停止时,回到第一帧
- both: 同时运用 forwards / backwards
- 常用钩子: animationend
动画属性: 尽量使用动画属性进行动画,能拥有较好的性能表现
- translate
- scale
- rotate
- skew
- opacity
- color
二、JavaScript
1. 原型 / 构造函数 / 实例
原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
构造函数: 可以通过new来 新建一个对象 的函数。
实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数。
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
实例.constructor === 构造函数
2.原型链:
原型链是由原型对象组成,每个对象都有 proto 属性,指向了创建该对象的构造函数的原型,proto 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。
属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最*的原型对象Object.prototype,如还是没找到,则输出undefined;
属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。
3.执行上下文(EC)
执行上下文可以简单理解为一个对象:
它包含三个部分:
- 变量对象(VO)
- 作用域链(词法作用域)
- this指向
它的类型:
- 全局执行上下文
- 函数执行上下文
- eval执行上下文
代码执行过程:
- 创建 全局上下文 (global EC)
- 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层
- 函数执行上下文被**,成为 active EC, 开始执行函数中的代码,caller 被挂起 函数执行完后,callee
被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行
4.作用域
执行上下文中还包含作用域链。理解作用域之前,先介绍下作用域。作用域其实可理解为该上下文中声明的 变量和声明的作用范围。可分为 块级作用域 和 函数作用域
5.作用域链
我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。
由两部分组成:
- [[scope]]属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]和AO
- AO: 自身活动对象
6.闭包
闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。
闭包会产生一个很经典的问题:
多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
解决方案:
- 变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
- 使用setTimeout包裹,通过第三个参数传入
- 使用 块级作用域,让变量成为自己上下文的属性,避免共享
7.script 引入方式:
html 静态<script>引入 js
动态插入<script>
<script defer>: 异步加载,元素解析完成后执行
<script async>: 异步加载,但执行时会阻塞元素渲染
8.对象的拷贝
浅拷贝:
以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响
Object.assign
展开运算符(…)
深拷贝:
完全拷贝一个新对象,修改时原对象不再受到任何影响
(1) JSON.parse(JSON.stringify(obj)):
- 性能最快
- 具有循环引用的对象时,报错
- 当值为函数、undefined、或symbol时,无法拷贝
(2) 递归进行逐一赋值
9.new运算符的执行过程
- 新生成一个对象
- 链接到原型: obj.proto = Con.prototype
- 绑定this: apply
- 返回新对象(如果构造函数有自己 retrun 时,则返回该值)
10.instanceof原理
能在实例的 原型对象链 中找到该构造函数的prototype属性所指向的 原型对象,就返回true。即:
// __proto__: 代表原型对象链
instance.[__proto__...] === instance.constructor.prototype
// return true
11.代码的复用
当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:
- 函数封装
- 继承
- 复制extend
- 混入mixin
- 借用apply/call
12.继承
在 JS 中,继承通常指的便是 原型链继承,也就是通过指定原型,并可以通过原型链继承原型上的属性或者方法。
(1) 最优化: 圣杯模式
var inherit = (function(c,p){
var F = function(){};
return function(c,p){
F.prototype = p.prototype;
c.prototype = new F();
c.uber = p.prototype;
c.prototype.constructor = c;
}
})();
(2) 使用 ES6 的语法糖 class / extends
13.类型转换
大家都知道 JS 中在使用运算符号或者对比符时,会自带隐式转换,规则如下:
- -、*、/、% :一律转换成数值后计算
- +:
数字 + 字符串 = 字符串, 运算顺序是从左到右
数字 + 对象, 优先调用对象的valueOf -> toString
数字 + boolean/null = 数字
数字 + undefined == NaN
[1].toString() === '1'
{}.toString() === '[object object]'
NaN !== NaN 、+undefined === NaN
14. 类型判断
- 基本类型(null): 使用 String(null)
- 基本类型(string / number / boolean / undefined) + function: 直接使用 typeof即可
- 其余引用类型(Array / Date / RegExp Error): 调用toString后根据[object XXX]进行判断
15. 模块化
模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持。
分类:
- es6: import / exports
- commonjs: require / module.exports / exports
- amd: require / defined
require与import的区别
- require支持 动态导入,import不支持,正在提案 (babel 下可支持)
- require是 同步 导入,import属于 异步 导入
- require是 值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化
16. 防抖与节流
防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。
防抖 (debounce): 将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
function debounce(fn, wait, immediate) {
let timer = null
return function() {
let args = arguments
let context = this
if (immediate && !timer) {
fn.apply(context, args)
}
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
节流(throttle): 每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。
function throttle(fn, wait, immediate) {
let timer = null
let callNow = immediate
return function() {
let context = this,
args = arguments
if (callNow) {
fn.apply(context, args)
callNow = false
}
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args)
timer = null
}, wait)
}
}
}
17. 函数执行改变this
由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境,这便是this。
因此要明白 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如:
- obj.fn(),便是 obj 调用了函数,既函数中的 this === obj
- fn(),这里可以看成 window.fn(),因此 this === window
但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改 this 的指向:
call: fn.call(target, 1, 2)
apply: fn.apply(target, [1, 2])
bind: fn.bind(target)(1,2)
18. ES6/ES7
由于 Babel 的强大和普及,现在 ES6/ES7 基本上已经是现代化开发的必备了。通过新的语法糖,能让代码整体更为简洁和易读。
- let / const: 块级作用域、不存在变量提升、暂时性死区、不允许重复声明
- const: 声明常量,无法修改
- 解构赋值
- class / extend: 类声明与继承
- Set / Map: 新的数据结构
- 异步解决方案:
- Promise的使用与实现
- generator:
yield: 暂停代码
next(): 继续执行代码
function* helloWorld() {
yield 'hello';
yield 'world';
return 'ending';
}
const generator = helloWorld();
generator.next() // { value: 'hello', done: false }
generator.next() // { value: 'world', done: false }
generator.next() // { value: 'ending', done: true }
generator.next() // { value: undefined, done: true }
- await / async: 是generator的语法糖, babel中是基于promise实现。
async function getUserByAsync(){
let user = await fetchUser();
return user;
}
const user = await getUserByAsync()
console.log(user)
19. AST
抽象语法树 (Abstract Syntax Tree),是将代码逐字母解析成 树状对象 的形式。这是语言之间的转换、代码语法检查,代码风格检查,代码格式化,代码高亮,代码错误提示,代码自动补全等等的基础。例如:
通过解析转化成的AST如下图:
20. babel编译原理
- babylon 将 ES6/ES7 代码解析成 AST
- babel-traverse 对 AST 进行遍历转译,得到新的 AST
- 新 AST 通过 babel-generator 转换成 ES5
21. 函数柯里化
在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3
add1(20) === 21
22. 数组(array)
-
map: 遍历数组,返回回调返回值组成的新数组
-
forEach: 无法break,可以用try/catch中throw new Error来停止
-
filter: 过滤
-
some: 有一项返回true,则整体为true
-
every: 有一项返回false,则整体为false
-
join: 通过指定连接符生成字符串
-
push / pop: 末尾推入和弹出,改变原数组, 返回推入/弹出项
-
unshift / shift: 头部推入和弹出,改变原数组,返回操作项
-
sort(fn) / reverse: 排序与反转,改变原数组
-
concat: 连接数组,不影响原数组, 浅拷贝
-
slice(start, end): 返回截断后的新数组,不改变原数组
-
splice(start, number, value…): 返回删除元素组成的数组,value 为插入项,改变原数组
-
indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标
-
reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev
为上次化简函数的return值,cur 为当前值(从第二项开始) -
数组乱序:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(function () {
return Math.random() - 0.5;
});
- 数组拆解: flat: [1,[2,3]] --> [1, 2, 3]
arr.prototype.flat = function() {
this.toString().split(',').map(item => +item )
}
三、浏览器
1. 跨标签页通讯
不同标签页间的通讯,本质原理就是去运用一些可以 共享的中间介质,因此比较常用的有以下方法:
(1) 通过父页面window.open()和子页面postMessage
- 异步下,通过 window.open(‘about: blank’) 和 tab.location.href = ‘*’
(2) 设置同域下共享的localStorage与监听window.onstorage
-
重复写入相同的值无法触发
-
会受到浏览器隐身模式等的限制
(3) 设置共享cookie与不断轮询脏检查(setInterval)
(4)借助服务端或者中间层实现
2. 浏览器架构
3. 浏览器下事件循环(Event Loop)
事件循环是指: 执行一个宏任务,然后执行清空微任务列表,循环再执行宏任务,再清微任务列表
- 微任务 microtask(jobs): promise / ajax / Object.observe(该方法已废弃)
- 宏任务 macrotask(task): setTimout / script / IO / UI Rendering
4. 从输入 url 到展示的过程
5. 重绘与回流
当元素的样式发生变化时,浏览器需要触发更新,重新绘制元素。这个过程中,有两种类型的操作,即重绘与回流。
重绘(repaint): 当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此 损耗较少
回流(reflow): 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。会触发回流的操作:
回流必定触发重绘,重绘不一定触发回流。重绘的开销较小,回流的代价较高。
最佳实践:
6. 存储
我们经常需要对业务中的一些数据进行存储,通常可以分为 短暂性存储 和 持久性储存。
7. Web Worker
现代浏览器为JavaScript创造的 多线程环境。可以新建并将部分任务分配到worker线程并行运行,两个线程可 独立运行,互不干扰,可通过自带的 消息机制 相互通信。
// 创建 worker
const worker = new Worker('work.js');
// 向主进程推送消息
worker.postMessage('Hello World');
// 监听主进程来的消息
worker.onmessage = function (event) {
console.log('Received message ' + event.data);
}
8. V8垃圾回收机制
9. 内存泄露
服务端与网络
-
http/https 协议
-
常见状态码
-
get / post
两者详细对比如下图:
-
Websocket
-
TCP三次握手
建立连接前,客户端和服务端需要通过握手来确认对方:
- 客户端发送 syn(同步序列编号) 请求,进入 syn_send 状态,等待确认
- 服务端接收并确认 syn 包后发送 syn+ack 包,进入 syn_recv 状态
- 客户端接收 syn+ack 包后,发送 ack 包,双方进入 established 状态
-
TCP四次挥手
-
Node 的 Event Loop: 6个阶段
8.跨域
9.安全
框架:Vue
- nextTick
- 数据响应(数据劫持)
let data = {a: 1}
// 数据响应性
observe(data)
// 初始化观察者
new Watcher(data, 'name', updateComponent)
data.a = 2
// 简单表示用于数据更新后的操作
function updateComponent() {
vm._update() // patchs
}
// 监视对象
function observe(obj) {
// 遍历对象,使用 get/set 重新定义对象的每个属性值
Object.keys(obj).map(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, k, v) {
// 递归子属性
if (type(v) == 'object') observe(v)
// 新建依赖收集器
let dep = new Dep()
// 定义get/set
Object.defineProperty(obj, k, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 当有获取该属性时,证明依赖于该对象,因此被添加进收集器中
if (Dep.target) {
dep.addSub(Dep.target)
}
return v
},
// 重新设置值时,触发收集器的通知机制
set: function reactiveSetter(nV) {
v = nV
dep.nofify()
},
})
}
// 依赖收集器
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.map(sub => {
sub.update()
})
}
}
Dep.target = null
// 观察者
class Watcher {
constructor(obj, key, cb) {
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
addDep(Dep) {
Dep.addSub(this)
}
update() {
this.value = this.obj[this.key]
this.cb(this.value)
}
before() {
callHook('beforeUpdate')
}
}
- virtual dom 原理实现
// diff算法的实现
function diff(oldTree, newTree) {
// 差异收集
let pathchs = {}
dfs(oldTree, newTree, 0, pathchs)
return pathchs
}
function dfs(oldNode, newNode, index, pathchs) {
let curPathchs = []
if (newNode) {
// 当新旧节点的 tagName 和 key 值完全一致时
if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// 继续比对属性差异
let props = diffProps(oldNode.props, newNode.props)
curPathchs.push({ type: 'changeProps', props })
// 递归进入下一层级的比较
diffChildrens(oldNode.children, newNode.children, index, pathchs)
} else {
// 当 tagName 或者 key 修改了后,表示已经是全新节点,无需再比
curPathchs.push({ type: 'replaceNode', node: newNode })
}
}
// 构建出整颗差异树
if (curPathchs.length) {
if(pathchs[index]){
pathchs[index] = pathchs[index].concat(curPathchs)
} else {
pathchs[index] = curPathchs
}
}
}
// 属性对比实现
function diffProps(oldProps, newProps) {
let propsPathchs = []
// 遍历新旧属性列表
// 查找删除项
// 查找修改项
// 查找新增项
forin(olaProps, (k, v) => {
if (!newProps.hasOwnProperty(k)) {
propsPathchs.push({ type: 'remove', prop: k })
} else {
if (v !== newProps[k]) {
propsPathchs.push({ type: 'change', prop: k , value: newProps[k] })
}
}
})
forin(newProps, (k, v) => {
if (!oldProps.hasOwnProperty(k)) {
propsPathchs.push({ type: 'add', prop: k, value: v })
}
})
return propsPathchs
}
// 对比子级差异
function diffChildrens(oldChild, newChild, index, pathchs) {
// 标记子级的删除/新增/移动
let { change, list } = diffList(oldChild, newChild, index, pathchs)
if (change.length) {
if (pathchs[index]) {
pathchs[index] = pathchs[index].concat(change)
} else {
pathchs[index] = change
}
}
// 根据 key 获取原本匹配的节点,进一步递归从头开始对比
oldChild.map((item, i) => {
let keyIndex = list.indexOf(item.key)
if (keyIndex) {
let node = newChild[keyIndex]
// 进一步递归对比
dfs(item, node, index, pathchs)
}
})
}
// 列表对比,主要也是根据 key 值查找匹配项
// 对比出新旧列表的新增/删除/移动
function diffList(oldList, newList, index, pathchs) {
let change = []
let list = []
const newKeys = getKey(newList)
oldList.map(v => {
if (newKeys.indexOf(v.key) > -1) {
list.push(v.key)
} else {
list.push(null)
}
})
// 标记删除
for (let i = list.length - 1; i>= 0; i--) {
if (!list[i]) {
list.splice(i, 1)
change.push({ type: 'remove', index: i })
}
}
// 标记新增和移动
newList.map((item, i) => {
const key = item.key
const index = list.indexOf(key)
if (index === -1 || key == null) {
// 新增
change.push({ type: 'add', node: item, index: i })
list.splice(i, 0, key)
} else {
// 移动
if (index !== i) {
change.push({
type: 'move',
form: index,
to: i,
})
move(list, index, i)
}
}
})
return { change, list }
}
- Proxy 相比于 defineProperty 的优势
- 数组变化也能监听到
- 不需要深度遍历监听
let data = { a: 1 }
let reactiveData = new Proxy(data, {
get: function(target, name){
// ...
},
// ...
})
-
vue-router
-
vuex
转载: https://juejin.im/post/5c64d15d6fb9a049d37f9c20#heading-47
上一篇: 阿里提前批面试查缺补漏
下一篇: 一道Python面试题