评论区可以纠错完善,也可以留言面试题目
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 的渲染规则(能说出以下四点就够了)。包括:
1. BFC 内部的子元素,在垂直方向,边距会发生重叠。
2. BFC 在页面中是独立的容器,外面的元素不会影响里面的元素,反之亦然。
3. BFC 区域不与旁边的 float box 区域重叠。(可以用来清除浮动带来的影响)。
4. 计算 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(面试常问,略)
js 部分
call, apply, bind 区别? 怎么实现 call,apply 方法
Function.prototype.myBind = function(content) {
if(type of 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,content.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 的区别
事件捕获
处于目标阶段
事件冒泡阶段
事件委托 事件委托是指将事件绑定目标元素的到父元素上,利用冒泡机制触发该事件
可以减少事件注册,节省大量内存占用可以将事件应用于动态添加的子元素上
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 的渲染性能。
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
面试相关
正在整理中的问题
写过 webpack loader 吗
vue 怎么监听数组
手写快排,时间复杂度,优化
webpack 用过吗?摇树是什么,什么场景下用过?
你遇到过最难的问题是什么
react 虚拟 dom 实现,diff 算法;
手写快排,怎么优化;说下 sort 实现原理;
解释一个你最近遇到的技术挑战