1.4-前端知识点
Create by fall on 18 Oct 2021 Recently revised in 11 Oct 2023
简单
从输入一个 URL 地址到浏览器完成渲染的整个过程?
- 浏览器地址栏输入 URL 并回车
- 浏览器查找当前 URL 是否存在缓存,并比较缓存是否过期
- DNS 解析 URL 对应的 IP
- 根据 IP 建立 TCP 连接(三次握手)
- 发送 http 请求
- 服务器处理请求,浏览器接受 HTTP 响应
- 浏览器解析并渲染页面
- 关闭 TCP 连接(四次握手)
这个问题属于老生常谈的经典问题了 下面给出面试简单版作答,面试官可以基于这道题进行前端很多知识点的考察 从 http 网络知识到浏览器原理再到前端性能优化 这个题目都可以作为引子开始。
http 状态码
- 204 (无内容) 服务器成功处理了请求,但没有返回任何内容
- 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
- 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
- 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
- 400 (错误请求) 服务器不理解请求的语法(一般为参数错误)。
- 401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
- 403 (禁止) 服务器拒绝请求。(一般为客户端的用户权限不够)
- 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 特性如下
- 连接迁移
- 无队头阻塞
- 自定义的拥塞控制
- 前向安全和前向纠错
建议大家详细看看这篇文章Http2.0 的一些思考以及 Http3.0 的优势
浏览器缓存策略是怎样的(强缓存 协商缓存)具体是什么过程?
前端浏览器缓存知识梳理:https://juejin.cn/post/6947936223126093861
https 加密过程是怎样的
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
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
defer和async区别
在没有 defer 或者 async 的情况下:会立即执行脚本,所以通常建议把 script 放在 body 最后
<script src="script.js"></script>
async:有 async 的话,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。 但是多个 js 文件的加载顺序不会按照书写顺序进行
<script async src="script.js"></script>
derer:有 derer 的话,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,并且多个defer会按照顺序进行加载。
<script defer src="script.js"></script>
为什么外链css为什么要放头部?
首先整个页面展示给用户会经过html 的解析与渲染过程。
而外链css无论放在html的任何位置都不影响html的解析,但是影响html的渲染。
如果将css放在尾部,html的内容可以第一时间显示出来,但是会阻塞html行内css的渲染。
浏览器的这个策略其实很明智的,想象一下,如果没有这个策略,页面首先会呈现出一个行内css样式,待CSS下载完之后又突然变了一个模样。用户体验可谓极差,而且渲染是有成本的。
如果将css放在头部,css的下载解析是可以和html的解析同步进行的,放到尾部,要花费额外时间来解析CSS,并且浏览器会先渲染出一个没有样式的页面,等CSS加载完后会再渲染成一个有样式的页面,页面会出现明显的闪动的现象。
为什么script要放在尾部?
因为当浏览器解析到script的时候,就会立即下载执行,中断html的解析过程,如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
具体的流程是这样的:
-
浏览器一边下载HTML网页,一边开始解析。
-
解析过程中,发现script标签
-
暂停解析,网页渲染的控制权转交给JavaScript引擎
-
如果script标签引用了外部脚本,就下载该脚本,否则就直接执行
-
执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页
外链的script包含async或者defer如何处理?
这两个属性只是script标签在header标签中使用的,如果你把它放在body后面是无效的。
script 这两个属性主要用于其js文件没有操作DOM
的情况,这时候就可以将该js脚本设置为异步加载,通过async或defer来标记代码。
async和defer的区别:
0、async和defer都仅对外部脚本有效,对于内置而不是连接外部脚本的script标签,以及动态生成的script标签不起作用。
1、async和defer虽然都是异步的,不过使用async标志的脚本文件一旦加载完成就会立即执行;而使用defer标记的脚本文件,会在 DOMContentLoaded 事件之前(也就是页面DOM加载完成时)执行。
2、如果有多个js脚本文件,async标记不保证按照书写的顺序执行,哪个脚本先下载结束,就先执行那个脚本。而defer标记则会按照js脚本书写顺序执行。
3、一般来说,如果脚本之间没有依赖关系,就使用async属性,如果脚本之间有依赖关系,就使用defer属性。如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定。
对于async标记,浏览器的解析过程是这样的:
-
浏览器开始解析HTML网页
-
解析过程中,发现带有async属性的script标签
-
浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本
-
脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本
-
脚本执行完毕,浏览器恢复解析HTML网页
对于defer标记,浏览器的解析过程是这样的:
-
浏览器开始解析HTML网页
-
解析过程中,发现带有defer属性的script标签
-
浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本
-
浏览器完成解析HTML网页,此时再执行下载的脚本
由于使用了async或defer的script会放在header中,而header又会存在外链css,那么二者有顺序要求吗?
header中script和外链css的位置顺序
先说结论:
如果在html的header中同时有js脚本和外链css,js脚本最好放外链css前面。
其实js的执行是依赖css样式
的。即只有css样式全部下载完成后才会执行js。
因为如果脚本的内容是获取元素的样式,宽高等CSS控制的属性,浏览器是需要计算的,也就是依赖于CSS。浏览器无法感知脚本内容到底是什么,为避免样式获取错误,因而只好等前面所有的样式下载完后,再执行JS。
但是如果css下载事件很长的话,js也无法正常运行,导致html无法正常解析出来。如果css的内容下载更快的话,是没影响的,但反过来的话,JS就要等待了,然而这些等待的时间是完全不必要的。
实现异步的方法
回调函数 -> 时间监听 -> 发布订阅 -> Promise -> Generate -> async / await
回调函数(Callback)
回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:
ajax(url, () => {
// 处理逻辑
})
但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。
事件监听
这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
下面是两个函数 f1 和 f2,编程的意图是f2必须等到f1执行完成,才能执行。首先,为f1绑定一个事件(这里采用的jQuery的写法)
f1.on('done', f2);
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
首先,f2向信号中心jQuery订阅done信号。
jQuery.subscribe('done', f2);
然后,f1进行如下改写:
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
上面代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。 f2完成执行后,可以取消订阅(unsubscribe)
jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promise
Promise 本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等
Promise 的三种状态:
- Pending----Promise 对象实例创建时候的初始状态
- Fulfilled----可以理解为成功的状态
- Rejected----可以理解为失败的状态
一旦从等待状态变成为其他状态就永远不能更改状态了
生成器 Generators/ yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
- 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
- Generator 函数除了状态机,还是一个遍历器对象生成函数。
- 可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
- yield 表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
Async/Await
-
用async/await,你可以轻松地达成之前使用生成器和co函数所做到的工作,它有如下特点:
1. async/await是基于Promise实现的,它不能用于普通的回调函数。
2. async/await与Promise一样,是非阻塞的。
3. async/await使得异步代码看起来像同步代码,这正是它的魔力所在。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}
Generator函数依次调用三个文件那个例子用async/await写法,只需几句话便可实现
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
async function readResult(params) {
try {
let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例
let p2 = await read(p1, 'utf8')
let p3 = await read(p2, 'utf8')
console.log('p1', p1)
console.log('p2', p2)
console.log('p3', p3)
return p3
} catch (error) {
console.log(error)
}
}
readResult('1.txt').then( // async函数返回的也是个promise
data => {
console.log(data)
},
err => console.log(err)
)
// p1 2.txt
// p2 3.txt
// p3 结束
// 结束
5.2 Async/Await并发请求
如果请求两个文件,毫无关系,可以通过并发请求
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function readAll() {
read1()
read2()//这个函数同步执行
}
async function read1() {
let r = await read('1.txt','utf8')
console.log(r)
}
async function read2() {
let r = await read('2.txt','utf8')
console.log(r)
}
readAll() // 2.txt 3.txt
中等
前端性能定位以及优化指标
前端性能优化手段从以下几个方面入手:加载优化、执行优化、渲染优化、样式优化、脚本优化
- 网络优化:减少 HTTP 请求、缓存资源、压缩代码、无阻塞、首屏加载、按需加载、预加载、压缩图像、减少Cookie、避免重定向、异步加载第三方资源,代码打包,treeshaking,gzip 压缩
- 渲染步骤优化:设置 viewport、减少 DOM 节点、优化动画、优化高频事件、GPU 加速,CSS 写在头部,JS 写在尾部并异步、避免img、iframe等的src为空、尽量避免重置图像大小、图像尽量避免使用DataURL
- 样式优化:避免在 HTML 中书写 style、避免 CSS 表达式、移除 CSS 空规则、正确使用 display、减少使用 float。
- 脚本优化:减少重绘和回流、缓存 DOM 选择与计算、缓存 .length 的值、尽量使用事件代理、尽量使用id选择器、touch事件优化
- 首次渲染时间:减少等待时间,让用户更快看到页面
- 我们可以从 前端性能监控-埋点以及 window.performance相关的 api 去回答
- 也可以从性能分析工具 Performance 和 Lighthouse
- 还可以从性能指标 LCP FCP FID CLS 等去着手
以下为性能相关的文章 大家可以去看看
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源而造成阻塞的现象,若无外力作用,它们都将无法继续执行
- 竞争资源引起进程死锁
- 可剥夺和非剥夺资源
- 竞争非剥夺资源
- 竞争临时性资源
- 进程推进顺序不当
产生条件
互斥条件:涉及的资源是非共享的
- 涉及的资源是非共享的,一段时间内某资源只由一个进程占用,如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放
不剥夺条件:不能强行剥夺进程拥有的资源
- 进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
请求和保持条件:进程在等待一新资源时继续占有已分配的资源
- 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
- 环路等待条件:存在一种进程的循环链,链中的每一个进程已获得的资源同时被链中的下一个进程所请求 在发生死锁时,必然存在一个进程——资源的环形链
解决办法
只要打破四个必要条件之一就能有效预防死锁的发生
暂时性死区
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
扩展:
let 、const与暂时性死区
let 或 const 声明的变量拥有暂时性死区(TDZ):当进入它的作用域,它不能被访问(获取或设置)直到执行到达声明。
首先看看不具有暂时性死区的var:
- 当进入var变量的作用域(包围它的函数),立即为它创建(绑定)存储空间。变量会立即被初始化并赋值为undefined。
- 当执行到变量声明的时候,如果变量定义了值则会被赋值。
通过let声明的变量拥有暂时性死区,生命周期如下:
- 当进入let变量的作用域(包围它的语法块),立即为它创建(绑定)存储空间。此时变量仍是未初始化的。
- 获取或设置未初始化的变量将抛出异常ReferenceError。
- 当执行到变量声明的时候,如果变量定义了值则会被赋值。如果没有定义值,则赋值为undefined。
const 工作方式与 let 类似,但是定义的时候必须赋值并且不能改变。
在 TDZ 内部,如果获取或设置变量将抛出异常:
if (true) { // enter new scope, TDZ starts
// Uninitialized binding for `tmp` is created
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ ends, `tmp` is initialized with `undefined`
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
下面的示例将演示死区(dead zone)是真正短暂的(基于时间)和不受空间条件限制(基于位置)
if (true) { // enter new scope, TDZ starts
const func = function () {
console.log(myVar); // OK!
};
// Here we are within the TDZ and
// accessing `myVar` would cause a `ReferenceError`
let myVar = 3; // TDZ ends
func(); // called outside TDZ
}
typeof 与暂时性死区
变量在暂时性死区无法被访问,所以无法对它使用 typeof:
if (true) {
console.log(typeof tmp); // ReferenceError
let tmp;
}
闭包的理解
闭包:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包的特点:
让外部访问函数内部变量成为可能; 可以避免使用全局变量,防止全局变量污染; 可以让局部变量常驻在内存中; 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
应用场景
- 埋点(是网站分析的一种常用的数据采集方法)计数器
function count() {
var num = 0;
return function () {
return ++num
}
}
var getNum = count();
var getNewNum = count();
document.querySelectorAll('button')[0].onclick = function(){
console.log('点击加入购物车次数: '+getNum());
}
document.querySelectorAll('button')[1].onclick = function(){
console.log('点击付款次数: '+getNewNum());
}
事件+循环
按照以下方式添加事件,打印出来的i不是按照序号的
形成原因就是操作的是同一个词法环境,因为onclick后面的函数都是一个闭包,但是操作的是同一个词法环境
var lis = document.querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
alert(i)
}
}
解决办法:
使用匿名函数之后,就形成一个闭包, 操作的就是不同的词法环境
var lis = document.querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
(function (j) {
lis[j].onclick = function () {
alert(j)
}
})(i)
}
v8 垃圾回收机制
首先js因为是单线程,垃圾回收会占用主线程,导致页面卡顿,所以需要一个算法或者说策略,而v8采用的是分代式回收,而垃圾回收在堆中分成了很多部分用作不同的作用(我在说什么啊!当时),回收主要表现在新老生代上,新生代就活得短一点的对象,老生代就活得长一点的对象。
在新生代里有一个算法,将新生代分成了两个区,一个FORM,一个TO,每次经过Scavenge会将FORM区中的没引用的销毁,然后活着的TO区调换位置,反复如此,当经过一次acavange后就会晋升的老生代还有个条件就是TO区的内存超过多少了也会晋升。
而老生代,采用标记清除和标记整理,但标记清除会造成内存不连续,所以会有标记整理取解决掉内存碎片,就是清理掉边界碎片
第一个问题是为了不影响后续 FORM 空间的分配,第二个问题你应该看过有关这方面的文章把,垃圾回收会构建一个根列表,从根节点去访问那些变量,可访问到就是活动,不可就是垃圾
谷歌面试题
- 什么是时间复杂度?为什么它很有用?
- 什么是DOM?
- 什么是事件循环?
- 什么是闭包?
- 原型继承是如何工作的?它和类式继承有什么区别? (在我看来这不是一个有用的问题,但是很多人喜欢问)
- this 是如何工作的?
- 什么是事件冒泡?它是怎么工作的? (在我看来这也是一个不好的问题,但是很多人喜欢问)
- 讲一下服务端和客户端通信的方式有哪些,一些高层级的网络协议如何是工作的?(IP, TCP, HTTP/S/2, UDP, RTC, DNS, 等等)
- 什么是 REST?我们为什么要使用它?
- 如果一个网站速度很慢,如何诊断和修复?网站性能优化的方式有哪些?以及这些方式的适用场景。
- 你用过哪些框架?它们的优点和缺点是什么?为什么要使用它们?这些框架解决了哪些问题?