跳到主要内容

1.4-前端知识点

Create by fall on 18 Oct 2021 Recently revised in 11 Oct 2023

简单

从输入一个 URL 地址到浏览器完成渲染的整个过程?

  1. 浏览器地址栏输入 URL 并回车
  2. 浏览器查找当前 URL 是否存在缓存,并比较缓存是否过期
  3. DNS 解析 URL 对应的 IP
  4. 根据 IP 建立 TCP 连接(三次握手)
  5. 发送 http 请求
  6. 服务器处理请求,浏览器接受 HTTP 响应
  7. 浏览器解析并渲染页面
  8. 关闭 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 加密过程是怎样的

大前端知识点-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的解析过程,如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。

具体的流程是这样的:

  1. 浏览器一边下载HTML网页,一边开始解析。

  2. 解析过程中,发现script标签

  3. 暂停解析,网页渲染的控制权转交给JavaScript引擎

  4. 如果script标签引用了外部脚本,就下载该脚本,否则就直接执行

  5. 执行完毕,控制权交还渲染引擎,恢复往下解析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

  1. 用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事件优化
  • 首次渲染时间:减少等待时间,让用户更快看到页面
  1. 我们可以从 前端性能监控-埋点以及 window.performance相关的 api 去回答
  2. 也可以从性能分析工具 Performance 和 Lighthouse
  3. 还可以从性能指标 LCP FCP FID CLS 等去着手

以下为性能相关的文章 大家可以去看看

5 分钟撸一个前端性能监控工具

前端性能优化之谈谈通用性能指标及上报策略

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源而造成阻塞的现象,若无外力作用,它们都将无法继续执行

  • 竞争资源引起进程死锁
  • 可剥夺和非剥夺资源
  • 竞争非剥夺资源
  • 竞争临时性资源
  • 进程推进顺序不当

产生条件

互斥条件:涉及的资源是非共享的

  • 涉及的资源是非共享的,一段时间内某资源只由一个进程占用,如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放

不剥夺条件:不能强行剥夺进程拥有的资源

  • 进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放

请求和保持条件:进程在等待一新资源时继续占有已分配的资源

  • 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
  • 环路等待条件:存在一种进程的循环链,链中的每一个进程已获得的资源同时被链中的下一个进程所请求 在发生死锁时,必然存在一个进程——资源的环形链

​ 解决办法

只要打破四个必要条件之一就能有效预防死锁的发生

暂时性死区

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

扩展:

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 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

闭包的特点:

让外部访问函数内部变量成为可能; 可以避免使用全局变量,防止全局变量污染; 可以让局部变量常驻在内存中; 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)

应用场景

  1. 埋点(是网站分析的一种常用的数据采集方法)计数器
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?我们为什么要使用它?
  • 如果一个网站速度很慢,如何诊断和修复?网站性能优化的方式有哪些?以及这些方式的适用场景。
  • 你用过哪些框架?它们的优点和缺点是什么?为什么要使用它们?这些框架解决了哪些问题?