前端面试题
说一下Promise
Promise是什么?
- Promise是一种用于解决异步问题的思路、方案或者对象方式。
Promise怎么用?
- Promise是一个对象,所以先用new的方式创建一个,然后给它传一个函数作为参数,这个函数有两个参数,一个叫reolve,另一个叫reject、 紧接着,就用then来进行调用
Promise原理
-
在Promise内部,有一个状态管理器的存在,有三种状态: pending、fulfilled、rejected
(1) promise初始化状态为pending
(2) 当前调用resolve(成功), 会由pending => fulfilled
(3) 当调用reject(失败), 会由pending => rejected
跨域
什么是跨域
协议、端口和域名不一致导致的跨域 跨域是因为浏览器需要遵守同源策略,发出的请求即使相应成功,也被浏览器拦截下来
同源策略
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互、这是一个用于隔离潜在恶意文件的重要安全机制、
为什么
如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。
1、 防御 XSS 攻击
- XSS,即 Cross Site Script,中译是跨站脚本攻击。
- HttpOnly 防止劫取 Cookie
- 用户的输入检查
- 服务端的输出检查
2、防御 CSRF 攻击
- CSRF,即 Cross Site Request Forgery,中译是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。
- 验证码
- Referer Check
- Token验证
跨域的解决方案
1、通过jsonp跨域 2、document.domain + iframe跨域 3、location.hash + iframe 4、window.name + iframe跨域 5、postMessage跨域 6、跨域资源共享(CORS) 7、nginx代理跨域 8、nodejs中间代理跨域 9、WebSocket协议跨域
jsonp原理
jsonp的核心则是动态添加 script 标签调用服务器提供的js脚本,允许用户传递一个callback参数给服务器,然后服务器返回数据时会将这个callback参数作为函数名老包裹JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了
- 仅支持GET方法
如何进行网站性能优化
1、Content方面
- 减少HTTP请求:合并文件、CSS精灵、inline image
- 减少DNS查询: DNS查询完之前浏览器不能从这个主机下载任何文件、方法:DNS缓存、讲资源分布到恰当的数量的主机名,平衡并行下载和DNS查询
- 避免重定向 : 多余的中间访问
- 使用AJAX缓存
- 非必须组件延迟加载
- 未来所需组件预加载
- 减少DOM元素数量
- 将资源放到不同的域下面:浏览器同时从一个域下载资源的数目有限,增加域可以提高并行下载量
- 减少iframe数量
- 不要404
2、Server方面
- 使用CDN
- 添加Expires或者Cache-Control: 当Cache-Control和Expires同时存在时,Cache-Control会覆盖Expires。相关链接
- 使用Gzip压缩
- 配置Etag
- Flush Buffer Early
- Ajax使用GET进行请求
- 避免空src的img标签
3、Cookie方面
- 减小Cookie
- 引入资源的域名不要包含cookie
4、CSS方面
- 将样式表放到顶部
- 不要使用CSS表达式
- 不使用@import
- 不使用IE的Filter
5、JavaScript
- 将脚本放到页面的底部
- 将JavaScript和CSS从外部引入
- 压缩JavaScript和CSS
- 删除不需要的脚本
- 减少DOM的查询
- 合理设计事件监听器
6、图片方面
- 优化图片: 根据实际颜色需要选择色深、压缩
- 优化CSS精灵
- 不要在HTML中拉伸图片
- 保证favicon、ico小并且可缓存
7、移动方面
- 保证组件小于25K
- Pack Components into a Multipart Document
从浏览器地址栏输入url到显示页面的步骤(以HTTP为例)
大概流程
- URL输入
- DNS解析
- TCP连接
- 发送HTTP请求
- 服务器处理请求
- 服务器响应请求
- 浏览器解析渲染页面
- 连接结束
1、在浏览器数地址栏输入URL
2、浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤
- 如果资源为缓存,发起新请求
- 如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
- 检验新鲜通常有两个HTTP头进行控制
Expires
和Cache-Control
- HTTP1.0提供Expires,值为一个绝对值表示
- HTTP1.1增加了Cache-Control : max-age=,值为以秒为单位的最大新鲜时间
3、浏览器解析URL获取协议,主机,端口,path
4、浏览器组装一个HTTP(GET)请求报文
5、浏览器获取主机ip地址,过程如下:
- 浏览器缓存
- 本机缓存
- hosts文件
- 路由器缓存
- ISP DNS缓存
- DNS递归查询(可能存在负载均衡导致每次IP不一样)
6、打开一个sokcet与目标地址,端口建立TCP链接, 三次握手如下:
- 客户端发送一个TCP的SYN=1,Seq=X的包到服务器端口
- 服务器发送SYN=1,ACK=X+1,Seq=Y的响应包
- 客户端发送ACK=Y+1,Seq=Z
7、TCP链接建立后发送HTTP请求
8、服务器接受请求并解析,将请求转发到服务程序,如虚拟机使用HTTP Host头部判断请求的服务程序
9、服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
10、处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作
11、服务器将响应报文通过TCP链接发送回浏览器
12、浏览器接受HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,关闭TCP连接的四次握手如下:
- 主动发送Fin=1,Ack=Z,Seq=X报文
- 被动发送ACK=X+1,Seq=Z报文
- 被动发送Fin=1,ACK=X,Seq=Y报文
- 主动发送ACK=Y,Seq=X报文
13、浏览器检查响应状态码:是否为1XX、3XX、4XX、5XX,这些情况处理与2XX不同
14、如果资源可缓存,进行缓存
15、对响应进行解码(例如gzip压缩)
16、根据资源类型决定如何处理(假设资源为HTML文档)
17、解析HTML文档、构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序,以下分别解释
18、构建DOM树:
- Tokenizing: 根据HTML规范将字符流解析为标记
- Lexing:词法分析将标记转换为对象并定义属性和规则
- DOM construction: 根据HTML标记关系将对象组成DOM树
19、解析过程中遇到图片、样式表、js文件,启动下载
20、构建CSSOM树
- Tokenizing: 字符流转换为标记流
- Node:根据标记创建节点
- CSSOM:节点创建CSSOM树
21、根据DOM树和CSSOM树构建渲染树:
- 从DOM树的根节点遍历所有可见节点,不可见节点包括:1)
script,meta
这样本身不可见的标签。2)被CSS隐藏的节点,入display:none
- 对每一个可节点,找到恰当的CSSOM规则并应用
- 发布可视节点的内容和计算样式
22、js解析如下
- 浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading
- HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或者外部脚本。这些脚本同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常定义为函数和注册事件处理事件,他们可以遍历和操作script和他们之前的文档内容。
- 当解析器遇到设置了async属性的script时,开始下载脚本并继续解析文档。脚本在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素
- 所有deter脚本会按照在文档上出现的顺序执行,延迟脚本能访问完整文档时,禁止使用document.write()
- 浏览器在Document对象上触发DOMContentLoaded事件
- 此时文档完成解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成和执行,document.readState变为complete,window触发load事件
23、显示页面(HTML解析过程中会逐步显示页面)
移动端开发自适应页面如何做?
1、通过meta标签设置viewport,移动端的理想适口。
<meta name="viewport" content="width=width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
2、设置rem单位来进行适配、加上Flex布局、百分比布局
3、其它方案,响应式适配、vw+rem
rem原理
- rem是是指相对于根元素的字体大小的单位
- 比如设置html font-size=100px;那么1rem=100px;之后的所有元素都可以用这个基准值来设置大小;
- rem作用于非根元素时,相对于根元素字体大小;rem作用于根元素字体大小时,相对于其出初始字体大小——MDN
说一下this
JavaScript 函数中的 this 指向并不是在函数定义的时候确定的,而是在调用的时候确定的。换句话说,函数的调用方式决定了 this 指向。 函数调用的方式
- 直接调用
直接调用,就是通过 函数名(...) 这种方式调用
- 方法调用
方法调用是指通过对象来调用其方法函数,它是 **对象.方法函数(...)** 这样的调用形式
- new关键字调用
- 通过 bind() 将函数绑定到对象之后再进行调用
- 通过 call()、apply() 进行调用
箭头函数的特点?
官方解释:箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或 new.target。
-
引用箭头函数有两个方面的作用:更简短函数和并且不绑定this
-
箭头函数不会创建this,它只会从自己的作用域链上一层继承this。
-
简而言之,箭头函数,永远指向当前调用的对象
== 和 === 的区别?
- == 会进行隐式转换,比较前将两个被比较的值转换为相同类型。然后比较两个值是否相等
- === 不进行隐式转换,会比较类型和值
CSS选择器优先级
- 每个选择器都有权值,权值越大越优先
- 继承的样式优先级低于自身制定样式
- !important优先级最高,js也无法修改
- 权值相同的时候,靠近元素的样式优先级搞,顺序为内联样式 > 内部样式表 > 外部样式表
BFC
什么是BFC
BFC就是"块级格式化上下文"的意思,创建了BFC的元素就是一个独立的盒子,不过只有Block-level Box 可以参与创建BFC,它规定了内部的Block-level Box如何布局,并且与这个独立盒子里的布局不受外部影响,当然它不会影响到外面的元素。
BFC特性:
- 内部的Box会在垂直方向,从顶部开始一个接一个地放置
- Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生叠加
- 每个元素的margin box的左边,与包含的 border box 的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
- BFC的区域不会与float box叠加
- BFC就是页面的一个隔离的独立容器,容器里面的子元素不会受影响到外面的元素,反之亦然。
- 计算BFC的高度时,浮动元素也参与计算
如何触发BFC
- 根元素或包含根元素的元素
- 浮动元素,float 除了 none 以外
- 绝对定位元素, position 为 absolute、fixed
- display 为 inline-block、table-cell、table-caption、flow-root
- overflow 值不为 visible 的元素
- 弹性元素(display 为 flex 或 inline-flex 元素的直接子元素)
- 网格元素 (display 为 grid 或 inline-grid 元素的直接子元素)
- 多列容器(元素的 column-count 或 column-width 不为 auto,包括 column-count 为 1)
- column-span 为 all 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)
EventBus如何实现?
利用发布/订阅模式,发布/订阅模式由一个发布者、多一个订阅者以及一个调度中心所组成。订阅者们先在调度中心订阅某一事件并注册相应的回调函数,当某一个时刻发布者发布了一个事件,调度中心取出订阅了该事件的订阅者们所注册的回调函数来执行。
在发布/订阅模式中,订阅者和发布者并不需要关心对方的状态,订阅者只管订阅事件并注册回调、发布者只管发布事件,其余一切交给调度中心来调度,从而实现解耦。
Vue双向绑定的原理
Vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发响应的监听回调。
具体步骤:
第一步:需要 Observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到数据变化。
第二步:Compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步:Watcher 订阅者是 Observe 和 Compile 之间通信的桥梁,主要的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()
3、待属性变动dep.notify()通知时,能调用自身的 update() 方法,并触发 Compile 中绑定回调,则功成身退。
第四步:MVVM作为数据绑定的入口,整合 Observe、Compile 和 Watcher 三者,通过 Observe 来监听自己的 Model 数据变化。 通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observe 和 Compile 之间的通信桥梁; 达到数据变化 -> 视图更新; 视图交互(input) -> 数据 Model 变更的双向绑定效果。
Vue Computed的实现原理和缓存原理
第一步:创建一个computedWathcers 空对象, 对 computed 对象遍历,获取计算属性每一个 userDef(自定义的函数或对象),然后尝试获取 userDef 的getter,并且为每一个 getter 添加一个watcher
第二步:判断遍历 computed 对象的key,是否已经存在 data 和 props 所占用,存在则发出警告,不存在就调用 defineComputed 函数,给对应的key添加getter 和 setter
第三步:在调用 defineComputed 函数,会进行依赖收集 computedWatcher ,通过computedWatcher来进行派发通知,更新视图
第四步:缓存就是在获取 getter 数据的,判断是否值相等,相等的话就直接返回,不再进行更新视图
MVVM框架是什么?它和其它框架(Jquery)的区别是什么?哪些场景适合?
MVVM分为Model、View、ViewModel三者
- Model 代表数据模型,数据和业务逻辑都在Model层中定义
- View 代表UI视图,负责数据展示
- ViewModel 负责监听 Model 中数据的改变并且控制视图更新,处理用户交互操作:
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的, Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步
区别:这种模式实现了 Model 和 View的数据自动同步,因此开发时这需要要专注对数据的维护操作即可,而不需要自己操作dom 场景:数据操作比较多的场景,更加便捷
nextTick 实现原理
JS,是单线程的,利用JS的事件循环
事件循环大致分为以下几个步骤:
(1) 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
(2) 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3) 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4) 主线程不断重复上面的第三步
宏任务(macro task) 和 微任务(micro task)
先执行宏观任务,再执行微观
- 宏观任务:setTimeout、MessageChannel、postMessage、setImmediate...
- 微观:MutationObsever、Promise.then
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
复制代码
nextTick原理:
-
会有一个callbacks数组,接受nextTick的回调函数,push进去
-
首先判断是否支持Promise,支持则利用的Promise.then进行调用遍历调用callbacks数组
-
判断是否支持 MutationObserver,支持则利用 MutationObserver 遍历调用callbacks数组
-
判断是否支持 setImmediate,支持则利用 setImmediate 遍历调用callbacks数组
-
都不支持,则利用setTimeout进行遍历调用 callbacks数组
面试回答 : 它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,而它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
什么是虚拟 dom ?
VNode是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展VNode的灵活性以及实现一些特殊 feature的。
Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode的 create、diff、 patch等过程。
Vue组件之间的通信
-
父子组件通信,props、emit、ref调用函数
-
兄弟组件通信,vuex、eventBus
说一下Vuex
vuex有哪几种属性?
vuex具有五种属性: state、getter、mutation、action、module
vuex的state特性是?
-
vuex就是一个仓库,仓库里面放很多对象。state就是数据存放地,对应于一般vue对象里面的data
-
state里面存放的数据是响应式的
vuex的getter特性是?
-
getters可以对state进行计算操作
-
可以在多组件之间复用
vuex的mutation特性是?
-
action类似于mutation
-
action提价的是mutation,而是不是直接变更状态
-
action可以包含任何异步操作
不用vuex会带来什么问题?
-
可维护性会下降,你要想修改数据,你得维护三个地方
-
可读性下降,因为一个组件里的数据,你根本看不出来是从哪来的
-
增加耦合,大量的上传派发,会让耦合性大大的增加,本来Vue用Component就是为了减少耦合,现在这么用,和组件化的初衷相背。
请详细说下你对vue生命周期的理解?
总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后
创建前/后: 在beforeCreated阶段,vue实例的挂载元素el还没有。
载入前/后: 在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。
更新前/后: 当data变化时,会触发beforeUpdate和updated方法。
销毁前/后: 在执行destroy方法后,对data的改变不会触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在
请说下封装 vue 组件的过程?
首先,组件可以提升整个项目的开发效率。能够把页面抽象成多个相对独立的模快,解决了我们传统项目开发:效率低、难维护、复用性等问题。
然后,使用Vue.extend方法创建一个组件,然后使用Vue.component方法注册组件。子组件需要数据,可以在props中接受定义。而子组件修改好数据后,想把数据递给父组件。可以采用emit方法。
Proxy 和 Object.defineProperty 的优劣?
-
Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是 **Object.defineProperty()**不具备的
-
Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改
-
Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
-
当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。
聊一聊常见的浏览器端数据存储方案
数据存储方案:
- Cookie
- Web存储(localStorage和sessionStorage)
- IndexedDB
大概说一下Cookie和localStorage、sessionStorage的功能特性。问到的话,Cookie的缺点就是,存储量少、数据大影响性能、只能储存字符串、安全性问题、需要检查Cookie能否使用
Flexible布局方案的原理
- 获取document的适口宽度 除以 10
- 得出 1rem = viewWidth / 10
- 然后设置 html的font-size为 rem + 'px'
为什么会有深拷贝和浅拷贝?日常开发中如何使用?,如何实现一个深拷贝?
/**
* @desc 深拷贝,支持常见类型
* @param {Any} values
* @return {Any}
*/
function deepClone(values) {
var copy;
// Handle the 3 simple types, and null or undefined
if (null == values || "object" != typeof values) return values;
// Handle Date
if (values instanceof Date) {
copy = new Date();
copy.setTime(values.getTime());
return copy;
}
// Handle Array
if (values instanceof Array) {
copy = [];
for (var i = 0, len = values.length; i < len; i++) {
copy[i] = deepClone(values[i]);
}
return copy;
}
// Handle Object
if (values instanceof Object) {
copy = {};
for (var attr in values) {
if (values.hasOwnProperty(attr)) copy[attr] = deepClone(values[attr]);
}
return copy;
}
throw new Error("Unable to copy values! Its type isn't supported.");
}
复制代码
如果是一个数组,就声明一个数据组,然后循环遍历,递归赋值。 如果是一个对象,就声明一个对象,然后判断是否子元素,递归赋值
除了递归,我们还可以借用JSON对象的parse和stringify
function deepClone(obj){
let _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone
}