前言
Hello 大家好,年底了,又是跳槽的好时机,好多前端小伙伴跟我说某某公司太扣了,天天加班,还不给加班费,有一位小伙要求涨薪,领导口头上答应,最后涨薪邮件发来时,小伙一脸一脸懵圈的看着邮件上的数字,涨薪50元
,想着自己前几个月的付出真的不值,小伙直接群发了邮件走了,换了一家公司工资直接翻倍。这里我分类整理了一些常见的面试题,希望能帮助到一些想跳槽加薪的小伙伴。
你们的鼓励是我最大的支持 记得点赞三连哦 十分感谢!
网络基础
从输入一个 URL 地址到浏览器完成渲染的整个过程
前一段时间,面试问到了这个问题,感觉自己回答的不是很好, 当时我的回答是
- 域名解析(这个说了一下域名解析的过程) ,解析出对应IP地址
- 解析成功之后,发起TCP三次握手建立连接
- 建立连接后发起HTTPS请求
- 服务器响应https请求,浏览器得到html代码
- 浏览器解析html代码,并请求静态资源(html/css/js等)
- 然后浏览器渲染,展示给用户
但是当时面试官问我, 发起http请求到服务器然后返回给客户端主要做了那些工作,但是我一下蒙了就没有想起来,当时回答这一块就感觉很一般。今天来总结一下一次完整的HTTP请求过程到底经历了什么
注意! 面试官可以基于这道题进行前端很多知识点的考察 从 http 网络知识到浏览器原理再到前端性能优化 这个题目都可以作为引子开始
http 状态码含义
- http 状态码 204 (无内容) 服务器成功处理了请求,但没有返回任何内容
- http 状态码 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
- http 状态码 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
- http 状态码 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
- http 状态码 400 (错误请求) 服务器不理解请求的语法(一般为参数错误)。
- http 状态码 401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
- http 状态码 403 (禁止) 服务器拒绝请求。(一般为客户端的用户权限不够)
- http 状态码 404 (未找到) 服务器找不到请求的网页。
http2.0 做了哪些改进 3.0 呢
http2.0 特性如下
- 二进制分帧传输
- 多路复用
- 头部压缩
- 服务器推送
Http3.0 相对于 Http2.0 是一种脱胎换骨的改变!
http 协议是应用层协议,都是建立在传输层之上的。我们也都知道传输层上面不只有 TCP 协议,还有另外一个强大的协议 UDP 协议,2.0 和 1.0 都是基于 TCP 的,因此都会有 TCP 带来的硬伤以及局限性。而 Http3.0 则是建立在 UDP 的基础上。所以其与 Http2.0 之间有质的不同。
http3.0 特性如下
- 连接迁移
- 无队头阻塞
- 自定义的拥塞控制
- 前向安全和前向纠错
网络七层协议(OSI模型)
OSI是一个定义良好的协议规范集,并有许多可选部分完成类似的任务。它定义了开放系统的层次结构、层次之间的相互关系以及各层所包括的可能的任务,作为一个框架来协调和组织各层所提供的服务。
OSI参考模型并没有提供一个可以实现的方法,而是描述了一些概念,用来协调进程间通信标准的制定。即OSI参考模型并不是一个标准,而是一个在制定标准时所使用的概念性框架。
- 第7层 应用层
应用层(Application Layer)提供为应用软件而设计的接口,以设置与另一应用软件之间的通信。例如:HTTP、HTTPS、FTP、Telnet、SSH、SMTP、POP3等。
- 第6层 表示层
表示层(Presentation Layer)把数据转换为能与接收者的系统格式兼容并适合传输的格式。
- 第5层 会话层
会话层(Session Layer)负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。
- 第4层 传输层
传输层(Transport Layer)把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。例如:传输控制协议(TCP)等。
- 第3层 网络层
网络层(Network Layer)决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成分组。网络表头包含了网络资料。例如:互联网协议(IP)等。
- 第2层 数据链路层
数据链路层(Data Link Layer)负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成信息框(Data Frame)。数据链表头(DLH)是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。例如以太网、无线局域网(Wi-Fi)和通用分组无线服务(GPRS)等。
分为两个子层:逻辑链路控制(logical link control,LLC)子层和介质访问控制(Media access control,MAC)子层。
- 第1层 物理层
物理层(Physical Layer)在局部局域网上发送数据帧(Data Frame),它负责管理电脑通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机接口卡等。
TCP 协议三次握手
- 客户端通过
SYN
报文段发送连接请求,确定服务端是否开启端口准备连接。状态设置为SYN_SEND
; - 服务器如果有开着的端口并且决定接受连接,就会返回一个
SYN+ACK
报文段给客户端,状态设置为SYN_RECV
; - 客户端收到服务器的
SYN+ACK
报文段,向服务器发送ACK
报文段表示确认。此时客户端和服务器都设置为ESTABLISHED
状态。连接建立,可以开始数据传输了。
翻译成大白话就是: 客户端:你能接收到我的消息吗? 服务端:可以的,那你能接收到我的回复吗? 客户端:可以,那我们开始聊正事吧。
为什么是3次? :避免历史连接,确认客户端发来的请求是这次通信的人 为什么不是4次? :3次够了第四次浪费
TCP 协议四次挥手
-
为什么不是两次?
- 两次情况客户端说完结束就立马断开不再接收,无法确认服务端是否接收到断开消息,但且服务端可能还有消息未发送完。
-
为什么不是三次?
- 3次情况服务端接收到断开消息,向客户端发送确认接受消息,客户端未给最后确认断开的回复。
http
1. HTTP 请求报文结构
- 首行是Request-Line包括:请求方法,请求URI,协议版本,CRLF
- 首行之后是若干行请求头,包括general-header,request-header或者entity-header,每个一行以CRLF结束
- 请求头和消息实体之间有一个CRLF分隔
- 根据实际请求需要可能包含一个消息实体 一个请求报文例子如下:
GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1
Host: www.w3.org
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
Referer: https://www.google.com.hk/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: authorstyle=yes
If-None-Match: "2cc8-3e3073913b100"
If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT
name=qiu&age=25
复制代码
2. HTTP 响应报文结构
- 首行是状态行包括:HTTP版本,状态码,状态描述,后面跟一个CRLF
- 首行之后是若干行响应头,包括:通用头部,响应头部,实体头部
- 响应头部和响应实体之间用一个CRLF空行分隔
- 最后是一个可能的消息实体 响应报文例子如下:
HTTP/1.1 200 OK
Date: Tue, 08 Jul 2014 05:28:43 GMT
Server: Apache/2
Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT # 最后修改时间,用于协商缓存
ETag: "40d7-3e3073913b100" # 文件hash,用于协商缓存
Accept-Ranges: bytes
Content-Length: 16599
Cache-Control: max-age=21600 # 强缓存(浏览器端)最大过期时间
Expires: Tue, 08 Jul 2014 11:28:43 GMT # 强缓存(浏览器端)过期时间
P3P: policyref="http://www.w3.org/2001/05/P3P/p3p.xml"
Content-Type: text/html; charset=iso-8859-1
{"name": "qiu", "age": 25}
复制代码
3. HTTP常见状态码及其含义
-
1XX:信息状态码
- 100 Continue 继续,一般在发送post请求时,已发送了http header之后服务端将返回此信息,表示确认,之后发送具体参数信息
-
2XX:成功状态码
- 200 OK 正常返回信息
- 201 Created 请求成功并且服务器创建了新的资源
- 202 Accepted 服务器已接受请求,但尚未处理
-
3XX:重定向
- 301 Moved Permanently 请求的网页已永久移动到新位置。
- 302 Found 临时性重定向。
- 303 See Other 临时性重定向,且总是使用 GET 请求新的 URI。
- 304 Not Modified 自从上次请求后,请求的网页未修改过。
-
4XX:客户端错误
- 400 Bad Request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。
- 401 Unauthorized 请求未授权。
- 403 Forbidden 禁止访问。
- 404 Not Found 找不到如何与 URI 相匹配的资源。
-
5XX: 服务器错误
- 500 Internal Server Error 最常见的服务器端错误。
- 503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)。
http1.0、http1.1、http2.0区别
§ http1.0 vs http1.1
- 长连接,Connection请求头的值为Keep-Alive时,客户端通知服务器返回本次请求结果后保持连接,可有效减少TCP的三次握手开销;
- 缓存处理,增加了
Cache-Control
等缓存相关头(看下面详细介绍); - 请求头增加Host,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
- 请求头增加
range
可用于断点续传
,它支持只请求资源的某个部分,可用于断点续传
。 - 增加请求方法:(OPTIONS,PUT, DELETE, TRACE, CONNECT)
- 新增了24个状态码,(如100Continue,发请求体之前先用
请求头
试探一下服务器,再决定要不要发请求体
)
http1.x vs http2.0
- 服务端推送
- 多路复用,多个请求都在同一个TCP连接上完成
- 报文头部压缩,HTTP2.0可以维护一个字典,差量更新HTTP头部,大大降低因头部传输产生的流量
https
HTTPS = HTTP + 加密 + 认证 + 完整性保护
http的不足
:
- 通信使用未加密的明文,内容容易被窃取
- 不验证通信方的身份,容易遭遇伪装
- 无法验证报文的完整性,容易被篡改
https
就是为了解决上述http协议的安全性问题诞生的。https并非是应用层的新协议,是基于http协议的,将http和tcp协议接口部分用SSL
和TLS
协议代替而已。
http: IP ➜ TCP ➜ HTTP(应用层)
https: IP ➜ TCP ➜ SSL ➜ HTTP(应用层)
§ 加密
- SSL(Secure Sockets Layer安全套接字层协议)
- TLS(Transport Layer Security传输层安全协议)
对称加密:用同一个密钥加密、解密。常用对称加密算法:AES,RC4,3DES 非对称加密:用一个密钥加密的数据,必须使用另一个密钥才能解密。常用非对称加密算法:RSA,DSA/DSS
常用HASH算法:MD5,SHA1,SHA256
其中非对称加密
算法用于在握手过程中加密生成的密码,对称加密
算法用于对真正传输的数据进行加密,而HASH
算法用于验证数据的完整性。
由于浏览器生成的密码是整个数据加密的关键,因此在传输的时候使用了非对称加密算法对其加密。
非对称加密算法会生成公钥和私钥,公钥只能用于加密数据,因此可以随意传输,而网站的私钥用于对数据进行解密,所以网站都会非常小心的保管自己的私钥,防止泄漏。
§ CA 证书包含的信息: :
CA(Certification Authority证书颁发机构)
- 公钥
- 网站地址
- 证书的颁发机构
- 过期时间
§ https加密通信的流程:
-
浏览器输入url请求网站,并附带自己支持的一套加密规则。
-
网站制作证书,这个应该是网站建站的时候就已经弄好了。证书分为服务器证书和客户端证书,我们所说的证书一般都是指服务器证书(
ssl证书详细解读
)。制作过程如下:
- 制作CSR文件。就是制作Certificate Secure Request证书请求文件,制作过程中,系统会产生2个密钥,一个是公钥就是这个CSR文件,另一个是私钥,存在服务器上。
- CA认证。将CSR文件提交给CA,一般有两种认证方式:域名认证 和 企业文档认证。
- 证书安装。收到CA证书后,将证书部署在服务器上。
-
网站从浏览器发过来的加密规则中选一组自身也支持的加密算法和hash算法,并向浏览器发送带有公钥的证书,当然证书还包含了很多信息,如网站地址、证书的颁发机构、过期时间等。
-
浏览器解析证书。
- 验证证书的合法性。如颁发机构是否合法、证书中的网站地址是否与访问的地址一致,若不合法,则浏览器提示证书不受信任,若合法,浏览器会显示一个小锁头。
- 若合法,或用户接受了不合法的证书,浏览器会生成一串随机数的密码(即密钥),并用证书中提供的公钥加密。
- 使用约定好的hash计算握手消息,并使用生成的随机数(即密钥)对消息进行加密,最后将之前生成的所有消息一并发送给网站服务器。
-
网站服务器解析消息。用已有的私钥将密钥解密出来,然后用密钥解密发过来的握手消息,并验证是否跟浏览器传过来的一致。然后再用密钥加密一段握手消息,发送给浏览器。
-
浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。
下图表示https加密通信的过程:
JS
什么是事件代理(事件委托) 有什么好处
事件委托的原理:不给每个子节点单独设置事件监听器,而是设置在其父节点上,然后利用冒泡原理设置每个子节点。
优点:
- 减少内存消耗和 dom 操作,提高性能 在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的操作 dom,那么引起浏览器重绘和回流的可能也就越多,页面交互的事件也就变的越长,这也就是为什么要减少 dom 操作的原因。每一个事件处理函数,都是一个对象,多一个事件处理函数,内存中就会被多占用一部分空间。如果要用事件委托,就会将所有的操作放到 js 程序里面,只对它的父级进行操作,与 dom 的操作就只需要交互一次,这样就能大大的减少与 dom 的交互次数,提高性能;
- 动态绑定事件 因为事件绑定在父级元素 所以新增的元素也能触发同样的事件
addEventListener 默认是捕获还是冒泡
默认是冒泡
addEventListener第三个参数默认为 false 代表执行事件冒泡行为。
当为 true 时执行事件捕获行为。
apply call bind 区别
- 三者都可以改变函数的 this 对象指向。
- 三者第一个参数都是 this 要指向的对象,如果如果没有这个参数或参数为 undefined 或 null,则默认指向全局 window。
- 三者都可以传参,但是 apply 是数组,而 call 是参数列表,且 apply 和 call 是一次性传入参数,而 bind 可以分为多次传入。
- bind 是返回绑定 this 之后的函数,便于稍后调用;apply 、call 则是立即执行 。
- bind()会返回一个新的函数,如果这个返回的新的函数作为构造函数创建一个新的对象,那么此时 this 不再指向传入给 bind 的第一个参数,而是指向用 new 创建的实例
注意!很多同学可能会忽略 bind 绑定的函数作为构造函数进行 new 实例化的情况
举出闭包实际场景运用的例子
比如常见的防抖节流
// 防抖
function debounce(fn, delay = 300) {
let timer; //闭包引用的外界变量
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
复制代码
使用闭包可以在 JavaScript 中模拟块级作用域
function outputNumbers(count) {
(function () {
for (var i = 0; i < count; i++) {
alert(i);
}
})();
alert(i); //导致一个错误!
}
复制代码
闭包可以用于在对象中创建私有变量
var aaa = (function () {
var a = 1;
function bbb() {
a++;
console.log(a);
}
function ccc() {
a++;
console.log(a);
}
return {
b: bbb, //json结构
c: ccc,
};
})();
console.log(aaa.a); //undefined
aaa.b(); //2
aaa.c(); //3
事件循环相关题目(代码输出顺序)
setTimeout(function () {
console.log("1");
}, 0);
async function async1() {
console.log("2");
const data = await async2();
console.log("3");
return data;
}
async function async2() {
return new Promise((resolve) => {
console.log("4");
resolve("async2的结果");
}).then((data) => {
console.log("5");
return data;
});
}
async1().then((data) => {
console.log("6");
console.log(data);
});
new Promise(function (resolve) {
console.log("7");
// resolve()
}).then(function () {
console.log("8");
});
复制代码
输出结果:247536 async2 的结果 1
注意!我在最后一个 Promise 埋了个坑 我没有调用 resolve 方法 这个是在面试美团的时候遇到了 当时自己没看清楚 以为都是一样的套路 最后面试官说不对 找了半天才发现是这个坑 哈哈
手写 bind
//bind实现要复杂一点 因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
let _this = this;
// bind情况要复杂一点
const result = function (...innerArgs) {
// 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
// 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论
// this.__proto__ === result.prototype //this instanceof result =>true
// this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
if (this instanceof _this === true) {
// 此时this指向指向result的实例 这时候不需要改变this指向
this[fn] = _this;
this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并
delete this[fn];
} else {
// 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
context[fn](...[...args, ...innerArgs]);
delete context[fn];
}
};
// 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
// 实现继承的方式: 使用Object.create
result.prototype = Object.create(this.prototype);
return result;
};
//用法如下
// function Person(name, age) {
// console.log(name); //'我是参数传进来的name'
// console.log(age); //'我是参数传进来的age'
// console.log(this); //构造函数this指向实例对象
// }
// // 构造函数原型的方法
// Person.prototype.say = function() {
// console.log(123);
// }
// let obj = {
// objName: '我是obj传进来的name',
// objAge: '我是obj传进来的age'
// }
// // 普通函数
// function normalFun(name, age) {
// console.log(name); //'我是参数传进来的name'
// console.log(age); //'我是参数传进来的age'
// console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
// console.log(this.objName); //'我是obj传进来的name'
// console.log(this.objAge); //'我是obj传进来的age'
// }
// 先测试作为构造函数调用
// let bindFun = Person.myBind(obj, '我是参数传进来的name')
// let a = new bindFun('我是参数传进来的age')
// a.say() //123
// 再测试作为普通函数调用
// let bindFun = normalFun.myBind(obj, '我是参数传进来的name')
// bindFun('我是参数传进来的age')
复制代码
手写 promise.all 和 race
//静态方法
static all(promiseArr) {
let result = [];
//声明一个计数器 每一个promise返回就加一
let count = 0;
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
//这里用 Promise.resolve包装一下 防止不是Promise类型传进来
Promise.resolve(promiseArr[i]).then(
(res) => {
//这里不能直接push数组 因为要控制顺序一一对应(感谢评论区指正)
result[i] = res;
count++;
//只有全部的promise执行成功之后才resolve出去
if (count === promiseArr.length) {
resolve(result);
}
},
(err) => {
reject(err);
}
);
}
});
}
//静态方法
static race(promiseArr) {
return new Mypromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
Promise.resolve(promiseArr[i]).then(
(res) => {
//promise数组只要有任何一个promise 状态变更 就可以返回
resolve(res);
},
(err) => {
reject(err);
}
);
}
});
}
}
复制代码
手写-实现一个寄生组合继承
function Parent(name) {
this.name = name;
this.say = () => {
console.log(111);
};
}
Parent.prototype.play = () => {
console.log(222);
};
function Children(name) {
Parent.call(this);
this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
// let child = new Children("111");
// // console.log(child.name);
// // child.say();
// // child.play();
复制代码
手写-new 操作符
function myNew(fn, ...args) {
let obj = Object.create(fn.prototype);
let res = fn.call(obj, ...args);
if (res && (typeof res === "object" || typeof res === "function")) {
return res;
}
return obj;
}
用法如下:
// // function Person(name, age) {
// // this.name = name;
// // this.age = age;
// // }
// // Person.prototype.say = function() {
// // console.log(this.age);
// // };
// // let p1 = myNew(Person, "lihua", 18);
// // console.log(p1.name);
// // console.log(p1);
// // p1.say();
复制代码
手写-setTimeout 模拟实现 setInterval
function mySetInterval(fn, time = 1000) {
let timer = null,
isClear = false;
function interval() {
if (isClear) {
isClear = false;
clearTimeout(timer);
return;
}
fn();
timer = setTimeout(interval, time);
}
timer = setTimeout(interval, time);
return () => {
isClear = true;
};
}
复制代码
手写-发布订阅模式
class EventEmitter {
constructor() {
this.events = {};
}
// 实现订阅
on(type, callBack) {
if (!this.events[type]) {
this.events[type] = [callBack];
} else {
this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter((item) => {
return item !== callBack;
});
}
// 只执行一次订阅事件
once(type, callBack) {
function fn() {
callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {
this.events[type] &&
this.events[type].forEach((fn) => fn.apply(this, rest));
}
}
// 使用如下
// const event = new EventEmitter();
// const handle = (...rest) => {
// console.log(rest);
// };
// event.on("click", handle);
// event.emit("click", 1, 2, 3, 4);
// event.off("click", handle);
// event.emit("click", 1, 2);
// event.once("dbClick", () => {
// console.log(123456);
// });
// event.emit("dbClick");
// event.emit("dbClick");
复制代码
手写-防抖节流
// 防抖
function debounce(fn, delay = 300) {
//默认300毫秒
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debounce(() => {
console.log(111);
}, 1000)
);
// 节流
// 设置一个标志
function throttle(fn, delay) {
let flag = true;
return () => {
if (!flag) return;
flag = false;
timer = setTimeout(() => {
fn();
flag = true;
}, delay);
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);
复制代码
手写-将虚拟 Dom 转化为真实 Dom(类似的递归题-必考)
{
tag: 'DIV',
attrs:{
id:'app'
},
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
}
把上诉虚拟Dom转化成下方真实Dom
<div id="app">
<span>
<a></a>
</span>
<span>
<a></a>
<a></a>
</span>
</div>
复制代码
答案
// 真正的渲染函数
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作 这一步是关键
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
复制代码
手写-实现一个对象的 flatten 方法
题目描述
const obj = {
a: {
b: 1,
c: 2,
d: {e: 5}
},
b: [1, 3, {a: 2, b: 3}],
c: 3
}
flatten(obj) 结果返回如下
// {
// 'a.b': 1,
// 'a.c': 2,
// 'a.d.e': 5,
// 'b[0]': 1,
// 'b[1]': 3,
// 'b[2].a': 2,
// 'b[2].b': 3
// c: 3
// }
复制代码
答案
function isObject(val) {
return typeof val === "object" && val !== null;
}
function flatten(obj) {
if (!isObject(obj)) {
return;
}
let res = {};
const dfs = (cur, prefix) => {
if (isObject(cur)) {
if (Array.isArray(cur)) {
cur.forEach((item, index) => {
dfs(item, `${prefix}[${index}]`);
});
} else {
for (let k in cur) {
dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
}
}
} else {
res[prefix] = cur;
}
};
dfs(obj, "");
return res;
}
flatten();
复制代码
手写-判断括号字符串是否有效
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
复制代码
答案
const isValid = function (s) {
if (s.length % 2 === 1) {
return false;
}
const regObj = {
"{": "}",
"(": ")",
"[": "]",
};
let stack = [];
for (let i = 0; i < s.length; i++) {
if (s[i] === "{" || s[i] === "(" || s[i] === "[") {
stack.push(s[i]);
} else {
const cur = stack.pop();
if (s[i] !== regObj[cur]) {
return false;
}
}
}
if (stack.length) {
return false;
}
return true;
};
复制代码
手写-查找数组公共前缀
题目描述
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
复制代码
答案
const longestCommonPrefix = function (strs) {
const str = strs[0];
let index = 0;
while (index < str.length) {
const strCur = str.slice(0, index + 1);
for (let i = 0; i < strs.length; i++) {
if (!strs[i] || !strs[i].startsWith(strCur)) {
return str.slice(0, index);
}
}
index++;
}
return str;
};
复制代码
手写-字符串最长的不重复子串
题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""
输出: 0
复制代码
答案
const lengthOfLongestSubstring = function (s) {
if (s.length === 0) {
return 0;
}
let left = 0;
let right = 1;
let max = 0;
while (right <= s.length) {
let lr = s.slice(left, right);
const index = lr.indexOf(s[right]);
if (index > -1) {
left = index + left + 1;
} else {
lr = s.slice(left, right + 1);
max = Math.max(max, lr.length);
}
right++;
}
return max;
};
复制代码
手写-如何找到数组中第一个没出现的最小正整数 怎么优化
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例 1:
输入:nums = [1,2,0]
输出:3
示例 2:
输入:nums = [3,4,-1,1]
输出:2
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
复制代码
这是一道字节的算法题 目的在于不断地去优化算法思路
- 第一版 O(n^2) 的方法
const firstMissingPositive = (nums) => {
let i = 0;
let res = 1;
while (i < nums.length) {
if (nums[i] == res) {
res++;
i = 0;
} else {
i++;
}
}
return res;
};
复制代码
- 第二版 时间空间均为 O(n)
const firstMissingPositive = (nums) => {
const set = new Set();
for (let i = 0; i < nums.length; i++) {
set.add(nums[i]);
}
for (let i = 1; i <= nums.length + 1; i++) {
if (!set.has(i)) {
return i;
}
}
};
复制代码
- 最终版 时间复杂度为 O(n) 并且只使用常数级别空间
const firstMissingPositive = (nums) => {
for (let i = 0; i < nums.length; i++) {
while (
nums[i] >= 1 &&
nums[i] <= nums.length && // 对1~nums.length范围内的元素进行安排
nums[nums[i] - 1] !== nums[i] // 已经出现在理想位置的,就不用交换
) {
const temp = nums[nums[i] - 1]; // 交换
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
// 现在期待的是 [1,2,3,...],如果遍历到不是放着该放的元素
for (let i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return nums.length + 1; // 发现元素 1~nums.length 占满了数组,一个没缺
};
复制代码
手写-怎么在制定数据源里面生成一个长度为 n 的不重复随机数组 能有几种方法 时间复杂度多少
- 第一版 时间复杂度为 O(n^2)
function getTenNum(testArray, n) {
let result = [];
for (let i = 0; i < n; ++i) {
const random = Math.floor(Math.random() * testArray.length);
const cur = testArray[random];
if (result.includes(cur)) {
i--;
break;
}
result.push(cur);
}
return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 10);
复制代码
- 第二版 标记法 / 自定义属性法 时间复杂度为 O(n)
function getTenNum(testArray, n) {
let hash = {};
let result = [];
let ranNum = n;
while (ranNum > 0) {
const ran = Math.floor(Math.random() * testArray.length);
if (!hash[ran]) {
hash[ran] = true;
result.push(ran);
ranNum--;
}
}
return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 10);
复制代码
- 第三版 交换法 时间复杂度为 O(n)
function getTenNum(testArray, n) {
const cloneArr = [...testArray];
let result = [];
for (let i = 0; i < n; i++) {
debugger;
const ran = Math.floor(Math.random() * (cloneArr.length - i));
result.push(cloneArr[ran]);
cloneArr[ran] = cloneArr[cloneArr.length - i - 1];
}
return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 14);
复制代码
值得一提的是操作数组的时候使用交换法 这种思路在算法里面很常见
- 最终版 边遍历边删除 时间复杂度为 O(n)
function getTenNum(testArray, n) {
const cloneArr = [...testArray];
let result = [];
for (let i = 0; i < n; ++i) {
const random = Math.floor(Math.random() * cloneArr.length);
const cur = cloneArr[random];
result.push(cur);
cloneArr.splice(random, 1);
}
return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 14);
复制代码
原型链判断
请写出下面的答案
Object.prototype.__proto__;
Function.prototype.__proto__;
Object.__proto__;
Object instanceof Function;
Function instanceof Object;
Function.prototype === Function.__proto__;
复制代码
Object.prototype.__proto__; //null
Function.prototype.__proto__; //Object.prototype
Object.__proto__; //Function.prototype
Object instanceof Function; //true
Function instanceof Object; //true
Function.prototype === Function.__proto__; //true
复制代码
这道题目深入考察了原型链相关知识点 尤其是 Function 和 Object 的之间的关系
Es6 的 let 实现原理
原始 es6 代码
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
复制代码
babel 编译之后的 es5 代码(polyfill)
var funcs = [];
var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
funcs[0](); // 0
复制代码
其实我们根据 babel 编译之后的结果可以看得出来 let 是借助闭包和函数作用域来实现块级作用域的效果的 在不同的情况下 let 的编译结果是不一样的
js正则替换
要把一段文字里面符合某个正则的都按一定规则替换成另一段与原文相关的文字
用string.replace(regex,function(element,index){})方法即可
var t="33aabb44aabb55aabbcc";
t=t.replace(/\d{2}/g,function(m,i){return "<b>"+m+"</b>";});
CSS
css 的渲染层合成是什么 浏览器如何创建新的渲染层
在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite) ,从而正确处理透明元素和重叠元素的显示。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。
css 优先级是怎么计算的
- 第一优先级:!important 会覆盖页面内任何位置的元素样式
- 1.内联样式,如 style="color: green",权值为 1000
- 2.ID 选择器,如#app,权值为 0100
- 3.类、伪类、属性选择器,如.foo, :first-child, div[class="foo"],权值为 0010
- 4.标签、伪元素选择器,如 div::first-line,权值为 0001
- 5.通配符、子类选择器、兄弟选择器,如*, >, +,权值为 0000
- 6.继承的样式没有权值
position 有哪些值,作用分别是什么
-
static
static(没有定位)是 position 的默认值,元素处于正常的文档流中,会忽略 left、top、right、bottom 和 z-index 属性。
-
relative
relative(相对定位)是指给元素设置相对于原本位置的定位,元素并不脱离文档流,因此元素原本的位置会被保留,其他的元素位置不会受到影响。
使用场景:子元素相对于父元素进行定位
-
absolute absolute(绝对定位)是指给元素设置绝对的定位,相对定位的对象可以分为两种情况:
- 设置了 absolute 的元素如果存在有祖先元素设置了 position 属性为 relative 或者 absolute,则这时元素的定位对象为此已设置 position 属性的祖先元素。
- 如果并没有设置了 position 属性的祖先元素,则此时相对于 body 进行定位。 使用场景:跟随图标 图标使用不依赖定位父级的 absolute 和 margin 属性进行定位,这样,当文本的字符个数改变时,图标的位置可以自适应
-
fixed 可以简单说 fixed 是特殊版的 absolute,fixed 元素总是相对于 body 定位的。 使用场景:侧边栏或者广告图
-
inherit 继承父元素的 position 属性,但需要注意的是 IE8 以及往前的版本都不支持 inherit 属性。
-
sticky 设置了 sticky 的元素,在屏幕范围(viewport)时该元素的位置并不受到定位影响(设置是 top、left 等属性无效),当该元素的位置将要移出偏移范围时,定位又会变成 fixed,根据设置的 left、top 等属性成固定位置的效果。 当元素在容器中被滚动超过指定的偏移值时,元素在容器内固定在指定位置。亦即如果你设置了 top: 50px,那么在 sticky 元素到达距离相对定位的元素顶部 50px 的位置时固定,不再向上移动(相当于此时 fixed 定位)。
使用场景:跟随窗口
垂直水平居中实现方式
这道题基本也是 css 经典题目 但是网上已经有太多千篇一律的答案了 如果大家想在这道题加分
可以针对定宽高和不定宽高的实现多种不同的方案
css 怎么开启硬件加速(GPU 加速)
浏览器在处理下面的 css 的时候,会使用 GPU 渲染
- transform(当 3D 变换的样式出现时会使用 GPU 加速)
- opacity
- filter
- will-change
采用 transform: translateZ(0)
采用 transform: translate3d(0, 0, 0)
使用 CSS 的 will-change属性。 will-change 可以设置为opacity、transform、top、left、bottom、right。
复制代码
注意!层爆炸,由于某些原因可能导致产生大量不在预期内的合成层,虽然有浏览器的层压缩机制,但是也有很多无法进行压缩的情况,这就可能出现层爆炸的现象(简单理解就是,很多不需要提升为合成层的元素因为某些不当操作成为了合成层)。解决层爆炸的问题,最佳方案是打破 overlap 的条件,也就是说让其他元素不要和合成层元素重叠。简单直接的方式:使用 3D 硬件加速提升动画性能时,最好给元素增加一个 z-index 属性,人为干扰合成的排序,可以有效减少创建不必要的合成层,提升渲染性能,移动端优化效果尤为明显。
flex:1 是哪些属性组成的
flex 实际上是 flex-grow、flex-shrink 和 flex-basis 三个属性的缩写。
flex-grow:定义项目的的放大比例;
默认为0,即 即使存在剩余空间,也不会放大;
所有项目的flex-grow为1:等分剩余空间(自动放大占位);
flex-grow为n的项目,占据的空间(放大的比例)是flex-grow为1的n倍。
复制代码
flex-shrink:定义项目的缩小比例;
默认为1,即 如果空间不足,该项目将缩小;
所有项目的flex-shrink为1:当空间不足时,缩小的比例相同;
flex-shrink为0:空间不足时,该项目不会缩小;
flex-shrink为n的项目,空间不足时缩小的比例是flex-shrink为1的n倍。
复制代码
flex-basis: 定义在分配多余空间之前,项目占据的主轴空间(main size),浏览器根据此属性计算主轴是否有多余空间
默认值为auto,即 项目原本大小;
设置后项目将占据固定空间。
复制代码
BFC
BFC
是Block Formatting Context
,也就是块级格式化上下文
,是用于布局块级盒子的一块渲染区域。
简单来说,BFC 实际上是一块区域,在这块区域中遵循一定的规则,有一套独特的渲染规则。
文档流其实分为普通流
、定位流
和浮动流
和三种,普通流其实就是指BFC中的FC,也即格式化上下文
。
普通流:元素按照其在 HTML 中的先后位置从上到下、从左到右布局,在这个过程中,行内元素水平排列,直到当行被占满然后换行,块级元素则会被渲染为完整的一个新行。
格式化上下文:页面中的一块渲染区域,有一套渲染规则,决定了其子元素如何布局,以及和其他元素之间的关系和作用
§ BFC的几条规则: 1)BFC 区域内的元素外边距会发生重叠。 2)BFC 区域内的元素不会与浮动元素重叠。 3)计算 BFC 区域的高度时,浮动元素也参与计算。 4)BFC 区域就相当于一个容器,内部的元素不会影响到外部,同样外部的元素也不会影响到内部。
§ BFC的应用:
- 清除浮动:父元素设置overflow: hidden触发BFC实现清除浮动,防止父元素高度塌陷,后面的元素被覆盖,实现文字环绕等等。
- 消除相邻元素垂直方向的边距重叠:第二个子元素套一层,并设置overflow: hidden,构建BFC使其不影响外部元素。
- 消除父子元素边距重叠,父元素设置overflow: hidden
§ 触发BFC的方式:
- float 不为 none,浮动元素所在的区域就是一个 BFC 区域。
- position 的值不是 static 或 relative 的元素所在的区域就是一个 BFC 区域
- display为 table-cell 的表格单元格元素所在的区域也是一个 BFC 区域
- overflow 不为 visible 的元素所在的区域也是一个 BFC 区域
浏览器
浏览器如何创建新的渲染层
- 根元素 document
- 有明确的定位属性(relative、fixed、sticky、absolute)
- opacity < 1
- 有 CSS fliter 属性
- 有 CSS mask 属性
- 有 CSS mix-blend-mode 属性且值不为 normal
- 有 CSS transform 属性且值不为 none
- backface-visibility 属性为 hidden
- 有 CSS reflection 属性
- 有 CSS column-count 属性且值不为 auto 或者有 CSS column-width 属性且值不为 auto
- 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画
- overflow 不为 visible
注意!不少人会将这些合成层的条件和渲染层产生的条件混淆,这两种条件发生在两个不同的层处理环节,是完全不一样的
304 是什么意思 一般什么场景出现 ,命中强缓存返回什么状态码
协商缓存命中返回 304
这种方式使用到了 headers 请求头里的两个字段,Last-Modified & If-Modified-Since 。服务器通过响应头 Last-Modified 告知浏览器,资源最后被修改的时间:
Last-Modified: Thu, 20 Jun 2019 15:58:05 GMT
当再次请求该资源时,浏览器需要再次向服务器确认,资源是否过期,其中的凭证就是请求头 If-Modified-Since 字段,值为上次请求中响应头 Last-Modified 字段的值:
If-Modified-Since: Thu, 20 Jun 2019 15:58:05 GMT
浏览器在发送请求的时候服务器会检查请求头 request header 里面的 If-modified-Since,如果最后修改时间相同则返回 304,否则给返回头(response header)添加 last-Modified 并且返回数据(response body)。
另外,浏览器在发送请求的时候服务器会检查请求头(request header)里面的 if-none-match 的值与当前文件的内容通过 hash 算法(例如 nodejs: cryto.createHash('sha1'))生成的内容摘要字符对比,相同则直接返回 304,否则给返回头(response header)添加 etag 属性为当前的内容摘要字符,并且返回内容。
综上总结为:
请求头last-modified的日期与响应头的last-modified一致
请求头if-none-match的hash与响应头的etag一致
这两种情况会返回Status Code: 304
复制代码
强缓存命中返回 200 200(from cache)
RAF 和 RIC 是什么
requestAnimationFrame: 告诉浏览器在下次重绘之前执行传入的回调函数(通常是操纵 dom,更新动画的函数);由于是每帧执行一次,那结果就是每秒的执行次数与浏览器屏幕刷新次数一样,通常是每秒 60 次。
requestIdleCallback: : 会在浏览器空闲时间执行回调,也就是允许开发人员在主事件循环中执行低优先级任务,而不影响一些延迟关键事件。如果有多个回调,会按照先进先出原则执行,但是当传入了 timeout,为了避免超时,有可能会打乱这个顺序。
Cache-Control
用于判断强缓存
,也就是是否直接取在客户端缓存文件,不请求后端。
请求头和响应头都可以使用。
请求时的缓存指令包括:no-cache、no-store、max-age、max-stale、min-fresh、only-if- cached, 响应消息中的指令包括:public、private、no-cache、no-store、no-transform、must- revalidate、proxy-revalidate、max-age。
常用指令含义如下:
no-cache
:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。no-store
:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。public
:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。private
:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。max-age
:指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
打包
webpack
webpack
是一个模块打包工具,你可以使用webpack管理你的模块依赖,并编绎输出模块们所需的静态文件。- 它能够很好地管理、打包Web开发中所用到的HTML、Javascript、CSS以及各种静态文件(图片、字体等),让开发过程更加高效。
- 对于不同类型的资源,webpack有对应的模块加载器
loader
。 - webpack模块打包器会分析模块间的依赖关系,最后生成优化且合并后的静态资源。
- 其插件功能提供了处理各种文件过程中的各个生命周期钩子,使开发者能够利用插件功能开发很多自定义的功能。
webpack Plugin 和 Loader 的区别
-
Loader:
用于对模块源码的转换,loader 描述了 webpack 如何处理非 javascript 模块,并且在 buld 中引入这些依赖。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或者将内联图像转换为 data URL。比如说:CSS-Loader,Style-Loader 等。
-
Plugin
目的在于解决 loader 无法实现的其他事,它直接作用于 webpack,扩展了它的功能。在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
Tree Shaking 摇树
当我们写了好几个方法,但是实际上只用到了一个,那么我们需要在打包的时候剔除没有用到的代码,这就是tree shaking的作用,tree shaking只支持ES module的语法,即import、export这种,而不支持require这种commonJS的语法。
背景: 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。
思路: 基于ES6提供的模块系统对代码进行静态分析,并将代码中的死代码(dead code)移除的一种技术。因此,利用Tree Shaking技术可以很方便地实现我们代码上的优化,减少代码体积。
Tree Shaking 摇树 是借鉴了 rollup 的实现。
摇树删除代码的原理: webpack基于ES6提供的模块系统,对代码的依赖树进行静态分析,把import & export标记为3类:
- 所有import标记为/* harmony import */
- 被使用过的export标记为/harmony export([type])/,其中[type]和webpack内部有关,可能是binding,immutable等;
- 没有被使用的export标记为/* unused harmony export [FuncName] */,其中[FuncName]为export的方法名,之后使用Uglifyjs(或者其他类似的工具)进行代码精简,把没用的都删除。
为何基于es6模块实现(ES6 module 特点:):
- 只能作为模块顶层的语句出现
- import的模块名只能是字符串常量
- import binding是immutable的
条件:
- 首先源码必须遵循 ES6 的模块规范 (import & export),如果是 CommonJS 规范 (require) 则无法使用。
- 编写的模块代码不能有副作用,如果在代码内部改变了外部的变量则不会被移除。
配置方法:
- 在package.json里添加一个属性:
{
// sideEffects如果设为false,webpack就会认为所有没用到的函数都是没副作用的,即删了也没关系。
"sideEffects": false,
// 设置黑名单,用于防止误删代码
"sideEffects": [
// 数组里列出黑名单,禁止shaking下列代码
"@babel/polly-fill",
"*.less",
// 其它有副作用的模块
"./src/some-side-effectful-file.js"
],
}
tree-shaking 摇掉代码中未使用的代码 在生产模式下自动开启
tree-shaking并不是webpack中的某一个配置选项,是一组功能搭配使用后的优化效果,会在生产模式下自动启动
// 在开发模式下,设置 usedExports: true ,打包时只会标记出哪些模块没有被使用,不会删除,因为可能会影响 source-map的标记位置的准确性。
{
mode: 'develpoment',
optimization: {
// 优化导出的模块
usedExports: true
},
}
// 在生产模式下默认开启 usedExports: true ,打包压缩时就会将没用到的代码移除
{
mode: 'production',
// 这个属性的作用就是集中配置webpack内部的优化功能
optimizition: {
// 只导出外部使用的模块成员 负责标记枯树叶
usedExports: true,
minimize: true, // 自动压缩代码 负责摇掉枯树叶
/**
* webpack打包默认会将一个模块单独打包到一个闭包中
* webpack3中新增的API 将所有模块都放在一个函数中 ,尽可能将所有模块合并在一起,
* 提升效率,减少体积 达到作用域提升的效果
*/
concatenateModules: true,
},
}
使用摇树的注意事项:
- 使用 ES6 模块语法编写代码
- 工具类函数尽量以单独的形式输出,不要集中成一个对象或者类
- 声明 sideEffects
- 自己在重构代码时也要注意副作用
tree-shaking & babel 使用babel-loader处理js代码会导致tree-shaking失效的原因:
- treeshaking 使用的前提必须是ES module组织的代码,也就是说交给ESMOdule处理的代码必须是ESM。当我们使用babel-loader处理js代码之后就有可能将ESM 转换 成commonjs规范(preset-env插件工作的时候就会将esm => coommonjs)
解决办法: 收到配置preset-env的modules: false,确保不会开启自动转换的插件(在最新版本的babel-loader中自动帮我们关闭了转换成commonjs规范的功能)
presets: [
['@babel/preset-env', {"modules": false}]
]
常用插件简述
- webpack-dev-server
- clean-webpack-plugin:编译前清理输出目录
- CopyWebpackPlugin:复制文件
- HotModuleReplacementPlugin:热更新
- ProvidePlugin:全局变量设置
- DefinePlugin:定义全局常量
- splitChunks(老版本用CommonsChunkPlugin):提取公共模块,将符合引用次数的模块打包到一起
- mini-css-extract-plugin(老版本用ExtractTextWebpackPlugin):css单独打包
- TerserPlugin(老版本用UglifyJsPlugin):压缩代码
- progress-bar-webpack-plugin:编译进度条
- DllPlugin& DllReferencePlugin:提高打包效率,仅打包一次第三方模块
- webpack-bundle-analyzer:可视化的查看webpack打包出来的各个文件体积大小
- thread-loader,happypack:多进程编译,加快编译速度
插件如何实现
webpack本质是一个事件流机制,核心模块:tabable(Sync + Async)Hooks 构造出 === Compiler(编译) + Compiletion(创建bundles)
compiler对象代表了完整的webpack环境配置。这个对象在启动webpack时被一次性建立,并配置好所有可操作的设置,包括options、loader和plugin。当在webpack环境中应用一插件时,插件将收到此compiler对象的引用。可以使用它来访问webpack的主环境
compilation对象代表了一次资源版本构建。当运行webpack开发环境中间件时,每当检测到一个文件变化,就会创建一个新的compilation,从而生成一个新的编译资源。一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态的信息。compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用
创建一个插件函数,在其prototype上定义apply方法,指定一个webpack自身的事件钩子
函数内部处理webpack内部实例的特定数据
处理完成后,调用webpack提供的回调函数
function MyWebpackPlugin()(
};
// prototype 上定义 apply 方法
MyWebpackPlugin.prototype.apply=function(){
// 指定一个事件函数挂载到webpack
compiler.pluginCwebpacksEventHook"funcion (compiler)( console. log(“这是一个插件”);
// 功能完成调用后webpack提供的回调函数
callback()
})
tree shaking 是什么,原理是什么
Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination
tree shaking 的原理是什么?
ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码
复制代码
扩展:common.js 和 es6 中模块引入的区别?
CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。在使用上的差别主要有:
1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口(静态编译)。
3、CommonJs 是单个值导出,ES6 Module 可以导出多个
4、CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
5、CommonJs 的 this 是当前模块,ES6 Module 的 this 是 undefined
babel 是什么,原理了解吗
Babel 是一个 JavaScript 编译器。他把最新版的 javascript 编译成当下可以执行的版本,简言之,利用 babel 就可以让我们在当前的项目中随意的使用这些新最新的 es6,甚至 es7 的语法。
Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。
- 解析 将代码解析成抽象语法树(AST),每个 js 引擎(比如 Chrome 浏览器中的 V8 引擎)都有自己的 AST 解析器,而 Babel 是通过 Babylon 实现的。在解析过程中有两个阶段:词法分析和语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于 AST 中节点;而语法分析阶段则会把一个令牌流转换成 AST 的形式,同时这个阶段会把令牌中的信息转换成 AST 的表述结构。
- 转换 在这个阶段,Babel 接受得到 AST 并通过 babel-traverse 对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是 Babel 插件介入工作的部分。
- 生成 将经过转换的 AST 通过 babel-generator 再转换成 js 代码,过程就是深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
Vue
vue 组件通讯方式有哪些方法
- props 和emit 触发事件来做到的
- children 获取当前组件的父组件和当前组件的子组件
- listeners A->B->C。Vue 2.4 开始提供了listeners 来解决这个问题
- 父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)
- $refs 获取组件实例
- envetBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式
- vuex 状态管理
Vue 响应式原理
整体思路是数据劫持+观察者模式
对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。
相关代码如下
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");
//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}
复制代码
Vue nextTick 原理
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
相关代码如下
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}
复制代码
Vue diff 原理
路由原理 history 和 hash 两种路由方式的特点
hash 模式
- location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
- 可以为 hash 的改变添加监听事件
window.addEventListener("hashchange", funcRef, false);
复制代码
每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点:兼容性好但是不美观
history 模式
利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
特点:虽然美观,但是刷新会出现 404 需要后端进行配置
手写 Vue.extend 实现
// src/global-api/initExtend.js
import { mergeOptions } from "../util/index";
export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}
vue-router 中路由方法 pushState 和 replaceState 能否触发 popSate 事件
答案是:不能
pushState 和 replaceState
HTML5 新接口,可以改变网址(存在跨域限制)而不刷新页面,这个强大的特性后来用到了单页面应用如:vue-router,react-router-dom 中。
注意:仅改变网址,网页不会真的跳转,也不会获取到新的内容,本质上网页还停留在原页面
window.history.pushState(state, title, targetURL);
@状态对象:传给目标路由的信息,可为空
@页面标题:目前所有浏览器都不支持,填空字符串即可
@可选url:目标url,不会检查url是否存在,且不能跨域。如不传该项,即给当前url添加data
window.history.replaceState(state, title, targetURL);
@类似于pushState,但是会直接替换掉当前url,而不会在history中留下记录
复制代码
popstate 事件会在点击后退、前进按钮(或调用 history.back()、history.forward()、history.go()方法)时触发
注意:用 history.pushState()或者 history.replaceState()不会触发 popstate 事件
nodejs
nodejs常用模块
- path
- fs
- http,url
- express
- koa
- mongoose
- process
- cookie, session
- crypto加密相关
- os
koa 中间件思想,洋葱模型
class Middleware {
constructor() {
this.middlewares = [];
}
use(fn) {
if(typeof fn !== 'function') {
throw new Error('Middleware must be function, but get ' + typeof fn);
}
this.middlewares.push(fn);
return this;
}
compose() {
const middlewares = this.middlewares;
return dispatch(0);
function dispatch(index) {
const middleware = middlewares[index];
if (!middleware) {return;}
try{
const ctx = {};
const result = middleware(ctx, dispatch.bind(null, index + 1));
return Promise.resolve(result);
} catch(err) {
return Promise.reject(err);
}
}
}
}
// 使用
const middleware = new Middleware();
middleware.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
middleware.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
middleware.compose();// 1 3 4 2
复制代码
express和koa的区别
- 编码风格:express采用回调函数风格,koa1 采用 generator,koa2(默认)使用await,风格上更加优雅,koa与es6,7的结合更加紧密;
- 错误处理:express采用在回调中错误优先的处理方式,深层次的异常捕获不了,使得必须在每一层回调里面处理错误,koa的使用try catch捕获错误,将错误上传可以统一处理错误;
- Koa 把 Express 中内置的 router、view 等功能都移除了,使得框架本身更轻量;
- express社区较大,文档也相对较多,koa社区相对较小;
数据结构经典面试题目
1、数组和链表的区别。
从逻辑结构上来看,数组必须实现定于固定的长度,不能适应数据动态增减的情况,即数组的大小一旦定义就不能改变。当数据增加是,可能超过原先定义的元素的个数;当数据减少时,造成内存浪费;链表动态进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。
从内存存储的角度看;数组从栈中分配空间(用new则在堆上创建),对程序员方便快速,但是自由度小;链表从堆中分配空间,自由度大但是申请管理比较麻烦。
从访问方式类看,数组在内存中是连续的存储,因此可以利用下标索引进行访问;链表是链式存储结构,在访问元素时候只能够通过线性方式由前到后顺序的访问,所以访问效率比数组要低。
2、简述快速排序过程
1)选择一个基准元素,通常选择第一个元素或者最后一个元素,
2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的元素值比基准值大。
3)此时基准元素在其排好序后的正确位置
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
3、快速排序的改进
只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。
选择基准元的方式
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。
方法1 固定基准元
如果输入序列是随机的,处理时间是可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。
方法2 随机基准元
这是一种相对安全的策略。由于基准元的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
方法3 三数取中
引入的原因:虽然随机选取基准时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取基准。
分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。
稳定性:
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序,这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对次序发生了改变,则称该算法是不稳定的。
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
选择排序算法准则:
一般而言,需要考虑的因素有以下四点:
设待排序元素的个数为n.
1) 当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
2) 当n较大,内存空间允许,且要求稳定性:归并排序
3) 当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5) 一般不使用或不直接使用传统的冒泡排序。
6)基数排序 它是一种稳定的排序算法,但有一定的局限性: 1、关键字可分解。 2、记录的关键字位数较少,如果密集更好 3、如果是数字时,最好是无符号的
5、冒泡排序算法的改进
1.设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
2.传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。
6、邻接矩阵与邻接表
邻接矩阵表示法:在一个一维数组中存储所有的点,在一个二维数组中存储顶点之间的边的权值
邻接表表示法:图中顶点用一个一维数组存储,图中每个顶点vi的所有邻接点构成单链表
对比
1)在邻接矩阵表示中,无向图的邻接矩阵是对称的。矩阵中第 i 行或 第 i 列有效元素个数之和就是顶点的度。
在有向图中 第 i 行有效元素个数之和是顶点的出度,第 i 列有效元素个数之和是顶点的入度。
2)在邻接表的表示中,无向图的同一条边在邻接表中存储的两次。如果想要知道顶点的度,只需要求出所对应链表的结点个数即可。
有向图中每条边在邻接表中只出现一次,求顶点的出度只需要遍历所对应链表即可。求入度则需要遍历其他顶点的链表。
3)邻接矩阵与邻接表优缺点:
邻接矩阵的优点是可以快速判断两个顶点之间是否存在边,可以快速添加边或者删除边。而其缺点是如果顶点之间的边比较少,会比较浪费空间。因为是一个 n∗n 的矩阵。
而邻接表的优点是节省空间,只存储实际存在的边。其缺点是关注顶点的度时,就可能需要遍历一个链表。
7.用循环比递归效率高吗?
递归和循环两者完全可以互换。不能完全决定性地说循环地效率比递归的效率高。
2.1递归算法:
优点:代码简洁、清晰,并且容易验证正确性。
缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。但是,对于某些问题,如果不使用递归,那将是极端难看的代码。在编译器优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
2.2循环算法:
优点:速度快,结构简单。
缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。
8、解决哈希冲突的方法
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。
1) 线性探测法
2) 平方探测法
3) 伪随机序列法
4) 拉链法
9、KMP算法:在一个字符串中查找是否包含目标的匹配字符串。其主要思想是每趟比较过程让子串先后滑动一个合适的位置。当发生不匹配的情况时,不是右移一位,而是移动(当前匹配的长度– 当前匹配子串的部分匹配值)位。
10、B树
根据B类树的特点,构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,保证层数尽量的少,以便后面我们可以更快的找到信息,磁盘的I/O操作也少一些,而且B类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定的。
B树和B+树的区别,以一个m阶树为例。
- 关键字的数量不同;B+树中分支结点有m个关键字,其叶子结点也有m个,其关键字只是起到了一个索引的作用,但是B树虽然也有m个子结点,但是其只拥有m-1个关键字。
- 存储的位置不同;B+树中的数据都存储在叶子结点上,也就是其所有叶子结点的数据组合起来就是完整的数据,但是B树的数据存储在每一个结点中,并不仅仅存储在叶子结点上。
- 分支结点的构造不同;B+树的分支结点仅仅存储着关键字信息和儿子的指针(这里的指针指的是磁盘块的偏移量),也就是说内部结点仅仅包含着索引信息。
- 查询不同;B树在找到具体的数值以后,则结束,而B+树则需要通过索引找到叶子结点中的数据才结束,也就是说B+树的搜索过程中走了一条从根结点到叶子结点的路径。
小结
以上的面经仅仅是起到一个抛砖引玉的作用,前端各种面试题层出不穷,如果只是记答案显然不现实,一定要理解其中的原理,才能用不变应万变。
由于我整理的面经太多,不能一一贴出,需要更多面经的点链接免费领取400道面试大礼包.