css部分
rem原理
- rem布局的本质是等比缩放,一般是基于宽度,假设将屏幕宽度分为100份,每份宽度是1rem,1rem的宽度是屏幕宽度/100,,然后子元素设置rem单位的属性,
通过改变html元素的字体大小,就可以设置子元素的实际大小。 - rem布局加载闪烁的问题
- 解决方案,媒体查询设置根元素字体大小,比如设计稿是750px;对应的开发方式是1rem=100px,那375px的font-size 大小就是50px(具体方法可以百度一下)
- 比rem更好的方案(缺点兼容不好)
- vw(1vw是视口宽度的1%,100vw就是视口宽度),vh(100vh就是视口高度)
实现三栏布局(两侧定宽,中间自适应)
- 采用了 absolute,导致父元素脱离了文档流,那所有的子元素也需要脱离文档流。如果页面复杂,那开发的难度可想而知
- 利用浮动 当中间内容高于两侧时,两侧高度不会随中间内容变高而变高
- 弹性盒子布局(flex)
- 利用负边距和浮动,实现起来比较复杂
- 利用网格布局
.container {
display: grid;
grid-template-columns: 100px auto 200px;
}
BFC(块级格式化上下文)
- BFC 的原理
其实也就是 BFC 的渲染规则(能说出以下四点就够了)。包括:- BFC 内部的子元素,在垂直方向,边距会发生重叠。
- BFC在页面中是独立的容器,外面的元素不会影响里面的元素,反之亦然。
- BFC区域不与旁边的float box区域重叠。(可以用来清除浮动带来的影响)。
- 计算BFC的高度时,浮动的子元素也参与计算。
- 如何生成BFC
- 方法1:overflow: 不为visible,可以让属性是 hidden、auto。【最常用】
- 方法2:浮动中:float的属性值不为none。意思是,只要设置了浮动,当前元素就创建了BFC。
- 方法3:定位中:只要posiiton的值不是 static或者是relative即可,可以是absolute或fixed,也就生成了一个BFC。
- 方法4:display为inline-block, table-cell, table-caption, flex, inline-flex
- BFC应用
- 阻止margin重叠
- 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)
- 自适应两栏布局
- 可以阻止元素被浮动元素覆盖
flex(面试常问,略)
盒子模型
- css的盒模型由content(内容)、padding(内边距)、border(边框)、margin(外边距)组成。
- 在w3c盒模型中,设置的width/height是content的宽度/高度,在怪异模式中width/height设置的是content+padding+border宽度/高度。
- 在w3c盒子模型中盒子的大小由content、padding、border决定,在在怪异模式中盒子大小由width和height决定。
js部分
call, apply, bind区别? 怎么实现call,apply方法
Function.prototype.myBind = function(content) {
if(typeof this !='function'){
throw Error('not a function')
}
let _this = this;
let args = [...arguments].slice(1)
let resFn=function(){
return _this.apply(this instanceof resFn?this:content,args.concat(...arguments))
}
return resFn
};
/**
* 每个函数都可以调用call方法,来改变当前这个函数执行的this关键字,并且支持传入参数
*/
Function.prototype.myCall=function(context=window){
context.fn = this;//此处this是指调用myCall的function
let args=[...arguments].slice(1);
let result=content.fn(...args)
//将this指向销毁
delete context.fn;
return result;
}
/**
* apply函数传入的是this指向和参数数组
*/
Function.prototype.myApply = function(context=window) {
context.fn = this;
let result;
if(arguments[1]){
result=context.fn(...arguments[1])
}else{
result=context.fn()
}
//将this指向销毁
delete context.fn;
return result;
}
函数柯里化
js继承,构造函数,原型链,构造函数、原型链组合式继承,寄生式组合继承,Object.create polyfill;
数组去重
[...new Set(arr)]
var arr = [1,2,1,2,3,5,4,5,3,4,4,4,4],
init=[]
var result = arr.sort().reduce((init, current)=>{
console.log(init,current)
if(init.length===0 || init[init.length-1]!==current){
init.push(current);
}
return init;
}, []);
console.log(result);//1,2,3,4,5
防抖节流
var deBounce=function(fn,wait=300){
let timer
return function(){
if(timer){
clearTimeOut(timer)
}
timer=setTimeOut(()=>{
fn.apply(this,arguments)
},wait)
}
}
var throttle = function (fn, wait = 300) {
let prev = +new Date();
return function () {
const args = argument,
now = +new Date();
if (now > prev + wait) {
prev = now;
fn.apply(this, args)
}
}
}
实现Promise思路
//0 pending,1 resolve,2 reject
function Promise(fn) {...
this._state = 0 // 状态标记
doResolve(fn, this)
}
function doResolve(fn, self) {
var done = false // 保证只执行一个监听
try {
fn(function(value) {
if (done) return
done = true
resolve(self, value)
}, function(reason) {
if (done) return;
done = true
reject(self, value)
})
} catch (err) {
if (done) return
done = true
reject(self, err)
}
}
function resolve(self, newValue) {
try {
self._state = 1;
...
} catch (err) {
reject(self, err)
}
}
function reject(self, newValue) {
self._state = 2;
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
实现深拷贝
funtion deepCopy(obj){
let result;
if(typeofObj=='object'){
//复杂数据类型
result=obj.constructor==Array?[]:{}
for (let i in obj){
result[i]=typeof obj[i]=='object'?deepCopy(obj[i]):obj[i]
}
}else{
//简单数据类型
result=obj
}
return result
}
正则实现千位分隔符
function commafy(num) {
return num && num
.toString()
.replace(/(\d)(?=(\d{3})+\.)/g, function($0, $1) {
return $1 + ",";
});
}
console.log(commafy(1312567.903000))
js事件循环
javascript是单线程语言,任务设计成了两类,同步任务和异步任务
同步和异步任务分别进入不同的执行“场所”,同步进入主线程,异步进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,回去了Event Queue读取对应的函数,进入主线程。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
但是,JS异步还有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入event queue,然后再执行微任务,将微任务放入eventqueue,但是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回调函数,然后再从宏任务的queue拿宏任务的回调函数
宏任务一般包括:整体代码script,setTimeout,setInterval。
微任务:Promise,process.nextTick
事件流机制,事件委托 event.targe和event.currentTarget的区别
-
事件冒泡和事件捕获
-
事件流分为:冒泡和捕获,顺序是先捕获再冒泡。
-
事件冒泡:子元素的触发事件会一直向父节点传递,一直到根结点停止。此过程中,可以在每个节点捕捉到相关事件。可以通过stopPropagation方法终止冒泡。
-
事件捕获:和“事件冒泡”相反,从根节点开始执行,一直向子节点传递,直到目标节点。
-
事件捕获阶段
addEventListener给出了第三个参数同时支持冒泡与捕获:默认是false,事件冒泡;设置为true时,是事件捕获。 -
事件委托 事件委托是指将事件绑定目标元素的到父元素上,利用冒泡机制触发该事件
- 可以减少事件注册,节省大量内存占用可以将事件应用于动态添加的子元素上
-
event.target返回触发事件的元素
-
event.currentTarget返回绑定事件的元素
new的过程以及实现new
//方法1
function create(){
//1.创建一个空对象
let obj={}
//2.获取构造函数
let Con=[].shift.call(arguments)
//3.设置空对象的原型
obj._proto_=Con.prototype
//4.绑定this并执行构造函数,给新对象添加属性和方法
let result=Con.apply(obj,arguments)
//5.确保返回值为对象
return result instanceof Object?result:obj
}
//方法2
//通过分析原生的new方法可以看出,在new一个函数的时候,
// 会返回一个func同时在这个func里面会返回一个对象Object,
// 这个对象包含父类func的属性以及隐藏的__proto__
function New(f) {
//返回一个func
return function () {
var o = {"__proto__": f.prototype};
f.apply(o, arguments);//继承父类的属性
return o; //返回一个Object
}
}
封装ajax
/* 封装ajax函数
* @param {string}opt.type http连接的方式,包括POST和GET两种方式
* @param {string}opt.url 发送请求的url
* @param {boolean}opt.async 是否为异步请求,true为异步的,false为同步的
* @param {object}opt.data 发送的参数,格式为对象类型
* @param {function}opt.success ajax发送并接收成功调用的回调函数
*/
function myAjax(opt){
opt = opt || {};
opt.method = opt.method.toUpperCase() || 'POST';
opt.url = opt.url || '';
opt.async = opt.async || true;
opt.data = opt.data || null;
opt.success = opt.success || function () {}
let xmlHttp = null;
if (XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();
}else{
xmlHttp =new ActiveXObject('Microsoft.XMLHTTP')
}
let params;
for (var key in opt.data){
params.push(key + '=' + opt.data[key]);
}
let postData = params.join('&');
if (opt.method.toUpperCase() === 'POST') {
xmlHttp.open(opt.method, opt.url, opt.async);
xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
xmlHttp.send(postData);
}else if (opt.method.toUpperCase() === 'GET') {
xmlHttp.open(opt.method, opt.url + '?' + postData, opt.async);
xmlHttp.send(null);
}
xmlHttp.onreadystatechange= function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
opt.success(xmlHttp.responseText);//如果是json数据可以在这使用opt.success(JSON.parse( xmlHttp.responseText))
}
};
}
url拿参数
var url = "http://www.taobao.com/index.php?key0=0&key1=1&key2=2";
function parseQueryString(url){
var str = url.split("?")[1], //通过?得到一个数组,取?后面的参数
items = str.split("&"); //分割成数组
var arr,name,value;
for(var i=0; i<items.length; i++){
arr = items[i].split("="); //["key0", "0"]
name = arr[0];
value = arr[1];
this[name] = value;
}
}
var obj = new parseQueryString(url);
alert(obj.key2)
HTTP部分
http协议
HTTP协议(超文本传输协议)
- 1.0 协议缺陷:
- 无法复用链接,完成即断开,重新慢启动和 TCP 3次握手
- head of line blocking: 线头阻塞,导致请求之间互相影响
1.1 改进:
- 长连接(默认 keep-alive),复用
- host 字段指定对应的虚拟站点
- 新增功能:
- 断点续传
- 身份认证
- 状态管理
- cache 缓存
- Cache-Control
- Expires
- Last-Modified
- Etag
- 2.0:
- 多路复用
- 二进制分帧层: 应用层和传输层之间
- 首部压缩
- 服务端推送
HTTP之请求消息Request
- 请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
- 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本.
- 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息
- 空行,请求头部后面的空行是必须的
- 请求数据也叫主体,可以添加任意的其他数据。
HTTP之响应消息Response
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
- 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
- 消息报头,用来说明客户端要使用的一些附加信息
- 第三部分:空行,消息报头后面的空行是必须的
- 第四部分:响应正文,服务器返回给客户端的文本信息。
在浏览器地址栏键入URL,按下回车之后会经历以下流程:
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 建立TCP连接(三次握手);
- 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
- 释放 TCP连接(四次挥手);
- 浏览器将该 html 文本并显示内容;
三次握手
SYN (同步序列编号)ACK(确认字符)
- 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
- 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
- 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
四次挥手
- 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
- 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
- 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
- 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
网页生成的过程,大致可以分为五步:
- html代码转化为dom
- css代码转化为cssom
- 结合dom和cssom,生成一颗渲染树
- 生成布局layout,即将所有的渲染树的节点进行平面合成
- 将布局绘制paint在屏幕上(可以拓展讲一下减少浏览器渲染的回流和重绘)
HTTPS的工作原理
非对称加密与对称加密双剑合璧,使用非对称加密算法传递用于对称加密算法的密钥,然后使用对称加密算法进行信息传递。这样既安全又高效
浏览器缓存
当浏览器再次访问一个已经访问过的资源时,它会这样做:
- 看看是否命中强缓存,如果命中,就直接使用缓存了。
- 如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。
- 如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。
- 否则,返回最新的资源。
- 强缓存
- Expires
- Cache-control
- 协商缓存
- Last-Modified/If-Modified-Since
- Etag/If-None-Match
vue&react
Virtual DOM
其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。
Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create(用JS对象模拟DOM树)、diff(比较两棵虚拟DOM树的差异)、patch(把差异应用到真正的DOM树上) 等过程。
diff算法
- diff算法比较新旧节点的时候,比较只会在同层级比较,不会跨层级比较
- 当数据发生变化的时候会生成一个新的VNode,然后新VNode和oldNode做对比,发现不一样的地方直接修改在真实的dom上,比较新旧节点,一边比较一边给真是的dom打补丁
- 节点设置key可以高效的利用dom(key最好不要设置成index索引)
- 虚拟DOM diff算法主要就是对以下三种场景进行优化:
- tree diff
对树进行分层比较,两棵树只会对同一层次的节点进行比较。(因为 DOM 节点跨层级的移动操作少到可以忽略不计)
如果父节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。
注意:
React 官方建议不要进行 DOM 节点跨层级的操作,非常影响 React 性能。
在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
- component diff
如果是同一类型的组件,按照原策略继续比较 virtual DOM tree(tree diff)。
对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
如果不是,直接替换整个组件下的所有子节点。
- element diff
对处于同一层级的节点进行对比。
这时 React 建议:添加唯一 key 进行区分。虽然只是小小的改动,性能上却发生了翻天覆地的变化!
如: A B C D --> B A D C
添加 key 之前: 发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
添加 key 之后: B、D 不做任何操作,A、C 进行移动操作,即可。
建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
- 总结
- React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
- React 通过分层求异的策略,对 tree diff 进行算法优化;
- React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
- React 通过设置唯一 key的策略,对 element diff 进行算法优化;
- 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
- 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
vue的响应式原理
Object.defineProperty(obj, prop, descriptor)
- obj 是要在其上定义属性的对象;prop 是要定义或修改的属性的名称;descriptor 是将被定义或修改的属性描述符。
比较核心的是 descriptor,它有很多可选键值,具体的可以去参阅它的文档。这里我们最关心的是 get 和 set,get 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。一旦对象拥有了 getter 和 setter,我们可以简单地把这个对象称为响应式对象- 对象递归调用
- 数组变异方法的解决方法:代理原型/实例方法
observe
- observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。
- observe 的功能就是用来监测数据的变化.
- Observer 是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新:
- 依赖收集和派发更新
- 收集依赖的目的是为了当这些响应式数据发生变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 Watcher 和 Dep 就是一个非常经典的观察者设计模式的实现
- 派发更新就是数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数
- vue编译Compile的过程主要分以下几步
parse(生成AST)=> optimize(优化静态节点) => generate(生成render function)
// 解析模板字符串生成 AST
const ast = parse(template.trim(), options)
//优化语法树
optimize(ast, options)
//生成代码
const code = generate(ast, options)
vue compute和watch的区别
- computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。
- watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
- 所以一般来说需要依赖别的属性来动态获得值的时候可以使用computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch。
对vuex的理解,单向数据流
vuex
- state: 状态中心
- mutations: 更改状态
- actions: 异步更改状态
- getters: 获取状态
- modules: 将state分成多个modules,便于管理
- mutations和action的区别
前端路由的两种实现原理
- Hash模式
- window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。
- History 模式
- popstate监听历史栈信息变化,变化时重新渲染
- 使用pushState方法实现添加功能
- 使用replaceState实现替换功能
前端安全
XSS和CSRF
- XSS:跨站脚本攻击,是一种网站应用程序的安全漏洞攻击,是代码注入的一种。常见方式是将恶意代码注入合法代码里隐藏起来,再诱发恶意代码,从而进行各种各样的非法活动。
预防:
-
使用XSS Filter
- 输入过滤,对用户提交的数据进行有效性验证,仅接受指定长度范围内并符合我们期望格式的的内容提交,阻止或者忽略除此外的其他任何数据。
- 输出转义,当需要将一个字符串输出到Web网页时,同时又不确定这个字符串中是否包括XSS特殊字符,为了确保输出内容的完整性和正确性,输出HTML属性时可以使用HTML转义编码(HTMLEncode)进行处理,输出到
<script>
中,可以进行JS编码。
-
使用 HttpOnly Cookie
将重要的cookie标记为httponly,这样的话当浏览器向Web服务器发起请求的时就会带上cookie字段,但是在js脚本中却不能访问这个cookie,这样就避免了XSS攻击利用JavaScript的document.cookie获取cookie。 -
CSRF:跨站请求伪造,也称 XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。与 XSS 相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
- 预防:用户操作限制——验证码机制
- 方法:添加验证码来识别是不是用户主动去发起这个请求,由于一定强度的验证码机器无法识别,因此危险网站不能伪造一个完整的请求。
- 优点:简单粗暴,低成本,可靠,能防范99.99%的攻击者。
- 缺点:对用户不友好。
- 请求来源限制——验证 HTTP Referer 字段
- 方法:在HTTP请求头中有一个字段叫Referer,它记录了请求的来源地址。 服务器需要做的是验证这个来源地址是否合法,如果是来自一些不受信任的网站,则拒绝响应。
- 优点:零成本,简单易实现。
- 缺点:由于这个方法严重依赖浏览器自身,因此安全性全看浏览器。
- 额外验证机制——token的使用
- 方法:使用token来代替验证码验证。由于黑客并不能拿到和看到cookie里的内容,所以无法伪造一个完整的请求。基本思路如下:
- 服务器随机产生token(比如把cookie hash化生成),存在session中,放在cookie中或者以ajax的形式交给前端。
- 前端发请求的时候,解析cookie中的token,放到请求url里或者请求头中。
- 服务器验证token,由于黑客无法得到或者伪造token,所以能防范csrf